316 lines
11 KiB
JavaScript
Executable File
316 lines
11 KiB
JavaScript
Executable File
/* ***** 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});
|
|
}
|
|
}
|
|
});
|
|
|
|
});
|