incremental_search.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  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. ;(function() {
  61. this.activate = function(ed, backwards) {
  62. this.$editor = ed;
  63. this.$startPos = this.$currentPos = ed.getCursorPosition();
  64. this.$options.needle = '';
  65. this.$options.backwards = backwards;
  66. ed.keyBinding.addKeyboardHandler(this.$keyboardHandler);
  67. this.$mousedownHandler = ed.addEventListener('mousedown', this.onMouseDown.bind(this));
  68. this.selectionFix(ed);
  69. this.statusMessage(true);
  70. }
  71. this.deactivate = function(reset) {
  72. this.cancelSearch(reset);
  73. this.$editor.keyBinding.removeKeyboardHandler(this.$keyboardHandler);
  74. if (this.$mousedownHandler) {
  75. this.$editor.removeEventListener('mousedown', this.$mousedownHandler);
  76. delete this.$mousedownHandler;
  77. }
  78. this.message('');
  79. }
  80. this.selectionFix = function(editor) {
  81. // Fix selection bug: When clicked inside the editor
  82. // editor.selection.$isEmpty is false even if the mouse click did not
  83. // open a selection. This is interpreted by the move commands to
  84. // extend the selection. To only extend the selection when there is
  85. // one, we clear it here
  86. if (editor.selection.isEmpty() && !editor.session.$emacsMark) {
  87. editor.clearSelection();
  88. }
  89. }
  90. this.highlight = function(regexp) {
  91. var sess = this.$editor.session,
  92. hl = sess.$isearchHighlight = sess.$isearchHighlight || sess.addDynamicMarker(
  93. new SearchHighlight(null, "ace_isearch-result", "text"));
  94. hl.setRegexp(regexp);
  95. sess._emit("changeBackMarker"); // force highlight layer redraw
  96. }
  97. this.cancelSearch = function(reset) {
  98. var e = this.$editor;
  99. this.$prevNeedle = this.$options.needle;
  100. this.$options.needle = '';
  101. if (reset) {
  102. e.moveCursorToPosition(this.$startPos);
  103. this.$currentPos = this.$startPos;
  104. } else {
  105. e.pushEmacsMark && e.pushEmacsMark(this.$startPos, false);
  106. }
  107. this.highlight(null);
  108. return Range.fromPoints(this.$currentPos, this.$currentPos);
  109. }
  110. this.highlightAndFindWithNeedle = function(moveToNext, needleUpdateFunc) {
  111. if (!this.$editor) return null;
  112. var options = this.$options;
  113. // get search term
  114. if (needleUpdateFunc) {
  115. options.needle = needleUpdateFunc.call(this, options.needle || '') || '';
  116. }
  117. if (options.needle.length === 0) {
  118. this.statusMessage(true);
  119. return this.cancelSearch(true);
  120. };
  121. // try to find the next occurence and enable highlighting marker
  122. options.start = this.$currentPos;
  123. var session = this.$editor.session,
  124. found = this.find(session);
  125. if (found) {
  126. if (options.backwards) found = Range.fromPoints(found.end, found.start);
  127. this.$editor.moveCursorToPosition(found.end);
  128. if (moveToNext) this.$currentPos = found.end;
  129. // highlight after cursor move, so selection works properly
  130. this.highlight(options.re)
  131. }
  132. this.statusMessage(found);
  133. return found;
  134. }
  135. this.addChar = function(c) {
  136. return this.highlightAndFindWithNeedle(false, function(needle) {
  137. return needle + c;
  138. });
  139. }
  140. this.removeChar = function(c) {
  141. return this.highlightAndFindWithNeedle(false, function(needle) {
  142. return needle.length > 0 ? needle.substring(0, needle.length-1) : needle;
  143. });
  144. }
  145. this.next = function(options) {
  146. // try to find the next occurence of whatever we have searched for
  147. // earlier.
  148. // options = {[backwards: BOOL], [useCurrentOrPrevSearch: BOOL]}
  149. options = options || {};
  150. this.$options.backwards = !!options.backwards;
  151. this.$currentPos = this.$editor.getCursorPosition();
  152. return this.highlightAndFindWithNeedle(true, function(needle) {
  153. return options.useCurrentOrPrevSearch && needle.length === 0 ?
  154. this.$prevNeedle || '' : needle;
  155. });
  156. }
  157. this.onMouseDown = function(evt) {
  158. // when mouse interaction happens then we quit incremental search
  159. this.deactivate();
  160. return true;
  161. }
  162. this.statusMessage = function(found) {
  163. var options = this.$options, msg = '';
  164. msg += options.backwards ? 'reverse-' : '';
  165. msg += 'isearch: ' + options.needle;
  166. msg += found ? '' : ' (not found)';
  167. this.message(msg);
  168. }
  169. this.message = function(msg) {
  170. if (this.$editor.showCommandLine) {
  171. this.$editor.showCommandLine(msg);
  172. this.$editor.focus();
  173. } else {
  174. console.log(msg);
  175. }
  176. }
  177. }).call(IncrementalSearch.prototype);
  178. exports.IncrementalSearch = IncrementalSearch;
  179. /**
  180. *
  181. * Config settings for enabling/disabling [[IncrementalSearch `IncrementalSearch`]].
  182. *
  183. **/
  184. var dom = require('./lib/dom');
  185. dom.importCssString && dom.importCssString("\
  186. .ace_marker-layer .ace_isearch-result {\
  187. position: absolute;\
  188. z-index: 6;\
  189. -moz-box-sizing: border-box;\
  190. -webkit-box-sizing: border-box;\
  191. box-sizing: border-box;\
  192. }\
  193. div.ace_isearch-result {\
  194. border-radius: 4px;\
  195. background-color: rgba(255, 200, 0, 0.5);\
  196. box-shadow: 0 0 4px rgb(255, 200, 0);\
  197. }\
  198. .ace_dark div.ace_isearch-result {\
  199. background-color: rgb(100, 110, 160);\
  200. box-shadow: 0 0 4px rgb(80, 90, 140);\
  201. }", "incremental-search-highlighting");
  202. // support for default keyboard handler
  203. var commands = require("./commands/command_manager");
  204. (function() {
  205. this.setupIncrementalSearch = function(editor, val) {
  206. if (this.usesIncrementalSearch == val) return;
  207. this.usesIncrementalSearch = val;
  208. var iSearchCommands = iSearchCommandModule.iSearchStartCommands;
  209. var method = val ? 'addCommands' : 'removeCommands';
  210. this[method](iSearchCommands);
  211. };
  212. }).call(commands.CommandManager.prototype);
  213. // incremental search config option
  214. var Editor = require("./editor").Editor;
  215. require("./config").defineOptions(Editor.prototype, "editor", {
  216. useIncrementalSearch: {
  217. set: function(val) {
  218. this.keyBinding.$handlers.forEach(function(handler) {
  219. if (handler.setupIncrementalSearch) {
  220. handler.setupIncrementalSearch(this, val);
  221. }
  222. });
  223. this._emit('incrementalSearchSettingChanged', {isEnabled: val});
  224. }
  225. }
  226. });
  227. });