UNPKG

10.7 kBJavaScriptView Raw
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("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog"));
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(/\\(.)/g, function(_, ch) {
82 if (ch == "n") return "\n"
83 if (ch == "r") return "\r"
84 return ch
85 })
86 }
87
88 function parseQuery(query) {
89 var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
90 if (isRE) {
91 try { query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); }
92 catch(e) {} // Not a regular expression after all, do a string search
93 } else {
94 query = parseString(query)
95 }
96 if (typeof query == "string" ? query == "" : query.test(""))
97 query = /x^/;
98 return query;
99 }
100
101 function startSearch(cm, state, query) {
102 state.queryText = query;
103 state.query = parseQuery(query);
104 cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
105 state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
106 cm.addOverlay(state.overlay);
107 if (cm.showMatchesOnScrollbar) {
108 if (state.annotate) { state.annotate.clear(); state.annotate = null; }
109 state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query));
110 }
111 }
112
113 function doSearch(cm, rev, persistent, immediate) {
114 var state = getSearchState(cm);
115 if (state.query) return findNext(cm, rev);
116 var q = cm.getSelection() || state.lastQuery;
117 if (q instanceof RegExp && q.source == "x^") q = null
118 if (persistent && cm.openDialog) {
119 var hiding = null
120 var searchNext = function(query, event) {
121 CodeMirror.e_stop(event);
122 if (!query) return;
123 if (query != state.queryText) {
124 startSearch(cm, state, query);
125 state.posFrom = state.posTo = cm.getCursor();
126 }
127 if (hiding) hiding.style.opacity = 1
128 findNext(cm, event.shiftKey, function(_, to) {
129 var dialog
130 if (to.line < 3 && document.querySelector &&
131 (dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) &&
132 dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top)
133 (hiding = dialog).style.opacity = .4
134 })
135 };
136 persistentDialog(cm, getQueryDialog(cm), q, searchNext, function(event, query) {
137 var keyName = CodeMirror.keyName(event)
138 var extra = cm.getOption('extraKeys'), cmd = (extra && extra[keyName]) || CodeMirror.keyMap[cm.getOption("keyMap")][keyName]
139 if (cmd == "findNext" || cmd == "findPrev" ||
140 cmd == "findPersistentNext" || cmd == "findPersistentPrev") {
141 CodeMirror.e_stop(event);
142 startSearch(cm, getSearchState(cm), query);
143 cm.execCommand(cmd);
144 } else if (cmd == "find" || cmd == "findPersistent") {
145 CodeMirror.e_stop(event);
146 searchNext(query, event);
147 }
148 });
149 if (immediate && q) {
150 startSearch(cm, state, q);
151 findNext(cm, rev);
152 }
153 } else {
154 dialog(cm, getQueryDialog(cm), "Search for:", q, function(query) {
155 if (query && !state.query) cm.operation(function() {
156 startSearch(cm, state, query);
157 state.posFrom = state.posTo = cm.getCursor();
158 findNext(cm, rev);
159 });
160 });
161 }
162 }
163
164 function findNext(cm, rev, callback) {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(); state.posTo = cursor.to();
174 if (callback) callback(cursor.from(), cursor.to())
175 });}
176
177 function clearSearch(cm) {cm.operation(function() {
178 var state = getSearchState(cm);
179 state.lastQuery = state.query;
180 if (!state.query) return;
181 state.query = state.queryText = null;
182 cm.removeOverlay(state.overlay);
183 if (state.annotate) { state.annotate.clear(); state.annotate = null; }
184 });}
185
186
187 function getQueryDialog(cm) {
188 return '<span class="CodeMirror-search-label">' + cm.phrase("Search:") + '</span> <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">' + cm.phrase("(Use /re/ syntax for regexp search)") + '</span>';
189 }
190 function getReplaceQueryDialog(cm) {
191 return ' <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">' + cm.phrase("(Use /re/ syntax for regexp search)") + '</span>';
192 }
193 function getReplacementQueryDialog(cm) {
194 return '<span class="CodeMirror-search-label">' + cm.phrase("With:") + '</span> <input type="text" style="width: 10em" class="CodeMirror-search-field"/>';
195 }
196 function getDoReplaceConfirm(cm) {
197 return '<span class="CodeMirror-search-label">' + cm.phrase("Replace?") + '</span> <button>' + cm.phrase("Yes") + '</button> <button>' + cm.phrase("No") + '</button> <button>' + cm.phrase("All") + '</button> <button>' + cm.phrase("Stop") + '</button> ';
198 }
199
200 function replaceAll(cm, query, text) {
201 cm.operation(function() {
202 for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {
203 if (typeof query != "string") {
204 var match = cm.getRange(cursor.from(), cursor.to()).match(query);
205 cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
206 } else cursor.replace(text);
207 }
208 });
209 }
210
211 function replace(cm, all) {
212 if (cm.getOption("readOnly")) return;
213 var query = cm.getSelection() || getSearchState(cm).lastQuery;
214 var dialogText = '<span class="CodeMirror-search-label">' + (all ? cm.phrase("Replace all:") : cm.phrase("Replace:")) + '</span>';
215 dialog(cm, dialogText + getReplaceQueryDialog(cm), dialogText, query, function(query) {
216 if (!query) return;
217 query = parseQuery(query);
218 dialog(cm, getReplacementQueryDialog(cm), cm.phrase("Replace with:"), "", function(text) {
219 text = parseString(text)
220 if (all) {
221 replaceAll(cm, query, text)
222 } else {
223 clearSearch(cm);
224 var cursor = getSearchCursor(cm, query, cm.getCursor("from"));
225 var advance = function() {
226 var start = cursor.from(), match;
227 if (!(match = cursor.findNext())) {
228 cursor = getSearchCursor(cm, query);
229 if (!(match = cursor.findNext()) ||
230 (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;
231 }
232 cm.setSelection(cursor.from(), cursor.to());
233 cm.scrollIntoView({from: cursor.from(), to: cursor.to()});
234 confirmDialog(cm, getDoReplaceConfirm(cm), cm.phrase("Replace?"),
235 [function() {doReplace(match);}, advance,
236 function() {replaceAll(cm, query, text)}]);
237 };
238 var doReplace = function(match) {
239 cursor.replace(typeof query == "string" ? text :
240 text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
241 advance();
242 };
243 advance();
244 }
245 });
246 });
247 }
248
249 CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);};
250 CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);};
251 CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);};
252 CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);};
253 CodeMirror.commands.findNext = doSearch;
254 CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};
255 CodeMirror.commands.clearSearch = clearSearch;
256 CodeMirror.commands.replace = replace;
257 CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);};
258});