diff --git a/lib/ace/keyboard/vim.js b/lib/ace/keyboard/vim.js index cbf3dda3..d3a01ebc 100644 --- a/lib/ace/keyboard/vim.js +++ b/lib/ace/keyboard/vim.js @@ -167,7 +167,7 @@ define(function(require, exports, module) { this.removeOverlay(); }; this.virtualSelectionMode = function() { - return this.ace.inVirtualSelectionMode && this.ace.selection.index + return this.ace.inVirtualSelectionMode && this.ace.selection.index; }; this.onChange = function(delta) { var oldDelta = delta.data; @@ -356,7 +356,7 @@ define(function(require, exports, module) { point.row = start.row; point.column = start.column; if (cmp2 === 0) - point.bias = 1 + point.bias = 1; } } }; @@ -421,9 +421,6 @@ define(function(require, exports, module) { throw "not implemented"; } }; - this.openDialog = function() { - debugger - }; this.getSearchCursor = function(query, pos, caseFold) { var caseSensitive = false; var isRegexp = false; @@ -566,12 +563,11 @@ define(function(require, exports, module) { var row = delta.start.row; if (row == delta.end.row) highlight.cache[row] = undefined; else highlight.cache.splice(row, highlight.cache.length); - } + }; highlight.session.on("changeEditor", highlight.destroy); highlight.session.on("change", highlight.updateOnChange); } var re = new RegExp(o.query.source, "gmi"); - console.log(re) this.$searchHighlight = o.highlight = highlight; this.$searchHighlight.setRegexp(re); this.ace.renderer.updateBackMarkers(); @@ -866,11 +862,11 @@ dom.importCssString(".normal-mode .ace_cursor{\ { keys: '', type: 'keyToKey', toKeys: 'k' }, { keys: '', type: 'keyToKey', toKeys: 'j' }, { keys: '', type: 'keyToKey', toKeys: 'l' }, - { keys: '', type: 'keyToKey', toKeys: 'h' }, + { keys: '', type: 'keyToKey', toKeys: 'h', context: 'normal'}, { keys: '', type: 'keyToKey', toKeys: 'W' }, - { keys: '', type: 'keyToKey', toKeys: 'B' }, + { keys: '', type: 'keyToKey', toKeys: 'B', context: 'normal' }, { keys: '', type: 'keyToKey', toKeys: 'w' }, - { keys: '', type: 'keyToKey', toKeys: 'b' }, + { keys: '', type: 'keyToKey', toKeys: 'b', context: 'normal' }, { keys: '', type: 'keyToKey', toKeys: 'j' }, { keys: '', type: 'keyToKey', toKeys: 'k' }, { keys: '', type: 'keyToKey', toKeys: '' }, @@ -1018,55 +1014,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ var Pos = CodeMirror.Pos; - var modifierCodes = [16, 17, 18, 91]; - var specialKey = {Enter:'CR',Backspace:'BS',Delete:'Del'}; - var mac = /Mac/.test(navigator.platform); var Vim = function() { return vimApi; } //{ - function lookupKey(e) { - var keyCode = e.keyCode; - if (modifierCodes.indexOf(keyCode) != -1) { return; } - var hasModifier = e.ctrlKey || e.metaKey; - var key = CodeMirror.keyNames[keyCode]; - key = specialKey[key] || key; - var name = ''; - if (e.ctrlKey) { name += 'C-'; } - if (e.altKey) { name += 'A-'; } - if (mac && e.metaKey || (!hasModifier && e.shiftKey) && key.length < 2) { - // Shift key bindings can only specified for special characters. - return; - } else if (e.shiftKey && !/^[A-Za-z]$/.test(key)) { - name += 'S-'; - } - if (key.length == 1) { key = key.toLowerCase(); } - name += key; - if (name.length > 1) { name = '<' + name + '>'; } - return name; - } - // Keys with modifiers are handled using keydown due to limitations of - // keypress event. - function handleKeyDown(cm, e) { - var name = lookupKey(e); - if (!name) { return; } - - CodeMirror.signal(cm, 'vim-keypress', name); - if (CodeMirror.Vim.handleKey(cm, name, 'user')) { - CodeMirror.e_stop(e); - } - } - // Keys without modifiers are handled using keypress to work best with - // non-standard keyboard layouts. - function handleKeyPress(cm, e) { - var code = e.charCode || e.keyCode; - if (e.ctrlKey || e.metaKey || e.altKey || - e.shiftKey && code < 32) { return; } - var name = String.fromCharCode(code); - - CodeMirror.signal(cm, 'vim-keypress', name); - if (CodeMirror.Vim.handleKey(cm, name, 'user')) { - CodeMirror.e_stop(e); - } - } - function enterVimMode(cm) { cm.setOption('disableInput', true); cm.setOption('showCursorWhenSelecting', false); @@ -1074,8 +1022,6 @@ dom.importCssString(".normal-mode .ace_cursor{\ cm.on('cursorActivity', onCursorActivity); maybeInitVimState(cm); CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm)); - cm.on('keypress', handleKeyPress); - cm.on('keydown', handleKeyDown); } function leaveVimMode(cm) { @@ -1083,8 +1029,6 @@ dom.importCssString(".normal-mode .ace_cursor{\ cm.off('cursorActivity', onCursorActivity); CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm)); cm.state.vim = null; - cm.off('keypress', handleKeyPress); - cm.off('keydown', handleKeyDown); } function detachVimMap(cm, next) { @@ -1109,6 +1053,60 @@ dom.importCssString(".normal-mode .ace_cursor{\ else if (!val && prev != CodeMirror.Init && /^vim/.test(cm.getOption("keyMap"))) cm.setOption("keyMap", "default"); }); + + function cmKey(key, cm) { + if (!cm) { return undefined; } + var vimKey = cmKeyToVimKey(key); + if (!vimKey) { + return false; + } + var cmd = CodeMirror.Vim.findKey(cm, vimKey); + if (typeof cmd == 'function') { + CodeMirror.signal(cm, 'vim-keypress', vimKey); + } + return cmd; + } + + var modifiers = {'Shift': 'S', 'Ctrl': 'C', 'Alt': 'A', 'Cmd': 'D', 'Mod': 'A'}; + var specialKeys = {Enter:'CR',Backspace:'BS',Delete:'Del'}; + function cmKeyToVimKey(key) { + if (key.charAt(0) == '\'') { + // Keypress character binding of format "'a'" + return key.charAt(1); + } + var pieces = key.split('-'); + if (/-$/.test(key)) { + // If the - key was typed, split will result in 2 extra empty strings + // in the array. Replace them with 1 '-'. + pieces.splice(-2, 2, '-'); + } + var lastPiece = pieces[pieces.length - 1]; + if (pieces.length == 1 && pieces[0].length == 1) { + // No-modifier bindings use literal character bindings above. Skip. + return false; + } else if (pieces.length == 2 && pieces[0] == 'Shift' && lastPiece.length == 1) { + // Ignore Shift+char bindings as they should be handled by literal character. + return false; + } + var hasCharacter = false; + for (var i = 0; i < pieces.length; i++) { + var piece = pieces[i]; + if (piece in modifiers) { pieces[i] = modifiers[piece]; } + else { hasCharacter = true; } + if (piece in specialKeys) { pieces[i] = specialKeys[piece]; } + } + if (!hasCharacter) { + // Vim does not support modifier only keys. + return false; + } + // TODO: Current bindings expect the character to be lower case, but + // it looks like vim key notation uses upper case. + if (isUpperCase(lastPiece)) { + pieces[pieces.length - 1] = lastPiece.toLowerCase(); + } + return '<' + pieces.join('-') + '>'; + } + function getOnPasteFn(cm) { var vim = cm.state.vim; if (!vim.onPasteFn) { @@ -1168,15 +1166,10 @@ dom.importCssString(".normal-mode .ace_cursor{\ function defineOption(name, defaultValue, type) { if (defaultValue === undefined) { throw Error('defaultValue is required'); } if (!type) { type = 'string'; } - var opt = name; - if (typeof name == "string") - opt = { - type: type, - defaultValue: defaultValue - }; - else - name = opt.name; - options[name] = opt; + options[name] = { + type: type, + defaultValue: defaultValue + }; setOption(name, defaultValue); } @@ -1193,8 +1186,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ value = true; } } - option.value = value; - if (option.set) option.set(value, cm); + option.value = option.type == 'boolean' ? !!value : value; } function getOption(name) { @@ -1428,9 +1420,23 @@ dom.importCssString(".normal-mode .ace_cursor{\ exCommands[name]=func; exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'}; }, - // This is the outermost function called by CodeMirror, after keys have - // been mapped to their Vim equivalents. - handleKey: function(cm, key, origin) { + handleKey: function (cm, key, origin) { + var command = this.findKey(cm, key, origin); + if (typeof command === 'function') { + return command(); + } + }, + /** + * This is the outermost function called by CodeMirror, after keys have + * been mapped to their Vim equivalents. + * + * Finds a command based on the key (and cached keys if there is a + * multi-key sequence). Returns `undefined` if no key is matched, a noop + * function if a partial match is found (multi-key), and a function to + * execute the bound command if a a key is matched. The function always + * returns true. + */ + findKey: function(cm, key, origin) { var vim = maybeInitVimState(cm); function handleMacroRecording() { var macroModeState = vimGlobalState.macroModeState; @@ -1496,13 +1502,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ cm.replaceRange('', offsetCursor(here, 0, -(keys.length - 1)), here, '+input'); } clearInputState(cm); - var command = match.command; - if (command.type == 'keyToKey') { - doKeyToKey(command.toKeys); - } else { - commandDispatcher.processCommand(cm, vim, command); - } - return true; + return match.command; } function handleKeyNonInsertMode() { @@ -1520,31 +1520,44 @@ dom.importCssString(".normal-mode .ace_cursor{\ else if (match.type == 'partial') { return true; } vim.inputState.keyBuffer = ''; - var command = match.command; var keysMatcher = /^(\d*)(.*)$/.exec(keys); if (keysMatcher[1] && keysMatcher[1] != '0') { vim.inputState.pushRepeatDigit(keysMatcher[1]); } - if (command.type == 'keyToKey') { - doKeyToKey(command.toKeys); - } else { - commandDispatcher.processCommand(cm, vim, command); - } - return true; + return match.command; } - return cm.operation(function() { - cm.curOp.isVimOp = true; - try { - if (vim.insertMode) { return handleKeyInsertMode(); } - else { return handleKeyNonInsertMode(); } - } catch (e) { - // clear VIM state in case it's in a bad state. - cm.state.vim = undefined; - maybeInitVimState(cm); - throw e; - } - }); + var command; + if (vim.insertMode) { command = handleKeyInsertMode(); } + else { command = handleKeyNonInsertMode(); } + if (command === false) { + return undefined; + } else if (command === true) { + // TODO: Look into using CodeMirror's multi-key handling. + // Return no-op since we are caching the key. Counts as handled, but + // don't want act on it just yet. + return function() {}; + } else { + return function() { + return cm.operation(function() { + cm.curOp.isVimOp = true; + try { + if (command.type == 'keyToKey') { + doKeyToKey(command.toKeys); + } else { + commandDispatcher.processCommand(cm, vim, command); + } + } catch (e) { + // clear VIM state in case it's in a bad state. + cm.state.vim = undefined; + maybeInitVimState(cm); + console['log'](e); + throw e; + } + return true; + }); + }; + } }, handleEx: function(cm, input) { exCommandDispatcher.processCommand(cm, input); @@ -2105,7 +2118,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ newHead = copyCursor(origHead); } if (vim.visualMode) { - newHead = clipCursorToContent(cm, newHead, true); + newHead = clipCursorToContent(cm, newHead, vim.visualBlock); if (newAnchor) { newAnchor = clipCursorToContent(cm, newAnchor, true); } @@ -2409,20 +2422,8 @@ dom.importCssString(".normal-mode .ace_cursor{\ return cm.findPosV(curStart, (motionArgs.forward ? repeat : -repeat), 'page'); }, moveByParagraph: function(cm, head, motionArgs) { - var line = head.line; - var repeat = motionArgs.repeat; - var inc = motionArgs.forward ? 1 : -1; - for (var i = 0; i < repeat; i++) { - if ((!motionArgs.forward && line === cm.firstLine() ) || - (motionArgs.forward && line == cm.lastLine())) { - break; - } - line += inc; - while (line !== cm.firstLine() && line != cm.lastLine() && cm.getLine(line)) { - line += inc; - } - } - return Pos(line, 0); + var dir = motionArgs.forward ? 1 : -1; + return findParagraph(cm, head, motionArgs.repeat, dir); }, moveByScroll: function(cm, head, motionArgs, vim) { var scrollbox = cm.getScrollInfo(); @@ -2522,7 +2523,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ return Pos(lineNum, findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum))); }, - textObjectManipulation: function(cm, head, motionArgs) { + textObjectManipulation: function(cm, head, motionArgs, vim) { // TODO: lots of possible exceptions that can be thrown here. Try da( // outside of a () block. @@ -2561,8 +2562,15 @@ dom.importCssString(".normal-mode .ace_cursor{\ tmp = expandWordUnderCursor(cm, inclusive, true /** forward */, false /** bigWord */); } else if (character === 'p') { - tmp = expandParagraphUnderCursor(cm, inclusive, true /** forward */, - false /** bigWord */); + tmp = findParagraph(cm, head, motionArgs.repeat, 0, inclusive); + motionArgs.linewise = true; + if (vim.visualMode) { + if (!vim.visualLine) { vim.visualLine = true; } + } else { + var operatorArgs = vim.inputState.operatorArgs; + if (operatorArgs) { operatorArgs.linewise = true; } + tmp.end.line--; + } } else { // No text object defined for this, don't move. return null; @@ -4070,6 +4078,54 @@ dom.importCssString(".normal-mode .ace_cursor{\ return idx; } + function findParagraph(cm, head, repeat, dir, inclusive) { + var line = head.line; + var min = cm.firstLine(); + var max = cm.lastLine(); + var start, end, i = line; + function isEmpty(i) { return !cm.getLine(i); } + function isBoundary(i, dir, any) { + if (any) { return isEmpty(i) != isEmpty(i + dir); } + return !isEmpty(i) && isEmpty(i + dir); + } + if (dir) { + while (min <= i && i <= max && repeat > 0) { + if (isBoundary(i, dir)) { repeat--; } + i += dir; + } + return new Pos(i, 0); + } + + var vim = cm.state.vim; + if (vim.visualLine && isBoundary(line, 1, true)) { + var anchor = vim.sel.anchor; + if (isBoundary(anchor.line, -1, true)) { + if (!inclusive || anchor.line != line) { + line += 1; + } + } + } + var startState = isEmpty(line); + for (i = line; i <= max && repeat; i++) { + if (isBoundary(i, 1, true)) { + if (!inclusive || isEmpty(i) != startState) { + repeat--; + } + } + } + end = new Pos(i, 0); + // select boundary before paragraph for the last one + if (i > max && !startState) { startState = true; } + else { inclusive = false; } + for (i = line; i > min; i--) { + if (!inclusive || isEmpty(i) == startState || i == line) { + if (isBoundary(i, -1, true)) { break; } + } + } + start = new Pos(i, 0); + return { start: start, end: end }; + } + // TODO: perhaps this finagling of start and end positions belonds // in codmirror/replaceRange? function selectCompanionObject(cm, head, symb, inclusive) { @@ -5276,7 +5332,8 @@ dom.importCssString(".normal-mode .ace_cursor{\ CodeMirror.keyMap.vim = { attach: attachVimMap, - detach: detachVimMap + detach: detachVimMap, + call: cmKey }; function exitInsertMode(cm) { @@ -5349,20 +5406,16 @@ dom.importCssString(".normal-mode .ace_cursor{\ }, fallthrough: ['default'], attach: attachVimMap, - detach: detachVimMap - }; - - CodeMirror.keyMap['await-second'] = { - fallthrough: ['vim-insert'], - attach: attachVimMap, - detach: detachVimMap + detach: detachVimMap, + call: cmKey }; CodeMirror.keyMap['vim-replace'] = { 'Backspace': 'goCharLeft', fallthrough: ['vim-insert'], attach: attachVimMap, - detach: detachVimMap + detach: detachVimMap, + call: cmKey }; function executeMacroRegister(cm, vim, macroModeState, registerName) { @@ -5628,7 +5681,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ Vim = CodeMirror.Vim; - specialKey = {'return':'CR',backspace:'BS','delete':'Del',esc:'Esc', + var specialKey = {'return':'CR',backspace:'BS','delete':'Del',esc:'Esc', left:'Left',right:'Right',up:'Up',down:'Down',space: 'Space', home:'Home',end:'End',pageup:'PageUp',pagedown:'PageDown', enter: 'CR' }; @@ -5646,7 +5699,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ if (name.length > 1) { name = '<' + name + '>'; } return name; } - var handleKey = Vim.handleKey + var handleKey = Vim.handleKey.bind(Vim); Vim.handleKey = function(cm, key, origin) { return cm.operation(function() { return handleKey(cm, key, origin); @@ -5750,7 +5803,6 @@ dom.importCssString(".normal-mode .ace_cursor{\ }); return {command: "null", passEvent: true}; } - return {command: coreCommands.stop}; } else if (!vim.insertMode) { if (useragent.isMac && this.handleMacRepeat(data, hashId, key)) { hashId = -1; @@ -5925,10 +5977,10 @@ dom.importCssString(".normal-mode .ace_cursor{\ { keys: 'zf', type: 'action', action: 'fold', actionArgs: { open: true, all: true } }, { keys: 'zd', type: 'action', action: 'fold', actionArgs: { open: true, all: true } }, - { keys: '', type: 'action', action: 'aceCommand', actionArgs: { name: "addCursorAbove" } }, - { keys: '', type: 'action', action: 'aceCommand', actionArgs: { name: "addCursorBelow" } }, - { keys: '', type: 'action', action: 'aceCommand', actionArgs: { name: "addCursorAboveSkipCurrent" } }, - { keys: '', type: 'action', action: 'aceCommand', actionArgs: { name: "addCursorBelowSkipCurrent" } }, + { keys: '', type: 'action', action: 'aceCommand', actionArgs: { name: "addCursorAbove" } }, + { keys: '', type: 'action', action: 'aceCommand', actionArgs: { name: "addCursorBelow" } }, + { keys: '', type: 'action', action: 'aceCommand', actionArgs: { name: "addCursorAboveSkipCurrent" } }, + { keys: '', type: 'action', action: 'aceCommand', actionArgs: { name: "addCursorBelowSkipCurrent" } }, { keys: '', type: 'action', action: 'aceCommand', actionArgs: { name: "selectMoreBefore" } }, { keys: '', type: 'action', action: 'aceCommand', actionArgs: { name: "selectMoreAfter" } }, { keys: '', type: 'action', action: 'aceCommand', actionArgs: { name: "selectNextBefore" } }, @@ -5955,6 +6007,8 @@ dom.importCssString(".normal-mode .ace_cursor{\ }, exports.handler.defaultKeymap = defaultKeymap; + exports.handler.actions = actions; + exports.Vim = Vim; Vim.map("Y", "yy"); }); diff --git a/lib/ace/keyboard/vim_test.js b/lib/ace/keyboard/vim_test.js index 8c9eeb65..7b51f23b 100644 --- a/lib/ace/keyboard/vim_test.js +++ b/lib/ace/keyboard/vim_test.js @@ -592,6 +592,80 @@ testVim('{', function(cm, vim, helpers) { helpers.doKeys('6', '{'); helpers.assertCursorAt(0, 0); }, { value: 'a\n\nb\nc\n\nd' }); +testVim('paragraph motions', function(cm, vim, helpers) { + cm.setCursor(10, 0); + helpers.doKeys('{'); + helpers.assertCursorAt(4, 0); + helpers.doKeys('{'); + helpers.assertCursorAt(0, 0); + helpers.doKeys('2', '}'); + helpers.assertCursorAt(7, 0); + helpers.doKeys('2', '}'); + helpers.assertCursorAt(16, 0); + + cm.setCursor(9, 0); + helpers.doKeys('}'); + helpers.assertCursorAt(14, 0); + + cm.setCursor(6, 0); + helpers.doKeys('}'); + helpers.assertCursorAt(7, 0); + + // ip inside empty space + cm.setCursor(10, 0); + helpers.doKeys('v', 'i', 'p'); + eqPos(Pos(7, 0), cm.getCursor('anchor')); + eqPos(Pos(12, 0), cm.getCursor('head')); + helpers.doKeys('i', 'p'); + eqPos(Pos(7, 0), cm.getCursor('anchor')); + eqPos(Pos(13, 1), cm.getCursor('head')); + helpers.doKeys('2', 'i', 'p'); + eqPos(Pos(7, 0), cm.getCursor('anchor')); + eqPos(Pos(16, 1), cm.getCursor('head')); + + // should switch to visualLine mode + cm.setCursor(14, 0); + helpers.doKeys('', 'v', 'i', 'p'); + helpers.assertCursorAt(14, 0); + + cm.setCursor(14, 0); + helpers.doKeys('', 'V', 'i', 'p'); + eqPos(Pos(16, 1), cm.getCursor('head')); + + // ap inside empty space + cm.setCursor(10, 0); + helpers.doKeys('', 'v', 'a', 'p'); + eqPos(Pos(7, 0), cm.getCursor('anchor')); + eqPos(Pos(13, 1), cm.getCursor('head')); + helpers.doKeys('a', 'p'); + eqPos(Pos(7, 0), cm.getCursor('anchor')); + eqPos(Pos(16, 1), cm.getCursor('head')); + + cm.setCursor(13, 0); + helpers.doKeys('v', 'a', 'p'); + eqPos(Pos(13, 0), cm.getCursor('anchor')); + eqPos(Pos(14, 0), cm.getCursor('head')); + + cm.setCursor(16, 0); + helpers.doKeys('v', 'a', 'p'); + eqPos(Pos(14, 0), cm.getCursor('anchor')); + eqPos(Pos(16, 1), cm.getCursor('head')); + + cm.setCursor(0, 0); + helpers.doKeys('v', 'a', 'p'); + eqPos(Pos(0, 0), cm.getCursor('anchor')); + eqPos(Pos(4, 0), cm.getCursor('head')); + + cm.setCursor(0, 0); + helpers.doKeys('d', 'i', 'p'); + var register = helpers.getRegisterController().getRegister(); + eq('a\na\n', register.toString()); + is(register.linewise); + helpers.doKeys('3', 'j', 'p'); + helpers.doKeys('y', 'i', 'p'); + is(register.linewise); + eq('b\na\na\nc\n', register.toString()); +}, { value: 'a\na\n\n\n\nb\nc\n\n\n\n\n\n\nd\n\ne\nf' }); // Operator tests testVim('dl', function(cm, vim, helpers) {