diff --git a/lib/ace/ext/statusbar.js b/lib/ace/ext/statusbar.js index 666febfa..5e5b0572 100644 --- a/lib/ace/ext/statusbar.js +++ b/lib/ace/ext/statusbar.js @@ -28,9 +28,8 @@ var StatusBar = function(editor, parentNode) { str && status.push(str, separator || "|"); } - if (editor.$vimModeHandler) - add(editor.$vimModeHandler.getStatusText()); - else if (editor.commands.recording) + add(editor.keyBinding.getStatusText(editor)); + if (editor.commands.recording) add("REC"); var c = editor.selection.lead; diff --git a/lib/ace/incremental_search.js b/lib/ace/incremental_search.js index 4e7f7bf3..a64e1857 100644 --- a/lib/ace/incremental_search.js +++ b/lib/ace/incremental_search.js @@ -173,7 +173,7 @@ function objectToRegExp(obj) { !!this.$editor.emacsMark() : !this.$editor.selection.isEmpty(); if (found) { if (options.backwards) found = Range.fromPoints(found.end, found.start); - this.$editor.moveCursorToPosition(found.end); + 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) diff --git a/lib/ace/incremental_search_test.js b/lib/ace/incremental_search_test.js index 991291c5..d1303b42 100644 --- a/lib/ace/incremental_search_test.js +++ b/lib/ace/incremental_search_test.js @@ -44,6 +44,8 @@ var MultiSelect = require("./multi_select").MultiSelect; var assert = require("./test/assertions"); var IncrementalSearch = require("./incremental_search").IncrementalSearch; +require("./multi_select"); + var editor, iSearch; function testRanges(str, ranges) { ranges = ranges || editor.selection.getAllRanges(); @@ -199,7 +201,6 @@ module.exports = { emacs.handler.commands.setMark.exec(editor); iSearch.activate(editor); iSearch.addString('1'); iSearch.addString('2'); - testRanges("Range: [0/0] -> [0/5]", [editor.getSelectionRange()], "sel range"); } diff --git a/lib/ace/keyboard/emacs.js b/lib/ace/keyboard/emacs.js index ab4471b2..945eddf3 100644 --- a/lib/ace/keyboard/emacs.js +++ b/lib/ace/keyboard/emacs.js @@ -125,6 +125,24 @@ exports.handler.attach = function(editor) { return this.session.$emacsMark || this.session.$emacsMarkRing.slice(-1)[0]; }; + editor.emacsMarkForSelection = function(replacement) { + // find the mark in $emacsMarkRing corresponding to the current + // selection + var sel = this.selection, + multiRangeLength = this.multiSelect ? + this.multiSelect.getAllRanges().length : 1, + selIndex = sel.index || 0, + markRing = this.session.$emacsMarkRing, + markIndex = markRing.length - (multiRangeLength - selIndex), + lastMark = markRing[markIndex] || sel.anchor; + if (replacement) { + markRing.splice(markIndex, 1, + "row" in replacement && "column" in replacement ? + replacement : undefined); + } + return lastMark; + } + editor.on("click", $resetMarkMode); editor.on("changeSession", $kbSessionChange); editor.renderer.screenToTextCoordinates = screenToTextBlockCoordinates; @@ -146,6 +164,7 @@ exports.handler.detach = function(editor) { editor.commands.removeCommands(commands); editor.removeEventListener('copy', this.onCopy); editor.removeEventListener('paste', this.onPaste); + editor.$emacsModeHandler = null; }; var $kbSessionChange = function(e) { @@ -187,7 +206,7 @@ exports.handler.onCopy = function(e, editor) { if (editor.$handlesEmacsOnCopy) return; editor.$handlesEmacsOnCopy = true; exports.handler.commands.killRingSave.exec(editor); - delete editor.$handlesEmacsOnCopy; + editor.$handlesEmacsOnCopy = false; }; exports.handler.onPaste = function(e, editor) { @@ -220,12 +239,22 @@ exports.handler.bindKey = function(key, command) { }, this); }; +exports.handler.getStatusText = function(editor, data) { + var str = ""; + if (data.count) + str += data.count; + if (data.keyChain) + str += " " + data.keyChain + return str; +}; + exports.handler.handleKeyboard = function(data, hashId, key, keyCode) { // if keyCode == -1 a non-printable key was pressed, such as just // control. Handling those is currently not supported in this handler if (keyCode === -1) return undefined; var editor = data.editor; + editor._signal("changeStatus"); // insertstring data.count times if (hashId == -1) { editor.pushEmacsMark(); @@ -236,24 +265,17 @@ exports.handler.handleKeyboard = function(data, hashId, key, keyCode) { } } - if (key == "\x00") return undefined; - var modifier = eMods[hashId]; // CTRL + number / universalArgument for setting data.count - if (modifier == "c-" || data.universalArgument) { - var prevCount = String(data.count || 0); + if (modifier == "c-" || data.count) { var count = parseInt(key[key.length - 1]); if (typeof count === 'number' && !isNaN(count)) { - data.count = parseInt(prevCount + count); + data.count = Math.max(data.count, 0) || 0; + data.count = 10 * data.count + count; return {command: "null"}; - } else if (data.universalArgument) { - // if no number pressed use emacs defaults for universalArgument - // which is 4 - data.count = 4; } } - data.universalArgument = false; // this.commandKeyBinding maps key specs like "c-p" (for CTRL + P) to // command objects, for lookup key needs to include the modifier @@ -275,7 +297,9 @@ exports.handler.handleKeyboard = function(data, hashId, key, keyCode) { if (command === "null") return {command: "null"}; if (command === "universalArgument") { - data.universalArgument = true; + // if no number pressed emacs repeats action 4 times. + // minus sign is needed to allow next keypress to replace it + data.count = -4; return {command: "null"}; } @@ -318,7 +342,8 @@ exports.handler.handleKeyboard = function(data, hashId, key, keyCode) { exec: function(editor, args) { for (var i = 0; i < count; i++) command.exec(editor, args); - } + }, + multiSelectAction: command.multiSelectAction } }; } else { @@ -439,59 +464,63 @@ exports.handler.addCommands({ // selection modification commands. That is, // "goto" commands become "select" commands. // Any insertion or mouse click resets mark-mode. - // setMark twice in a row at the same place resets markmode + // setMark twice in a row at the same place resets markmode. + // in multi select mode, ea selection is handled individually + if (args && args.count) { - var mark = editor.popEmacsMark(); - mark && editor.selection.moveCursorToPosition(mark); + if (editor.inMultiSelectMode) editor.forEachSelection(moveToMark); + else moveToMark(); + moveToMark(); return; } var mark = editor.emacsMark(), - transientMarkModeActive = true; - + ranges = editor.selection.getAllRanges(), + rangePositions = ranges.map(function(r) { return {row: r.start.row, column: r.start.column}; }), + transientMarkModeActive = true, + hasNoSelection = ranges.every(function(range) { return range.isEmpty(); }); // if transientMarkModeActive then mark behavior is a little // different. Deactivate the mark when setMark is run with active // mark - if (transientMarkModeActive && (mark || !editor.selection.isEmpty())) { - editor.pushEmacsMark(); - editor.clearSelection(); + if (transientMarkModeActive && (mark || !hasNoSelection)) { + if (editor.inMultiSelectMode) editor.forEachSelection({exec: editor.clearSelection.bind(editor)}) + else editor.clearSelection(); + if (mark) editor.pushEmacsMark(null); return; } - if (mark) { - var cp = editor.getCursorPosition(); - if (editor.selection.isEmpty() && - mark.row == cp.row && mark.column == cp.column) { - editor.pushEmacsMark(); - return; - } + if (!mark) { + rangePositions.forEach(function(pos) { editor.pushEmacsMark(pos); }); + editor.setEmacsMark(rangePositions[rangePositions.length-1]); + return; } - // turn on mark mode - mark = editor.getCursorPosition(); - editor.setEmacsMark(mark); - editor.selection.setSelectionAnchor(mark.row, mark.column); + + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + + function moveToMark() { + var mark = editor.popEmacsMark(); + mark && editor.moveCursorToPosition(mark); + } + }, readOnly: true, - handlesCount: true, - multiSelectAction: "forEach" + handlesCount: true }, exchangePointAndMark: { - exec: function(editor, args) { + exec: function exchangePointAndMark$exec(editor, args) { var sel = editor.selection; - if (args.count) { - var pos = editor.getCursorPosition(); + if (!args.count && !sel.isEmpty()) { // just invert selection + sel.setSelectionRange(sel.getRange(), !sel.isBackwards()); + return; + } + + if (args.count) { // replace mark and point + var pos = {row: sel.lead.row, column: sel.lead.column}; sel.clearSelection(); - sel.moveCursorToPosition(editor.popEmacsMark()); - editor.pushEmacsMark(pos); - return; + sel.moveCursorToPosition(editor.emacsMarkForSelection(pos)); + } else { // create selection to last mark + sel.selectToPosition(editor.emacsMarkForSelection()); } - var lastMark = editor.getLastEmacsMark(); - var range = sel.getRange(); - if (range.isEmpty()) { - sel.selectToPosition(lastMark); - return; - } - sel.setSelectionRange(range, !sel.isBackwards()); }, readOnly: true, handlesCount: true, @@ -544,6 +573,7 @@ exports.handler.addCommands({ if (editor.keyBinding.$data.lastCommand != "yank") return; editor.undo(); + editor.session.$emacsMarkRing.pop(); // also undo recording mark editor.onPaste(exports.killRing.rotate()); editor.keyBinding.$data.lastCommand = "yank"; }, @@ -557,12 +587,25 @@ exports.handler.addCommands({ }, killRingSave: { exec: function(editor) { + // copy text and deselect. will save marks for starts of the + // selection(s) + + editor.$handlesEmacsOnCopy = true; + var marks = editor.session.$emacsMarkRing.slice(), + deselectedMarks = []; exports.killRing.add(editor.getCopyText()); + setTimeout(function() { - var sel = editor.selection, - range = sel.getRange(); - editor.pushEmacsMark(sel.isBackwards() ? range.end : range.start); - sel.clearSelection(); + function deselect() { + var sel = editor.selection, range = sel.getRange(), + pos = sel.isBackwards() ? range.end : range.start; + deselectedMarks.push({row: pos.row, column: pos.column}); + sel.clearSelection(); + } + editor.$handlesEmacsOnCopy = false; + if (editor.inMultiSelectMode) editor.forEachSelection({exec: deselect}); + else deselect(); + editor.session.$emacsMarkRing = marks.concat(deselectedMarks.reverse()); }, 0); }, readOnly: true @@ -570,6 +613,7 @@ exports.handler.addCommands({ keyboardQuit: function(editor) { editor.selection.clearSelection(); editor.setEmacsMark(null); + editor.keyBinding.$data.count = null; }, focusCommandLine: function(editor, arg) { if (editor.showCommandLine) diff --git a/lib/ace/keyboard/emacs_test.js b/lib/ace/keyboard/emacs_test.js index d1aba564..3ba5efaa 100644 --- a/lib/ace/keyboard/emacs_test.js +++ b/lib/ace/keyboard/emacs_test.js @@ -35,17 +35,29 @@ if (typeof process !== "undefined") { define(function(require, exports, module) { "use strict"; +require("../multi_select"); + var EditSession = require("./../edit_session").EditSession, Editor = require("./../editor").Editor, + Range = require("./../range").Range, MockRenderer = require("./../test/mockrenderer").MockRenderer, emacs = require('./emacs'), assert = require("./../test/assertions"), - editor; + editor, sel; function initEditor(docString) { var doc = new EditSession(docString.split("\n")); editor = new Editor(new MockRenderer(), doc); editor.setKeyboardHandler(emacs.handler); + sel = editor.selection; +} + +function print(obj) { + return JSON.stringify(obj, null, 2); +} + +function pluck(arr, what) { + return arr.map(function(ea) { return ea[what]; }); } module.exports = { @@ -62,6 +74,72 @@ module.exports = { editor.selectAll(); editor.execCommand('keyboardQuit'); assert.ok(editor.selection.isEmpty(), 'selection non-empty'); + }, + + "test: exchangePointAndMark without mark set": function() { + initEditor('foo'); + sel.setRange(Range.fromPoints({row: 0, column: 1}, {row: 0, column: 3})); + editor.execCommand('exchangePointAndMark'); + assert.deepEqual({row: 0, column: 1}, editor.getCursorPosition(), print(editor.getCursorPosition())); + }, + + "test: exchangePointAndMark with mark set": function() { + initEditor('foo'); + editor.pushEmacsMark({row: 0, column: 1}); + editor.pushEmacsMark({row: 0, column: 2}); + editor.execCommand('exchangePointAndMark', {count: 4}); + assert.deepEqual({row: 0, column: 2}, editor.getCursorPosition(), print(editor.getCursorPosition())); + assert.deepEqual([{row: 0, column: 1}, {row: 0, column: 0}], editor.session.$emacsMarkRing, print(editor.session.$emacsMarkRing)); + }, + + "test: exchangePointAndMark with selection": function() { + initEditor('foo'); + editor.pushEmacsMark({row: 0, column: 1}); + editor.pushEmacsMark({row: 0, column: 2}); + sel.setRange(Range.fromPoints({row: 0, column: 0}, {row: 0, column: 1}), true); + editor.execCommand('exchangePointAndMark'); + assert.deepEqual({row: 0, column: 1}, editor.getCursorPosition(), print(editor.getCursorPosition())); + assert.deepEqual([{row: 0, column: 1}, {row: 0, column: 2}], editor.session.$emacsMarkRing, print(editor.session.$emacsMarkRing)); + }, + + "test: exchangePointAndMark with multi selection": function() { + initEditor('foo\nhello world\n123'); + var ranges = [[{row: 0, column: 0}, {row: 0, column: 3}], + [{row: 1, column: 0}, {row: 1, column: 5}], + [{row: 1, column: 6}, {row: 1, column: 11}]] + ranges.forEach(function(r) { + sel.addRange(Range.fromPoints(r[0], r[1])); + }); + assert.equal("foo\nhello\nworld", editor.getSelectedText()); + editor.execCommand('exchangePointAndMark'); + assert.equal("foo\nhello\nworld", editor.getSelectedText()); + assert.deepEqual(pluck(ranges, 0), pluck(sel.getAllRanges(), 'cursor'), "selections dir not inverted"); + }, + + "test: exchangePointAndMark with multi cursors": function() { + initEditor('foo\nhello world\n123'); + var ranges = [[{row: 0, column: 0}, {row: 0, column: 3}], + [{row: 1, column: 0}, {row: 1, column: 5}], + [{row: 1, column: 6}, {row: 1, column: 11}]]; + // move cursors to the start of each range and set a mark to its end + // without selecting anything + ranges.forEach(function(r) { + editor.pushEmacsMark(r[1]); + sel.addRange(Range.fromPoints(r[0], r[0])); + }); + assert.deepEqual(pluck(ranges, 0), pluck(sel.getAllRanges(), 'cursor'), print(sel.getAllRanges())); + editor.execCommand('exchangePointAndMark'); + assert.deepEqual(pluck(ranges, 1), pluck(sel.getAllRanges(), 'cursor'), "not inverted: " + print(sel.getAllRanges())); + }, + + "test: setMark with multi cursors": function() { + initEditor('foo\nhello world\n123'); + var positions = [{row: 0, column: 0}, + {row: 1, column: 0}, + {row: 1, column: 6}]; + positions.forEach(function(p) { sel.addRange(Range.fromPoints(p,p)); }); + editor.execCommand('setMark'); + assert.deepEqual(positions, editor.session.$emacsMarkRing, print(editor.session.$emacsMarkRing)); } }; diff --git a/lib/ace/keyboard/keybinding.js b/lib/ace/keyboard/keybinding.js index c43d7930..cddf0666 100644 --- a/lib/ace/keyboard/keybinding.js +++ b/lib/ace/keyboard/keybinding.js @@ -89,8 +89,16 @@ var KeyBinding = function(editor) { this.getKeyboardHandler = function() { return this.$handlers[this.$handlers.length - 1]; }; + + this.getStatusText = function() { + var data = this.$data; + var editor = data.editor; + return this.$handlers.map(function(h) { + return h.getStatusText && h.getStatusText(editor, data) || ""; + }).filter(Boolean).join(" "); + }; - this.$callKeyboardHandlers = function (hashId, keyString, keyCode, e) { + this.$callKeyboardHandlers = function(hashId, keyString, keyCode, e) { var toExecute; var success = false; var commands = this.$editor.commands;