diff --git a/lib/ace/keyboard/emacs.js b/lib/ace/keyboard/emacs.js index d6c54b72..7fb93b0f 100644 --- a/lib/ace/keyboard/emacs.js +++ b/lib/ace/keyboard/emacs.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 @@ -56,7 +56,6 @@ var $formerLineStart; exports.handler.attach = function(editor) { if (!initialized) { initialized = true; - dom.importCssString('\ .emacs-mode .ace_cursor{\ border: 2px rgba(50,250,50,0.8) solid!important;\ @@ -89,9 +88,9 @@ exports.handler.attach = function(editor) { // CTRL-A should go to actual beginning of line $formerLineStart = editor.session.$useEmacsStyleLineStart; editor.session.$useEmacsStyleLineStart = true; - + editor.session.$emacsMark = null; - + exports.markMode = function() { return editor.session.$emacsMark; } @@ -101,27 +100,21 @@ exports.handler.attach = function(editor) { } editor.on("click",$resetMarkMode); - editor.on("changeSession",$kbSessionChange); - editor.renderer.screenToTextCoordinates = screenToTextBlockCoordinates; - editor.setStyle("emacs-mode"); - + editor.commands.addCommands(commands); + exports.handler.platform = editor.commands.platform; }; exports.handler.detach = function(editor) { - delete editor.renderer.screenToTextCoordinates; - editor.session.$selectLongWords = $formerLongWords; editor.session.$useEmacsStyleLineStart = $formerLineStart; - - editor.removeEventListener("click",$resetMarkMode); editor.removeEventListener("changeSession",$kbSessionChange); - editor.unsetStyle("emacs-mode"); + editor.commands.removeCommands(commands); }; var $kbSessionChange = function(e) { @@ -129,7 +122,7 @@ var $kbSessionChange = function(e) { e.oldSession.$selectLongWords = $formerLongWords; e.oldSession.$useEmacsStyleLineStart = $formerLineStart; } - + $formerLongWords = e.session.$selectLongWords; e.session.$selectLongWords = true; $formerLineStart = e.session.$useEmacsStyleLineStart; @@ -137,19 +130,21 @@ var $kbSessionChange = function(e) { if (!e.session.hasOwnProperty('$emacsMark')) e.session.$emacsMark = null; -} +} var $resetMarkMode = function(e) { e.editor.session.$emacsMark = null; } -var keys = require("../lib/keys").KEY_MODS; -var eMods = { - C: "ctrl", S: "shift", M: "alt" -}; -["S-C-M", "S-C", "S-M", "C-M", "S", "C", "M"].forEach(function(c) { +var keys = require("../lib/keys").KEY_MODS, + eMods = {C: "ctrl", S: "shift", M: "alt", CMD: "command"}, + combinations = ["C-S-M-CMD", + "S-M-CMD", "C-M-CMD", "C-S-CMD", "C-S-M", + "M-CMD", "S-CMD", "S-M", "C-CMD", "C-M", "C-S", + "CMD", "M", "S", "C"]; +combinations.forEach(function(c) { var hashId = 0; - c.split("-").forEach(function(c){ + c.split("-").forEach(function(c) { hashId = hashId | keys[eMods[c]]; }); eMods[hashId] = c.toLowerCase() + "-"; @@ -171,6 +166,7 @@ exports.handler.bindKey = function(key, command) { exports.handler.handleKeyboard = function(data, hashId, key, keyCode) { + // insertstring data.count times if (hashId == -1) { exports.setMarkMode(null); if (data.count) { @@ -184,6 +180,8 @@ exports.handler.handleKeyboard = function(data, hashId, key, keyCode) { return; var modifier = eMods[hashId]; + + // CTRL + number / universalArgument for setting data.count if (modifier == "c-" || data.universalArgument) { var count = parseInt(key[key.length - 1]); if (count) { @@ -193,42 +191,47 @@ exports.handler.handleKeyboard = function(data, hashId, key, keyCode) { } data.universalArgument = false; - if (modifier) - key = modifier + key; + // this.commandKeyBinding maps key specs like "c-p" (for CTRL + P) to + // command objects, for lookup key needs to include the modifier + if (modifier) key = modifier + key; - if (data.keyChain) - key = data.keyChain += " " + key; + // Key combos like CTRL+X H build up the data.keyChain + if (data.keyChain) key = data.keyChain += " " + key; + // Key combo prefixes get stored as "null" (String!) in this + // this.commmandKeyBinding. When encountered no command is invoked but we + // buld up data.keyChain var command = this.commmandKeyBinding[key]; data.keyChain = command == "null" ? key : ""; - if (!command) - return; + // there really is no command + if (!command) return; - if (command == "null") - return {command: "null"}; + // we pass b/c of key combo or universalArgument + if (command === "null") return {command: "null"}; - if (command == "universalArgument") { + if (command === "universalArgument") { data.universalArgument = true; return {command: "null"}; } - if (typeof command != "string") { - var args = command.args; - command = command.command; - if (command == "goorselect") { - command = args[0]; - if (exports.markMode()) { - command = args[1]; - } + // lookup command + // TODO extract special handling of markmode + // TODO special case command.command is really unnecessary, remove + var args; + if (typeof command !== "string") { + args = command.args; + if (command.command) command = command.command; + if (command === "goorselect") { + command = exports.markMode() ? args[1] : args[0]; args = null; } } - if (typeof command == "string") { - if (command == "insertstring" || - command == "splitline" || - command == "togglecomment") { + if (typeof command === "string") { + if (command === "insertstring" || + command === "splitline" || + command === "togglecomment") { exports.setMarkMode(null); } command = this.commands[command] || data.editor.commands.commands[command]; @@ -256,11 +259,11 @@ exports.handler.handleKeyboard = function(data, hashId, key, keyCode) { exports.emacsKeys = { // movement - "Up|C-p" : {command: "goorselect", args: ["golineup","selectup"]}, - "Down|C-n" : {command: "goorselect", args: ["golinedown","selectdown"]}, + "Up|C-p" : {command: "goorselect", args: ["golineup","selectup"]}, + "Down|C-n" : {command: "goorselect", args: ["golinedown","selectdown"]}, "Left|C-b" : {command: "goorselect", args: ["gotoleft","selectleft"]}, "Right|C-f" : {command: "goorselect", args: ["gotoright","selectright"]}, - "C-Left|M-b" : {command: "goorselect", args: ["gotowordleft","selectwordleft"]}, + "C-Left|M-b" : {command: "goorselect", args: ["gotowordleft","selectwordleft"]}, "C-Right|M-f" : {command: "goorselect", args: ["gotowordright","selectwordright"]}, "Home|C-a" : {command: "goorselect", args: ["gotolinestart","selecttolinestart"]}, "End|C-e" : {command: "goorselect", args: ["gotolineend","selecttolineend"]}, @@ -320,7 +323,7 @@ exports.emacsKeys = { "M-u": "touppercase", // Doesn't work "M-l": "tolowercase", "M-/": "autocomplete", // Doesn't work - "C-u": "universalArgument", + "C-u": "universalArgument", "M-;": "togglecomment", @@ -340,7 +343,7 @@ exports.handler.addCommands({ recenterTopBottom: function(editor) { var renderer = editor.renderer; var pos = renderer.$cursorLayer.getPixelPosition(); - var h = renderer.$size.scrollerHeight - renderer.lineHeight; + var h = renderer.$size.scrollerHeight - renderer.lineHeight; var scrollTop = renderer.scrollTop; if (Math.abs(pos.top - scrollTop) < 2) { scrollTop = pos.top - h; @@ -357,16 +360,14 @@ exports.handler.addCommands({ 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 + // 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 = exports.markMode(); - if (markMode) { - - cp = editor.getCursorPosition(); + var cp = editor.getCursorPosition(); if (editor.selection.isEmpty() && markMode.row == cp.row && markMode.column == cp.column) { exports.setMarkMode(null); @@ -374,17 +375,15 @@ exports.handler.addCommands({ return; } } - // turn on mark mode markMode = editor.getCursorPosition(); exports.setMarkMode(markMode); editor.selection.setSelectionAnchor(markMode.row, markMode.column); - + }, exchangePointAndMark: { exec: function(editor) { var range = editor.selection.getRange(); - editor.selection.setSelectionRange(range, !editor.selection.isBackwards()); }, readonly: true, @@ -410,7 +409,6 @@ exports.handler.addCommands({ killLine: function(editor) { exports.setMarkMode(null); var pos = editor.getCursorPosition(); - if (pos.column == 0 && editor.session.doc.getLine(pos.row).length == 0) { // If an already empty line is killed, remove @@ -422,7 +420,6 @@ exports.handler.addCommands({ // the cursor) editor.clearSelection(); editor.selection.selectLineEnd(); - } var range = editor.getSelectionRange(); var text = editor.session.getTextRange(range); @@ -438,7 +435,6 @@ exports.handler.addCommands({ yankRotate: function(editor) { if (editor.keyBinding.$data.lastCommand != "yank") return; - editor.undo(); editor.onPaste(exports.killRing.rotate()); editor.keyBinding.$data.lastCommand = "yank"; @@ -449,6 +445,10 @@ exports.handler.addCommands({ }, killRingSave: function(editor) { exports.killRing.add(editor.getCopyText()); + }, + keyboardQuit: function(editor) { + editor.selection.clearSelection(); + exports.setMarkMode(null); } }); diff --git a/lib/ace/keyboard/emacs_test.js b/lib/ace/keyboard/emacs_test.js new file mode 100644 index 00000000..d1aba564 --- /dev/null +++ b/lib/ace/keyboard/emacs_test.js @@ -0,0 +1,73 @@ +/* ***** 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, + Editor = require("./../editor").Editor, + MockRenderer = require("./../test/mockrenderer").MockRenderer, + emacs = require('./emacs'), + assert = require("./../test/assertions"), + editor; + +function initEditor(docString) { + var doc = new EditSession(docString.split("\n")); + editor = new Editor(new MockRenderer(), doc); + editor.setKeyboardHandler(emacs.handler); +} + +module.exports = { + + "test: detach removes emacs commands from command manager": function() { + initEditor(''); + assert.ok(!!editor.commands.byName["keyboardQuit"], 'setup error: emacs commands not installed'); + editor.keyBinding.removeKeyboardHandler(editor.getKeyboardHandler()); + assert.ok(!editor.commands.byName["keyboardQuit"], 'emacs commands not removed'); + }, + + "test: keyboardQuit clears selection": function() { + initEditor('foo'); + editor.selectAll(); + editor.execCommand('keyboardQuit'); + assert.ok(editor.selection.isEmpty(), 'selection non-empty'); + } + +}; + +}); + +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 9437426a..ce5857dc 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/keyboard/emacs_test", "ace/layer/text_test", "ace/lib/event_emitter_test", "ace/mode/coffee/parser_test",