From 1cba46b8d3340e17244157aede38d36970e3354b Mon Sep 17 00:00:00 2001 From: Julian Viereck Date: Wed, 22 Dec 2010 19:25:02 +0100 Subject: [PATCH] First iteration of porting Bespin's keyboardmapping over to ace. --- lib/ace/commands/default_commands.js | 12 +- lib/ace/conf/keybindings/default_mac.js | 2 +- lib/ace/conf/keybindings/default_win.js | 2 +- lib/ace/editor.js | 22 ++- lib/ace/keybinding.js | 32 +++- lib/ace/keyboardstate.js | 193 ++++++++++++++++++++++++ lib/ace/mode/vim.js | 121 +++++++++++++++ 7 files changed, 364 insertions(+), 20 deletions(-) create mode 100644 lib/ace/keyboardstate.js create mode 100644 lib/ace/mode/vim.js diff --git a/lib/ace/commands/default_commands.js b/lib/ace/commands/default_commands.js index 7ff66bf9..43e31aa3 100644 --- a/lib/ace/commands/default_commands.js +++ b/lib/ace/commands/default_commands.js @@ -20,6 +20,7 @@ * * Contributor(s): * Fabian Jakobs + * Julian Viereck * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -39,6 +40,11 @@ define(function(require, exports, module) { var canon = require("pilot/canon"); +canon.addCommand({ + name: "null", + exec: function(env, args, request) { } +}); + canon.addCommand({ name: "selectall", exec: function(env, args, request) { env.editor.getSelection().selectAll(); } @@ -113,7 +119,7 @@ canon.addCommand({ }); canon.addCommand({ name: "golineup", - exec: function(env, args, request) { env.editor.navigateUp(); } + exec: function(env, args, request) { env.editor.navigateUp(args.times); } }); canon.addCommand({ name: "copylinesdown", @@ -136,8 +142,8 @@ canon.addCommand({ exec: function(env, args, request) { env.editor.getSelection().selectDown(); } }); canon.addCommand({ - name: "godown", - exec: function(env, args, request) { env.editor.navigateDown(); } + name: "golinedown", + exec: function(env, args, request) { env.editor.navigateDown(args.times); } }); canon.addCommand({ name: "selectwordleft", diff --git a/lib/ace/conf/keybindings/default_mac.js b/lib/ace/conf/keybindings/default_mac.js index 788ed577..99b7d6d3 100644 --- a/lib/ace/conf/keybindings/default_mac.js +++ b/lib/ace/conf/keybindings/default_mac.js @@ -60,7 +60,7 @@ exports.bindings = { "selecttoend": "Command-Shift-Down", "gotoend": "Command-End|Command-Down", "selectdown": "Shift-Down", - "godown": "Down", + "golinedown": "Down", "selectwordleft": "Option-Shift-Left", "gotowordleft": "Option-Left", "selecttolinestart": "Command-Shift-Left", diff --git a/lib/ace/conf/keybindings/default_win.js b/lib/ace/conf/keybindings/default_win.js index 3ae134b9..a27c098d 100644 --- a/lib/ace/conf/keybindings/default_win.js +++ b/lib/ace/conf/keybindings/default_win.js @@ -60,7 +60,7 @@ exports.bindings = { "selecttoend": "Alt-Shift-Down", "gotoend": "Ctrl-End|Ctrl-Down", "selectdown": "Shift-Down", - "godown": "Down", + "golinedown": "Down", "selectwordleft": "Ctrl-Shift-Left", "gotowordleft": "Ctrl-Left", "selecttolinestart": "Alt-Shift-Left", diff --git a/lib/ace/editor.js b/lib/ace/editor.js index 1e7da684..eb46cf96 100644 --- a/lib/ace/editor.js +++ b/lib/ace/editor.js @@ -857,9 +857,9 @@ var Editor =function(renderer, doc) { this.$updateDesiredColumn(column); }; - this.navigateUp = function() { + this.navigateUp = function(times) { this.selection.clearSelection(); - this.selection.moveCursorBy(-1, 0); + this.selection.moveCursorBy(-(times || 1), 0); if (this.$desiredColumn) { var cursor = this.getCursorPosition(); @@ -868,9 +868,9 @@ var Editor =function(renderer, doc) { } }; - this.navigateDown = function() { + this.navigateDown = function(times) { this.selection.clearSelection(); - this.selection.moveCursorBy(1, 0); + this.selection.moveCursorBy(times || 1, 0); if (this.$desiredColumn) { var cursor = this.getCursorPosition(); @@ -884,24 +884,30 @@ var Editor =function(renderer, doc) { this.$desiredColumn = this.doc.documentToScreenColumn(cursor.row, cursor.column); }; - this.navigateLeft = function() { + this.navigateLeft = function(times) { if (!this.selection.isEmpty()) { var selectionStart = this.getSelectionRange().start; this.moveCursorToPosition(selectionStart); } else { - this.selection.moveCursorLeft(); + times = times | 1; + while (times--) { + this.selection.moveCursorLeft(); + } } this.clearSelection(); }; - this.navigateRight = function() { + this.navigateRight = function(times) { if (!this.selection.isEmpty()) { var selectionEnd = this.getSelectionRange().end; this.moveCursorToPosition(selectionEnd); } else { - this.selection.moveCursorRight(); + times = times | 1; + while (times--) { + this.selection.moveCursorRight(); + } } this.clearSelection(); }; diff --git a/lib/ace/keybinding.js b/lib/ace/keybinding.js index db54ac00..ba005f30 100644 --- a/lib/ace/keybinding.js +++ b/lib/ace/keybinding.js @@ -20,6 +20,7 @@ * * Contributor(s): * Fabian Jakobs + * Julian Viereck * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -41,11 +42,13 @@ var useragent = require("pilot/useragent"); var event = require("pilot/event"); var default_mac = require("ace/conf/keybindings/default_mac").bindings; var default_win = require("ace/conf/keybindings/default_win").bindings; +var vim_mode = require("ace/mode/vim"); var canon = require("pilot/canon"); require("ace/commands/default_commands"); var KeyBinding = function(element, editor, config) { this.setConfig(config); + var data = { }; var _self = this; event.addKeyListener(element, function(e) { @@ -56,15 +59,30 @@ var KeyBinding = function(element, editor, config) { else var hashId = 0 | (e.ctrlKey ? 1 : 0) | (e.altKey ? 2 : 0) | (e.shiftKey ? 4 : 0) | (e.metaKey ? 8 : 0); - - var key = _self.keyNames[e.keyCode]; - var commandName = (_self.config.reverse[hashId] || {})[(key - || String.fromCharCode(e.keyCode)).toLowerCase()]; + var key = (_self.keyNames[e.keyCode] || + String.fromCharCode(e.keyCode)).toLowerCase(); - var success = canon.exec(commandName, {editor: editor}); - if (success) { - return event.stopEvent(e); + var toExecute; + if (true) { + var toExecute = + vim_mode.handleKeyboard(data, hashId, key, e); + } + + // If there is nothing to execute yet, then use the default keymapping. + if (!toExecute) { + toExecute = { + command: (_self.config.reverse[hashId] || {})[key] + }; + } + + // If there is something to execute, then go for it. + if (toExecute) { + var success = canon.exec(toExecute.command, + {editor: editor}, toExecute.args); + if (success) { + return event.stopEvent(e); + } } }); }; diff --git a/lib/ace/keyboardstate.js b/lib/ace/keyboardstate.js new file mode 100644 index 00000000..c204c527 --- /dev/null +++ b/lib/ace/keyboardstate.js @@ -0,0 +1,193 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Skywriter. + * + * The Initial Developer of the Original Code is + * Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Julian Viereck (julian.viereck@gmail.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +define(function(require, exports, module) { + +function KeyboardStateMapper(keymapping) { + this.keymapping = this.$buildKeymappingRegex(keymapping); +} + +KeyboardStateMapper.prototype = { + /** + * Build the RegExp from the keymapping as RegExp can't stored directly + * in the metadata JSON and as the RegExp used to match the keys/buffer + * need to be adapted. + */ + $buildKeymappingRegex: function(keymapping) { + for (state in keymapping) { + this.$buildBindingsRegex(keymapping[state]); + } + return keymapping; + }, + + $buildBindingsRegex: function(bindings) { + // Escape a given Regex string. + bindings.forEach(function(binding) { + if (binding.key) { + binding.key = new RegExp('^' + binding.key + '$'); + } else if (Array.isArray(binding.regex)) { + binding.key = new RegExp('^' + binding.regex[1] + '$'); + binding.regex = new RegExp(binding.regex.join('') + '$'); + } else if (binding.regex) { + binding.regex = new RegExp(binding.regex + '$'); + } + }); + }, + + $composeBuffer: function(data, hashId, key) { + // Initialize the data object. + if (data.state == null || data.buffer == null) { + data.state = "start"; + data.buffer = ""; + } + + var keyArray = []; + if (hashId & 1) keyArray.push("Ctrl"); + if (hashId & 8) keyArray.push("Command"); + if (hashId & 2) keyArray.push("Option"); + if (hashId & 4) keyArray.push("Shift"); + keyArray.push(key); + + var symbolicName = keyArray.join("-").toLowerCase(); + var bufferToUse = data.buffer + symbolicName; + + // Don't add the symbolic name to the key buffer if the alt_ key is + // part of the symbolic name. If it starts with alt_, this means + // that the user hit an alt keycombo and there will be a single, + // new character detected after this event, which then will be + // added to the buffer (e.g. alt_j will result in ∆). + // + // We test for 2 and not for & 2 as we only want to exclude the case where + // the option key is pressed alone. + if (hashId != 2) { + data.buffer = bufferToUse; + } + + return { + bufferToUse: bufferToUse, + symbolicName: symbolicName + }; + }, + + $find: function(data, buffer, symbolicName, hashId, key) { + // Holds the command to execute and the args if a command matched. + var result = {}; + + // Loop over all the bindings of the keymapp until a match is found. + this.keymapping[data.state].some(function(binding) { + var match; + + // Check if the key matches. + if (binding.key && !binding.key.test(symbolicName)) { + return false; + } + + // Check if the regex matches. + if (binding.regex && !(match = binding.regex.exec(buffer))) { + return false; + } + + // Check if the match function matches. + if (binding.match && !binding.match(buffer, hashId, key, symbolicName)) { + return false; + } + + // Check for disallowed matches. + if (binding.disallowMatches) { + for (var i = 0; i < binding.disallowMatches.length; i++) { + if (!!match[binding.disallowMatches[i]]) { + return true; + } + } + } + + // If there is a command to execute, then figure out the + // comand and the arguments. + if (binding.exec) { + result.command = binding.exec; + + // Bulid the arguments. + if (binding.params) { + var value; + result.args = {}; + binding.params.forEach(function(param) { + if (param.match != null && match != null) { + value = match[param.match] || param.defaultValue; + } else { + value = param.defaultValue; + } + + if (param.type === 'number') { + value = parseInt(value); + } + + result.args[param.name] = value; + }); + } + data.buffer = ""; + } + + // Handle the 'then' property. + if (binding.then) { + data.state = binding.then; + data.buffer = ""; + if (result.command == null) { + result.command = "null"; + } + } + + return true; + }); + + return result.command ? result : false; + }, + + match: function(data, hashId, key) { + // Compute the current value of the keyboard input buffer. + var r = this.$composeBuffer(data, hashId, key); + var buffer = r.bufferToUse; + var symbolicName = r.symbolicName; + + r = this.$find(data, buffer, symbolicName, hashId, key); + console.log("KeyboardStateMapper#match", buffer, symbolicName, r); + + return r; + } +} + +exports.KeyboardStateMapper = KeyboardStateMapper; +}); \ No newline at end of file diff --git a/lib/ace/mode/vim.js b/lib/ace/mode/vim.js new file mode 100644 index 00000000..2e99538d --- /dev/null +++ b/lib/ace/mode/vim.js @@ -0,0 +1,121 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Skywriter. + * + * The Initial Developer of the Original Code is + * Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Julian Viereck (julian.viereck@gmail.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +define(function(require, exports, module) { + +var KeyboardStateMapper = require("ace/keyboardstate").KeyboardStateMapper; + +var vimStates = { + start: [ + { + key: "i", + then: "insertMode" + }, + { + regex: [ "([0-9]*)", "(k|up)" ], + exec: "golineup", + params: [ + { + name: "times", + match: 1, + type: "number", + defaultValue: 1 + } + ] + }, + { + regex: [ "([0-9]*)", "(j|down|enter)" ], + exec: "golinedown", + params: [ + { + name: "times", + match: 1, + type: "number", + defaultValue: 1 + } + ] + }, + { + regex: [ "([0-9]*)", "(l|right)" ], + exec: "gotoright", + params: [ + { + name: "times", + match: 1, + type: "number", + defaultValue: 1 + } + ] + }, + { + regex: [ "([0-9]*)", "(h|left)" ], + exec: "gotoleft", + params: [ + { + name: "times", + match: 1, + type: "number", + defaultValue: 1 + } + ] + }, + { + comment: "Let all combos of Command, Ctrl, Optional pass...", + match: function(buffer, hashId, key, symbolicName) { + return hashId != 0 && !(hashId & 4); + } + }, + { + comment: "...but stop all other input!", + key: ".*", + exec: "null" + } + ], + insertMode: [ + { + key: "esc", + then: "start" + } + ] +}; + +var vimKeyboardStateMapper = new KeyboardStateMapper(vimStates); + +exports.handleKeyboard = function(data, hashId, key, e) { + return vimKeyboardStateMapper.match(data, hashId, key); +} +}); \ No newline at end of file