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