jump-to-line.js
8 years ago
jump-to-line.min.js
8 years ago
match-highlighter.js
8 years ago
match-highlighter.min.js
8 years ago
matchesonscrollbar.css
8 years ago
matchesonscrollbar.js
8 years ago
matchesonscrollbar.min.js
8 years ago
search.js
8 years ago
search.min.js
8 years ago
searchcursor.js
8 years ago
searchcursor.min.js
8 years ago
searchcursor.js
294 lines
| 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others |
| 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE |
| 3 | |
| 4 | (function(mod) { |
| 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS |
| 6 | mod(require("../../lib/codemirror")) |
| 7 | else if (typeof define == "function" && define.amd) // AMD |
| 8 | define(["../../lib/codemirror"], mod) |
| 9 | else // Plain browser env |
| 10 | mod(CodeMirror) |
| 11 | })(function(CodeMirror) { |
| 12 | "use strict" |
| 13 | var Pos = CodeMirror.Pos |
| 14 | |
| 15 | function regexpFlags(regexp) { |
| 16 | var flags = regexp.flags |
| 17 | return flags != null ? flags : (regexp.ignoreCase ? "i" : "") |
| 18 | + (regexp.global ? "g" : "") |
| 19 | + (regexp.multiline ? "m" : "") |
| 20 | } |
| 21 | |
| 22 | function ensureFlags(regexp, flags) { |
| 23 | var current = regexpFlags(regexp), target = current |
| 24 | for (var i = 0; i < flags.length; i++) if (target.indexOf(flags.charAt(i)) == -1) |
| 25 | target += flags.charAt(i) |
| 26 | return current == target ? regexp : new RegExp(regexp.source, target) |
| 27 | } |
| 28 | |
| 29 | function maybeMultiline(regexp) { |
| 30 | return /\\s|\\n|\n|\\W|\\D|\[\^/.test(regexp.source) |
| 31 | } |
| 32 | |
| 33 | function searchRegexpForward(doc, regexp, start) { |
| 34 | regexp = ensureFlags(regexp, "g") |
| 35 | for (var line = start.line, ch = start.ch, last = doc.lastLine(); line <= last; line++, ch = 0) { |
| 36 | regexp.lastIndex = ch |
| 37 | var string = doc.getLine(line), match = regexp.exec(string) |
| 38 | if (match) |
| 39 | return {from: Pos(line, match.index), |
| 40 | to: Pos(line, match.index + match[0].length), |
| 41 | match: match} |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | function searchRegexpForwardMultiline(doc, regexp, start) { |
| 46 | if (!maybeMultiline(regexp)) return searchRegexpForward(doc, regexp, start) |
| 47 | |
| 48 | regexp = ensureFlags(regexp, "gm") |
| 49 | var string, chunk = 1 |
| 50 | for (var line = start.line, last = doc.lastLine(); line <= last;) { |
| 51 | // This grows the search buffer in exponentially-sized chunks |
| 52 | // between matches, so that nearby matches are fast and don't |
| 53 | // require concatenating the whole document (in case we're |
| 54 | // searching for something that has tons of matches), but at the |
| 55 | // same time, the amount of retries is limited. |
| 56 | for (var i = 0; i < chunk; i++) { |
| 57 | if (line > last) break |
| 58 | var curLine = doc.getLine(line++) |
| 59 | string = string == null ? curLine : string + "\n" + curLine |
| 60 | } |
| 61 | chunk = chunk * 2 |
| 62 | regexp.lastIndex = start.ch |
| 63 | var match = regexp.exec(string) |
| 64 | if (match) { |
| 65 | var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n") |
| 66 | var startLine = start.line + before.length - 1, startCh = before[before.length - 1].length |
| 67 | return {from: Pos(startLine, startCh), |
| 68 | to: Pos(startLine + inside.length - 1, |
| 69 | inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length), |
| 70 | match: match} |
| 71 | } |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | function lastMatchIn(string, regexp) { |
| 76 | var cutOff = 0, match |
| 77 | for (;;) { |
| 78 | regexp.lastIndex = cutOff |
| 79 | var newMatch = regexp.exec(string) |
| 80 | if (!newMatch) return match |
| 81 | match = newMatch |
| 82 | cutOff = match.index + (match[0].length || 1) |
| 83 | if (cutOff == string.length) return match |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | function searchRegexpBackward(doc, regexp, start) { |
| 88 | regexp = ensureFlags(regexp, "g") |
| 89 | for (var line = start.line, ch = start.ch, first = doc.firstLine(); line >= first; line--, ch = -1) { |
| 90 | var string = doc.getLine(line) |
| 91 | if (ch > -1) string = string.slice(0, ch) |
| 92 | var match = lastMatchIn(string, regexp) |
| 93 | if (match) |
| 94 | return {from: Pos(line, match.index), |
| 95 | to: Pos(line, match.index + match[0].length), |
| 96 | match: match} |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | function searchRegexpBackwardMultiline(doc, regexp, start) { |
| 101 | regexp = ensureFlags(regexp, "gm") |
| 102 | var string, chunk = 1 |
| 103 | for (var line = start.line, first = doc.firstLine(); line >= first;) { |
| 104 | for (var i = 0; i < chunk; i++) { |
| 105 | var curLine = doc.getLine(line--) |
| 106 | string = string == null ? curLine.slice(0, start.ch) : curLine + "\n" + string |
| 107 | } |
| 108 | chunk *= 2 |
| 109 | |
| 110 | var match = lastMatchIn(string, regexp) |
| 111 | if (match) { |
| 112 | var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n") |
| 113 | var startLine = line + before.length, startCh = before[before.length - 1].length |
| 114 | return {from: Pos(startLine, startCh), |
| 115 | to: Pos(startLine + inside.length - 1, |
| 116 | inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length), |
| 117 | match: match} |
| 118 | } |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | var doFold, noFold |
| 123 | if (String.prototype.normalize) { |
| 124 | doFold = function(str) { return str.normalize("NFD").toLowerCase() } |
| 125 | noFold = function(str) { return str.normalize("NFD") } |
| 126 | } else { |
| 127 | doFold = function(str) { return str.toLowerCase() } |
| 128 | noFold = function(str) { return str } |
| 129 | } |
| 130 | |
| 131 | // Maps a position in a case-folded line back to a position in the original line |
| 132 | // (compensating for codepoints increasing in number during folding) |
| 133 | function adjustPos(orig, folded, pos, foldFunc) { |
| 134 | if (orig.length == folded.length) return pos |
| 135 | for (var min = 0, max = pos + Math.max(0, orig.length - folded.length);;) { |
| 136 | if (min == max) return min |
| 137 | var mid = (min + max) >> 1 |
| 138 | var len = foldFunc(orig.slice(0, mid)).length |
| 139 | if (len == pos) return mid |
| 140 | else if (len > pos) max = mid |
| 141 | else min = mid + 1 |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | function searchStringForward(doc, query, start, caseFold) { |
| 146 | // Empty string would match anything and never progress, so we |
| 147 | // define it to match nothing instead. |
| 148 | if (!query.length) return null |
| 149 | var fold = caseFold ? doFold : noFold |
| 150 | var lines = fold(query).split(/\r|\n\r?/) |
| 151 | |
| 152 | search: for (var line = start.line, ch = start.ch, last = doc.lastLine() + 1 - lines.length; line <= last; line++, ch = 0) { |
| 153 | var orig = doc.getLine(line).slice(ch), string = fold(orig) |
| 154 | if (lines.length == 1) { |
| 155 | var found = string.indexOf(lines[0]) |
| 156 | if (found == -1) continue search |
| 157 | var start = adjustPos(orig, string, found, fold) + ch |
| 158 | return {from: Pos(line, adjustPos(orig, string, found, fold) + ch), |
| 159 | to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold) + ch)} |
| 160 | } else { |
| 161 | var cutFrom = string.length - lines[0].length |
| 162 | if (string.slice(cutFrom) != lines[0]) continue search |
| 163 | for (var i = 1; i < lines.length - 1; i++) |
| 164 | if (fold(doc.getLine(line + i)) != lines[i]) continue search |
| 165 | var end = doc.getLine(line + lines.length - 1), endString = fold(end), lastLine = lines[lines.length - 1] |
| 166 | if (endString.slice(0, lastLine.length) != lastLine) continue search |
| 167 | return {from: Pos(line, adjustPos(orig, string, cutFrom, fold) + ch), |
| 168 | to: Pos(line + lines.length - 1, adjustPos(end, endString, lastLine.length, fold))} |
| 169 | } |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | function searchStringBackward(doc, query, start, caseFold) { |
| 174 | if (!query.length) return null |
| 175 | var fold = caseFold ? doFold : noFold |
| 176 | var lines = fold(query).split(/\r|\n\r?/) |
| 177 | |
| 178 | search: for (var line = start.line, ch = start.ch, first = doc.firstLine() - 1 + lines.length; line >= first; line--, ch = -1) { |
| 179 | var orig = doc.getLine(line) |
| 180 | if (ch > -1) orig = orig.slice(0, ch) |
| 181 | var string = fold(orig) |
| 182 | if (lines.length == 1) { |
| 183 | var found = string.lastIndexOf(lines[0]) |
| 184 | if (found == -1) continue search |
| 185 | return {from: Pos(line, adjustPos(orig, string, found, fold)), |
| 186 | to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold))} |
| 187 | } else { |
| 188 | var lastLine = lines[lines.length - 1] |
| 189 | if (string.slice(0, lastLine.length) != lastLine) continue search |
| 190 | for (var i = 1, start = line - lines.length + 1; i < lines.length - 1; i++) |
| 191 | if (fold(doc.getLine(start + i)) != lines[i]) continue search |
| 192 | var top = doc.getLine(line + 1 - lines.length), topString = fold(top) |
| 193 | if (topString.slice(topString.length - lines[0].length) != lines[0]) continue search |
| 194 | return {from: Pos(line + 1 - lines.length, adjustPos(top, topString, top.length - lines[0].length, fold)), |
| 195 | to: Pos(line, adjustPos(orig, string, lastLine.length, fold))} |
| 196 | } |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | function SearchCursor(doc, query, pos, options) { |
| 201 | this.atOccurrence = false |
| 202 | this.doc = doc |
| 203 | pos = pos ? doc.clipPos(pos) : Pos(0, 0) |
| 204 | this.pos = {from: pos, to: pos} |
| 205 | |
| 206 | var caseFold |
| 207 | if (typeof options == "object") { |
| 208 | caseFold = options.caseFold |
| 209 | } else { // Backwards compat for when caseFold was the 4th argument |
| 210 | caseFold = options |
| 211 | options = null |
| 212 | } |
| 213 | |
| 214 | if (typeof query == "string") { |
| 215 | if (caseFold == null) caseFold = false |
| 216 | this.matches = function(reverse, pos) { |
| 217 | return (reverse ? searchStringBackward : searchStringForward)(doc, query, pos, caseFold) |
| 218 | } |
| 219 | } else { |
| 220 | query = ensureFlags(query, "gm") |
| 221 | if (!options || options.multiline !== false) |
| 222 | this.matches = function(reverse, pos) { |
| 223 | return (reverse ? searchRegexpBackwardMultiline : searchRegexpForwardMultiline)(doc, query, pos) |
| 224 | } |
| 225 | else |
| 226 | this.matches = function(reverse, pos) { |
| 227 | return (reverse ? searchRegexpBackward : searchRegexpForward)(doc, query, pos) |
| 228 | } |
| 229 | } |
| 230 | } |
| 231 | |
| 232 | SearchCursor.prototype = { |
| 233 | findNext: function() {return this.find(false)}, |
| 234 | findPrevious: function() {return this.find(true)}, |
| 235 | |
| 236 | find: function(reverse) { |
| 237 | var result = this.matches(reverse, this.doc.clipPos(reverse ? this.pos.from : this.pos.to)) |
| 238 | |
| 239 | // Implements weird auto-growing behavior on null-matches for |
| 240 | // backwards-compatiblity with the vim code (unfortunately) |
| 241 | while (result && CodeMirror.cmpPos(result.from, result.to) == 0) { |
| 242 | if (reverse) { |
| 243 | if (result.from.ch) result.from = Pos(result.from.line, result.from.ch - 1) |
| 244 | else if (result.from.line == this.doc.firstLine()) result = null |
| 245 | else result = this.matches(reverse, this.doc.clipPos(Pos(result.from.line - 1))) |
| 246 | } else { |
| 247 | if (result.to.ch < this.doc.getLine(result.to.line).length) result.to = Pos(result.to.line, result.to.ch + 1) |
| 248 | else if (result.to.line == this.doc.lastLine()) result = null |
| 249 | else result = this.matches(reverse, Pos(result.to.line + 1, 0)) |
| 250 | } |
| 251 | } |
| 252 | |
| 253 | if (result) { |
| 254 | this.pos = result |
| 255 | this.atOccurrence = true |
| 256 | return this.pos.match || true |
| 257 | } else { |
| 258 | var end = Pos(reverse ? this.doc.firstLine() : this.doc.lastLine() + 1, 0) |
| 259 | this.pos = {from: end, to: end} |
| 260 | return this.atOccurrence = false |
| 261 | } |
| 262 | }, |
| 263 | |
| 264 | from: function() {if (this.atOccurrence) return this.pos.from}, |
| 265 | to: function() {if (this.atOccurrence) return this.pos.to}, |
| 266 | |
| 267 | replace: function(newText, origin) { |
| 268 | if (!this.atOccurrence) return |
| 269 | var lines = CodeMirror.splitLines(newText) |
| 270 | this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin) |
| 271 | this.pos.to = Pos(this.pos.from.line + lines.length - 1, |
| 272 | lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0)) |
| 273 | } |
| 274 | } |
| 275 | |
| 276 | CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) { |
| 277 | return new SearchCursor(this.doc, query, pos, caseFold) |
| 278 | }) |
| 279 | CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) { |
| 280 | return new SearchCursor(this, query, pos, caseFold) |
| 281 | }) |
| 282 | |
| 283 | CodeMirror.defineExtension("selectMatches", function(query, caseFold) { |
| 284 | var ranges = [] |
| 285 | var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold) |
| 286 | while (cur.findNext()) { |
| 287 | if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break |
| 288 | ranges.push({anchor: cur.from(), head: cur.to()}) |
| 289 | } |
| 290 | if (ranges.length) |
| 291 | this.setSelections(ranges, 0) |
| 292 | }) |
| 293 | }); |
| 294 |