Change string normalization used by CodeMirror search.
[redakcja.git] / src / redakcja / static / js / lib / codemirror-5.49.0 / searchcursor.js
1 // CodeMirror, copyright (c) by Marijn Haverbeke and others
2 // Distributed under an MIT license: https://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     /* FNP: NFD changed to NFC, because of https://github.com/codemirror/CodeMirror/issues/6672 */
125     doFold = function(str) { return str.normalize("NFC").toLowerCase() }
126     noFold = function(str) { return str.normalize("NFC") }
127   } else {
128     doFold = function(str) { return str.toLowerCase() }
129     noFold = function(str) { return str }
130   }
131
132   // Maps a position in a case-folded line back to a position in the original line
133   // (compensating for codepoints increasing in number during folding)
134   function adjustPos(orig, folded, pos, foldFunc) {
135     if (orig.length == folded.length) return pos
136     for (var min = 0, max = pos + Math.max(0, orig.length - folded.length);;) {
137       if (min == max) return min
138       var mid = (min + max) >> 1
139       var len = foldFunc(orig.slice(0, mid)).length
140       if (len == pos) return mid
141       else if (len > pos) max = mid
142       else min = mid + 1
143     }
144   }
145
146   function searchStringForward(doc, query, start, caseFold) {
147     // Empty string would match anything and never progress, so we
148     // define it to match nothing instead.
149     if (!query.length) return null
150     var fold = caseFold ? doFold : noFold
151     var lines = fold(query).split(/\r|\n\r?/)
152
153     search: for (var line = start.line, ch = start.ch, last = doc.lastLine() + 1 - lines.length; line <= last; line++, ch = 0) {
154       var orig = doc.getLine(line).slice(ch), string = fold(orig)
155       if (lines.length == 1) {
156         var found = string.indexOf(lines[0])
157         if (found == -1) continue search
158         var start = adjustPos(orig, string, found, fold) + ch
159         return {from: Pos(line, adjustPos(orig, string, found, fold) + ch),
160                 to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold) + ch)}
161       } else {
162         var cutFrom = string.length - lines[0].length
163         if (string.slice(cutFrom) != lines[0]) continue search
164         for (var i = 1; i < lines.length - 1; i++)
165           if (fold(doc.getLine(line + i)) != lines[i]) continue search
166         var end = doc.getLine(line + lines.length - 1), endString = fold(end), lastLine = lines[lines.length - 1]
167         if (endString.slice(0, lastLine.length) != lastLine) continue search
168         return {from: Pos(line, adjustPos(orig, string, cutFrom, fold) + ch),
169                 to: Pos(line + lines.length - 1, adjustPos(end, endString, lastLine.length, fold))}
170       }
171     }
172   }
173
174   function searchStringBackward(doc, query, start, caseFold) {
175     if (!query.length) return null
176     var fold = caseFold ? doFold : noFold
177     var lines = fold(query).split(/\r|\n\r?/)
178
179     search: for (var line = start.line, ch = start.ch, first = doc.firstLine() - 1 + lines.length; line >= first; line--, ch = -1) {
180       var orig = doc.getLine(line)
181       if (ch > -1) orig = orig.slice(0, ch)
182       var string = fold(orig)
183       if (lines.length == 1) {
184         var found = string.lastIndexOf(lines[0])
185         if (found == -1) continue search
186         return {from: Pos(line, adjustPos(orig, string, found, fold)),
187                 to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold))}
188       } else {
189         var lastLine = lines[lines.length - 1]
190         if (string.slice(0, lastLine.length) != lastLine) continue search
191         for (var i = 1, start = line - lines.length + 1; i < lines.length - 1; i++)
192           if (fold(doc.getLine(start + i)) != lines[i]) continue search
193         var top = doc.getLine(line + 1 - lines.length), topString = fold(top)
194         if (topString.slice(topString.length - lines[0].length) != lines[0]) continue search
195         return {from: Pos(line + 1 - lines.length, adjustPos(top, topString, top.length - lines[0].length, fold)),
196                 to: Pos(line, adjustPos(orig, string, lastLine.length, fold))}
197       }
198     }
199   }
200
201   function SearchCursor(doc, query, pos, options) {
202     this.atOccurrence = false
203     this.doc = doc
204     pos = pos ? doc.clipPos(pos) : Pos(0, 0)
205     this.pos = {from: pos, to: pos}
206
207     var caseFold
208     if (typeof options == "object") {
209       caseFold = options.caseFold
210     } else { // Backwards compat for when caseFold was the 4th argument
211       caseFold = options
212       options = null
213     }
214
215     if (typeof query == "string") {
216       if (caseFold == null) caseFold = false
217       this.matches = function(reverse, pos) {
218         return (reverse ? searchStringBackward : searchStringForward)(doc, query, pos, caseFold)
219       }
220     } else {
221       query = ensureFlags(query, "gm")
222       if (!options || options.multiline !== false)
223         this.matches = function(reverse, pos) {
224           return (reverse ? searchRegexpBackwardMultiline : searchRegexpForwardMultiline)(doc, query, pos)
225         }
226       else
227         this.matches = function(reverse, pos) {
228           return (reverse ? searchRegexpBackward : searchRegexpForward)(doc, query, pos)
229         }
230     }
231   }
232
233   SearchCursor.prototype = {
234     findNext: function() {return this.find(false)},
235     findPrevious: function() {return this.find(true)},
236
237     find: function(reverse) {
238       var result = this.matches(reverse, this.doc.clipPos(reverse ? this.pos.from : this.pos.to))
239
240       // Implements weird auto-growing behavior on null-matches for
241       // backwards-compatiblity with the vim code (unfortunately)
242       while (result && CodeMirror.cmpPos(result.from, result.to) == 0) {
243         if (reverse) {
244           if (result.from.ch) result.from = Pos(result.from.line, result.from.ch - 1)
245           else if (result.from.line == this.doc.firstLine()) result = null
246           else result = this.matches(reverse, this.doc.clipPos(Pos(result.from.line - 1)))
247         } else {
248           if (result.to.ch < this.doc.getLine(result.to.line).length) result.to = Pos(result.to.line, result.to.ch + 1)
249           else if (result.to.line == this.doc.lastLine()) result = null
250           else result = this.matches(reverse, Pos(result.to.line + 1, 0))
251         }
252       }
253
254       if (result) {
255         this.pos = result
256         this.atOccurrence = true
257         return this.pos.match || true
258       } else {
259         var end = Pos(reverse ? this.doc.firstLine() : this.doc.lastLine() + 1, 0)
260         this.pos = {from: end, to: end}
261         return this.atOccurrence = false
262       }
263     },
264
265     from: function() {if (this.atOccurrence) return this.pos.from},
266     to: function() {if (this.atOccurrence) return this.pos.to},
267
268     replace: function(newText, origin) {
269       if (!this.atOccurrence) return
270       var lines = CodeMirror.splitLines(newText)
271       this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin)
272       this.pos.to = Pos(this.pos.from.line + lines.length - 1,
273                         lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0))
274     }
275   }
276
277   CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
278     return new SearchCursor(this.doc, query, pos, caseFold)
279   })
280   CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) {
281     return new SearchCursor(this, query, pos, caseFold)
282   })
283
284   CodeMirror.defineExtension("selectMatches", function(query, caseFold) {
285     var ranges = []
286     var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold)
287     while (cur.findNext()) {
288       if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break
289       ranges.push({anchor: cur.from(), head: cur.to()})
290     }
291     if (ranges.length)
292       this.setSelections(ranges, 0)
293   })
294 });