diff --git a/demo/kitchen-sink/demo.js b/demo/kitchen-sink/demo.js index adc47c00..26e96767 100644 --- a/demo/kitchen-sink/demo.js +++ b/demo/kitchen-sink/demo.js @@ -83,6 +83,9 @@ env.editor.setAnimatedScroll(true); // add multiple cursor support to editor require("ace/multi_select").MultiSelect(env.editor); +// add incremental search +window.iSearch = new (require("ace/incremental_search").IncrementalSearch)(); + var consoleEl = dom.createElement("div"); container.parentNode.appendChild(consoleEl); consoleEl.style.cssText = "position:fixed; bottom:1px; right:0;\ diff --git a/lib/ace/incremental_search.js b/lib/ace/incremental_search.js new file mode 100644 index 00000000..173c963d --- /dev/null +++ b/lib/ace/incremental_search.js @@ -0,0 +1,163 @@ +/* ***** 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 lang = require("./lib/lang"); +var oop = require("./lib/oop"); +var Range = require("./range").Range; +var Search = require("./search").Search; + +/** + * @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. Options: + * + * @constructor + **/ +function IncrementalSearch() { + this.$options = {wrap: false, skipCurrent: false}; + this.$keyboardHandler = this; +} + +oop.inherits(IncrementalSearch, Search); + +;(function() { + + var iSearch = this; + + iSearch.activate = function(editor) { + this.$editor = editor; + this.$startRange = this.$currentRange = editor.selection.toOrientedRange(); + this.installKeyboardHandler(editor); + } + + this.deactivate = function() { + this.uninstallKeyboardHandler(this.$editor); + delete this.$editor; + } + + iSearch.highlightAndFindWithNeedle = function(dir, moveToMatch, needleUpdateFunc) { + if (!this.$editor) return null; + dir = dir || 'forward'; + var session = this.$editor.session, + options = this.$options; + if (needleUpdateFunc) options.needle = needleUpdateFunc(options.needle || '') || ''; + if (dir === "forward") { + options.start = this.$currentRange.end; + options.backwards = false; + } else { + options.start = this.$currentRange.start; + options.backwards = true; + } + var range = this.find(session); + if (range && moveToMatch) { + this.$editor.selection.setRange(range); + this.$currentRange = range; + } + + session.highlight(options.re); + return range; + } + + this.addChar = function(c) { + return this.highlightAndFindWithNeedle('forward', false, function(needle) { + return needle + c; + }); + }, + + iSearch.removeChar = function(c) { + return this.highlightAndFindWithNeedle('forward', false, function(needle) { + return needle.length > 0 ? needle.substring(0, needle.length-1) : needle; + }); + } + + iSearch.forward = function() { + return this.highlightAndFindWithNeedle('forward', true); + } + + iSearch.backward = function() { + return this.highlightAndFindWithNeedle('backward', true); + } + + this.installKeyboardHandler = function(editor) { + this.$origKeyboardHandlers = [].concat(editor.keyBinding.$handlers); + this.$origKeyboardHandlers.reverse().forEach(function(handler) { + editor.keyBinding.removeKeyboardHandler(handler); + }); + editor.keyBinding.addKeyboardHandler(this.$keyboardHandler); + } + + this.uninstallKeyboardHandler = function(editor) { + editor.keyBinding.removeKeyboardHandler(this.$keyboardHandler); + if (this.$origKeyboardHandlers) { + this.$origKeyboardHandlers.forEach(function(handler) { + editor.keyBinding.addKeyboardHandler(handler); + }); + delete this.$origKeyboardHandlers; + } + } + + iSearch.handleKeyboard = function(data, hashId, key, keyCode) { + console.log("data: %s, hashId: %s, key: %s, keyCode: %s", + data, hashId, key, keyCode); + if (hashId === 0) { + if (key === 'backspace') this.removeChar(); + else if (key.length === 1) this.addChar(key); + } + + if (hashId === 1) { + if (key === 's') this.forward(); + if (key === 'r') this.backward(); + } + + console.log(this.$options.needle); + return {command: 'null'} + } + + +}).call(IncrementalSearch.prototype); + + +exports.IncrementalSearch = IncrementalSearch; + +}); diff --git a/lib/ace/incremental_search_test.js b/lib/ace/incremental_search_test.js new file mode 100644 index 00000000..40b09eb0 --- /dev/null +++ b/lib/ace/incremental_search_test.js @@ -0,0 +1,127 @@ +/* ***** 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 ***** */ + +if (typeof process !== "undefined") { + require("amd-loader"); +} + +define(function(require, exports, module) { +"use strict"; + +var EditSession = require("./edit_session").EditSession; +var Editor = require("./editor").Editor; +var MockRenderer = require("./test/mockrenderer").MockRenderer; +var Range = require("./range").Range; +var assert = require("./test/assertions"); +var IncrementalSearch = require("./incremental_search").IncrementalSearch; + +var editor, iSearch; +function testRanges(str, ranges) { + ranges = ranges || editor.selection.getAllRanges(); + assert.equal(ranges + "", str + ""); +} + +// force "rerender" +function callHighlighterUpdate() { + var session = editor.session, + ranges = [], + mockMarkerLayer = { + drawSingleLineMarker: function(_, markerRanges) { + ranges = ranges.concat(markerRanges); + } + } + session.$searchHighlight.update([], mockMarkerLayer, session, { + firstRow: 0, lastRow: session.getRowLength()}); + return ranges; +} + +module.exports = { + + name: "ACE incremental_search.js", + + setUp: function() { + var session = new EditSession(["abc123", "xyz124"]); + editor = new Editor(new MockRenderer(), session); + iSearch = new IncrementalSearch(); + }, + + "test: keyboard handler setup" : function() { + iSearch.activate(editor); + assert.equal(editor.getKeyboardHandler(), iSearch.$keyboardHandler); + iSearch.deactivate(); + assert.notEqual(editor.getKeyboardHandler(), iSearch.$keyboardHandler); + }, + + "test: find simple text incrementally" : function() { + iSearch.activate(editor); + var range = iSearch.addChar('1'), // "1" + highlightRanges = callHighlighterUpdate(editor.session); + testRanges("Range: [0/3] -> [0/4]", [range], "range"); + testRanges("Range: [0/3] -> [0/4],Range: [1/3] -> [1/4]", highlightRanges, "highlight"); + + range = iSearch.addChar('2'); // "12" + highlightRanges = callHighlighterUpdate(editor.session); + testRanges("Range: [0/3] -> [0/5]", [range], "range"); + testRanges("Range: [0/3] -> [0/5],Range: [1/3] -> [1/5]", highlightRanges, "highlight"); + range = iSearch.addChar('3'); // "123" + highlightRanges = callHighlighterUpdate(editor.session); + testRanges("Range: [0/3] -> [0/6]", [range], "range"); + testRanges("Range: [0/3] -> [0/6]", highlightRanges, "highlight"); + + range = iSearch.removeChar(); // "12" + highlightRanges = callHighlighterUpdate(editor.session); + testRanges("Range: [0/3] -> [0/5]", [range], "range"); + testRanges("Range: [0/3] -> [0/5],Range: [1/3] -> [1/5]", highlightRanges, "highlight"); + + range = iSearch.forward(); // "12", cursor forward + highlightRanges = callHighlighterUpdate(editor.session); + testRanges("Range: [1/3] -> [1/5]", [range], "range"); + testRanges("Range: [0/3] -> [0/5],Range: [1/3] -> [1/5]", highlightRanges, "highlight"); + + } + + // // "test: find simple text in document" : function() { + // var session = new EditSession(["juhu kinners 123", "456"]); + // var search = new Search().set({ + // needle: "kinners" + // }); + // session.getSelection().moveCursorTo(0, 13); + // var range = search.find(session); + // assert.position(range.start, 0, 5); + // assert.position(range.end, 0, 12); + // }, + +}; + +}); + +if (typeof module !== "undefined" && module === require.main) { + require("asyncjs").test.testcase(module.exports).exec() +} diff --git a/lib/ace/test/all_browser.js b/lib/ace/test/all_browser.js index 9a274329..b98d6558 100644 --- a/lib/ace/test/all_browser.js +++ b/lib/ace/test/all_browser.js @@ -45,6 +45,7 @@ var testNames = [ "ace/range_test", "ace/range_list_test", "ace/search_test", + "ace/incremental_search_test", "ace/selection_test", "ace/token_iterator_test", "ace/virtual_renderer_test"