/* ***** BEGIN LICENSE BLOCK ***** * Distributed under the BSD license: * * Copyright (c) 2010, Ajax.org B.V. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Ajax.org B.V. nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * ***** END LICENSE BLOCK ***** */ define(function(require, exports, module) { "use strict"; var oop = require("./lib/oop"); var Range = require("./range").Range; var Search = require("./search").Search; var SearchHighlight = require("./search_highlight").SearchHighlight; var iSearchCommandModule = require("./commands/incremental_search_commands"); var ISearchKbd = iSearchCommandModule.IncrementalSearchKeyboardHandler; /** * @class IncrementalSearch * * Implements immediate searching while the user is typing. When incremental * search is activated, keystrokes into the editor will be used for composing * a search term. Immediately after every keystroke the search is updated: * - so-far-matching characters are highlighted * - the cursor is moved to the next match * **/ /** * * * Creates a new `IncrementalSearch` object. * * @constructor **/ function IncrementalSearch() { this.$options = {wrap: false, skipCurrent: false}; this.$keyboardHandler = new ISearchKbd(this); } oop.inherits(IncrementalSearch, Search); // regexp handling function isRegExp(obj) { return obj instanceof RegExp; } function regExpToObject(re) { var string = String(re), start = string.indexOf('/'), flagStart = string.lastIndexOf('/'); return { expression: string.slice(start+1, flagStart), flags: string.slice(flagStart+1) }; } function stringToRegExp(string, flags) { try { return new RegExp(string, flags); } catch (e) { return string; } } function objectToRegExp(obj) { return stringToRegExp(obj.expression, obj.flags); } // iSearch class (function() { this.activate = function(ed, backwards) { this.$editor = ed; this.$startPos = this.$currentPos = ed.getCursorPosition(); this.$options.needle = ''; this.$options.backwards = backwards; ed.keyBinding.addKeyboardHandler(this.$keyboardHandler); // we need to completely intercept paste, just registering an event handler does not work this.$originalEditorOnPaste = ed.onPaste; ed.onPaste = this.onPaste.bind(this); this.$mousedownHandler = ed.addEventListener('mousedown', this.onMouseDown.bind(this)); this.selectionFix(ed); this.statusMessage(true); }; this.deactivate = function(reset) { this.cancelSearch(reset); var ed = this.$editor; ed.keyBinding.removeKeyboardHandler(this.$keyboardHandler); if (this.$mousedownHandler) { ed.removeEventListener('mousedown', this.$mousedownHandler); delete this.$mousedownHandler; } ed.onPaste = this.$originalEditorOnPaste; this.message(''); }; this.selectionFix = function(editor) { // Fix selection bug: When clicked inside the editor // editor.selection.$isEmpty is false even if the mouse click did not // open a selection. This is interpreted by the move commands to // extend the selection. To only extend the selection when there is // one, we clear it here if (editor.selection.isEmpty() && !editor.session.$emacsMark) { editor.clearSelection(); } }; this.highlight = function(regexp) { var sess = this.$editor.session, hl = sess.$isearchHighlight = sess.$isearchHighlight || sess.addDynamicMarker( new SearchHighlight(null, "ace_isearch-result", "text")); hl.setRegexp(regexp); sess._emit("changeBackMarker"); // force highlight layer redraw }; this.cancelSearch = function(reset) { var e = this.$editor; this.$prevNeedle = this.$options.needle; this.$options.needle = ''; if (reset) { e.moveCursorToPosition(this.$startPos); this.$currentPos = this.$startPos; } else { e.pushEmacsMark && e.pushEmacsMark(this.$startPos, false); } this.highlight(null); return Range.fromPoints(this.$currentPos, this.$currentPos); }; this.highlightAndFindWithNeedle = function(moveToNext, needleUpdateFunc) { if (!this.$editor) return null; var options = this.$options; // get search term if (needleUpdateFunc) { options.needle = needleUpdateFunc.call(this, options.needle || '') || ''; } if (options.needle.length === 0) { this.statusMessage(true); return this.cancelSearch(true); } // try to find the next occurrence and enable highlighting marker options.start = this.$currentPos; var session = this.$editor.session, found = this.find(session), shouldSelect = this.$editor.emacsMark ? !!this.$editor.emacsMark() : !this.$editor.selection.isEmpty(); if (found) { if (options.backwards) found = Range.fromPoints(found.end, found.start); this.$editor.selection.setRange(Range.fromPoints(shouldSelect ? this.$startPos : found.end, found.end)); if (moveToNext) this.$currentPos = found.end; // highlight after cursor move, so selection works properly this.highlight(options.re); } this.statusMessage(found); return found; }; this.addString = function(s) { return this.highlightAndFindWithNeedle(false, function(needle) { if (!isRegExp(needle)) return needle + s; var reObj = regExpToObject(needle); reObj.expression += s; return objectToRegExp(reObj); }); }; this.removeChar = function(c) { return this.highlightAndFindWithNeedle(false, function(needle) { if (!isRegExp(needle)) return needle.substring(0, needle.length-1); var reObj = regExpToObject(needle); reObj.expression = reObj.expression.substring(0, reObj.expression.length-1); return objectToRegExp(reObj); }); }; this.next = function(options) { // try to find the next occurrence of whatever we have searched for // earlier. // options = {[backwards: BOOL], [useCurrentOrPrevSearch: BOOL]} options = options || {}; this.$options.backwards = !!options.backwards; this.$currentPos = this.$editor.getCursorPosition(); return this.highlightAndFindWithNeedle(true, function(needle) { return options.useCurrentOrPrevSearch && needle.length === 0 ? this.$prevNeedle || '' : needle; }); }; this.onMouseDown = function(evt) { // when mouse interaction happens then we quit incremental search this.deactivate(); return true; }; this.onPaste = function(text) { this.addString(text); }; this.convertNeedleToRegExp = function() { return this.highlightAndFindWithNeedle(false, function(needle) { return isRegExp(needle) ? needle : stringToRegExp(needle, 'ig'); }); }; this.convertNeedleToString = function() { return this.highlightAndFindWithNeedle(false, function(needle) { return isRegExp(needle) ? regExpToObject(needle).expression : needle; }); }; this.statusMessage = function(found) { var options = this.$options, msg = ''; msg += options.backwards ? 'reverse-' : ''; msg += 'isearch: ' + options.needle; msg += found ? '' : ' (not found)'; this.message(msg); }; this.message = function(msg) { if (this.$editor.showCommandLine) { this.$editor.showCommandLine(msg); this.$editor.focus(); } else { console.log(msg); } }; }).call(IncrementalSearch.prototype); exports.IncrementalSearch = IncrementalSearch; /** * * Config settings for enabling/disabling [[IncrementalSearch `IncrementalSearch`]]. * **/ var dom = require('./lib/dom'); dom.importCssString && dom.importCssString("\ .ace_marker-layer .ace_isearch-result {\ position: absolute;\ z-index: 6;\ box-sizing: border-box;\ }\ div.ace_isearch-result {\ border-radius: 4px;\ background-color: rgba(255, 200, 0, 0.5);\ box-shadow: 0 0 4px rgb(255, 200, 0);\ }\ .ace_dark div.ace_isearch-result {\ background-color: rgb(100, 110, 160);\ box-shadow: 0 0 4px rgb(80, 90, 140);\ }", "incremental-search-highlighting"); // support for default keyboard handler var commands = require("./commands/command_manager"); (function() { this.setupIncrementalSearch = function(editor, val) { if (this.usesIncrementalSearch == val) return; this.usesIncrementalSearch = val; var iSearchCommands = iSearchCommandModule.iSearchStartCommands; var method = val ? 'addCommands' : 'removeCommands'; this[method](iSearchCommands); }; }).call(commands.CommandManager.prototype); // incremental search config option var Editor = require("./editor").Editor; require("./config").defineOptions(Editor.prototype, "editor", { useIncrementalSearch: { set: function(val) { this.keyBinding.$handlers.forEach(function(handler) { if (handler.setupIncrementalSearch) { handler.setupIncrementalSearch(this, val); } }); this._emit('incrementalSearchSettingChanged', {isEnabled: val}); } } }); });