0
|
1 // CodeMirror, copyright (c) by Marijn Haverbeke and others
|
|
2 // Distributed under an MIT license: https://codemirror.net/5/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, endMargin) {
|
|
76 var match, from = 0
|
|
77 while (from <= string.length) {
|
|
78 regexp.lastIndex = from
|
|
79 var newMatch = regexp.exec(string)
|
|
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
|
|
86 }
|
|
87 return match
|
|
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)
|
|
94 var match = lastMatchIn(string, regexp, ch < 0 ? 0 : string.length - ch)
|
|
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) {
|
|
103 if (!maybeMultiline(regexp)) return searchRegexpBackward(doc, regexp, start)
|
|
104 regexp = ensureFlags(regexp, "gm")
|
|
105 var string, chunkSize = 1, endMargin = doc.getLine(start.line).length - start.ch
|
|
106 for (var line = start.line, first = doc.firstLine(); line >= first;) {
|
|
107 for (var i = 0; i < chunkSize && line >= first; i++) {
|
|
108 var curLine = doc.getLine(line--)
|
|
109 string = string == null ? curLine : curLine + "\n" + string
|
|
110 }
|
|
111 chunkSize *= 2
|
|
112
|
|
113 var match = lastMatchIn(string, regexp, endMargin)
|
|
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.afterEmptyMatch = false
|
|
206 this.doc = doc
|
|
207 pos = pos ? doc.clipPos(pos) : Pos(0, 0)
|
|
208 this.pos = {from: pos, to: pos}
|
|
209
|
|
210 var caseFold
|
|
211 if (typeof options == "object") {
|
|
212 caseFold = options.caseFold
|
|
213 } else { // Backwards compat for when caseFold was the 4th argument
|
|
214 caseFold = options
|
|
215 options = null
|
|
216 }
|
|
217
|
|
218 if (typeof query == "string") {
|
|
219 if (caseFold == null) caseFold = false
|
|
220 this.matches = function(reverse, pos) {
|
|
221 return (reverse ? searchStringBackward : searchStringForward)(doc, query, pos, caseFold)
|
|
222 }
|
|
223 } else {
|
|
224 query = ensureFlags(query, "gm")
|
|
225 if (!options || options.multiline !== false)
|
|
226 this.matches = function(reverse, pos) {
|
|
227 return (reverse ? searchRegexpBackwardMultiline : searchRegexpForwardMultiline)(doc, query, pos)
|
|
228 }
|
|
229 else
|
|
230 this.matches = function(reverse, pos) {
|
|
231 return (reverse ? searchRegexpBackward : searchRegexpForward)(doc, query, pos)
|
|
232 }
|
|
233 }
|
|
234 }
|
|
235
|
|
236 SearchCursor.prototype = {
|
|
237 findNext: function() {return this.find(false)},
|
|
238 findPrevious: function() {return this.find(true)},
|
|
239
|
|
240 find: function(reverse) {
|
|
241 var head = this.doc.clipPos(reverse ? this.pos.from : this.pos.to);
|
|
242 if (this.afterEmptyMatch && this.atOccurrence) {
|
|
243 // do not return the same 0 width match twice
|
|
244 head = Pos(head.line, head.ch)
|
|
245 if (reverse) {
|
|
246 head.ch--;
|
|
247 if (head.ch < 0) {
|
|
248 head.line--;
|
|
249 head.ch = (this.doc.getLine(head.line) || "").length;
|
|
250 }
|
|
251 } else {
|
|
252 head.ch++;
|
|
253 if (head.ch > (this.doc.getLine(head.line) || "").length) {
|
|
254 head.ch = 0;
|
|
255 head.line++;
|
|
256 }
|
|
257 }
|
|
258 if (CodeMirror.cmpPos(head, this.doc.clipPos(head)) != 0) {
|
|
259 return this.atOccurrence = false
|
|
260 }
|
|
261 }
|
|
262 var result = this.matches(reverse, head)
|
|
263 this.afterEmptyMatch = result && CodeMirror.cmpPos(result.from, result.to) == 0
|
|
264
|
|
265 if (result) {
|
|
266 this.pos = result
|
|
267 this.atOccurrence = true
|
|
268 return this.pos.match || true
|
|
269 } else {
|
|
270 var end = Pos(reverse ? this.doc.firstLine() : this.doc.lastLine() + 1, 0)
|
|
271 this.pos = {from: end, to: end}
|
|
272 return this.atOccurrence = false
|
|
273 }
|
|
274 },
|
|
275
|
|
276 from: function() {if (this.atOccurrence) return this.pos.from},
|
|
277 to: function() {if (this.atOccurrence) return this.pos.to},
|
|
278
|
|
279 replace: function(newText, origin) {
|
|
280 if (!this.atOccurrence) return
|
|
281 var lines = CodeMirror.splitLines(newText)
|
|
282 this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin)
|
|
283 this.pos.to = Pos(this.pos.from.line + lines.length - 1,
|
|
284 lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0))
|
|
285 }
|
|
286 }
|
|
287
|
|
288 CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
|
|
289 return new SearchCursor(this.doc, query, pos, caseFold)
|
|
290 })
|
|
291 CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) {
|
|
292 return new SearchCursor(this, query, pos, caseFold)
|
|
293 })
|
|
294
|
|
295 CodeMirror.defineExtension("selectMatches", function(query, caseFold) {
|
|
296 var ranges = []
|
|
297 var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold)
|
|
298 while (cur.findNext()) {
|
|
299 if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break
|
|
300 ranges.push({anchor: cur.from(), head: cur.to()})
|
|
301 }
|
|
302 if (ranges.length)
|
|
303 this.setSelections(ranges, 0)
|
|
304 })
|
|
305 });
|