UNPKG

codemirror

Version:

In-browser code editing made bearable

1,262 lines (1,234 loc) 157 kB
/** * Supported keybindings: * * Motion: * h, j, k, l * gj, gk * e, E, w, W, b, B, ge, gE * f<character>, F<character>, t<character>, T<character> * $, ^, 0, -, +, _ * gg, G * % * '<character>, `<character> * * Operator: * d, y, c * dd, yy, cc * g~, g~g~ * >, <, >>, << * * Operator-Motion: * x, X, D, Y, C, ~ * * Action: * a, i, s, A, I, S, o, O * zz, z., z<CR>, zt, zb, z- * J * u, Ctrl-r * m<character> * r<character> * * Modes: * ESC - leave insert mode, visual mode, and clear input state. * Ctrl-[, Ctrl-c - same as ESC. * * Registers: unnamed, -, a-z, A-Z, 0-9 * (Does not respect the special case for number registers when delete * operator is made with these commands: %, (, ), , /, ?, n, N, {, } ) * TODO: Implement the remaining registers. * Marks: a-z, A-Z, and 0-9 * TODO: Implement the remaining special marks. They have more complex * behavior. * * Events: * 'vim-mode-change' - raised on the editor anytime the current mode changes, * Event object: {mode: "visual", subMode: "linewise"} * * Code structure: * 1. Default keymap * 2. Variable declarations and short basic helpers * 3. Instance (External API) implementation * 4. Internal state tracking objects (input state, counter) implementation * and instanstiation * 5. Key handler (the main command dispatcher) implementation * 6. Motion, operator, and action implementations * 7. Helper functions for the key handler, motions, operators, and actions * 8. Set up Vim to work as a keymap for CodeMirror. */ (function() { 'use strict'; 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: ['<Space>'], type: 'keyToKey', toKeys: ['l'] }, { keys: ['<BS>'], type: 'keyToKey', toKeys: ['h'] }, { keys: ['<C-Space>'], type: 'keyToKey', toKeys: ['W'] }, { keys: ['<C-BS>'], type: 'keyToKey', toKeys: ['B'] }, { keys: ['<S-Space>'], type: 'keyToKey', toKeys: ['w'] }, { keys: ['<S-BS>'], type: 'keyToKey', toKeys: ['b'] }, { 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: ['s'], type: 'keyToKey', toKeys: ['c', 'l'], context: 'normal' }, { keys: ['s'], type: 'keyToKey', toKeys: ['x', 'i'], context: 'visual'}, { keys: ['S'], type: 'keyToKey', toKeys: ['c', 'c'], context: 'normal' }, { keys: ['S'], type: 'keyToKey', toKeys: ['d', 'c', 'c'], 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' }, // 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: ['g','j'], type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: true }}, { keys: ['g','k'], 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: ['g', 'e'], type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, inclusive: true }}, { keys: ['g', 'E'], 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: ['<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: ['g', 'g'], 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: ['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 } }, { 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', motionArgs: { }}, { keys: ['o'], type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: { },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: 'indent', operatorArgs: { indentRight: true }}, { keys: ['<'], type: 'operator', operator: 'indent', operatorArgs: { indentRight: false }}, { keys: ['g', '~'], type: 'operator', operator: 'swapcase' }, { keys: ['n'], type: 'motion', motion: 'findNext', motionArgs: { forward: true, toJumplist: true }}, { keys: ['N'], type: 'motion', motion: 'findNext', motionArgs: { forward: false, toJumplist: true }}, // 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 }, operatorMotionArgs: { visualLine: true }}, { keys: ['Y'], type: 'operatorMotion', operator: 'yank', motion: 'moveToEol', motionArgs: { inclusive: true }, operatorMotionArgs: { visualLine: true }}, { keys: ['C'], type: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, operatorMotionArgs: { visualLine: true }}, { keys: ['~'], type: 'operatorMotion', operator: 'swapcase', operatorArgs: { shouldMoveCursor: true }, motion: 'moveByCharacters', motionArgs: { forward: true }}, // 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' }}, { keys: ['A'], type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'eol' }}, { keys: ['i'], type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'inplace' }}, { keys: ['I'], type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'firstNonBlank' }}, { keys: ['o'], type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: true }}, { keys: ['O'], type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: false }}, { keys: ['v'], type: 'action', action: 'toggleVisualMode' }, { keys: ['V'], type: 'action', action: 'toggleVisualMode', actionArgs: { linewise: true }}, { keys: ['g', 'v'], type: 'action', action: 'reselectLastSelection' }, { keys: ['J'], type: 'action', action: 'joinLines', 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 }}, { keys: ['u'], type: 'action', action: 'undo' }, { keys: ['<C-r>'], type: 'action', action: 'redo' }, { keys: ['m', 'character'], type: 'action', action: 'setMark' }, { keys: ['"', 'character'], type: 'action', action: 'setRegister' }, { keys: ['z', 'z'], type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }}, { keys: ['z', '.'], type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }, motion: 'moveToFirstNonWhiteSpaceCharacter' }, { keys: ['z', 't'], 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: ['z', 'b'], 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}}, // 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 Vim = function() { CodeMirror.defineOption('vimMode', false, function(cm, val) { if (val) { cm.setOption('keyMap', 'vim'); cm.setOption('disableInput', true); CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); cm.on('beforeSelectionChange', beforeSelectionChange); maybeInitVimState(cm); CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm)); } else if (cm.state.vim) { cm.setOption('keyMap', 'default'); cm.setOption('disableInput', false); cm.off('beforeSelectionChange', beforeSelectionChange); CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm)); cm.state.vim = null; } }); function beforeSelectionChange(cm, cur) { var vim = cm.state.vim; if (vim.insertMode || vim.exMode) return; var head = cur.head; if (head.ch && head.ch == cm.doc.getLine(head.line).length) { head.ch--; } } 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 wordRegexp = [(/\w/), (/[^\w\s]/)], bigWordRegexp = [(/\S/)]; 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 specialSymbols = '~`!@#$%^&*()_-+=[{}]\\|/?.,<>:;"\''.split(''); var specialKeys = ['Left', 'Right', 'Up', 'Down', 'Space', 'Backspace', 'Esc', 'Home', 'End', 'PageUp', 'PageDown', 'Enter']; var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']); var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"', '.', ':']); 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 (/^[A-Z]$/).test(k); } function isWhiteSpaceString(k) { return (/^\s*$/).test(k); } 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) { if (defaultValue === undefined) { throw Error('defaultValue is required'); } if (!type) { type = 'string'; } options[name] = { type: type, defaultValue: defaultValue }; setOption(name, defaultValue); } function setOption(name, value) { var option = options[name]; if (!option) { throw Error('Unknown option: ' + name); } if (option.type == 'boolean') { if (value && value !== true) { throw Error('Invalid argument: ' + name + '=' + value); } else if (value !== false) { // Boolean options are set to true if value is not defined. value = true; } } option.value = option.type == 'boolean' ? !!value : value; } function getOption(name) { var option = options[name]; if (!option) { throw Error('Unknown option: ' + name); } return option.value; } 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; } return { cachedCursor: undefined, //used for # and * jumps add: add, 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; 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; this.onRecordingDone = cm.openDialog( '(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, lastSelection: null }; } 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. lastChararacterSearch: {increment:0, forward:true, selectedCharacter:''}, registerController: new RegisterController({}) }; for (var optionName in options) { var option = options[optionName]; option.value = option.defaultValue; } } 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 anyways. getRegisterController: function() { return vimGlobalState.registerController; }, // Testing hook. resetVimGlobalState_: resetVimGlobalState, // Testing hook. getVimGlobalState_: function() { return vimGlobalState; }, // Testing hook. maybeInitVimState_: maybeInitVimState, InsertModeKey: InsertModeKey, map: function(lhs, rhs, ctx) { // Add user defined key bindings. exCommandDispatcher.map(lhs, rhs, ctx); }, setOption: setOption, getOption: getOption, defineOption: defineOption, defineEx: function(name, prefix, func){ 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'}; }, // This is the outermost function called by CodeMirror, after keys have // been mapped to their Vim equivalents. handleKey: function(cm, key) { var command; var vim = maybeInitVimState(cm); var macroModeState = vimGlobalState.macroModeState; if (macroModeState.isRecording) { if (key == 'q') { macroModeState.exitMacroRecordMode(); vim.inputState = new InputState(); return; } } if (key == '<Esc>') { // Clear input state and get back to normal mode. vim.inputState = new InputState(); if (vim.visualMode) { exitVisualMode(cm); } return; } // Enter visual mode when the mouse selects text. if (!vim.visualMode && !cursorEqual(cm.getCursor('head'), cm.getCursor('anchor'))) { vim.visualMode = true; vim.visualLine = false; CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"}); cm.on('mousedown', exitVisualMode); } if (key != '0' || (key == '0' && vim.inputState.getRepeat() === 0)) { // Have to special case 0 since it's both a motion and a number. command = commandDispatcher.matchCommand(key, defaultKeymap, vim); } if (!command) { if (isNumber(key)) { // Increment count unless count is 0 and key is 0. vim.inputState.pushRepeatDigit(key); } if (macroModeState.isRecording) { logKey(macroModeState, key); } return; } if (command.type == 'keyToKey') { // TODO: prevent infinite recursion. for (var i = 0; i < command.toKeys.length; i++) { this.handleKey(cm, command.toKeys[i]); } } else { if (macroModeState.isRecording) { logKey(macroModeState, key); } commandDispatcher.processCommand(cm, vim, command); } }, handleEx: function(cm, input) { exCommandDispatcher.processCommand(cm, input); } }; // 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; }; /* * 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) { this.clear(); this.keyBuffer = [text || '']; this.insertModeChanges = []; this.searchQueries = []; this.linewise = !!linewise; } Register.prototype = { setText: function(text, linewise) { this.keyBuffer = [text || '']; this.linewise = !!linewise; }, pushText: function(text, linewise) { // if this register has ever been set to linewise, use linewise. if (linewise || this.linewise) { this.keyBuffer.push('\n'); this.linewise = true; } this.keyBuffer.push(text); }, pushInsertModeChanges: function(changes) { this.insertModeChanges.push(createInsertModeChanges(changes)); }, pushSearchQuery: function(query) { this.searchQueries.push(query); }, clear: function() { this.keyBuffer = []; this.insertModeChanges = []; this.searchQueries = []; this.linewise = false; }, toString: function() { return this.keyBuffer.join(''); } }; /* * vim registers allow you to keep many independent copy and paste buffers. * See http://usevim.com/2012/04/13/registers/ for an introduction. * * RegisterController keeps the state of all the registers. An initial * state may be passed in. The unnamed register '"' will always be * overridden. */ function RegisterController(registers) { this.registers = registers; this.unnamedRegister = registers['"'] = new Register(); registers['.'] = new Register(); registers[':'] = new Register(); } RegisterController.prototype = { pushText: function(registerName, operator, text, linewise) { if (linewise && text.charAt(0) == '\n') { text = text.slice(1) + '\n'; } if (linewise && text.charAt(text.length - 1) !== '\n'){ text += '\n'; } // Lowercase and uppercase registers refer to the same register. // Uppercase just means append. var register = this.isValidRegister(registerName) ? this.getRegister(registerName) : null; // if no register/an invalid register was specified, things go to the // default registers if (!register) { switch (operator) { case 'yank': // The 0 register contains the text from the most recent yank. this.registers['0'] = new Register(text, linewise); break; case 'delete': case 'change': if (text.indexOf('\n') == -1) { // Delete less than 1 line. Update the small delete register. this.registers['-'] = new Register(text, linewise); } else { // Shift down the contents of the numbered registers and put the // deleted text into register 1. this.shiftNumericRegisters_(); this.registers['1'] = new Register(text, linewise); } break; } // Make sure the unnamed register is set to what just happened this.unnamedRegister.setText(text, linewise); return; } // If we've gotten to this point, we've actually specified a register var append = isUpperCase(registerName); if (append) { register.append(text, linewise); // The unnamed register always has the same value as the last used // register. this.unnamedRegister.append(text, linewise); } else { register.setText(text, linewise); this.unnamedRegister.setText(text, linewise); } }, // Gets the register named @name. If one of @name doesn't already exist, // create it. If @name is invalid, return the unnamedRegister. getRegister: function(name) { if (!this.isValidRegister(name)) { return this.unnamedRegister; } name = name.toLowerCase(); if (!this.registers[name]) { this.registers[name] = new Register(); } return this.registers[name]; }, isValidRegister: function(name) { return name && inArray(name, validRegisters); }, shiftNumericRegisters_: function() { for (var i = 9; i >= 2; i--) { this.registers[i] = this.getRegister('' + (i - 1)); } } }; var commandDispatcher = { matchCommand: function(key, keyMap, vim) { var inputState = vim.inputState; var keys = inputState.keyBuffer.concat(key); var matchedCommands = []; var selectedCharacter; for (var i = 0; i < keyMap.length; i++) { var command = keyMap[i]; if (matchKeysPartial(keys, command.keys)) { if (inputState.operator && command.type == 'action') { // Ignore matched action commands after an operator. Operators // only operate on motions. This check is really for text // objects since aW, a[ etcs conflicts with a. continue; } // Match commands that take <character> as an argument. if (command.keys[keys.length - 1] == 'character') { selectedCharacter = keys[keys.length - 1]; if (selectedCharacter.length>1){ switch(selectedCharacter){ case '<CR>': selectedCharacter='\n'; break; case '<Space>': selectedCharacter=' '; break; default: continue; } } } // Add the command to the list of matched commands. Choose the best // command later. matchedCommands.push(command); } } // Returns the command if it is a full match, or null if not. function getFullyMatchedCommandOrNull(command) { if (keys.length < command.keys.length) { // Matches part of a multi-key command. Buffer and wait for next // stroke. inputState.keyBuffer.push(key); return null; } else { if (command.keys[keys.length - 1] == 'character') { inputState.selectedCharacter = selectedCharacter; } // Clear the buffer since a full match was found. inputState.keyBuffer = []; return command; } } if (!matchedCommands.length) { // Clear the buffer since there were no matches. inputState.keyBuffer = []; return null; } else if (matchedCommands.length == 1) { return getFullyMatchedCommandOrNull(matchedCommands[0]); } else { // Find the best match in the list of matchedCommands. var context = vim.visualMode ? 'visual' : 'normal'; var bestMatch; // Default to first in the list. for (var i = 0; i < matchedCommands.length; i++) { var current = matchedCommands[i]; if (current.context == context) { bestMatch = current; break; } else if (!bestMatch && !current.context) { // Only set an imperfect match to best match if no best match is // set and the imperfect match is not restricted to another // context. bestMatch = current; } } return getFullyMatchedCommandOrNull(bestMatch); } }, processCommand: function(cm, vim, command) { vim.inputState.repeatOverride = command.repeatOverride; switch (command.type) { case 'motion': this.processMotion(cm, vim, command); break; case 'operator': this.processOperator(cm, vim, command); break; case 'operatorMotion': this.processOperatorMotion(cm, vim, command); break; case 'action': this.processAction(cm, vim, command); break; case 'search': this.processSearch(cm, vim, command); break; case 'ex': case 'keyToEx': this.processEx(cm, vim, command); break; default: break; } }, processMotion: function(cm, vim, command) { vim.inputState.motion = command.motion; vim.inputState.motionArgs = copyArgs(command.motionArgs); this.evalInput(cm, vim); }, processOperator: function(cm, vim, command) { var inputState = vim.inputState; if (inputState.operator) { if (inputState.operator == command.operator) { // Typing an operator twice like 'dd' makes the operator operate // linewise inputState.motion = 'expandToLine'; inputState.motionArgs = { linewise: true }; this.evalInput(cm, vim); return; } else { // 2 different operators in a row doesn't make sense. vim.inputState = new InputState(); } } inputState.operator = command.operator; inputState.operatorArgs = copyArgs(command.operatorArgs); if (vim.visualMode) { // Operating on a selection in visual mode. We don't need a motion. this.evalInput(cm, vim); } }, processOperatorMotion: function(cm, vim, command) { var visualMode = vim.visualMode; var operatorMotionArgs = copyArgs(command.operatorMotionArgs); if (operatorMotionArgs) { // Operator motions may have special behavior in visual mode. if (visualMode && operatorMotionArgs.visualLine) { vim.visualLine = true; } } this.processOperator(cm, vim, command); if (!visualMode) { this.processMotion(cm, vim, command); } }, processAction: function(cm, vim, command) { var inputState = vim.inputState; var repeat = inputState.getRepeat(); var repeatIsExplicit = !!repeat; var actionArgs = copyArgs(command.actionArgs) || {}; if (inputState.selectedCharacter) { actionArgs.selectedCharacter = inputState.selectedCharacter; } // Actions may or may not have motions and operators. Do these first. if (command.operator) { this.processOperator(cm, vim, command); } if (command.motion) { this.processMotion(cm, vim, command); } if (command.motion || command.operator) { this.evalInput(cm, vim); } actionArgs.repeat = repeat || 1; actionArgs.repeatIsExplicit = repeatIsExplicit; actionArgs.registerName = inputState.registerName; vim.inputState = new InputState(); vim.lastMotion = null; if (command.isEdit) { this.recordLastEdit(vim, inputState, command); } actions[command.action](cm, actionArgs, vim); }, processSearch: function(cm, vim, command) { if (!cm.getSearchCursor) { // Search depends on SearchCursor. return; } var forward = command.searchArgs.forward; var wholeWordOnly = command.searchArgs.wholeWordOnly; getSearchState(cm).setReversed(!forward); var promptPrefix = (forward) ? '/' : '?'; var originalQuery = getSearchState(cm).getQuery(); var originalScrollPos = cm.getScrollInfo(); function handleQuery(query, ignoreCase, smartCase) { try { updateSearchQuery(cm, query, ignoreCase, smartCase); } catch (e) { showConfirm(cm, 'Invalid regex: ' + query); return; } commandDispatcher.processMotion(cm, vim, { type: 'motion', motion: 'findNext', motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist } }); } function onPromptClose(query) { cm.scrollTo(originalScrollPos.left, originalScrollPos.top); handleQuery(query, true /** ignoreCase */, true /** smartCase */); var macroModeState = vimGlobalState.macroModeState; if (macroModeState.isRecording) { logSearchQuery(macroModeState, query); } } function onPromptKeyUp(_e, query) { var parsedQuery; try { parsedQuery = updateSearchQuery(cm, query, true /** ignoreCase */, true /** smartCase */); } catch (e) { // Swallow bad regexes for incremental search. } if (parsedQuery) { cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30); } else { clearSearchHighlight(cm); cm.scrollTo(originalScrollPos.left, originalScrollPos.top); } } function onPromptKeyDown(e, _query, close) { var keyName = CodeMirror.keyName(e); if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') { updateSearchQuery(cm, originalQuery); clearSearchHighlight(cm); cm.scrollTo(originalScrollPos.left, originalScrollPos.top); CodeMirror.e_stop(e); close(); cm.focus(); } } switch (command.searchArgs.querySrc) { case 'prompt': var macroModeState = vimGlobalState.macroModeState; if (macroModeState.isPlaying) { var query = macroModeState.replaySearchQueries.shift(); handleQuery(query, true /** ignoreCase */, false /** smartCase */); } else { showPrompt(cm, { onClose: onPromptClose, prefix: promptPrefix, desc: searchPromptDesc, onKeyUp: onPromptKeyUp, onKeyDown: onPromptKeyDown }); } break; case 'wordUnderCursor': var word = expandWordUnderCursor(cm, false /** inclusive */, true /** forward */, false /** bigWord */, true /** noSymbol */); var isKeyword = true; if (!word) { word = expandWordUnderCursor(cm, false /** inclusive */, true /** forward */, false /** bigWord */, false /** noSymbol */); isKeyword = false; } if (!word) { return; } var query = cm.getLine(word.start.line).substring(word.start.ch, word.end.ch); if (isKeyword && wholeWordOnly) { query = '\\b' + query + '\\b'; } else { query = escapeRegex(query); } // cachedCursor is used to save the old position of the cursor // when * or # causes vim to seek for the nearest word and shift // the cursor before entering the motion. vimGlobalState.jumpList.cachedCursor = cm.getCursor(); cm.setCursor(word.start); handleQuery(query, true /** ignoreCase */, false /** smartCase */); break; } }, processEx: function(cm, vim, command) { function onPromptClose(input) { // Give the prompt some time to close so that if processCommand shows // an error, the elements don't overlap. exCommandDispatcher.processCommand(cm, input); } function onPromptKeyDown(e, _input, close) { var keyName = CodeMirror.keyName(e); if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') { CodeMirror.e_stop(e); close(); cm.focus(); } } if (command.type == 'keyToEx') { // Handle user defined Ex to Ex mappings exCommandDispatcher.processCommand(cm, command.exArgs.input); } else { if (vim.visualMode) { showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>', onKeyDown: onPromptKeyDown}); } else { showPrompt(cm, { onClose: onPromptClose, prefix: ':', onKeyDown: onPromptKeyDown}); } } }, evalInput: function(cm, vim) { // If the motion comand is set, execute both the operator and motion. // Otherwise return. var inputState = vim.inputState; var motion = inputState.motion; var motionArgs = inputState.motionArgs || {}; var operator = inputState.operator; var operatorArgs = inputState.operatorArgs || {}; var registerName = inputState.registerName; var selectionEnd = cm.getCursor('head'); var selectionStart = cm.getCursor('anchor'); // The difference between cur and selection cursors are that cur is // being operated on and ignores that there is a selection. var curStart = copyCursor(selectionEnd); var curOriginal = copyCursor(curStart); var curEnd; var repeat; if (operator) { this.recordLastEdit(vim, inputState); } if (inputState.repeatOverride !== undefined) { // If repeatOverride is specified, that takes precedence over the // input state's repeat. Used by Ex mode and can be user defined. repeat = inputState.repeatOverride; } else { repeat = inputState.getRepeat(); } if (repeat > 0 && motionArgs.explicitRepeat) { motionArgs.repeatIsExplicit = true; } else if (motionArgs.noRepeat || (!motionArgs.explicitRepeat && repeat === 0)) { repeat = 1; motionArgs.repeatIsExplicit = false; } if (inputState.selectedCharacter) { // If there is a character input, stick it in all of the arg arrays. motionArgs.selectedCharacter = operatorArgs.selectedCharacter = inputState.selectedCharacter; } motionArgs.repeat = repeat; vim.inputState = new InputState(); if (motion) { var motionResult = motions[motion](cm, motionArgs, vim); vim.lastMotion = motions[motion]; if (!motionResult) { return; } if (motionArgs.toJumplist) { var jumpList = vimGlobalState.jumpList; // if the current motion is # or *, use cachedCursor var cachedCursor = jumpList.cachedCursor; if (cachedCursor) { recordJumpPosition(cm, cachedCursor, motionResult); delete jumpList.cachedCursor; } else { recordJumpPosition(cm, curOriginal, motionResult); } } if (motionResult instanceof Array) { curStart = motionResult[0]; curEnd = motionResult[1]; } else { curEnd = motionResult; } // TODO: Handle null returns from motion commands better. if (!curEnd) { curEnd = { ch: curStart.ch, line: curStart.line }; } if (vim.visualMode) { // Check if the selection crossed over itself. Will need to shift // the start point if that happened. if (cursorIsBefore(selectionStart, selectionEnd) && (cursorEqual(selectionStart, curEnd) || cursorIsBefore(curEnd, selectionStart))) { // The end of the selection has moved from after the start to // before the start. We will shift