diff --git a/demo/kitchen-sink/demo.js b/demo/kitchen-sink/demo.js index 28867a7e..55dad178 100644 --- a/demo/kitchen-sink/demo.js +++ b/demo/kitchen-sink/demo.js @@ -66,6 +66,8 @@ var bindDropdown = util.bindDropdown; var ElasticTabstopsLite = require("ace/ext/elastic_tabstops_lite").ElasticTabstopsLite; +var IncrementalSearch = require("ace/incremental_search").IncrementalSearch; + /*********** create editor ***************************/ var container = document.getElementById("editor-container"); @@ -173,7 +175,7 @@ commands.addCommand({ exec: function() {alert("Fake Save File");} }); -var keybindings = { +var keybindings = { ace: null, // Null = use "default" keymapping vim: require("ace/keyboard/vim").handler, emacs: "ace/keyboard/emacs", @@ -380,7 +382,7 @@ bindDropdown("split", function(value) { sp.setSplits(1); } else { var newEditor = (sp.getSplits() == 1); - sp.setOrientation(value == "below" ? sp.BELOW : sp.BESIDE); + sp.setOrientation(value == "below" ? sp.BELOW : sp.BESIDE); sp.setSplits(2); if (newEditor) { @@ -396,6 +398,14 @@ bindCheckbox("elastic_tabstops", function(checked) { env.editor.setOption("useElasticTabstops", checked); }); +var iSearchCheckbox = bindCheckbox("isearch", function(checked) { + env.editor.setOption("useIncrementalSearch", checked); +}); + +env.editor.addEventListener('incrementalSearchSettingChanged', function(event) { + iSearchCheckbox.checked = event.isEnabled; +}); + function synchroniseScrolling() { var s1 = env.split.$editors[0].session; @@ -451,4 +461,3 @@ var StatusBar = require("./statusbar").StatusBar; new StatusBar(env.editor, cmdLine.container); }); - diff --git a/demo/kitchen-sink/util.js b/demo/kitchen-sink/util.js index 28525e1a..752e79ba 100644 --- a/demo/kitchen-sink/util.js +++ b/demo/kitchen-sink/util.js @@ -171,6 +171,7 @@ exports.bindCheckbox = function(id, callback, noInit) { }; el.onclick = onCheck; noInit || onCheck(); + return el; }; exports.bindDropdown = function(id, callback, noInit) { @@ -235,4 +236,3 @@ function dropdown(values) { }); - diff --git a/kitchen-sink.html b/kitchen-sink.html index 614ec7be..86fd256c 100644 --- a/kitchen-sink.html +++ b/kitchen-sink.html @@ -250,6 +250,14 @@ + + + + + + + + diff --git a/lib/ace/commands/incremental_search_commands.js b/lib/ace/commands/incremental_search_commands.js new file mode 100644 index 00000000..1cce3912 --- /dev/null +++ b/lib/ace/commands/incremental_search_commands.js @@ -0,0 +1,172 @@ +/* ***** 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) { + +var config = require("../config"); + +// These commands can be installed in a normal key handler to start iSearch: +exports.iSearchStartCommands = [{ + name: "iSearch", + bindKey: {win: "Ctrl-F", mac: "Command-F"}, + exec: function(editor, options) { + config.loadModule(["core", "ace/incremental_search"], function(e) { + var iSearch = e.iSearch = e.iSearch || new e.IncrementalSearch(); + iSearch.activate(editor, options.backwards); + if (options.jumpToFirstMatch) iSearch.next(options); + }); + }, + readOnly: true +}, { + name: "iSearchBackwards", + exec: function(editor, jumpToNext) { editor.execCommand('iSearch', {backwards: true}); }, + readOnly: true +}, { + name: "iSearchAndGo", + bindKey: {win: "Ctrl-K", mac: "Command-G"}, + exec: function(editor, jumpToNext) { editor.execCommand('iSearch', {jumpToFirstMatch: true, useCurrentOrPrevSearch: true}); }, + readOnly: true +}, { + name: "iSearchBackwardsAndGo", + bindKey: {win: "Ctrl-Shift-K", mac: "Command-Shift-G"}, + exec: function(editor) { editor.execCommand('iSearch', {jumpToFirstMatch: true, backwards: true, useCurrentOrPrevSearch: true}); }, + readOnly: true +}]; + +// These commands are only available when incremental search mode is active: +exports.iSearchCommands = [{ + name: "restartSearch", + bindKey: {win: "Ctrl-F", mac: "Command-F"}, + exec: function(iSearch) { + iSearch.cancelSearch(true); + }, + readOnly: true, + isIncrementalSearchCommand: true +}, { + name: "searchForward", + bindKey: {win: "Ctrl-S|Ctrl-K", mac: "Ctrl-S|Command-G"}, + exec: function(iSearch, options) { + options.useCurrentOrPrevSearch = true; + iSearch.next(options); + }, + readOnly: true, + isIncrementalSearchCommand: true +}, { + name: "searchBackward", + bindKey: {win: "Ctrl-R|Ctrl-Shift-K", mac: "Ctrl-R|Command-Shift-G"}, + exec: function(iSearch, options) { + options.useCurrentOrPrevSearch = true; + options.backwards = true; + iSearch.next(options); + }, + readOnly: true, + isIncrementalSearchCommand: true +}, { + name: "extendSearchTerm", + exec: function(iSearch, string) { + iSearch.addChar(string); + }, + readOnly: true, + isIncrementalSearchCommand: true +}, { + name: "extendSearchTermSpace", + bindKey: "space", + exec: function(iSearch) { iSearch.addChar(' '); }, + readOnly: true, + isIncrementalSearchCommand: true +}, { + name: "shrinkSearchTerm", + bindKey: "backspace", + exec: function(iSearch) { + iSearch.removeChar(); + }, + readOnly: true, + isIncrementalSearchCommand: true +}, { + name: 'confirmSearch', + bindKey: 'return', + exec: function(iSearch) { iSearch.deactivate(); }, + readOnly: true, + isIncrementalSearchCommand: true +}, { + name: 'cancelSearch', + bindKey: 'esc|Ctrl-G', + exec: function(iSearch) { iSearch.deactivate(true); }, + readOnly: true, + isIncrementalSearchCommand: true +}]; + + + +var HashHandler = require("../keyboard/hash_handler").HashHandler; +var oop = require("../lib/oop"); + +function IncrementalSearchKeyboardHandler(iSearch) { + this.$iSearch = iSearch; +} + +oop.inherits(IncrementalSearchKeyboardHandler, HashHandler); + +;(function() { + + this.attach = function(editor) { + var iSearch = this.$iSearch; + HashHandler.call(this, exports.iSearchCommands, editor.commands.platform); + this.$commandExecHandler = editor.commands.addEventListener('exec', function(e) { + if (!e.command.isIncrementalSearchCommand) return undefined; + e.stopPropagation(); + e.preventDefault(); + return e.command.exec(iSearch, e.args || {}); + }); + } + + this.detach = function(editor) { + if (!this.$commandExecHandler) return; + editor.commands.removeEventListener('exec', this.$commandExecHandler); + delete this.$commandExecHandler; + } + + var handleKeyboard$super = this.handleKeyboard; + this.handleKeyboard = function(data, hashId, key, keyCode) { + var cmd = handleKeyboard$super.call(this, data, hashId, key, keyCode); + if (cmd.command) { return cmd; } + if (hashId == -1) { + var extendCmd = this.commands.extendSearchTerm; + if (extendCmd) { return {command: extendCmd, args: key}; } + } + return {command: "null", passEvent: hashId == 0 || hashId == 4}; + } + +}).call(IncrementalSearchKeyboardHandler.prototype); + + +exports.IncrementalSearchKeyboardHandler = IncrementalSearchKeyboardHandler; + +}); diff --git a/lib/ace/editor_highlight_selected_word_test.js b/lib/ace/editor_highlight_selected_word_test.js index 864de9a1..13e19c23 100644 --- a/lib/ace/editor_highlight_selected_word_test.js +++ b/lib/ace/editor_highlight_selected_word_test.js @@ -3,7 +3,7 @@ * * 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 @@ -14,7 +14,7 @@ * * 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 diff --git a/lib/ace/incremental_search.js b/lib/ace/incremental_search.js new file mode 100644 index 00000000..a15d7acc --- /dev/null +++ b/lib/ace/incremental_search.js @@ -0,0 +1,257 @@ +/* ***** 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); + +;(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); + this.$mousedownHandler = ed.addEventListener('mousedown', this.onMouseDown.bind(this)); + this.selectionFix(ed); + this.statusMessage(true); + } + + this.deactivate = function(reset) { + this.cancelSearch(reset); + this.$editor.keyBinding.removeKeyboardHandler(this.$keyboardHandler); + if (this.$mousedownHandler) { + this.$editor.removeEventListener('mousedown', this.$mousedownHandler); + delete this.$mousedownHandler; + } + 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; + } + 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 occurence and enable highlighting marker + options.start = this.$currentPos; + var session = this.$editor.session, + found = this.find(session); + if (found) { + if (options.backwards) found = Range.fromPoints(found.end, found.start); + this.$editor.moveCursorToPosition(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.addChar = function(c) { + return this.highlightAndFindWithNeedle(false, function(needle) { + return needle + c; + }); + } + + this.removeChar = function(c) { + return this.highlightAndFindWithNeedle(false, function(needle) { + return needle.length > 0 ? needle.substring(0, needle.length-1) : needle; + }); + } + + this.next = function(options) { + // try to find the next occurence 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.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;\ + -moz-box-sizing: border-box;\ + -webkit-box-sizing: border-box;\ + box-sizing: border-box;\ +}\ +div.ace_isearch-result {\ + border-radius: 4px;\ + border: 8px solid rgba(255, 200, 0, 0.5);\ + box-shadow: 0 0 4px rgb(255, 200, 0);\ +}\ +.ace_dark div.ace_isearch-result {\ + border: 8px solid 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}); + } + } +}); + +}); diff --git a/lib/ace/incremental_search_test.js b/lib/ace/incremental_search_test.js new file mode 100644 index 00000000..c351d8e2 --- /dev/null +++ b/lib/ace/incremental_search_test.js @@ -0,0 +1,208 @@ +/* ***** 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.$isearchHighlight.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: isearch highlight setup" : function() { + var sess = editor.session; + iSearch.activate(editor); + iSearch.highlight('foo'); + var highl = sess.$isearchHighlight.id; + assert.ok(sess.$isearchHighlight, 'session has no isearch highlighter'); + assert.equal(sess.getMarkers()[highl.id], highl.id, 'isearch highlight not in markers'); + iSearch.deactivate(); + iSearch.activate(editor); + iSearch.highlight('bar'); + var highl2 = sess.$isearchHighlight.id; + assert.equal(highl2, highl, 'multiple isearch highlights'); + }, + + "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"); + }, + + "test: forward / backward" : function() { + iSearch.activate(editor); + iSearch.addChar('1'); iSearch.addChar('2'); + var range = iSearch.next(); + testRanges("Range: [1/3] -> [1/5]", [range], "range"); + + range = iSearch.next(); // nothing to find + testRanges("", [range], "range"); + + range = iSearch.next({backwards: true}); // backwards + testRanges("Range: [1/5] -> [1/3]", [range], "range"); + }, + + "test: cancelSearch" : function() { + iSearch.activate(editor); + iSearch.addChar('1'); iSearch.addChar('2'); + var range = iSearch.cancelSearch(true); + testRanges("Range: [0/0] -> [0/0]", [range], "range"); + + iSearch.addChar('1'); range = iSearch.addChar('2'); + testRanges("Range: [0/3] -> [0/5]", [range], "range"); + }, + + "test: failing search keeps pos" : function() { + iSearch.activate(editor); + iSearch.addChar('1'); iSearch.addChar('2'); + var range = iSearch.addChar('x'); + testRanges("", [range], "range"); + assert.position(editor.getCursorPosition(), 0, 5); + }, + + "test: backwards search" : function() { + editor.moveCursorTo(1,0); + iSearch.activate(editor, true); + iSearch.addChar('1'); var range = iSearch.addChar('2');; + testRanges("Range: [0/5] -> [0/3]", [range], "range"); + assert.position(editor.getCursorPosition(), 0, 3); + }, + + "test: forwards then backwards, same result, reoriented range" : function() { + iSearch.activate(editor); + iSearch.addChar('1'); var range = iSearch.addChar('2');; + testRanges("Range: [0/3] -> [0/5]", [range], "range"); + assert.position(editor.getCursorPosition(), 0, 5); + + range = iSearch.next({backwards: true}); + testRanges("Range: [0/5] -> [0/3]", [range], "range"); + assert.position(editor.getCursorPosition(), 0, 3); + }, + + "test: reuse prev search via option" : function() { + iSearch.activate(editor); + iSearch.addChar('1'); iSearch.addChar('2');; + assert.position(editor.getCursorPosition(), 0, 5); + iSearch.deactivate(); + + iSearch.activate(editor); + iSearch.next({backwards: false, useCurrentOrPrevSearch: true}); + assert.position(editor.getCursorPosition(), 1, 5); + }, + + "test: don't extend selection range if selection is empty" : function() { + iSearch.activate(editor); + iSearch.addChar('1'); iSearch.addChar('2');; + testRanges("Range: [0/5] -> [0/5]", [editor.getSelectionRange()], "sel range"); + }, + + "test: extend selection range if selection exists" : function() { + iSearch.activate(editor); + editor.selection.selectTo(0, 1); + iSearch.addChar('1'); iSearch.addChar('2');; + testRanges("Range: [0/0] -> [0/5]", [editor.getSelectionRange()], "sel range"); + }, + + "test: extend selection in emacs mark mode" : function() { + var emacs = require('./keyboard/emacs'); + editor.keyBinding.addKeyboardHandler(emacs.handler); + emacs.handler.commands.setMark.exec(editor); + iSearch.activate(editor); + iSearch.addChar('1'); iSearch.addChar('2');; + testRanges("Range: [0/0] -> [0/5]", [editor.getSelectionRange()], "sel range"); + } + +}; + +}); + +if (typeof module !== "undefined" && module === require.main) { + require("asyncjs").test.testcase(module.exports).exec() +} diff --git a/lib/ace/keyboard/emacs.js b/lib/ace/keyboard/emacs.js index 65fc5840..81247be4 100644 --- a/lib/ace/keyboard/emacs.js +++ b/lib/ace/keyboard/emacs.js @@ -32,6 +32,9 @@ define(function(require, exports, module) { "use strict"; var dom = require("../lib/dom"); +require("../incremental_search"); +var iSearchCommandModule = require("../commands/incremental_search_commands"); + var screenToTextBlockCoordinates = function(x, y) { var canvasPos = this.scroller.getBoundingClientRect(); @@ -49,6 +52,8 @@ var screenToTextBlockCoordinates = function(x, y) { var HashHandler = require("./hash_handler").HashHandler; exports.handler = new HashHandler(); +exports.handler.isEmacs = true + var initialized = false; var $formerLongWords; var $formerLineStart; @@ -100,7 +105,7 @@ exports.handler.attach = function(editor) { } editor.on("click", $resetMarkMode); - editor.on("changeSession",$kbSessionChange); + editor.on("changeSession", $kbSessionChange); editor.renderer.screenToTextCoordinates = screenToTextBlockCoordinates; editor.setStyle("emacs-mode"); editor.commands.addCommands(commands); @@ -296,8 +301,10 @@ exports.emacsKeys = { "PageUp|M-v": {command: "goorselect", args: ["gotopageup","selectpageup"]}, "S-C-Down": "selectpagedown", "S-C-Up": "selectpageup", - "C-s": "findnext", - "C-r": "findprevious", + + "C-s": "iSearch", + "C-r": "iSearchBackwards", + "M-C-s": "findnext", "M-C-r": "findprevious", "S-M-5": "replace", @@ -457,6 +464,8 @@ exports.handler.addCommands({ } }); +exports.handler.addCommands(iSearchCommandModule.iSearchStartCommands); + var commands = exports.handler.commands; commands.yank.isYank = true; commands.yankRotate.isYank = true; @@ -482,5 +491,4 @@ exports.killRing = { } }; - }); diff --git a/lib/ace/test/all_browser.js b/lib/ace/test/all_browser.js index 9a274329..4ce68ae3 100644 --- a/lib/ace/test/all_browser.js +++ b/lib/ace/test/all_browser.js @@ -21,6 +21,7 @@ var testNames = [ "ace/editor_navigation_test", "ace/editor_text_edit_test", "ace/ext/static_highlight_test", + "ace/incremental_search_test", "ace/keyboard/emacs_test", "ace/keyboard/keybinding_test", "ace/layer/text_test",