searchcursor.js 12 KB

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