UNPKG

monaco-vim

Version:

Vim keybindings for monaco-editor

1,815 lines (1,767 loc) 222 kB
/** MIT License Copyright (C) 2017 by Marijn Haverbeke <marijnh@gmail.com> and others Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import CodeMirror from "../cm_adapter"; var Pos = CodeMirror.Pos; function transformCursor(cm, range) { var vim = cm.state.vim; if (!vim || vim.insertMode) return range.head; var head = vim.sel.head; if (!head) return range.head; if (vim.visualBlock) { if (range.head.line != head.line) { return; } } if (range.from() == range.anchor && !range.empty()) { if (range.head.line == head.line && range.head.ch != head.ch) return new Pos(range.head.line, range.head.ch - 1); } return range.head; } var defaultKeymap = [ // Key to key mapping. This goes first to make it possible to override // existing mappings. { keys: "<Left>", type: "keyToKey", toKeys: "h" }, { keys: "<Right>", type: "keyToKey", toKeys: "l" }, { keys: "<Up>", type: "keyToKey", toKeys: "k" }, { keys: "<Down>", type: "keyToKey", toKeys: "j" }, { keys: "g<Up>", type: "keyToKey", toKeys: "gk" }, { keys: "g<Down>", type: "keyToKey", toKeys: "gj" }, { keys: "<Space>", type: "keyToKey", toKeys: "l" }, { keys: "<BS>", type: "keyToKey", toKeys: "h", context: "normal" }, { keys: "<Del>", type: "keyToKey", toKeys: "x", context: "normal" }, { keys: "<C-Space>", type: "keyToKey", toKeys: "W" }, { keys: "<C-BS>", type: "keyToKey", toKeys: "B", context: "normal" }, { keys: "<S-Space>", type: "keyToKey", toKeys: "w" }, { keys: "<S-BS>", type: "keyToKey", toKeys: "b", context: "normal" }, { keys: "<C-n>", type: "keyToKey", toKeys: "j" }, { keys: "<C-p>", type: "keyToKey", toKeys: "k" }, { keys: "<C-[>", type: "keyToKey", toKeys: "<Esc>" }, { keys: "<C-c>", type: "keyToKey", toKeys: "<Esc>" }, { keys: "<C-[>", type: "keyToKey", toKeys: "<Esc>", context: "insert" }, { keys: "<C-c>", type: "keyToKey", toKeys: "<Esc>", context: "insert" }, { keys: "s", type: "keyToKey", toKeys: "cl", context: "normal" }, { keys: "s", type: "keyToKey", toKeys: "c", context: "visual" }, { keys: "S", type: "keyToKey", toKeys: "cc", context: "normal" }, { keys: "S", type: "keyToKey", toKeys: "VdO", context: "visual" }, { keys: "<Home>", type: "keyToKey", toKeys: "0" }, { keys: "<End>", type: "keyToKey", toKeys: "$" }, { keys: "<PageUp>", type: "keyToKey", toKeys: "<C-b>" }, { keys: "<PageDown>", type: "keyToKey", toKeys: "<C-f>" }, { keys: "<CR>", type: "keyToKey", toKeys: "j^", context: "normal" }, { keys: "<Ins>", type: "keyToKey", toKeys: "i", context: "normal" }, { keys: "<Ins>", type: "action", action: "toggleOverwrite", context: "insert", }, // Motions { keys: "H", type: "motion", motion: "moveToTopLine", motionArgs: { linewise: true, toJumplist: true }, }, { keys: "M", type: "motion", motion: "moveToMiddleLine", motionArgs: { linewise: true, toJumplist: true }, }, { keys: "L", type: "motion", motion: "moveToBottomLine", motionArgs: { linewise: true, toJumplist: true }, }, { keys: "h", type: "motion", motion: "moveByCharacters", motionArgs: { forward: false }, }, { keys: "l", type: "motion", motion: "moveByCharacters", motionArgs: { forward: true }, }, { keys: "j", type: "motion", motion: "moveByLines", motionArgs: { forward: true, linewise: true }, }, { keys: "k", type: "motion", motion: "moveByLines", motionArgs: { forward: false, linewise: true }, }, { keys: "gj", type: "motion", motion: "moveByDisplayLines", motionArgs: { forward: true }, }, { keys: "gk", type: "motion", motion: "moveByDisplayLines", motionArgs: { forward: false }, }, { keys: "w", type: "motion", motion: "moveByWords", motionArgs: { forward: true, wordEnd: false }, }, { keys: "W", type: "motion", motion: "moveByWords", motionArgs: { forward: true, wordEnd: false, bigWord: true }, }, { keys: "e", type: "motion", motion: "moveByWords", motionArgs: { forward: true, wordEnd: true, inclusive: true }, }, { keys: "E", type: "motion", motion: "moveByWords", motionArgs: { forward: true, wordEnd: true, bigWord: true, inclusive: true, }, }, { keys: "b", type: "motion", motion: "moveByWords", motionArgs: { forward: false, wordEnd: false }, }, { keys: "B", type: "motion", motion: "moveByWords", motionArgs: { forward: false, wordEnd: false, bigWord: true }, }, { keys: "ge", type: "motion", motion: "moveByWords", motionArgs: { forward: false, wordEnd: true, inclusive: true }, }, { keys: "gE", type: "motion", motion: "moveByWords", motionArgs: { forward: false, wordEnd: true, bigWord: true, inclusive: true, }, }, { keys: "{", type: "motion", motion: "moveByParagraph", motionArgs: { forward: false, toJumplist: true }, }, { keys: "}", type: "motion", motion: "moveByParagraph", motionArgs: { forward: true, toJumplist: true }, }, { keys: "(", type: "motion", motion: "moveBySentence", motionArgs: { forward: false }, }, { keys: ")", type: "motion", motion: "moveBySentence", motionArgs: { forward: true }, }, { keys: "<C-f>", type: "motion", motion: "moveByPage", motionArgs: { forward: true }, }, { keys: "<C-b>", type: "motion", motion: "moveByPage", motionArgs: { forward: false }, }, { keys: "<C-d>", type: "motion", motion: "moveByScroll", motionArgs: { forward: true, explicitRepeat: true }, }, { keys: "<C-u>", type: "motion", motion: "moveByScroll", motionArgs: { forward: false, explicitRepeat: true }, }, { keys: "gg", type: "motion", motion: "moveToLineOrEdgeOfDocument", motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true, }, }, { keys: "G", type: "motion", motion: "moveToLineOrEdgeOfDocument", motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true, }, }, { keys: "g$", type: "motion", motion: "moveToEndOfDisplayLine" }, { keys: "g^", type: "motion", motion: "moveToStartOfDisplayLine" }, { keys: "g0", type: "motion", motion: "moveToStartOfDisplayLine" }, { keys: "0", type: "motion", motion: "moveToStartOfLine" }, { keys: "^", type: "motion", motion: "moveToFirstNonWhiteSpaceCharacter" }, { keys: "+", type: "motion", motion: "moveByLines", motionArgs: { forward: true, toFirstChar: true }, }, { keys: "-", type: "motion", motion: "moveByLines", motionArgs: { forward: false, toFirstChar: true }, }, { keys: "_", type: "motion", motion: "moveByLines", motionArgs: { forward: true, toFirstChar: true, repeatOffset: -1 }, }, { keys: "$", type: "motion", motion: "moveToEol", motionArgs: { inclusive: true }, }, { keys: "%", type: "motion", motion: "moveToMatchedSymbol", motionArgs: { inclusive: true, toJumplist: true }, }, { keys: "f<character>", type: "motion", motion: "moveToCharacter", motionArgs: { forward: true, inclusive: true }, }, { keys: "F<character>", type: "motion", motion: "moveToCharacter", motionArgs: { forward: false }, }, { keys: "t<character>", type: "motion", motion: "moveTillCharacter", motionArgs: { forward: true, inclusive: true }, }, { keys: "T<character>", type: "motion", motion: "moveTillCharacter", motionArgs: { forward: false }, }, { keys: ";", type: "motion", motion: "repeatLastCharacterSearch", motionArgs: { forward: true }, }, { keys: ",", type: "motion", motion: "repeatLastCharacterSearch", motionArgs: { forward: false }, }, { keys: "'<character>", type: "motion", motion: "goToMark", motionArgs: { toJumplist: true, linewise: true }, }, { keys: "`<character>", type: "motion", motion: "goToMark", motionArgs: { toJumplist: true }, }, { keys: "]`", type: "motion", motion: "jumpToMark", motionArgs: { forward: true }, }, { keys: "[`", type: "motion", motion: "jumpToMark", motionArgs: { forward: false }, }, { keys: "]'", type: "motion", motion: "jumpToMark", motionArgs: { forward: true, linewise: true }, }, { keys: "['", type: "motion", motion: "jumpToMark", motionArgs: { forward: false, linewise: true }, }, // the next two aren't motions but must come before more general motion declarations { keys: "]p", type: "action", action: "paste", isEdit: true, actionArgs: { after: true, isEdit: true, matchIndent: true }, }, { keys: "[p", type: "action", action: "paste", isEdit: true, actionArgs: { after: false, isEdit: true, matchIndent: true }, }, { keys: "]<character>", type: "motion", motion: "moveToSymbol", motionArgs: { forward: true, toJumplist: true }, }, { keys: "[<character>", type: "motion", motion: "moveToSymbol", motionArgs: { forward: false, toJumplist: true }, }, { keys: "|", type: "motion", motion: "moveToColumn" }, { keys: "o", type: "motion", motion: "moveToOtherHighlightedEnd", context: "visual", }, { keys: "O", type: "motion", motion: "moveToOtherHighlightedEnd", motionArgs: { sameLine: true }, context: "visual", }, // Operators { keys: "d", type: "operator", operator: "delete" }, { keys: "y", type: "operator", operator: "yank" }, { keys: "c", type: "operator", operator: "change" }, { keys: "=", type: "operator", operator: "indentAuto" }, { keys: ">", type: "operator", operator: "indent", operatorArgs: { indentRight: true }, }, { keys: "<", type: "operator", operator: "indent", operatorArgs: { indentRight: false }, }, { keys: "g~", type: "operator", operator: "changeCase" }, { keys: "gu", type: "operator", operator: "changeCase", operatorArgs: { toLower: true }, isEdit: true, }, { keys: "gU", type: "operator", operator: "changeCase", operatorArgs: { toLower: false }, isEdit: true, }, { keys: "n", type: "motion", motion: "findNext", motionArgs: { forward: true, toJumplist: true }, }, { keys: "N", type: "motion", motion: "findNext", motionArgs: { forward: false, toJumplist: true }, }, { keys: "gn", type: "motion", motion: "findAndSelectNextInclusive", motionArgs: { forward: true }, }, { keys: "gN", type: "motion", motion: "findAndSelectNextInclusive", motionArgs: { forward: false }, }, // Operator-Motion dual commands { keys: "x", type: "operatorMotion", operator: "delete", motion: "moveByCharacters", motionArgs: { forward: true }, operatorMotionArgs: { visualLine: false }, }, { keys: "X", type: "operatorMotion", operator: "delete", motion: "moveByCharacters", motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }, }, { keys: "D", type: "operatorMotion", operator: "delete", motion: "moveToEol", motionArgs: { inclusive: true }, context: "normal", }, { keys: "D", type: "operator", operator: "delete", operatorArgs: { linewise: true }, context: "visual", }, { keys: "Y", type: "operatorMotion", operator: "yank", motion: "expandToLine", motionArgs: { linewise: true }, context: "normal", }, { keys: "Y", type: "operator", operator: "yank", operatorArgs: { linewise: true }, context: "visual", }, { keys: "C", type: "operatorMotion", operator: "change", motion: "moveToEol", motionArgs: { inclusive: true }, context: "normal", }, { keys: "C", type: "operator", operator: "change", operatorArgs: { linewise: true }, context: "visual", }, { keys: "~", type: "operatorMotion", operator: "changeCase", motion: "moveByCharacters", motionArgs: { forward: true }, operatorArgs: { shouldMoveCursor: true }, context: "normal", }, { keys: "~", type: "operator", operator: "changeCase", context: "visual" }, { keys: "<C-u>", type: "operatorMotion", operator: "delete", motion: "moveToStartOfLine", context: "insert", }, { keys: "<C-w>", type: "operatorMotion", operator: "delete", motion: "moveByWords", motionArgs: { forward: false, wordEnd: false }, context: "insert", }, //ignore C-w in normal mode { keys: "<C-w>", type: "idle", context: "normal" }, // Actions { keys: "<C-i>", type: "action", action: "jumpListWalk", actionArgs: { forward: true }, }, { keys: "<C-o>", type: "action", action: "jumpListWalk", actionArgs: { forward: false }, }, { keys: "<C-e>", type: "action", action: "scroll", actionArgs: { forward: true, linewise: true }, }, { keys: "<C-y>", type: "action", action: "scroll", actionArgs: { forward: false, linewise: true }, }, { keys: "a", type: "action", action: "enterInsertMode", isEdit: true, actionArgs: { insertAt: "charAfter" }, context: "normal", }, { keys: "A", type: "action", action: "enterInsertMode", isEdit: true, actionArgs: { insertAt: "eol" }, context: "normal", }, { keys: "A", type: "action", action: "enterInsertMode", isEdit: true, actionArgs: { insertAt: "endOfSelectedArea" }, context: "visual", }, { keys: "i", type: "action", action: "enterInsertMode", isEdit: true, actionArgs: { insertAt: "inplace" }, context: "normal", }, { keys: "gi", type: "action", action: "enterInsertMode", isEdit: true, actionArgs: { insertAt: "lastEdit" }, context: "normal", }, { keys: "I", type: "action", action: "enterInsertMode", isEdit: true, actionArgs: { insertAt: "firstNonBlank" }, context: "normal", }, { keys: "gI", type: "action", action: "enterInsertMode", isEdit: true, actionArgs: { insertAt: "bol" }, context: "normal", }, { keys: "I", type: "action", action: "enterInsertMode", isEdit: true, actionArgs: { insertAt: "startOfSelectedArea" }, context: "visual", }, { keys: "o", type: "action", action: "newLineAndEnterInsertMode", isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: true }, context: "normal", }, { keys: "O", type: "action", action: "newLineAndEnterInsertMode", isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: false }, context: "normal", }, { keys: "v", type: "action", action: "toggleVisualMode" }, { keys: "V", type: "action", action: "toggleVisualMode", actionArgs: { linewise: true }, }, { keys: "<C-v>", type: "action", action: "toggleVisualMode", actionArgs: { blockwise: true }, }, { keys: "<C-q>", type: "action", action: "toggleVisualMode", actionArgs: { blockwise: true }, }, { keys: "gv", type: "action", action: "reselectLastSelection" }, { keys: "J", type: "action", action: "joinLines", isEdit: true }, { keys: "gJ", type: "action", action: "joinLines", actionArgs: { keepSpaces: true }, isEdit: true, }, { keys: "p", type: "action", action: "paste", isEdit: true, actionArgs: { after: true, isEdit: true }, }, { keys: "P", type: "action", action: "paste", isEdit: true, actionArgs: { after: false, isEdit: true }, }, { keys: "r<character>", type: "action", action: "replace", isEdit: true }, { keys: "@<character>", type: "action", action: "replayMacro" }, { keys: "q<character>", type: "action", action: "enterMacroRecordMode" }, // Handle Replace-mode as a special case of insert mode. { keys: "R", type: "action", action: "enterInsertMode", isEdit: true, actionArgs: { replace: true }, context: "normal", }, { keys: "R", type: "operator", operator: "change", operatorArgs: { linewise: true, fullLine: true }, context: "visual", exitVisualBlock: true, }, { keys: "u", type: "action", action: "undo", context: "normal" }, { keys: "u", type: "operator", operator: "changeCase", operatorArgs: { toLower: true }, context: "visual", isEdit: true, }, { keys: "U", type: "operator", operator: "changeCase", operatorArgs: { toLower: false }, context: "visual", isEdit: true, }, { keys: "<C-r>", type: "action", action: "redo" }, { keys: "m<character>", type: "action", action: "setMark" }, { keys: '"<character>', type: "action", action: "setRegister" }, { keys: "zz", type: "action", action: "scrollToCursor", actionArgs: { position: "center" }, }, { keys: "z.", type: "action", action: "scrollToCursor", actionArgs: { position: "center" }, motion: "moveToFirstNonWhiteSpaceCharacter", }, { keys: "zt", type: "action", action: "scrollToCursor", actionArgs: { position: "top" }, }, { keys: "z<CR>", type: "action", action: "scrollToCursor", actionArgs: { position: "top" }, motion: "moveToFirstNonWhiteSpaceCharacter", }, { keys: "z-", type: "action", action: "scrollToCursor", actionArgs: { position: "bottom" }, }, { keys: "zb", type: "action", action: "scrollToCursor", actionArgs: { position: "bottom" }, motion: "moveToFirstNonWhiteSpaceCharacter", }, { keys: ".", type: "action", action: "repeatLastEdit" }, { keys: "<C-a>", type: "action", action: "incrementNumberToken", isEdit: true, actionArgs: { increase: true, backtrack: false }, }, { keys: "<C-x>", type: "action", action: "incrementNumberToken", isEdit: true, actionArgs: { increase: false, backtrack: false }, }, { keys: "<C-t>", type: "action", action: "indent", actionArgs: { indentRight: true }, context: "insert", }, { keys: "<C-d>", type: "action", action: "indent", actionArgs: { indentRight: false }, context: "insert", }, // Text object motions { keys: "a<character>", type: "motion", motion: "textObjectManipulation" }, { keys: "i<character>", type: "motion", motion: "textObjectManipulation", motionArgs: { textObjectInner: true }, }, // Search { keys: "/", type: "search", searchArgs: { forward: true, querySrc: "prompt", toJumplist: true }, }, { keys: "?", type: "search", searchArgs: { forward: false, querySrc: "prompt", toJumplist: true }, }, { keys: "*", type: "search", searchArgs: { forward: true, querySrc: "wordUnderCursor", wholeWordOnly: true, toJumplist: true, }, }, { keys: "#", type: "search", searchArgs: { forward: false, querySrc: "wordUnderCursor", wholeWordOnly: true, toJumplist: true, }, }, { keys: "g*", type: "search", searchArgs: { forward: true, querySrc: "wordUnderCursor", toJumplist: true, }, }, { keys: "g#", type: "search", searchArgs: { forward: false, querySrc: "wordUnderCursor", toJumplist: true, }, }, // Ex command { keys: ":", type: "ex" }, ]; var defaultKeymapLength = defaultKeymap.length; /** * Ex commands * Care must be taken when adding to the default Ex command map. For any * pair of commands that have a shared prefix, at least one of their * shortNames must not match the prefix of the other command. */ var defaultExCommandMap = [ { name: "colorscheme", shortName: "colo" }, { name: "map" }, { name: "imap", shortName: "im" }, { name: "nmap", shortName: "nm" }, { name: "vmap", shortName: "vm" }, { name: "unmap" }, { name: "write", shortName: "w" }, { name: "undo", shortName: "u" }, { name: "redo", shortName: "red" }, { name: "set", shortName: "se" }, { name: "setlocal", shortName: "setl" }, { name: "setglobal", shortName: "setg" }, { name: "sort", shortName: "sor" }, { name: "substitute", shortName: "s", possiblyAsync: true }, { name: "nohlsearch", shortName: "noh" }, { name: "yank", shortName: "y" }, { name: "delmarks", shortName: "delm" }, { name: "registers", shortName: "reg", excludeFromCommandHistory: true }, { name: "vglobal", shortName: "v" }, { name: "global", shortName: "g" }, ]; var Vim = function () { function enterVimMode(cm) { cm.setOption("disableInput", true); cm.setOption("showCursorWhenSelecting", false); CodeMirror.signal(cm, "vim-mode-change", { mode: "normal" }); cm.on("cursorActivity", onCursorActivity); maybeInitVimState(cm); // CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm)); cm.enterVimMode(); } function leaveVimMode(cm) { cm.setOption("disableInput", false); cm.off("cursorActivity", onCursorActivity); // CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm)); cm.state.vim = null; if (highlightTimeout) clearTimeout(highlightTimeout); cm.leaveVimMode(); } function detachVimMap(cm, next) { cm.attached = false; if (this == CodeMirror.keyMap.vim) { cm.options.$customCursor = null; // CodeMirror.rmClass(cm.getWrapperElement(), "cm-fat-cursor"); } if (!next || next.attach != attachVimMap) leaveVimMode(cm); } function attachVimMap(cm, prev) { if (this == CodeMirror.keyMap.vim) { cm.attached = true; if (cm.curOp) cm.curOp.selectionChanged = true; cm.options.$customCursor = transformCursor; } if (!prev || prev.attach != attachVimMap) enterVimMode(cm); } // Deprecated, simply setting the keymap works again. CodeMirror.defineOption("vimMode", false, function (cm, val, prev) { if (val && cm.getOption("keyMap") != "vim") cm.setOption("keyMap", "vim"); else if ( !val && prev != CodeMirror.Init && /^vim/.test(cm.getOption("keyMap")) ) cm.setOption("keyMap", "default"); }); function cmKey(key, cm) { if (!cm) { return undefined; } if (this[key]) { return this[key]; } var vimKey = cmKeyToVimKey(key); if (!vimKey) { return false; } var cmd = vimApi.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", CapsLock: "", }; var specialKeys = { Enter: "CR", Backspace: "BS", Delete: "Del", Insert: "Ins", }; function cmKeyToVimKey(key) { if (key.charAt(0) == "'") { // Keypress character binding of format "'a'" return key.charAt(1); } if (key === "AltGraph") { return false; } var pieces = key.split(/-(?!$)/); 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) { // vim.onPasteFn = function () { // if (!vim.insertMode) { // cm.setCursor(offsetCursor(cm.getCursor(), 0, 1)); // actions.enterInsertMode(cm, {}, vim); // } // }; // } // return vim.onPasteFn; // } var numberRegex = /[\d]/; var wordCharTest = [ CodeMirror.isWordChar, function (ch) { return ch && !CodeMirror.isWordChar(ch) && !/\s/.test(ch); }, ], bigWordCharTest = [ function (ch) { return /\S/.test(ch); }, ]; function makeKeyRange(start, size) { var keys = []; for (var i = start; i < start + size; i++) { keys.push(String.fromCharCode(i)); } return keys; } var upperCaseAlphabet = makeKeyRange(65, 26); var lowerCaseAlphabet = makeKeyRange(97, 26); var numbers = makeKeyRange(48, 10); var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, [ "<", ">", ]); var validRegisters = [].concat( upperCaseAlphabet, lowerCaseAlphabet, numbers, ["-", '"', ".", ":", "_", "/"] ); var upperCaseChars; try { upperCaseChars = new RegExp("^[\\p{Lu}]$", "u"); } catch (_) { upperCaseChars = /^[A-Z]$/; } function isLine(cm, line) { return line >= cm.firstLine() && line <= cm.lastLine(); } function isLowerCase(k) { return /^[a-z]$/.test(k); } function isMatchableSymbol(k) { return "()[]{}".indexOf(k) != -1; } function isNumber(k) { return numberRegex.test(k); } function isUpperCase(k) { return upperCaseChars.test(k); } function isWhiteSpaceString(k) { return /^\s*$/.test(k); } function isEndOfSentenceSymbol(k) { return ".?!".indexOf(k) != -1; } function inArray(val, arr) { for (var i = 0; i < arr.length; i++) { if (arr[i] == val) { return true; } } return false; } var options = {}; function defineOption(name, defaultValue, type, aliases, callback) { if (defaultValue === undefined && !callback) { throw Error("defaultValue is required unless callback is provided"); } if (!type) { type = "string"; } options[name] = { type: type, defaultValue: defaultValue, callback: callback, }; if (aliases) { for (var i = 0; i < aliases.length; i++) { options[aliases[i]] = options[name]; } } if (defaultValue) { setOption(name, defaultValue); } } function setOption(name, value, cm, cfg) { var option = options[name]; cfg = cfg || {}; var scope = cfg.scope; if (!option) { return new Error("Unknown option: " + name); } if (option.type == "boolean") { if (value && value !== true) { return new Error("Invalid argument: " + name + "=" + value); } else if (value !== false) { // Boolean options are set to true if value is not defined. value = true; } } if (option.callback) { if (scope !== "local") { option.callback(value, undefined); } if (scope !== "global" && cm) { option.callback(value, cm); } } else { if (scope !== "local") { option.value = option.type == "boolean" ? !!value : value; } if (scope !== "global" && cm) { cm.state.vim.options[name] = { value: value }; } } } function getOption(name, cm, cfg) { var option = options[name]; cfg = cfg || {}; var scope = cfg.scope; if (!option) { return new Error("Unknown option: " + name); } if (option.callback) { var local = cm && option.callback(undefined, cm); if (scope !== "global" && local !== undefined) { return local; } if (scope !== "local") { return option.callback(); } return; } else { var local = scope !== "global" && cm && cm.state.vim.options[name]; return (local || (scope !== "local" && option) || {}).value; } } defineOption("filetype", undefined, "string", ["ft"], function (name, cm) { // Option is local. Do nothing for global. if (cm === undefined) { return; } // The 'filetype' option proxies to the CodeMirror 'mode' option. if (name === undefined) { var mode = cm.getOption("mode"); return mode == "null" ? "" : mode; } else { var mode = name == "" ? "null" : name; cm.setOption("mode", mode); } }); var createCircularJumpList = function () { var size = 100; var pointer = -1; var head = 0; var tail = 0; var buffer = new Array(size); function add(cm, oldCur, newCur) { var current = pointer % size; var curMark = buffer[current]; function useNextSlot(cursor) { var next = ++pointer % size; var trashMark = buffer[next]; if (trashMark) { trashMark.clear(); } buffer[next] = cm.setBookmark(cursor); } if (curMark) { var markPos = curMark.find(); // avoid recording redundant cursor position if (markPos && !cursorEqual(markPos, oldCur)) { useNextSlot(oldCur); } } else { useNextSlot(oldCur); } useNextSlot(newCur); head = pointer; tail = pointer - size + 1; if (tail < 0) { tail = 0; } } function move(cm, offset) { pointer += offset; if (pointer > head) { pointer = head; } else if (pointer < tail) { pointer = tail; } var mark = buffer[(size + pointer) % size]; // skip marks that are temporarily removed from text buffer if (mark && !mark.find()) { var inc = offset > 0 ? 1 : -1; var newCur; var oldCur = cm.getCursor(); do { pointer += inc; mark = buffer[(size + pointer) % size]; // skip marks that are the same as current position if (mark && (newCur = mark.find()) && !cursorEqual(oldCur, newCur)) { break; } } while (pointer < head && pointer > tail); } return mark; } function find(cm, offset) { var oldPointer = pointer; var mark = move(cm, offset); pointer = oldPointer; return mark && mark.find(); } return { cachedCursor: undefined, //used for # and * jumps add: add, find: find, move: move, }; }; // Returns an object to track the changes associated insert mode. It // clones the object that is passed in, or creates an empty object one if // none is provided. var createInsertModeChanges = function (c) { if (c) { // Copy construction return { changes: c.changes, expectCursorActivityForChange: c.expectCursorActivityForChange, }; } return { // Change list changes: [], // Set to true on change, false on cursorActivity. expectCursorActivityForChange: false, }; }; function MacroModeState() { this.latestRegister = undefined; this.isPlaying = false; this.isRecording = false; this.replaySearchQueries = []; this.onRecordingDone = undefined; this.lastInsertModeChanges = createInsertModeChanges(); } MacroModeState.prototype = { exitMacroRecordMode: function () { var macroModeState = vimGlobalState.macroModeState; if (macroModeState.onRecordingDone) { macroModeState.onRecordingDone(); // close dialog } macroModeState.onRecordingDone = undefined; macroModeState.isRecording = false; }, enterMacroRecordMode: function (cm, registerName) { var register = vimGlobalState.registerController.getRegister(registerName); if (register) { register.clear(); this.latestRegister = registerName; if (cm.openDialog) { this.onRecordingDone = cm.openDialog( document.createTextNode("(recording)[" + registerName + "]"), null, { bottom: true } ); } this.isRecording = true; } }, }; function maybeInitVimState(cm) { if (!cm.state.vim) { // Store instance state in the CodeMirror object. cm.state.vim = { inputState: new InputState(), // Vim's input state that triggered the last edit, used to repeat // motions and operators with '.'. lastEditInputState: undefined, // Vim's action command before the last edit, used to repeat actions // with '.' and insert mode repeat. lastEditActionCommand: undefined, // When using jk for navigation, if you move from a longer line to a // shorter line, the cursor may clip to the end of the shorter line. // If j is pressed again and cursor goes to the next line, the // cursor should go back to its horizontal position on the longer // line if it can. This is to keep track of the horizontal position. lastHPos: -1, // Doing the same with screen-position for gj/gk lastHSPos: -1, // The last motion command run. Cleared if a non-motion command gets // executed in between. lastMotion: null, marks: {}, insertMode: false, // Repeat count for changes made in insert mode, triggered by key // sequences like 3,i. Only exists when insertMode is true. insertModeRepeat: undefined, visualMode: false, // If we are in visual line mode. No effect if visualMode is false. visualLine: false, visualBlock: false, lastSelection: null, lastPastedText: null, sel: {}, // Buffer-local/window-local values of vim options. options: {}, }; } return cm.state.vim; } var vimGlobalState; function resetVimGlobalState() { vimGlobalState = { // The current search query. searchQuery: null, // Whether we are searching backwards. searchIsReversed: false, // Replace part of the last substituted pattern lastSubstituteReplacePart: undefined, jumpList: createCircularJumpList(), macroModeState: new MacroModeState(), // Recording latest f, t, F or T motion command. lastCharacterSearch: { increment: 0, forward: true, selectedCharacter: "", }, registerController: new RegisterController({}), // search history buffer searchHistoryController: new HistoryController(), // ex Command history buffer exCommandHistoryController: new HistoryController(), }; for (var optionName in options) { var option = options[optionName]; option.value = option.defaultValue; } } var lastInsertModeKeyTimer; var vimApi = { buildKeyMap: function () { // TODO: Convert keymap into dictionary format for fast lookup. }, // Testing hook, though it might be useful to expose the register // controller anyway. getRegisterController: function () { return vimGlobalState.registerController; }, // Testing hook. resetVimGlobalState_: resetVimGlobalState, // Testing hook. getVimGlobalState_: function () { return vimGlobalState; }, // Testing hook. maybeInitVimState_: maybeInitVimState, suppressErrorLogging: false, InsertModeKey: InsertModeKey, map: function (lhs, rhs, ctx) { // Add user defined key bindings. exCommandDispatcher.map(lhs, rhs, ctx); }, unmap: function (lhs, ctx) { return exCommandDispatcher.unmap(lhs, ctx); }, // Non-recursive map function. // NOTE: This will not create mappings to key maps that aren't present // in the default key map. See TODO at bottom of function. noremap: function (lhs, rhs, ctx) { function toCtxArray(ctx) { return ctx ? [ctx] : ["normal", "insert", "visual"]; } var ctxsToMap = toCtxArray(ctx); // Look through all actual defaults to find a map candidate. var actualLength = defaultKeymap.length, origLength = defaultKeymapLength; for ( var i = actualLength - origLength; i < actualLength && ctxsToMap.length; i++ ) { var mapping = defaultKeymap[i]; // Omit mappings that operate in the wrong context(s) and those of invalid type. if ( mapping.keys == rhs && (!ctx || !mapping.context || mapping.context === ctx) && mapping.type.substr(0, 2) !== "ex" && mapping.type.substr(0, 3) !== "key" ) { // Make a shallow copy of the original keymap entry. var newMapping = {}; for (var key in mapping) { newMapping[key] = mapping[key]; } // Modify it point to the new mapping with the proper context. newMapping.keys = lhs; if (ctx && !newMapping.context) { newMapping.context = ctx; } // Add it to the keymap with a higher priority than the original. this._mapCommand(newMapping); // Record the mapped contexts as complete. var mappedCtxs = toCtxArray(mapping.context); ctxsToMap = ctxsToMap.filter(function (el) { return mappedCtxs.indexOf(el) === -1; }); } } // TODO: Create non-recursive keyToKey mappings for the unmapped contexts once those exist. }, // Remove all user-defined mappings for the provided context. mapclear: function (ctx) { // Partition the existing keymap into user-defined and true defaults. var actualLength = defaultKeymap.length, origLength = defaultKeymapLength; var userKeymap = defaultKeymap.slice(0, actualLength - origLength); defaultKeymap = defaultKeymap.slice(actualLength - origLength); if (ctx) { // If a specific context is being cleared, we need to keep mappings // from all other contexts. for (var i = userKeymap.length - 1; i >= 0; i--) { var mapping = userKeymap[i]; if (ctx !== mapping.context) { if (mapping.context) { this._mapCommand(mapping); } else { // `mapping` applies to all contexts so create keymap copies // for each context except the one being cleared. var contexts = ["normal", "insert", "visual"]; for (var j in contexts) { if (contexts[j] !== ctx) { var newMapping = {}; for (var key in mapping) { newMapping[key] = mapping[key]; } newMapping.context = contexts[j]; this._mapCommand(newMapping); } } } } } } }, // TODO: Expose setOption and getOption as instance methods. Need to decide how to namespace // them, or somehow make them work with the existing CodeMirror setOption/getOption API. setOption: setOption, getOption: getOption, defineOption: defineOption, defineEx: function (name, prefix, func) { if (!prefix) { prefix = name; } else if (name.indexOf(prefix) !== 0) { throw new Error( '(Vim.defineEx) "' + prefix + '" is not a prefix of "' + name + '", command not registered' ); } exCommands[name] = func; exCommandDispatcher.commandMap_[prefix] = { name: name, shortName: prefix, type: "api", }; }, 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; if (macroModeState.isRecording) { if (key == "q") { macroModeState.exitMacroRecordMode(); clearInputState(cm); return true; } if (origin != "mapping") { logKey(macroModeState, key); } } } function handleEsc() { if (key == "<Esc>") { if (vim.visualMode) { // Get back to normal mode. exitVisualMode(cm); } else if (vim.insertMode) { // Get back to normal mode. exitInsertMode(cm); } else { // We're already in normal mode. Let '<Esc>' be handled normally. return; } clearInputState(cm); return true; } } function doKeyToKey(keys) { // TODO: prevent infinite recursion. var match; while (keys) { // Pull off one command key, which is either a single character // or a special sequence wrapped in '<' and '>', e.g. '<Space>'. match = /<\w+-.+?>|<\w+>|./.exec(keys); key = match[0]; keys = keys.substring(match.index + key.length); vimApi.handleKey(cm, key, "mapping"); } } function handleKeyInsertMode() { if (handleEsc()) { return true; } var keys = (vim.inputState.keyBuffer = vim.inputState.keyBuffer + key); var keysAreChars = key.length == 1; var match = commandDispatcher.matchCommand( keys, defaultKeymap, vim.inputState, "insert" ); // Need to check all key substrings in insert mode. while (keys.length > 1 && match.type != "full") { var keys = (vim.inputState.keyBuffer = keys.slice(1)); var thisMatch = commandDispatcher.matchCommand( keys, defaultKeymap, vim.inputState, "insert" ); if (thisMatch.type != "none") { match = thisMatch; } } if (match.type == "none") { clearInputState(cm); return false; } else if (match.type == "partial") { if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); } lastInsertModeKeyTimer = window.setTimeout(function () { if (vim.insertMode && vim.inputState.keyBuffer) { clearInputState(cm); } }, getOption("insertModeEscKeysTimeout")); return !keysAreChars; } if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); } if (keysAreChars) { var selections = cm.listSelections(); for (var i = 0; i < selections.length; i++) { var here = selections[i].head; cm.replaceRange( "", offsetCursor(here, 0, -(keys.length - 1)), here, "+input" ); } vimGlobalState.macroModeState.lastInsertModeChanges.changes.pop(); } clearInputState(cm); return match.command; } function handleKeyNonInsertMode() { if (handleMacroRecording() || handleEsc()) { return true; } var keys = (vim.inputState.keyBuffer = vim.inputState.keyBuffer + key); if (/^[1-9]\d*$/.test(keys)) { return true; } var keysMatcher = /^(\d*)(.*)$/.exec(keys); if (!keysMatcher) { clearInputState(cm); return false; } var context = vim.visualMode ? "visual" : "normal"; var mainKey = keysMatcher[2] || keysMatcher[1]; if ( vim.inputState.operatorShortcut && vim.inputState.operatorShortcut.slice(-1) == mainKey ) { // multikey operators act linewise by repeating only the last character mainKey = vim.inputState.operatorShortcut; } var match = commandDispatcher.matchCommand( mainKey, defaultKeymap, vim.inputState, context ); if (match.type == "none") { clearInputState(cm); return false; } else if (match.type == "partial") { return true; } vim.inputState.keyBuffer = ""; var keysMatcher = /^(\d*)(.*)$/.exec(keys); if (keysMatcher[1] && keysMatcher[1] != "0") { vim.inputState.pushRepeatDigit(keysMatcher[1]); } return match.command; } var command; if (vim.insertMode) { command = handleKeyInsertMode(); } else { command = handleKeyNonInsertMode(); } if (command === false) { return !vim.insertMode && key.length === 1 ? function () { return true; } : 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 () { return true; }; } 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); if (!vimApi.suppressErrorLogging) { console["log"](e); } throw e; } return true; }); }; } }, handleEx: function (cm, input) { exCommandDispatcher.processCommand(cm, input); }, defineMotion: defineMotion, defineAction: defineAction, defineOperator: defineOperator, mapCommand: mapCommand, _mapCommand: _mapCommand, defineRegister: defineRegister, exitVisualMode: exitVisualMode, exitInsertMode: exitInsertMode, }; // Represents the current input state. function InputState() { this.prefixRepeat = []; this.motionRepeat = []; this.operator = null; this.operatorArgs = null; this.motion = null; this.motionArgs = null; this.keyBuffer = []; // For matching multi-key commands. this.registerName = null; // Defaults to the unnamed register. } InputState.prototype.pushRepeatDigit = function (n) { if (!this.operator) { this.prefixRepeat = this.prefixRepeat.concat(n); } else { this.motionRepeat = this.motionRepeat.concat(n); } }; InputState.prototype.getRepeat = function () { var repeat = 0; if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) { repeat = 1; if (this.prefixRepeat.length > 0) { repeat *= parseInt(this.prefixRepeat.join(""), 10); } if (this.motionRepeat.length > 0) { repeat *= parseInt(this.motionRepeat.join(""), 10); } } return repeat; }; function clearInputState(cm, reason) { cm.state.vim.inputState = new InputState(); CodeMirror.signal(cm, "vim-command-done", reason); } /* * Register stores information about copy and paste registers. Besides * text, a register must store whether it is linewise (i.e., when it is * pasted, should it insert itself into a new line, or should the text be * inserted at the cursor position.) */ function Register(text, linewise, blockwise) { this.clear(); this.keyBuffer = [text || ""]; this.insertModeChanges = []; this.searchQueries = []; this.linewise = !!linewise; this.blockwise = !!blockwise; } Register.prototype = { setText: function (text, linewise, blockwise)