Commit | Line | Data |
---|---|---|
77b7b761 TD |
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 | ||
837afb80 TD |
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 | } | |
77b7b761 TD |
27 | return {token: function(stream) { |
28 | if (stream.match(query)) return "searching"; | |
29 | while (!stream.eol()) { | |
30 | stream.next(); | |
837afb80 TD |
31 | if (startChar && !caseInsensitive) |
32 | stream.skipTo(startChar) || stream.skipToEnd(); | |
77b7b761 TD |
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 | } | |
837afb80 TD |
45 | function queryCaseInsensitive(query) { |
46 | return typeof query == "string" && query == query.toLowerCase(); | |
47 | } | |
77b7b761 TD |
48 | function getSearchCursor(cm, query, pos) { |
49 | // Heuristic: if the query string is all lowercase, do a case insensitive search. | |
837afb80 | 50 | return cm.getSearchCursor(query, pos, queryCaseInsensitive(query)); |
77b7b761 | 51 | } |
837afb80 TD |
52 | function dialog(cm, text, shortText, deflt, f) { |
53 | if (cm.openDialog) cm.openDialog(text, f, {value: deflt}); | |
54 | else f(prompt(shortText, deflt)); | |
77b7b761 TD |
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]*)$/); | |
837afb80 TD |
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; | |
77b7b761 TD |
69 | } |
70 | var queryDialog = | |
71 | 'Search: <input type="text" style="width: 10em"/> <span style="color: #888">(Use /re/ syntax for regexp search)</span>'; | |
72 | function doSearch(cm, rev) { | |
73 | var state = getSearchState(cm); | |
74 | if (state.query) return findNext(cm, rev); | |
837afb80 | 75 | dialog(cm, queryDialog, "Search for:", cm.getSelection(), function(query) { |
77b7b761 TD |
76 | cm.operation(function() { |
77 | if (!query || state.query) return; | |
78 | state.query = parseQuery(query); | |
837afb80 TD |
79 | cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query)); |
80 | state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query)); | |
77b7b761 TD |
81 | cm.addOverlay(state.overlay); |
82 | state.posFrom = state.posTo = cm.getCursor(); | |
83 | findNext(cm, rev); | |
84 | }); | |
85 | }); | |
86 | } | |
87 | function findNext(cm, rev) {cm.operation(function() { | |
88 | var state = getSearchState(cm); | |
89 | var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo); | |
90 | if (!cursor.find(rev)) { | |
91 | cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0)); | |
92 | if (!cursor.find(rev)) return; | |
93 | } | |
94 | cm.setSelection(cursor.from(), cursor.to()); | |
837afb80 | 95 | cm.scrollIntoView({from: cursor.from(), to: cursor.to()}); |
77b7b761 TD |
96 | state.posFrom = cursor.from(); state.posTo = cursor.to(); |
97 | });} | |
98 | function clearSearch(cm) {cm.operation(function() { | |
99 | var state = getSearchState(cm); | |
100 | if (!state.query) return; | |
101 | state.query = null; | |
102 | cm.removeOverlay(state.overlay); | |
103 | });} | |
104 | ||
105 | var replaceQueryDialog = | |
106 | 'Replace: <input type="text" style="width: 10em"/> <span style="color: #888">(Use /re/ syntax for regexp search)</span>'; | |
107 | var replacementQueryDialog = 'With: <input type="text" style="width: 10em"/>'; | |
108 | var doReplaceConfirm = "Replace? <button>Yes</button> <button>No</button> <button>Stop</button>"; | |
109 | function replace(cm, all) { | |
837afb80 | 110 | dialog(cm, replaceQueryDialog, "Replace:", cm.getSelection(), function(query) { |
77b7b761 TD |
111 | if (!query) return; |
112 | query = parseQuery(query); | |
837afb80 | 113 | dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) { |
77b7b761 TD |
114 | if (all) { |
115 | cm.operation(function() { | |
116 | for (var cursor = getSearchCursor(cm, query); cursor.findNext();) { | |
117 | if (typeof query != "string") { | |
118 | var match = cm.getRange(cursor.from(), cursor.to()).match(query); | |
837afb80 | 119 | cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];})); |
77b7b761 TD |
120 | } else cursor.replace(text); |
121 | } | |
122 | }); | |
123 | } else { | |
124 | clearSearch(cm); | |
125 | var cursor = getSearchCursor(cm, query, cm.getCursor()); | |
126 | var advance = function() { | |
127 | var start = cursor.from(), match; | |
128 | if (!(match = cursor.findNext())) { | |
129 | cursor = getSearchCursor(cm, query); | |
130 | if (!(match = cursor.findNext()) || | |
131 | (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return; | |
132 | } | |
133 | cm.setSelection(cursor.from(), cursor.to()); | |
837afb80 | 134 | cm.scrollIntoView({from: cursor.from(), to: cursor.to()}); |
77b7b761 TD |
135 | confirmDialog(cm, doReplaceConfirm, "Replace?", |
136 | [function() {doReplace(match);}, advance]); | |
137 | }; | |
138 | var doReplace = function(match) { | |
139 | cursor.replace(typeof query == "string" ? text : | |
837afb80 | 140 | text.replace(/\$(\d)/g, function(_, i) {return match[i];})); |
77b7b761 TD |
141 | advance(); |
142 | }; | |
143 | advance(); | |
144 | } | |
145 | }); | |
146 | }); | |
147 | } | |
148 | ||
149 | CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);}; | |
150 | CodeMirror.commands.findNext = doSearch; | |
151 | CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);}; | |
152 | CodeMirror.commands.clearSearch = clearSearch; | |
153 | CodeMirror.commands.replace = replace; | |
154 | CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);}; | |
837afb80 | 155 | }); |