Commit | Line | Data |
---|---|---|
44f4c339 | 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others |
373d1232 | 2 | // Distributed under an MIT license: https://codemirror.net/LICENSE |
44f4c339 | 3 | |
837afb80 TD |
4 | (function(mod) { |
5 | if (typeof exports == "object" && typeof module == "object") // CommonJS | |
373d1232 | 6 | mod(require("../../lib/codemirror")) |
837afb80 | 7 | else if (typeof define == "function" && define.amd) // AMD |
373d1232 | 8 | define(["../../lib/codemirror"], mod) |
837afb80 | 9 | else // Plain browser env |
373d1232 | 10 | mod(CodeMirror) |
837afb80 | 11 | })(function(CodeMirror) { |
373d1232 TD |
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 | ||
704cb1bf MS |
75 | function lastMatchIn(string, regexp, endMargin) { |
76 | var match, from = 0 | |
77 | while (from <= string.length) { | |
78 | regexp.lastIndex = from | |
373d1232 | 79 | var newMatch = regexp.exec(string) |
704cb1bf MS |
80 | if (!newMatch) break |
81 | var end = newMatch.index + newMatch[0].length | |
82 | if (end > string.length - endMargin) break | |
83 | if (!match || end > match.index + match[0].length) | |
84 | match = newMatch | |
85 | from = newMatch.index + 1 | |
373d1232 | 86 | } |
704cb1bf | 87 | return match |
373d1232 TD |
88 | } |
89 | ||
90 | function searchRegexpBackward(doc, regexp, start) { | |
91 | regexp = ensureFlags(regexp, "g") | |
92 | for (var line = start.line, ch = start.ch, first = doc.firstLine(); line >= first; line--, ch = -1) { | |
93 | var string = doc.getLine(line) | |
704cb1bf | 94 | var match = lastMatchIn(string, regexp, ch < 0 ? 0 : string.length - ch) |
373d1232 TD |
95 | if (match) |
96 | return {from: Pos(line, match.index), | |
97 | to: Pos(line, match.index + match[0].length), | |
98 | match: match} | |
99 | } | |
100 | } | |
101 | ||
102 | function searchRegexpBackwardMultiline(doc, regexp, start) { | |
704cb1bf | 103 | if (!maybeMultiline(regexp)) return searchRegexpBackward(doc, regexp, start) |
373d1232 | 104 | regexp = ensureFlags(regexp, "gm") |
704cb1bf | 105 | var string, chunkSize = 1, endMargin = doc.getLine(start.line).length - start.ch |
373d1232 | 106 | for (var line = start.line, first = doc.firstLine(); line >= first;) { |
704cb1bf | 107 | for (var i = 0; i < chunkSize && line >= first; i++) { |
373d1232 | 108 | var curLine = doc.getLine(line--) |
704cb1bf | 109 | string = string == null ? curLine : curLine + "\n" + string |
373d1232 | 110 | } |
704cb1bf | 111 | chunkSize *= 2 |
373d1232 | 112 | |
704cb1bf | 113 | var match = lastMatchIn(string, regexp, endMargin) |
373d1232 TD |
114 | if (match) { |
115 | var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n") | |
116 | var startLine = line + before.length, startCh = before[before.length - 1].length | |
117 | return {from: Pos(startLine, startCh), | |
118 | to: Pos(startLine + inside.length - 1, | |
119 | inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length), | |
120 | match: match} | |
121 | } | |
122 | } | |
123 | } | |
124 | ||
125 | var doFold, noFold | |
126 | if (String.prototype.normalize) { | |
127 | doFold = function(str) { return str.normalize("NFD").toLowerCase() } | |
128 | noFold = function(str) { return str.normalize("NFD") } | |
129 | } else { | |
130 | doFold = function(str) { return str.toLowerCase() } | |
131 | noFold = function(str) { return str } | |
132 | } | |
133 | ||
134 | // Maps a position in a case-folded line back to a position in the original line | |
135 | // (compensating for codepoints increasing in number during folding) | |
136 | function adjustPos(orig, folded, pos, foldFunc) { | |
137 | if (orig.length == folded.length) return pos | |
138 | for (var min = 0, max = pos + Math.max(0, orig.length - folded.length);;) { | |
139 | if (min == max) return min | |
140 | var mid = (min + max) >> 1 | |
141 | var len = foldFunc(orig.slice(0, mid)).length | |
142 | if (len == pos) return mid | |
143 | else if (len > pos) max = mid | |
144 | else min = mid + 1 | |
145 | } | |
146 | } | |
147 | ||
148 | function searchStringForward(doc, query, start, caseFold) { | |
149 | // Empty string would match anything and never progress, so we | |
150 | // define it to match nothing instead. | |
151 | if (!query.length) return null | |
152 | var fold = caseFold ? doFold : noFold | |
153 | var lines = fold(query).split(/\r|\n\r?/) | |
154 | ||
155 | search: for (var line = start.line, ch = start.ch, last = doc.lastLine() + 1 - lines.length; line <= last; line++, ch = 0) { | |
156 | var orig = doc.getLine(line).slice(ch), string = fold(orig) | |
157 | if (lines.length == 1) { | |
158 | var found = string.indexOf(lines[0]) | |
159 | if (found == -1) continue search | |
160 | var start = adjustPos(orig, string, found, fold) + ch | |
161 | return {from: Pos(line, adjustPos(orig, string, found, fold) + ch), | |
162 | to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold) + ch)} | |
163 | } else { | |
164 | var cutFrom = string.length - lines[0].length | |
165 | if (string.slice(cutFrom) != lines[0]) continue search | |
166 | for (var i = 1; i < lines.length - 1; i++) | |
167 | if (fold(doc.getLine(line + i)) != lines[i]) continue search | |
168 | var end = doc.getLine(line + lines.length - 1), endString = fold(end), lastLine = lines[lines.length - 1] | |
169 | if (endString.slice(0, lastLine.length) != lastLine) continue search | |
170 | return {from: Pos(line, adjustPos(orig, string, cutFrom, fold) + ch), | |
171 | to: Pos(line + lines.length - 1, adjustPos(end, endString, lastLine.length, fold))} | |
172 | } | |
173 | } | |
174 | } | |
175 | ||
176 | function searchStringBackward(doc, query, start, caseFold) { | |
177 | if (!query.length) return null | |
178 | var fold = caseFold ? doFold : noFold | |
179 | var lines = fold(query).split(/\r|\n\r?/) | |
180 | ||
181 | search: for (var line = start.line, ch = start.ch, first = doc.firstLine() - 1 + lines.length; line >= first; line--, ch = -1) { | |
182 | var orig = doc.getLine(line) | |
183 | if (ch > -1) orig = orig.slice(0, ch) | |
184 | var string = fold(orig) | |
185 | if (lines.length == 1) { | |
186 | var found = string.lastIndexOf(lines[0]) | |
187 | if (found == -1) continue search | |
188 | return {from: Pos(line, adjustPos(orig, string, found, fold)), | |
189 | to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold))} | |
190 | } else { | |
191 | var lastLine = lines[lines.length - 1] | |
192 | if (string.slice(0, lastLine.length) != lastLine) continue search | |
193 | for (var i = 1, start = line - lines.length + 1; i < lines.length - 1; i++) | |
194 | if (fold(doc.getLine(start + i)) != lines[i]) continue search | |
195 | var top = doc.getLine(line + 1 - lines.length), topString = fold(top) | |
196 | if (topString.slice(topString.length - lines[0].length) != lines[0]) continue search | |
197 | return {from: Pos(line + 1 - lines.length, adjustPos(top, topString, top.length - lines[0].length, fold)), | |
198 | to: Pos(line, adjustPos(orig, string, lastLine.length, fold))} | |
199 | } | |
200 | } | |
201 | } | |
202 | ||
203 | function SearchCursor(doc, query, pos, options) { | |
204 | this.atOccurrence = false | |
205 | this.doc = doc | |
206 | pos = pos ? doc.clipPos(pos) : Pos(0, 0) | |
207 | this.pos = {from: pos, to: pos} | |
208 | ||
209 | var caseFold | |
210 | if (typeof options == "object") { | |
211 | caseFold = options.caseFold | |
212 | } else { // Backwards compat for when caseFold was the 4th argument | |
213 | caseFold = options | |
214 | options = null | |
215 | } | |
216 | ||
217 | if (typeof query == "string") { | |
218 | if (caseFold == null) caseFold = false | |
77b7b761 | 219 | this.matches = function(reverse, pos) { |
373d1232 TD |
220 | return (reverse ? searchStringBackward : searchStringForward)(doc, query, pos, caseFold) |
221 | } | |
222 | } else { | |
223 | query = ensureFlags(query, "gm") | |
224 | if (!options || options.multiline !== false) | |
225 | this.matches = function(reverse, pos) { | |
226 | return (reverse ? searchRegexpBackwardMultiline : searchRegexpForwardMultiline)(doc, query, pos) | |
77b7b761 | 227 | } |
373d1232 | 228 | else |
77b7b761 | 229 | this.matches = function(reverse, pos) { |
373d1232 TD |
230 | return (reverse ? searchRegexpBackward : searchRegexpForward)(doc, query, pos) |
231 | } | |
77b7b761 TD |
232 | } |
233 | } | |
234 | ||
235 | SearchCursor.prototype = { | |
373d1232 TD |
236 | findNext: function() {return this.find(false)}, |
237 | findPrevious: function() {return this.find(true)}, | |
77b7b761 TD |
238 | |
239 | find: function(reverse) { | |
373d1232 | 240 | var result = this.matches(reverse, this.doc.clipPos(reverse ? this.pos.from : this.pos.to)) |
77b7b761 | 241 | |
373d1232 | 242 | // Implements weird auto-growing behavior on null-matches for |
704cb1bf | 243 | // backwards-compatibility with the vim code (unfortunately) |
373d1232 | 244 | while (result && CodeMirror.cmpPos(result.from, result.to) == 0) { |
77b7b761 | 245 | if (reverse) { |
373d1232 TD |
246 | if (result.from.ch) result.from = Pos(result.from.line, result.from.ch - 1) |
247 | else if (result.from.line == this.doc.firstLine()) result = null | |
248 | else result = this.matches(reverse, this.doc.clipPos(Pos(result.from.line - 1))) | |
249 | } else { | |
250 | if (result.to.ch < this.doc.getLine(result.to.line).length) result.to = Pos(result.to.line, result.to.ch + 1) | |
251 | else if (result.to.line == this.doc.lastLine()) result = null | |
252 | else result = this.matches(reverse, Pos(result.to.line + 1, 0)) | |
77b7b761 TD |
253 | } |
254 | } | |
373d1232 TD |
255 | |
256 | if (result) { | |
257 | this.pos = result | |
258 | this.atOccurrence = true | |
259 | return this.pos.match || true | |
260 | } else { | |
261 | var end = Pos(reverse ? this.doc.firstLine() : this.doc.lastLine() + 1, 0) | |
262 | this.pos = {from: end, to: end} | |
263 | return this.atOccurrence = false | |
264 | } | |
77b7b761 TD |
265 | }, |
266 | ||
373d1232 TD |
267 | from: function() {if (this.atOccurrence) return this.pos.from}, |
268 | to: function() {if (this.atOccurrence) return this.pos.to}, | |
77b7b761 | 269 | |
44f4c339 | 270 | replace: function(newText, origin) { |
373d1232 TD |
271 | if (!this.atOccurrence) return |
272 | var lines = CodeMirror.splitLines(newText) | |
273 | this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin) | |
77b7b761 | 274 | this.pos.to = Pos(this.pos.from.line + lines.length - 1, |
373d1232 | 275 | lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0)) |
837afb80 TD |
276 | } |
277 | } | |
278 | ||
77b7b761 | 279 | CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) { |
373d1232 TD |
280 | return new SearchCursor(this.doc, query, pos, caseFold) |
281 | }) | |
77b7b761 | 282 | CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) { |
373d1232 TD |
283 | return new SearchCursor(this, query, pos, caseFold) |
284 | }) | |
837afb80 TD |
285 | |
286 | CodeMirror.defineExtension("selectMatches", function(query, caseFold) { | |
373d1232 TD |
287 | var ranges = [] |
288 | var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold) | |
44f4c339 | 289 | while (cur.findNext()) { |
373d1232 TD |
290 | if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break |
291 | ranges.push({anchor: cur.from(), head: cur.to()}) | |
837afb80 TD |
292 | } |
293 | if (ranges.length) | |
373d1232 TD |
294 | this.setSelections(ranges, 0) |
295 | }) | |
837afb80 | 296 | }); |