incremental_search.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. /* ***** BEGIN LICENSE BLOCK *****
  2. * Distributed under the BSD license:
  3. *
  4. * Copyright (c) 2010, Ajax.org B.V.
  5. * All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions are met:
  9. * * Redistributions of source code must retain the above copyright
  10. * notice, this list of conditions and the following disclaimer.
  11. * * Redistributions in binary form must reproduce the above copyright
  12. * notice, this list of conditions and the following disclaimer in the
  13. * documentation and/or other materials provided with the distribution.
  14. * * Neither the name of Ajax.org B.V. nor the
  15. * names of its contributors may be used to endorse or promote products
  16. * derived from this software without specific prior written permission.
  17. *
  18. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  19. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  24. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  27. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *
  29. * ***** END LICENSE BLOCK ***** */
  30. define(function(require, exports, module) {
  31. "use strict";
  32. var oop = require("./lib/oop");
  33. var Range = require("./range").Range;
  34. var Search = require("./search").Search;
  35. var SearchHighlight = require("./search_highlight").SearchHighlight;
  36. var iSearchCommandModule = require("./commands/incremental_search_commands");
  37. var ISearchKbd = iSearchCommandModule.IncrementalSearchKeyboardHandler;
  38. /**
  39. * @class IncrementalSearch
  40. *
  41. * Implements immediate searching while the user is typing. When incremental
  42. * search is activated, keystrokes into the editor will be used for composing
  43. * a search term. Immediately after every keystroke the search is updated:
  44. * - so-far-matching characters are highlighted
  45. * - the cursor is moved to the next match
  46. *
  47. **/
  48. /**
  49. *
  50. *
  51. * Creates a new `IncrementalSearch` object.
  52. *
  53. * @constructor
  54. **/
  55. function IncrementalSearch() {
  56. this.$options = {wrap: false, skipCurrent: false};
  57. this.$keyboardHandler = new ISearchKbd(this);
  58. }
  59. oop.inherits(IncrementalSearch, Search);
  60. // regexp handling
  61. function isRegExp(obj) {
  62. return obj instanceof RegExp;
  63. }
  64. function regExpToObject(re) {
  65. var string = String(re),
  66. start = string.indexOf('/'),
  67. flagStart = string.lastIndexOf('/');
  68. return {
  69. expression: string.slice(start+1, flagStart),
  70. flags: string.slice(flagStart+1)
  71. }
  72. }
  73. function stringToRegExp(string, flags) {
  74. try {
  75. return new RegExp(string, flags);
  76. } catch (e) { return string; }
  77. }
  78. function objectToRegExp(obj) {
  79. return stringToRegExp(obj.expression, obj.flags);
  80. }
  81. // iSearch class
  82. ;(function() {
  83. this.activate = function(ed, backwards) {
  84. this.$editor = ed;
  85. this.$startPos = this.$currentPos = ed.getCursorPosition();
  86. this.$options.needle = '';
  87. this.$options.backwards = backwards;
  88. ed.keyBinding.addKeyboardHandler(this.$keyboardHandler);
  89. // we need to completely intercept paste, just registering an event handler does not work
  90. this.$originalEditorOnPaste = ed.onPaste; ed.onPaste = this.onPaste.bind(this);
  91. this.$mousedownHandler = ed.addEventListener('mousedown', this.onMouseDown.bind(this));
  92. this.selectionFix(ed);
  93. this.statusMessage(true);
  94. };
  95. this.deactivate = function(reset) {
  96. this.cancelSearch(reset);
  97. var ed = this.$editor;
  98. ed.keyBinding.removeKeyboardHandler(this.$keyboardHandler);
  99. if (this.$mousedownHandler) {
  100. ed.removeEventListener('mousedown', this.$mousedownHandler);
  101. delete this.$mousedownHandler;
  102. }
  103. ed.onPaste = this.$originalEditorOnPaste;
  104. this.message('');
  105. };
  106. this.selectionFix = function(editor) {
  107. // Fix selection bug: When clicked inside the editor
  108. // editor.selection.$isEmpty is false even if the mouse click did not
  109. // open a selection. This is interpreted by the move commands to
  110. // extend the selection. To only extend the selection when there is
  111. // one, we clear it here
  112. if (editor.selection.isEmpty() && !editor.session.$emacsMark) {
  113. editor.clearSelection();
  114. }
  115. };
  116. this.highlight = function(regexp) {
  117. var sess = this.$editor.session,
  118. hl = sess.$isearchHighlight = sess.$isearchHighlight || sess.addDynamicMarker(
  119. new SearchHighlight(null, "ace_isearch-result", "text"));
  120. hl.setRegexp(regexp);
  121. sess._emit("changeBackMarker"); // force highlight layer redraw
  122. };
  123. this.cancelSearch = function(reset) {
  124. var e = this.$editor;
  125. this.$prevNeedle = this.$options.needle;
  126. this.$options.needle = '';
  127. if (reset) {
  128. e.moveCursorToPosition(this.$startPos);
  129. this.$currentPos = this.$startPos;
  130. } else {
  131. e.pushEmacsMark && e.pushEmacsMark(this.$startPos, false);
  132. }
  133. this.highlight(null);
  134. return Range.fromPoints(this.$currentPos, this.$currentPos);
  135. };
  136. this.highlightAndFindWithNeedle = function(moveToNext, needleUpdateFunc) {
  137. if (!this.$editor) return null;
  138. var options = this.$options;
  139. // get search term
  140. if (needleUpdateFunc) {
  141. options.needle = needleUpdateFunc.call(this, options.needle || '') || '';
  142. }
  143. if (options.needle.length === 0) {
  144. this.statusMessage(true);
  145. return this.cancelSearch(true);
  146. }
  147. // try to find the next occurence and enable highlighting marker
  148. options.start = this.$currentPos;
  149. var session = this.$editor.session,
  150. found = this.find(session),
  151. shouldSelect = this.$editor.emacsMark ?
  152. !!this.$editor.emacsMark() : !this.$editor.selection.isEmpty();
  153. if (found) {
  154. if (options.backwards) found = Range.fromPoints(found.end, found.start);
  155. this.$editor.selection.setRange(Range.fromPoints(shouldSelect ? this.$startPos : found.end, found.end));
  156. if (moveToNext) this.$currentPos = found.end;
  157. // highlight after cursor move, so selection works properly
  158. this.highlight(options.re);
  159. }
  160. this.statusMessage(found);
  161. return found;
  162. };
  163. this.addString = function(s) {
  164. return this.highlightAndFindWithNeedle(false, function(needle) {
  165. if (!isRegExp(needle))
  166. return needle + s;
  167. var reObj = regExpToObject(needle);
  168. reObj.expression += s;
  169. return objectToRegExp(reObj);
  170. });
  171. };
  172. this.removeChar = function(c) {
  173. return this.highlightAndFindWithNeedle(false, function(needle) {
  174. if (!isRegExp(needle))
  175. return needle.substring(0, needle.length-1);
  176. var reObj = regExpToObject(needle);
  177. reObj.expression = reObj.expression.substring(0, reObj.expression.length-1);
  178. return objectToRegExp(reObj);
  179. });
  180. };
  181. this.next = function(options) {
  182. // try to find the next occurence of whatever we have searched for
  183. // earlier.
  184. // options = {[backwards: BOOL], [useCurrentOrPrevSearch: BOOL]}
  185. options = options || {};
  186. this.$options.backwards = !!options.backwards;
  187. this.$currentPos = this.$editor.getCursorPosition();
  188. return this.highlightAndFindWithNeedle(true, function(needle) {
  189. return options.useCurrentOrPrevSearch && needle.length === 0 ?
  190. this.$prevNeedle || '' : needle;
  191. });
  192. };
  193. this.onMouseDown = function(evt) {
  194. // when mouse interaction happens then we quit incremental search
  195. this.deactivate();
  196. return true;
  197. };
  198. this.onPaste = function(text) {
  199. this.addString(text);
  200. };
  201. this.convertNeedleToRegExp = function() {
  202. return this.highlightAndFindWithNeedle(false, function(needle) {
  203. return isRegExp(needle) ? needle : stringToRegExp(needle, 'ig');
  204. });
  205. };
  206. this.convertNeedleToString = function() {
  207. return this.highlightAndFindWithNeedle(false, function(needle) {
  208. return isRegExp(needle) ? regExpToObject(needle).expression : needle;
  209. });
  210. };
  211. this.statusMessage = function(found) {
  212. var options = this.$options, msg = '';
  213. msg += options.backwards ? 'reverse-' : '';
  214. msg += 'isearch: ' + options.needle;
  215. msg += found ? '' : ' (not found)';
  216. this.message(msg);
  217. };
  218. this.message = function(msg) {
  219. if (this.$editor.showCommandLine) {
  220. this.$editor.showCommandLine(msg);
  221. this.$editor.focus();
  222. } else {
  223. console.log(msg);
  224. }
  225. };
  226. }).call(IncrementalSearch.prototype);
  227. exports.IncrementalSearch = IncrementalSearch;
  228. /**
  229. *
  230. * Config settings for enabling/disabling [[IncrementalSearch `IncrementalSearch`]].
  231. *
  232. **/
  233. var dom = require('./lib/dom');
  234. dom.importCssString && dom.importCssString("\
  235. .ace_marker-layer .ace_isearch-result {\
  236. position: absolute;\
  237. z-index: 6;\
  238. -moz-box-sizing: border-box;\
  239. -webkit-box-sizing: border-box;\
  240. box-sizing: border-box;\
  241. }\
  242. div.ace_isearch-result {\
  243. border-radius: 4px;\
  244. background-color: rgba(255, 200, 0, 0.5);\
  245. box-shadow: 0 0 4px rgb(255, 200, 0);\
  246. }\
  247. .ace_dark div.ace_isearch-result {\
  248. background-color: rgb(100, 110, 160);\
  249. box-shadow: 0 0 4px rgb(80, 90, 140);\
  250. }", "incremental-search-highlighting");
  251. // support for default keyboard handler
  252. var commands = require("./commands/command_manager");
  253. (function() {
  254. this.setupIncrementalSearch = function(editor, val) {
  255. if (this.usesIncrementalSearch == val) return;
  256. this.usesIncrementalSearch = val;
  257. var iSearchCommands = iSearchCommandModule.iSearchStartCommands;
  258. var method = val ? 'addCommands' : 'removeCommands';
  259. this[method](iSearchCommands);
  260. };
  261. }).call(commands.CommandManager.prototype);
  262. // incremental search config option
  263. var Editor = require("./editor").Editor;
  264. require("./config").defineOptions(Editor.prototype, "editor", {
  265. useIncrementalSearch: {
  266. set: function(val) {
  267. this.keyBinding.$handlers.forEach(function(handler) {
  268. if (handler.setupIncrementalSearch) {
  269. handler.setupIncrementalSearch(this, val);
  270. }
  271. });
  272. this._emit('incrementalSearchSettingChanged', {isEnabled: val});
  273. }
  274. }
  275. });
  276. });