|
1 // Define search commands. Depends on dialog.js or another |
|
2 // implementation of the openDialog method. |
|
3 |
|
4 // Replace works a little oddly -- it will do the replace on the next |
|
5 // Ctrl-G (or whatever is bound to findNext) press. You prevent a |
|
6 // replace by making sure the match is no longer selected when hitting |
|
7 // Ctrl-G. |
|
8 |
|
9 (function(mod) { |
|
10 if (typeof exports == "object" && typeof module == "object") // CommonJS |
|
11 mod(require("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog")); |
|
12 else if (typeof define == "function" && define.amd) // AMD |
|
13 define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod); |
|
14 else // Plain browser env |
|
15 mod(CodeMirror); |
|
16 })(function(CodeMirror) { |
|
17 "use strict"; |
|
18 function searchOverlay(query, caseInsensitive) { |
|
19 var startChar; |
|
20 if (typeof query == "string") { |
|
21 startChar = query.charAt(0); |
|
22 query = new RegExp("^" + query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), |
|
23 caseInsensitive ? "i" : ""); |
|
24 } else { |
|
25 query = new RegExp("^(?:" + query.source + ")", query.ignoreCase ? "i" : ""); |
|
26 } |
|
27 return {token: function(stream) { |
|
28 if (stream.match(query)) return "searching"; |
|
29 while (!stream.eol()) { |
|
30 stream.next(); |
|
31 if (startChar && !caseInsensitive) |
|
32 stream.skipTo(startChar) || stream.skipToEnd(); |
|
33 if (stream.match(query, false)) break; |
|
34 } |
|
35 }}; |
|
36 } |
|
37 |
|
38 function SearchState() { |
|
39 this.posFrom = this.posTo = this.query = null; |
|
40 this.overlay = null; |
|
41 } |
|
42 function getSearchState(cm) { |
|
43 return cm.state.search || (cm.state.search = new SearchState()); |
|
44 } |
|
45 function queryCaseInsensitive(query) { |
|
46 return typeof query == "string" && query == query.toLowerCase(); |
|
47 } |
|
48 function getSearchCursor(cm, query, pos) { |
|
49 // Heuristic: if the query string is all lowercase, do a case insensitive search. |
|
50 return cm.getSearchCursor(query, pos, queryCaseInsensitive(query)); |
|
51 } |
|
52 function dialog(cm, text, shortText, deflt, f) { |
|
53 if (cm.openDialog) cm.openDialog(text, f, {value: deflt}); |
|
54 else f(prompt(shortText, deflt)); |
|
55 } |
|
56 function confirmDialog(cm, text, shortText, fs) { |
|
57 if (cm.openConfirm) cm.openConfirm(text, fs); |
|
58 else if (confirm(shortText)) fs[0](); |
|
59 } |
|
60 function parseQuery(query) { |
|
61 var isRE = query.match(/^\/(.*)\/([a-z]*)$/); |
|
62 if (isRE) { |
|
63 query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); |
|
64 if (query.test("")) query = /x^/; |
|
65 } else if (query == "") { |
|
66 query = /x^/; |
|
67 } |
|
68 return query; |
|
69 } |
|
70 var queryDialog; |
|
71 function doSearch(cm, rev) { |
|
72 if (!queryDialog) { |
|
73 let doc = cm.getWrapperElement().ownerDocument; |
|
74 let inp = doc.createElement("input"); |
|
75 let txt = doc.createTextNode(cm.l10n("findCmd.promptMessage")); |
|
76 |
|
77 inp.type = "text"; |
|
78 inp.style.width = "10em"; |
|
79 inp.style.MozMarginStart = "1em"; |
|
80 |
|
81 queryDialog = doc.createElement("div"); |
|
82 queryDialog.appendChild(txt); |
|
83 queryDialog.appendChild(inp); |
|
84 } |
|
85 var state = getSearchState(cm); |
|
86 if (state.query) return findNext(cm, rev); |
|
87 dialog(cm, queryDialog, "Search for:", cm.getSelection(), function(query) { |
|
88 cm.operation(function() { |
|
89 if (!query || state.query) return; |
|
90 state.query = parseQuery(query); |
|
91 cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query)); |
|
92 state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query)); |
|
93 cm.addOverlay(state.overlay); |
|
94 state.posFrom = state.posTo = cm.getCursor(); |
|
95 findNext(cm, rev); |
|
96 }); |
|
97 }); |
|
98 } |
|
99 function findNext(cm, rev) {cm.operation(function() { |
|
100 var state = getSearchState(cm); |
|
101 var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo); |
|
102 if (!cursor.find(rev)) { |
|
103 cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0)); |
|
104 if (!cursor.find(rev)) return; |
|
105 } |
|
106 cm.setSelection(cursor.from(), cursor.to()); |
|
107 cm.scrollIntoView({from: cursor.from(), to: cursor.to()}); |
|
108 state.posFrom = cursor.from(); state.posTo = cursor.to(); |
|
109 });} |
|
110 function clearSearch(cm) {cm.operation(function() { |
|
111 var state = getSearchState(cm); |
|
112 if (!state.query) return; |
|
113 state.query = null; |
|
114 cm.removeOverlay(state.overlay); |
|
115 });} |
|
116 |
|
117 var replaceQueryDialog = |
|
118 'Replace: <input type="text" style="width: 10em"/> <span style="color: #888">(Use /re/ syntax for regexp search)</span>'; |
|
119 var replacementQueryDialog = 'With: <input type="text" style="width: 10em"/>'; |
|
120 var doReplaceConfirm = "Replace? <button>Yes</button> <button>No</button> <button>Stop</button>"; |
|
121 function replace(cm, all) { |
|
122 dialog(cm, replaceQueryDialog, "Replace:", cm.getSelection(), function(query) { |
|
123 if (!query) return; |
|
124 query = parseQuery(query); |
|
125 dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) { |
|
126 if (all) { |
|
127 cm.operation(function() { |
|
128 for (var cursor = getSearchCursor(cm, query); cursor.findNext();) { |
|
129 if (typeof query != "string") { |
|
130 var match = cm.getRange(cursor.from(), cursor.to()).match(query); |
|
131 cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];})); |
|
132 } else cursor.replace(text); |
|
133 } |
|
134 }); |
|
135 } else { |
|
136 clearSearch(cm); |
|
137 var cursor = getSearchCursor(cm, query, cm.getCursor()); |
|
138 var advance = function() { |
|
139 var start = cursor.from(), match; |
|
140 if (!(match = cursor.findNext())) { |
|
141 cursor = getSearchCursor(cm, query); |
|
142 if (!(match = cursor.findNext()) || |
|
143 (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return; |
|
144 } |
|
145 cm.setSelection(cursor.from(), cursor.to()); |
|
146 cm.scrollIntoView({from: cursor.from(), to: cursor.to()}); |
|
147 confirmDialog(cm, doReplaceConfirm, "Replace?", |
|
148 [function() {doReplace(match);}, advance]); |
|
149 }; |
|
150 var doReplace = function(match) { |
|
151 cursor.replace(typeof query == "string" ? text : |
|
152 text.replace(/\$(\d)/g, function(_, i) {return match[i];})); |
|
153 advance(); |
|
154 }; |
|
155 advance(); |
|
156 } |
|
157 }); |
|
158 }); |
|
159 } |
|
160 |
|
161 CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);}; |
|
162 CodeMirror.commands.findNext = doSearch; |
|
163 CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);}; |
|
164 CodeMirror.commands.clearSearch = clearSearch; |
|
165 CodeMirror.commands.replace = replace; |
|
166 CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);}; |
|
167 }); |