diff --git a/lib/ace/editor.js b/lib/ace/editor.js index 5ddd14f2..62a4c37f 100644 --- a/lib/ace/editor.js +++ b/lib/ace/editor.js @@ -1800,7 +1800,7 @@ var Editor = function(renderer, session) { this.$blockScrolling += 1; // todo: find a way to automatically exit multiselect mode - this.exitMultiSelectMode && this.exitMultiSelectMode() + this.exitMultiSelectMode && this.exitMultiSelectMode(); this.moveCursorTo(lineNumber - 1, column || 0); this.$blockScrolling -= 1; diff --git a/lib/ace/incremental_search.js b/lib/ace/incremental_search.js index e4b84229..e6fa8928 100644 --- a/lib/ace/incremental_search.js +++ b/lib/ace/incremental_search.js @@ -113,6 +113,8 @@ oop.inherits(IncrementalSearch, Search); 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); diff --git a/lib/ace/keyboard/emacs.js b/lib/ace/keyboard/emacs.js index 87423711..1cb84f59 100644 --- a/lib/ace/keyboard/emacs.js +++ b/lib/ace/keyboard/emacs.js @@ -95,16 +95,36 @@ exports.handler.attach = function(editor) { $formerLineStart = editor.session.$useEmacsStyleLineStart; editor.session.$useEmacsStyleLineStart = true; - editor.session.$emacsMark = null; + editor.session.$emacsMark = null; // the active mark + editor.session.$emacsMarkRing = editor.session.$emacsMarkRing || []; - editor.emacsMarkMode = function() { + editor.emacsMark = function() { return this.session.$emacsMark; } - editor.setEmacsMarkMode = function(p) { + editor.setEmacsMark = function(p) { + // to deactivate pass in a falsy value this.session.$emacsMark = p; } + editor.pushEmacsMark = function(p, activate) { + var prevMark = this.session.$emacsMark; + if (prevMark) + this.session.$emacsMarkRing.push(prevMark); + if (!p || activate) this.setEmacsMark(p) + else this.session.$emacsMarkRing.push(p); + } + + editor.popEmacsMark = function() { + var mark = this.emacsMark(); + if (mark) { this.setEmacsMark(null); return mark; } + return this.session.$emacsMarkRing.pop(); + } + + editor.getLastEmacsMark = function(p) { + return this.session.$emacsMark || this.session.$emacsMarkRing.slice(-1)[0]; + } + editor.on("click", $resetMarkMode); editor.on("changeSession", $kbSessionChange); editor.renderer.screenToTextCoordinates = screenToTextBlockCoordinates; @@ -112,6 +132,8 @@ exports.handler.attach = function(editor) { editor.commands.addCommands(commands); exports.handler.platform = editor.commands.platform; editor.$emacsModeHandler = this; + editor.addEventListener('copy', this.onCopy); + editor.addEventListener('paste', this.onPaste); }; exports.handler.detach = function(editor) { @@ -122,6 +144,8 @@ exports.handler.detach = function(editor) { editor.removeEventListener("changeSession", $kbSessionChange); editor.unsetStyle("emacs-mode"); editor.commands.removeCommands(commands); + editor.removeEventListener('copy', this.onCopy); + editor.removeEventListener('paste', this.onPaste); }; var $kbSessionChange = function(e) { @@ -137,6 +161,8 @@ var $kbSessionChange = function(e) { if (!e.session.hasOwnProperty('$emacsMark')) e.session.$emacsMark = null; + if (!e.session.hasOwnProperty('$emacsMarkRing')) + e.session.$emacsMarkRing = []; } var $resetMarkMode = function(e) { @@ -157,6 +183,17 @@ combinations.forEach(function(c) { eMods[hashId] = c.toLowerCase() + "-"; }); +exports.handler.onCopy = function(e, editor) { + if (editor.$handlesEmacsOnCopy) return; + editor.$handlesEmacsOnCopy = true; + exports.handler.commands.killRingSave.exec(editor); + delete editor.$handlesEmacsOnCopy; +} + +exports.handler.onPaste = function(e, editor) { + editor.pushEmacsMark(editor.getCursorPosition()); +} + exports.handler.bindKey = function(key, command) { if (!key) return; @@ -165,18 +202,27 @@ exports.handler.bindKey = function(key, command) { key.split("|").forEach(function(keyPart) { keyPart = keyPart.toLowerCase(); ckb[keyPart] = command; - keyPart = keyPart.split(" ")[0]; - if (!ckb[keyPart]) - ckb[keyPart] = "null"; + // register all partial key combos as null commands + // to be able to activate key combos with arbitrary length + // Example: if keyPart is "C-c C-l t" then "C-c C-l t" will + // get command assigned and "C-c" and "C-c C-l" will get + // a null command assigned in this.commmandKeyBinding. For + // the lookup logic see handleKeyboard() + var keyParts = keyPart.split(" ").slice(0,-1); + keyParts.reduce(function(keyMapKeys, keyPart, i) { + var prefix = keyMapKeys[i-1] ? keyMapKeys[i-1] + ' ' : ''; + return keyMapKeys.concat([prefix + keyPart]); + }, []).forEach(function(keyPart) { + if (!ckb[keyPart]) ckb[keyPart] = "null"; + }); }, this); -}; - +} exports.handler.handleKeyboard = function(data, hashId, key, keyCode) { var editor = data.editor; // insertstring data.count times if (hashId == -1) { - editor.setEmacsMarkMode(null); + editor.pushEmacsMark(); if (data.count) { var str = Array(data.count + 1).join(key); data.count = null; @@ -190,10 +236,15 @@ exports.handler.handleKeyboard = function(data, hashId, key, keyCode) { // CTRL + number / universalArgument for setting data.count if (modifier == "c-" || data.universalArgument) { + var prevCount = String(data.count || 0); var count = parseInt(key[key.length - 1]); - if (count) { - data.count = count; + if (typeof count === 'number' && !isNaN(count)) { + data.count = parseInt(prevCount + 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; @@ -230,7 +281,7 @@ exports.handler.handleKeyboard = function(data, hashId, key, keyCode) { args = command.args; if (command.command) command = command.command; if (command === "goorselect") { - command = editor.emacsMarkMode() ? args[1] : args[0]; + command = editor.emacsMark() ? args[1] : args[0]; args = null; } } @@ -239,7 +290,7 @@ exports.handler.handleKeyboard = function(data, hashId, key, keyCode) { if (command === "insertstring" || command === "splitline" || command === "togglecomment") { - editor.setEmacsMarkMode(null); + editor.pushEmacsMark(); } command = this.commands[command] || editor.commands.commands[command]; if (!command) return undefined; @@ -251,15 +302,20 @@ exports.handler.handleKeyboard = function(data, hashId, key, keyCode) { if (data.count) { var count = data.count; data.count = 0; - return { - args: args, - command: { - exec: function(editor, args) { - for (var i = 0; i < count; i++) - command.exec(editor, args); + if (!command || !command.handlesCount) { + return { + args: args, + command: { + exec: function(editor, args) { + for (var i = 0; i < count; i++) + command.exec(editor, args); + } } - } - }; + }; + } else { + if (!args) args = {} + if (typeof args === 'object') args.count = count; + } } return {command: command, args: args}; @@ -367,36 +423,70 @@ exports.handler.addCommands({ selectRectangularRegion: function(editor) { editor.multiSelect.toggleBlockSelection(); }, - setMark: function(editor) { - // Emulate emacs highlighting behaviour in transient-mark-mode. - // Sets mark-mode and clears current selection. - // When mark is set, keyboard cursor movement commands become - // 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 - var markMode = editor.emacsMarkMode(); - if (markMode) { - var cp = editor.getCursorPosition(); - if (editor.selection.isEmpty() && - markMode.row == cp.row && markMode.column == cp.column) { - editor.setEmacsMarkMode(null); - // console.log("Mark mode off"); + setMark: { + exec: function(editor, args) { + // Sets mark-mode and clears current selection. + // When mark is set, keyboard cursor movement commands become + // 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 + if (args && args.count) { + var mark = editor.popEmacsMark(); + mark && editor.selection.moveCursorToPosition(mark); return; } - } - // turn on mark mode - markMode = editor.getCursorPosition(); - editor.setEmacsMarkMode(markMode); - editor.selection.setSelectionAnchor(markMode.row, markMode.column); - }, - exchangePointAndMark: { - exec: function(editor) { - var range = editor.selection.getRange(); - editor.selection.setSelectionRange(range, !editor.selection.isBackwards()); + + var mark = editor.emacsMark(), + transientMarkModeActive = true; + + // 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(); + return; + } + + if (mark) { + var cp = editor.getCursorPosition(); + if (editor.selection.isEmpty() && + mark.row == cp.row && mark.column == cp.column) { + editor.pushEmacsMark(); + return; + } + } + // turn on mark mode + mark = editor.getCursorPosition(); + editor.setEmacsMark(mark); + editor.selection.setSelectionAnchor(mark.row, mark.column); }, readonly: true, - multiselectAction: "forEach" + handlesCount: true, + multiSelectAction: "forEach" + }, + exchangePointAndMark: { + exec: function(editor, args) { + var sel = editor.selection; + if (args.count) { + var pos = editor.getCursorPosition(); + sel.clearSelection(); + sel.moveCursorToPosition(editor.popEmacsMark()); + editor.pushEmacsMark(pos); + return; + } + var lastMark = editor.getLastEmacsMark(); + var range = sel.getRange(); + if (range.isEmpty()) { + sel.selectToPosition(lastMark); + return; + } + sel.setSelectionRange(range, !sel.isBackwards()); + }, + readonly: true, + handlesCount: true, + multiSelectAction: "forEach" }, killWord: { exec: function(editor, dir) { @@ -413,10 +503,10 @@ exports.handler.addCommands({ editor.session.remove(range); editor.clearSelection(); }, - multiselectAction: "forEach" + multiSelectAction: "forEach" }, killLine: function(editor) { - editor.setEmacsMarkMode(null); + editor.pushEmacsMark(null); var pos = editor.getCursorPosition(); if (pos.column == 0 && editor.session.doc.getLine(pos.row).length == 0) { @@ -438,7 +528,7 @@ exports.handler.addCommands({ editor.clearSelection(); }, yank: function(editor) { - editor.onPaste(exports.killRing.get()); + editor.onPaste(exports.killRing.get() || ''); editor.keyBinding.$data.lastCommand = "yank"; }, yankRotate: function(editor) { @@ -448,16 +538,29 @@ exports.handler.addCommands({ editor.onPaste(exports.killRing.rotate()); editor.keyBinding.$data.lastCommand = "yank"; }, - killRegion: function(editor) { - exports.killRing.add(editor.getCopyText()); - editor.commands.byName.cut.exec(editor); + killRegion: { + exec: function(editor) { + exports.killRing.add(editor.getCopyText()); + editor.commands.byName.cut.exec(editor); + }, + readonly: true, + multiSelectAction: "forEach" }, - killRingSave: function(editor) { - exports.killRing.add(editor.getCopyText()); + killRingSave: { + exec: function(editor) { + exports.killRing.add(editor.getCopyText()); + setTimeout(function() { + var sel = editor.selection, + range = sel.getRange(); + editor.pushEmacsMark(sel.isBackwards() ? range.end : range.start); + sel.clearSelection(); + }, 0); + }, + readonly: true }, keyboardQuit: function(editor) { editor.selection.clearSelection(); - editor.setEmacsMarkMode(null); + editor.setEmacsMark(null); }, focusCommandLine: function(editor, arg) { if (editor.showCommandLine) @@ -478,8 +581,9 @@ exports.killRing = { if (this.$data.length > 30) this.$data.shift(); }, - get: function() { - return this.$data[this.$data.length - 1] || ""; + get: function(n) { + n = n || 1; + return this.$data.slice(this.$data.length-n, this.$data.length).reverse().join('\n'); }, pop: function() { if (this.$data.length > 1) diff --git a/lib/ace/multi_select.js b/lib/ace/multi_select.js index d00bfb05..2de7fc1d 100644 --- a/lib/ace/multi_select.js +++ b/lib/ace/multi_select.js @@ -514,14 +514,14 @@ var Editor = require("./editor").Editor; * @method Editor.exitMultiSelectMode **/ this.exitMultiSelectMode = function() { - if (this.inVirtualSelectionMode) + if (!this.inMultiSelectMode || this.inVirtualSelectionMode) return; this.multiSelect.toSingleRange(); }; this.getCopyText = function() { var text = ""; - if (this.inMultiSelectMode) { + if (this.inMultiSelectMode && !this.inVirtualSelectionMode) { var ranges = this.multiSelect.rangeList.ranges; var buf = []; for (var i = 0; i < ranges.length; i++) { @@ -550,10 +550,10 @@ var Editor = require("./editor").Editor; var lines = text.split(/\r\n|\r|\n/); var ranges = this.selection.rangeList.ranges; - if (lines.length > ranges.length || (lines.length <= 2 && !lines[1])) + if (lines.length > ranges.length || lines.length < 2 || !lines[1]) return this.commands.exec("insertstring", this, text); - for (var i = ranges.length; i--; ) { + for (var i = ranges.length; i--;) { var range = ranges[i]; if (!range.isEmpty()) this.session.remove(range);