search.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. // CodeMirror, copyright (c) by Marijn Haverbeke and others
  2. // Distributed under an MIT license: https://codemirror.net/LICENSE
  3. // Define search commands. Depends on dialog.js or another
  4. // implementation of the openDialog method.
  5. // Replace works a little oddly -- it will do the replace on the next
  6. // Ctrl-G (or whatever is bound to findNext) press. You prevent a
  7. // replace by making sure the match is no longer selected when hitting
  8. // Ctrl-G.
  9. ;(function (mod) {
  10. if (typeof exports == 'object' && typeof module == 'object')
  11. // CommonJS
  12. mod(require('../../lib/codemirror'), require('./searchcursor'), require('../dialog/dialog'))
  13. else if (typeof define == 'function' && define.amd)
  14. // AMD
  15. define(['../../lib/codemirror', './searchcursor', '../dialog/dialog'], mod)
  16. // Plain browser env
  17. else mod(CodeMirror)
  18. })(function (CodeMirror) {
  19. 'use strict'
  20. // default search panel location
  21. CodeMirror.defineOption('search', { bottom: false })
  22. function searchOverlay(query, caseInsensitive) {
  23. if (typeof query == 'string') query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), caseInsensitive ? 'gi' : 'g')
  24. else if (!query.global) query = new RegExp(query.source, query.ignoreCase ? 'gi' : 'g')
  25. return {
  26. token: function (stream) {
  27. query.lastIndex = stream.pos
  28. var match = query.exec(stream.string)
  29. if (match && match.index == stream.pos) {
  30. stream.pos += match[0].length || 1
  31. return 'searching'
  32. } else if (match) {
  33. stream.pos = match.index
  34. } else {
  35. stream.skipToEnd()
  36. }
  37. },
  38. }
  39. }
  40. function SearchState() {
  41. this.posFrom = this.posTo = this.lastQuery = this.query = null
  42. this.overlay = null
  43. }
  44. function getSearchState(cm) {
  45. return cm.state.search || (cm.state.search = new SearchState())
  46. }
  47. function queryCaseInsensitive(query) {
  48. return typeof query == 'string' && query == query.toLowerCase()
  49. }
  50. function getSearchCursor(cm, query, pos) {
  51. // Heuristic: if the query string is all lowercase, do a case insensitive search.
  52. return cm.getSearchCursor(query, pos, { caseFold: queryCaseInsensitive(query), multiline: true })
  53. }
  54. function persistentDialog(cm, text, deflt, onEnter, onKeyDown) {
  55. cm.openDialog(text, onEnter, {
  56. value: deflt,
  57. selectValueOnOpen: true,
  58. closeOnEnter: false,
  59. onClose: function () {
  60. clearSearch(cm)
  61. },
  62. onKeyDown: onKeyDown,
  63. bottom: cm.options.search.bottom,
  64. })
  65. }
  66. function dialog(cm, text, shortText, deflt, f) {
  67. if (cm.openDialog) cm.openDialog(text, f, { value: deflt, selectValueOnOpen: true, bottom: cm.options.search.bottom })
  68. else f(prompt(shortText, deflt))
  69. }
  70. function confirmDialog(cm, text, shortText, fs) {
  71. if (cm.openConfirm) cm.openConfirm(text, fs)
  72. else if (confirm(shortText)) fs[0]()
  73. }
  74. function parseString(string) {
  75. return string.replace(/\\([nrt\\])/g, function (match, ch) {
  76. if (ch == 'n') return '\n'
  77. if (ch == 'r') return '\r'
  78. if (ch == 't') return '\t'
  79. if (ch == '\\') return '\\'
  80. return match
  81. })
  82. }
  83. function parseQuery(query) {
  84. var isRE = query.match(/^\/(.*)\/([a-z]*)$/)
  85. if (isRE) {
  86. try {
  87. query = new RegExp(isRE[1], isRE[2].indexOf('i') == -1 ? '' : 'i')
  88. } catch (e) {} // Not a regular expression after all, do a string search
  89. } else {
  90. query = parseString(query)
  91. }
  92. if (typeof query == 'string' ? query == '' : query.test('')) query = /x^/
  93. return query
  94. }
  95. function startSearch(cm, state, query) {
  96. state.queryText = query
  97. state.query = parseQuery(query)
  98. cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query))
  99. state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query))
  100. cm.addOverlay(state.overlay)
  101. if (cm.showMatchesOnScrollbar) {
  102. if (state.annotate) {
  103. state.annotate.clear()
  104. state.annotate = null
  105. }
  106. state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query))
  107. }
  108. }
  109. function doSearch(cm, rev, persistent, immediate) {
  110. var state = getSearchState(cm)
  111. if (state.query) return findNext(cm, rev)
  112. var q = cm.getSelection() || state.lastQuery
  113. if (q instanceof RegExp && q.source == 'x^') q = null
  114. if (persistent && cm.openDialog) {
  115. var hiding = null
  116. var searchNext = function (query, event) {
  117. CodeMirror.e_stop(event)
  118. if (!query) return
  119. if (query != state.queryText) {
  120. startSearch(cm, state, query)
  121. state.posFrom = state.posTo = cm.getCursor()
  122. }
  123. if (hiding) hiding.style.opacity = 1
  124. findNext(cm, event.shiftKey, function (_, to) {
  125. var dialog
  126. if (
  127. to.line < 3 &&
  128. document.querySelector &&
  129. (dialog = cm.display.wrapper.querySelector('.CodeMirror-dialog')) &&
  130. dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, 'window').top
  131. )
  132. (hiding = dialog).style.opacity = 0.4
  133. })
  134. }
  135. persistentDialog(cm, getQueryDialog(cm), q, searchNext, function (event, query) {
  136. var keyName = CodeMirror.keyName(event)
  137. var extra = cm.getOption('extraKeys'),
  138. cmd = (extra && extra[keyName]) || CodeMirror.keyMap[cm.getOption('keyMap')][keyName]
  139. if (cmd == 'findNext' || cmd == 'findPrev' || cmd == 'findPersistentNext' || cmd == 'findPersistentPrev') {
  140. CodeMirror.e_stop(event)
  141. startSearch(cm, getSearchState(cm), query)
  142. cm.execCommand(cmd)
  143. } else if (cmd == 'find' || cmd == 'findPersistent') {
  144. CodeMirror.e_stop(event)
  145. searchNext(query, event)
  146. }
  147. })
  148. if (immediate && q) {
  149. startSearch(cm, state, q)
  150. findNext(cm, rev)
  151. }
  152. } else {
  153. dialog(cm, getQueryDialog(cm), 'Search for:', q, function (query) {
  154. if (query && !state.query)
  155. cm.operation(function () {
  156. startSearch(cm, state, query)
  157. state.posFrom = state.posTo = cm.getCursor()
  158. findNext(cm, rev)
  159. })
  160. })
  161. }
  162. }
  163. function findNext(cm, rev, callback) {
  164. cm.operation(function () {
  165. var state = getSearchState(cm)
  166. var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo)
  167. if (!cursor.find(rev)) {
  168. cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0))
  169. if (!cursor.find(rev)) return
  170. }
  171. cm.setSelection(cursor.from(), cursor.to())
  172. cm.scrollIntoView({ from: cursor.from(), to: cursor.to() }, 20)
  173. state.posFrom = cursor.from()
  174. state.posTo = cursor.to()
  175. if (callback) callback(cursor.from(), cursor.to())
  176. })
  177. }
  178. function clearSearch(cm) {
  179. cm.operation(function () {
  180. var state = getSearchState(cm)
  181. state.lastQuery = state.query
  182. if (!state.query) return
  183. state.query = state.queryText = null
  184. cm.removeOverlay(state.overlay)
  185. if (state.annotate) {
  186. state.annotate.clear()
  187. state.annotate = null
  188. }
  189. })
  190. }
  191. function el(tag, attrs) {
  192. var element = tag ? document.createElement(tag) : document.createDocumentFragment()
  193. for (var key in attrs) {
  194. element[key] = attrs[key]
  195. }
  196. for (var i = 2; i < arguments.length; i++) {
  197. var child = arguments[i]
  198. element.appendChild(typeof child == 'string' ? document.createTextNode(child) : child)
  199. }
  200. return element
  201. }
  202. function getQueryDialog(cm) {
  203. return el(
  204. '',
  205. null,
  206. el('span', { className: 'CodeMirror-search-label' }, cm.phrase('Search:')),
  207. ' ',
  208. el('input', { type: 'text', style: 'width: 10em', className: 'CodeMirror-search-field' }),
  209. ' ',
  210. el('span', { style: 'color: #888', className: 'CodeMirror-search-hint' }, cm.phrase('(Use /re/ syntax for regexp search)'))
  211. )
  212. }
  213. function getReplaceQueryDialog(cm) {
  214. return el(
  215. '',
  216. null,
  217. ' ',
  218. el('input', { type: 'text', style: 'width: 10em', className: 'CodeMirror-search-field' }),
  219. ' ',
  220. el('span', { style: 'color: #888', className: 'CodeMirror-search-hint' }, cm.phrase('(Use /re/ syntax for regexp search)'))
  221. )
  222. }
  223. function getReplacementQueryDialog(cm) {
  224. return el('', null, el('span', { className: 'CodeMirror-search-label' }, cm.phrase('With:')), ' ', el('input', { type: 'text', style: 'width: 10em', className: 'CodeMirror-search-field' }))
  225. }
  226. function getDoReplaceConfirm(cm) {
  227. return el(
  228. '',
  229. null,
  230. el('span', { className: 'CodeMirror-search-label' }, cm.phrase('Replace?')),
  231. ' ',
  232. el('button', {}, cm.phrase('Yes')),
  233. ' ',
  234. el('button', {}, cm.phrase('No')),
  235. ' ',
  236. el('button', {}, cm.phrase('All')),
  237. ' ',
  238. el('button', {}, cm.phrase('Stop'))
  239. )
  240. }
  241. function replaceAll(cm, query, text) {
  242. cm.operation(function () {
  243. for (var cursor = getSearchCursor(cm, query); cursor.findNext(); ) {
  244. if (typeof query != 'string') {
  245. var match = cm.getRange(cursor.from(), cursor.to()).match(query)
  246. cursor.replace(
  247. text.replace(/\$(\d)/g, function (_, i) {
  248. return match[i]
  249. })
  250. )
  251. } else cursor.replace(text)
  252. }
  253. })
  254. }
  255. function replace(cm, all) {
  256. if (cm.getOption('readOnly')) return
  257. var query = cm.getSelection() || getSearchState(cm).lastQuery
  258. var dialogText = all ? cm.phrase('Replace all:') : cm.phrase('Replace:')
  259. var fragment = el('', null, el('span', { className: 'CodeMirror-search-label' }, dialogText), getReplaceQueryDialog(cm))
  260. dialog(cm, fragment, dialogText, query, function (query) {
  261. if (!query) return
  262. query = parseQuery(query)
  263. dialog(cm, getReplacementQueryDialog(cm), cm.phrase('Replace with:'), '', function (text) {
  264. text = parseString(text)
  265. if (all) {
  266. replaceAll(cm, query, text)
  267. } else {
  268. clearSearch(cm)
  269. var cursor = getSearchCursor(cm, query, cm.getCursor('from'))
  270. var advance = function () {
  271. var start = cursor.from(),
  272. match
  273. if (!(match = cursor.findNext())) {
  274. cursor = getSearchCursor(cm, query)
  275. if (!(match = cursor.findNext()) || (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return
  276. }
  277. cm.setSelection(cursor.from(), cursor.to())
  278. cm.scrollIntoView({ from: cursor.from(), to: cursor.to() })
  279. confirmDialog(cm, getDoReplaceConfirm(cm), cm.phrase('Replace?'), [
  280. function () {
  281. doReplace(match)
  282. },
  283. advance,
  284. function () {
  285. replaceAll(cm, query, text)
  286. },
  287. ])
  288. }
  289. var doReplace = function (match) {
  290. cursor.replace(
  291. typeof query == 'string'
  292. ? text
  293. : text.replace(/\$(\d)/g, function (_, i) {
  294. return match[i]
  295. })
  296. )
  297. advance()
  298. }
  299. advance()
  300. }
  301. })
  302. })
  303. }
  304. CodeMirror.commands.find = function (cm) {
  305. clearSearch(cm)
  306. doSearch(cm)
  307. }
  308. CodeMirror.commands.findPersistent = function (cm) {
  309. clearSearch(cm)
  310. doSearch(cm, false, true)
  311. }
  312. CodeMirror.commands.findPersistentNext = function (cm) {
  313. doSearch(cm, false, true, true)
  314. }
  315. CodeMirror.commands.findPersistentPrev = function (cm) {
  316. doSearch(cm, true, true, true)
  317. }
  318. CodeMirror.commands.findNext = doSearch
  319. CodeMirror.commands.findPrev = function (cm) {
  320. doSearch(cm, true)
  321. }
  322. CodeMirror.commands.clearSearch = clearSearch
  323. CodeMirror.commands.replace = replace
  324. CodeMirror.commands.replaceAll = function (cm) {
  325. replace(cm, true)
  326. }
  327. })