searchcursor.js 13 KB

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