UNPKG

jsx

Version:

a faster, safer, easier JavaScript

1,257 lines (1,237 loc) 113 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: unamed, -, 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. * * 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: ['Backspace'], type: 'keyToKey', toKeys: ['h'] }, { keys: ['Ctrl-Space'], type: 'keyToKey', toKeys: ['W'] }, { keys: ['Ctrl-Backspace'], type: 'keyToKey', toKeys: ['B'] }, { keys: ['Shift-Space'], type: 'keyToKey', toKeys: ['w'] }, { keys: ['Shift-Backspace'], type: 'keyToKey', toKeys: ['b'] }, { keys: ['Ctrl-n'], type: 'keyToKey', toKeys: ['j'] }, { keys: ['Ctrl-p'], type: 'keyToKey', toKeys: ['k'] }, { keys: ['Ctrl-['], type: 'keyToKey', toKeys: ['Esc'] }, { keys: ['Ctrl-c'], type: 'keyToKey', toKeys: ['Esc'] }, { keys: ['s'], type: 'keyToKey', toKeys: ['c', 'l'] }, { keys: ['S'], type: 'keyToKey', toKeys: ['c', 'c'] }, { keys: ['Home'], type: 'keyToKey', toKeys: ['0'] }, { keys: ['End'], type: 'keyToKey', toKeys: ['$'] }, { keys: ['PageUp'], type: 'keyToKey', toKeys: ['Ctrl-b'] }, { keys: ['PageDown'], type: 'keyToKey', toKeys: ['Ctrl-f'] }, // Motions { keys: ['H'], type: 'motion', motion: 'moveToTopLine', motionArgs: { linewise: true }}, { keys: ['M'], type: 'motion', motion: 'moveToMiddleLine', motionArgs: { linewise: true }}, { keys: ['L'], type: 'motion', motion: 'moveToBottomLine', motionArgs: { linewise: 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 }}, { keys: ['}'], type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: true }}, { keys: ['Ctrl-f'], type: 'motion', motion: 'moveByPage', motionArgs: { forward: true }}, { keys: ['Ctrl-b'], type: 'motion', motion: 'moveByPage', motionArgs: { forward: false }}, { keys: ['Ctrl-d'], type: 'motion', motion: 'moveByScroll', motionArgs: { forward: true, explicitRepeat: true }}, { keys: ['Ctrl-u'], type: 'motion', motion: 'moveByScroll', motionArgs: { forward: false, explicitRepeat: true }}, { keys: ['g', 'g'], type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: false, explicitRepeat: true, linewise: true }}, { keys: ['G'], type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: true, explicitRepeat: true, linewise: 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 }}, { 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' }, { keys: ['`', 'character'], type: 'motion', motion: 'goToMark' }, { 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}}, { keys: ['[', 'character'], type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: false}}, { keys: ['|'], type: 'motion', motion: 'moveToColumn', motionArgs: { }}, // Operators { keys: ['d'], type: 'operator', operator: 'delete' }, { keys: ['y'], type: 'operator', operator: 'yank' }, { keys: ['c'], type: 'operator', operator: 'change', operatorArgs: { enterInsertMode: true } }, { 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 }}, { keys: ['N'], type: 'motion', motion: 'findNext', 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 }, operatorMotionArgs: { visualLine: true }}, { keys: ['Y'], type: 'operatorMotion', operator: 'yank', motion: 'moveToEol', motionArgs: { inclusive: true }, operatorMotionArgs: { visualLine: true }}, { keys: ['C'], type: 'operatorMotion', operator: 'change', operatorArgs: { enterInsertMode: true }, motion: 'moveToEol', motionArgs: { inclusive: true }, operatorMotionArgs: { visualLine: true }}, { keys: ['~'], type: 'operatorMotion', operator: 'swapcase', motion: 'moveByCharacters', motionArgs: { forward: true }}, // Actions { keys: ['a'], type: 'action', action: 'enterInsertMode', actionArgs: { insertAt: 'charAfter' }}, { keys: ['A'], type: 'action', action: 'enterInsertMode', actionArgs: { insertAt: 'eol' }}, { keys: ['i'], type: 'action', action: 'enterInsertMode' }, { keys: ['I'], type: 'action', action: 'enterInsertMode', motion: 'moveToFirstNonWhiteSpaceCharacter' }, { keys: ['o'], type: 'action', action: 'newLineAndEnterInsertMode', actionArgs: { after: true }}, { keys: ['O'], type: 'action', action: 'newLineAndEnterInsertMode', actionArgs: { after: false }}, { keys: ['v'], type: 'action', action: 'toggleVisualMode' }, { keys: ['V'], type: 'action', action: 'toggleVisualMode', actionArgs: { linewise: true }}, { keys: ['J'], type: 'action', action: 'joinLines' }, { keys: ['p'], type: 'action', action: 'paste', actionArgs: { after: true }}, { keys: ['P'], type: 'action', action: 'paste', actionArgs: { after: false }}, { keys: ['r', 'character'], type: 'action', action: 'replace' }, { keys: ['u'], type: 'action', action: 'undo' }, { keys: ['Ctrl-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', 'Enter'], 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: ['Ctrl-a'], type: 'action', action: 'incrementNumberToken', actionArgs: {increase: true, backtrack: false}}, { keys: ['Ctrl-x'], type: 'action', action: 'incrementNumberToken', 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' }}, { keys: ['?'], type: 'search', searchArgs: { forward: false, querySrc: 'prompt' }}, { keys: ['*'], type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor' }}, { keys: ['#'], type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor' }}, // Ex command { keys: [':'], type: 'ex' } ]; var Vim = function() { var alphabetRegex = /[A-Za-z]/; var numberRegex = /[\d]/; var whiteSpaceRegex = /\s/; 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 SPECIAL_SYMBOLS = '~`!@#$%^&*()_-+=[{}]\\|/?.,<>:;\"\''; var specialSymbols = SPECIAL_SYMBOLS.split(''); var specialKeys = ['Left', 'Right', 'Up', 'Down', 'Space', 'Backspace', 'Esc', 'Home', 'End', 'PageUp', 'PageDown', 'Enter']; var validMarks = upperCaseAlphabet.concat(lowerCaseAlphabet).concat( numbers).concat(['<', '>']); var validRegisters = upperCaseAlphabet.concat(lowerCaseAlphabet).concat( numbers).concat('-\"'.split('')); function isAlphabet(k) { return alphabetRegex.test(k); } 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 isAlphanumeric(k) { return (/^[\w]$/).test(k); } function isWhiteSpace(k) { return whiteSpaceRegex.test(k); } function isWhiteSpaceString(k) { return (/^\s*$/).test(k); } function inRangeInclusive(x, start, end) { return x >= start && x <= end; } function inArray(val, arr) { for (var i = 0; i < arr.length; i++) { if (arr[i] == val) { return true; } } return false; } // Global Vim state. Call getVimGlobalState to get and initialize. var vimGlobalState; function getVimGlobalState() { if (!vimGlobalState) { vimGlobalState = { // The current search query. searchQuery: null, // Whether we are searching backwards. searchIsReversed: false, // Recording latest f, t, F or T motion command. lastChararacterSearch: {increment:0, forward:true, selectedCharacter:''}, registerController: new RegisterController({}) }; } return vimGlobalState; } function getVimState(cm) { if (!cm.vimState) { // Store instance state in the CodeMirror object. cm.vimState = { inputState: new InputState(), // 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: {}, visualMode: false, // If we are in visual line mode. No effect if visualMode is false. visualLine: false }; } return cm.vimState; } 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 getVimGlobalState().registerController; }, // Testing hook. clearVimGlobalState_: function() { vimGlobalState = null; }, map: function(lhs, rhs) { // Add user defined key bindings. exCommandDispatcher.map(lhs, rhs); }, defineEx: function(name, prefix, func){ if (name.indexOf(prefix) === 0) { exCommands[name]=func; exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'}; }else throw new Error("(Vim.defineEx) \""+prefix+"\" is not a prefix of \""+name+"\", command not registered"); }, // Initializes vim state variable on the CodeMirror object. Should only be // called lazily by handleKey or for testing. maybeInitState: function(cm) { getVimState(cm); }, // 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 = getVimState(cm); if (key == 'Esc') { // Clear input state and get back to normal mode. vim.inputState = new InputState(); if (vim.visualMode) { exitVisualMode(cm, vim); } return; } if (vim.visualMode && cursorEqual(cm.getCursor('head'), cm.getCursor('anchor'))) { // The selection was cleared. Exit visual mode. exitVisualMode(cm, vim); } if (!vim.visualMode && !cursorEqual(cm.getCursor('head'), cm.getCursor('anchor'))) { vim.visualMode = true; vim.visualLine = false; } 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); } 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 { commandDispatcher.processCommand(cm, vim, command); } } }; // 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 unamed 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(); if (text) { this.set(text, linewise); } } Register.prototype = { set: function(text, linewise) { this.text = text; this.linewise = !!linewise; }, append: function(text, linewise) { // if this register has ever been set to linewise, use linewise. if (linewise || this.linewise) { this.text += '\n' + text; this.linewise = true; } else { this.text += text; } }, clear: function() { this.text = ''; this.linewise = false; }, toString: function() { return this.text; } }; /* * 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.unamedRegister = registers['\"'] = new Register(); } RegisterController.prototype = { pushText: function(registerName, operator, text, linewise) { // 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.unamedRegister.set(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 unamed register always has the same value as the last used // register. this.unamedRegister.append(text, linewise); } else { register.set(text, linewise); this.unamedRegister.set(text, linewise); } }, // Gets the register named @name. If one of @name doesn't already exist, // create it. If @name is invalid, return the unamedRegister. getRegister: function(name) { if (!this.isValidRegister(name)) { return this.unamedRegister; } 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); for (var i = 0; i < keyMap.length; i++) { var command = keyMap[i]; if (matchKeysPartial(keys, command.keys)) { 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 (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; } // Matches whole comand. Return the command. if (command.keys[keys.length - 1] == 'character') { inputState.selectedCharacter = keys[keys.length - 1]; if(inputState.selectedCharacter.length>1){ switch(inputState.selectedCharacter){ case "Enter": inputState.selectedCharacter='\n'; break; case "Space": inputState.selectedCharacter=' '; break; default: continue; } } } inputState.keyBuffer = []; return command; } } } // Clear the buffer since there are no partial matches. inputState.keyBuffer = []; return null; }, 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, actions[command.action](cm, actionArgs, vim); }, processSearch: function(cm, vim, command) { if (!cm.getSearchCursor) { // Search depends on SearchCursor. return; } var forward = command.searchArgs.forward; 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: ' + regexPart); return; } commandDispatcher.processMotion(cm, vim, { type: 'motion', motion: 'findNext', motionArgs: { forward: true } }); } function onPromptClose(query) { cm.scrollTo(originalScrollPos.left, originalScrollPos.top); handleQuery(query, true /** ignoreCase */, true /** smartCase */); } 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': 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 + 1); if (isKeyword) { query = '\\b' + query + '\\b'; } else { query = escapeRegex(query); } cm.setCursor(word.start); handleQuery(query, true /** ignoreCase */, false /** smartCase */); break; } }, processEx: function(cm, vim, command) { function onPromptClose(input) { 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(cm, 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 (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 the start right by 1. selectionStart.ch += 1; } else if (cursorIsBefore(selectionEnd, selectionStart) && (cursorEqual(selectionStart, curEnd) || cursorIsBefore(selectionStart, curEnd))) { // The opposite happened. We will shift the start left by 1. selectionStart.ch -= 1; } selectionEnd = curEnd; if (vim.visualLine) { if (cursorIsBefore(selectionStart, selectionEnd)) { selectionStart.ch = 0; selectionEnd.ch = lineLength(cm, selectionEnd.line); } else { selectionEnd.ch = 0; selectionStart.ch = lineLength(cm, selectionStart.line); } } cm.setSelection(selectionStart, selectionEnd); updateMark(cm, vim, '<', cursorIsBefore(selectionStart, selectionEnd) ? selectionStart : selectionEnd); updateMark(cm, vim, '>', cursorIsBefore(selectionStart, selectionEnd) ? selectionEnd : selectionStart); } else if (!operator) { curEnd = clipCursorToContent(cm, curEnd); cm.setCursor(curEnd.line, curEnd.ch); } } if (operator) { var inverted = false; vim.lastMotion = null; operatorArgs.repeat = repeat; // Indent in visual mode needs this. if (vim.visualMode) { curStart = selectionStart; curEnd = selectionEnd; motionArgs.inclusive = true; } // Swap start and end if motion was backward. if (cursorIsBefore(curEnd, curStart)) { var tmp = curStart; curStart = curEnd; curEnd = tmp; inverted = true; } if (motionArgs.inclusive && !(vim.visualMode && inverted)) { // Move the selection end one to the right to include the last // character. curEnd.ch++; } var linewise = motionArgs.linewise || (vim.visualMode && vim.visualLine); if (linewise) { // Expand selection to entire line. expandSelectionToLine(cm, curStart, curEnd); } else if (motionArgs.forward) { // Clip to trailing newlines only if we the motion goes forward. clipToLine(cm, curStart, curEnd); } operatorArgs.registerName = registerName; // Keep track of linewise as it affects how paste and change behave. operatorArgs.linewise = linewise; operators[operator](cm, operatorArgs, vim, curStart, curEnd, curOriginal); if (vim.visualMode) { exitVisualMode(cm, vim); } if (operatorArgs.enterInsertMode) { actions.enterInsertMode(cm); } } }, recordLastEdit: function(cm, vim, inputState) { vim.lastEdit = inputState; } }; /** * typedef {Object{line:number,ch:number}} Cursor An object containing the * position of the cursor. */ // All of the functions below return Cursor objects. var motions = { moveToTopLine: function(cm, motionArgs) { var line = getUserVisibleLines(cm).top + motionArgs.repeat -1; return { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) }; }, moveToMiddleLine: function(cm) { var range = getUserVisibleLines(cm); var line = Math.floor((range.top + range.bottom) * 0.5); return { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) }; }, moveToBottomLine: function(cm, motionArgs) { var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1; return { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) }; }, expandToLine: function(cm, motionArgs) { // Expands forward to end of line, and then to next line if repeat is // >1. Does not handle backward motion! var cur = cm.getCursor(); return { line: cur.line + motionArgs.repeat - 1, ch: Infinity }; }, findNext: function(cm, motionArgs, vim) { var state = getSearchState(cm); var query = state.getQuery(); if (!query) { return; } var prev = !motionArgs.forward; // If search is initiated with ? instead of /, negate direction. prev = (state.isReversed()) ? !prev : prev; highlightSearchMatches(cm, query); return findNext(cm, prev/** prev */, query, motionArgs.repeat); }, goToMark: function(cm, motionArgs, vim) { var mark = vim.marks[motionArgs.selectedCharacter]; if (mark) { return mark.find(); } return null; }, jumpToMark: function(cm, motionArgs, vim) { var best = cm.getCursor(); for (var i = 0; i < motionArgs.repeat; i++) { var cursor = best; for (var key in vim.marks) { if (!isLowerCase(key)) { continue; } var mark = vim.marks[key].find(); var isWrongDirection = (motionArgs.forward) ? cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark) if (isWrongDirection) { continue; } if (motionArgs.linewise && (mark.line == cursor.line)) { continue; } var equal = cursorEqual(cursor, best); var between = (motionArgs.forward) ? cusrorIsBetween(cursor, mark, best) : cusrorIsBetween(best, mark, cursor); if (equal || between) { best = mark; } } } if (motionArgs.linewise) { // Vim places the cursor on the first non-whitespace character of // the line if there is one, else it places the cursor at the end // of the line, regardless of whether a mark was found. best.ch = findFirstNonWhiteSpaceCharacter(cm.getLine(best.line)); } return best; }, moveByCharacters: function(cm, motionArgs) { var cur = cm.getCursor(); var repeat = motionArgs.repeat; var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat; return { line: cur.line, ch: ch }; }, moveByLines: function(cm, motionArgs, vim) { var cur = cm.getCursor(); var endCh = cur.ch; // Depending what our last motion was, we may want to do different // things. If our last motion was moving vertically, we want to // preserve the HPos from our last horizontal move. If our last motion // was going to the end of a line, moving vertically we should go to // the end of the line, etc. switch (vim.lastMotion) { case this.moveByLines: case this.moveByDisplayLines: case this.moveByScroll: case this.moveToColumn: case this.moveToEol: endCh = vim.lastHPos; break; default: vim.lastHPos = endCh; } var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0); var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat; if (line < cm.firstLine() || line > cm.lastLine() ) { return null; } if(motionArgs.toFirstChar){ endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line)); vim.lastHPos = endCh; } vim.lastHSPos = cm.charCoords({line:line, ch:endCh},"div").left; return { line: line, ch: endCh }; }, moveByDisplayLines: function(cm, motionArgs, vim) { var cur = cm.getCursor(); switch (vim.lastMotion) { case this.moveByDisplayLines: case this.moveByScroll: case this.moveByLines: case this.moveToColumn: case this.moveToEol: break; default: vim.lastHSPos = cm.charCoords(cur,"div").left; } var repeat = motionArgs.repeat; var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),"line",vim.lastHSPos); if (res.hitSide) { if (motionArgs.forward) { var lastCharCoords = cm.charCoords(res, 'div'); var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos }; var res = cm.coordsChar(goalCoords, 'div'); } else { var resCoords = cm.charCoords({ line: cm.firstLine(), ch: 0}, 'div'); resCoords.left = vim.lastHSPos; res = cm.coordsChar(resCoords, 'div'); } } vim.lastHPos = res.ch; return res; }, moveByPage: function(cm, motionArgs) { // CodeMirror only exposes functions that move the cursor page down, so // doing this bad hack to move the cursor and move it back. evalInput // will move the cursor to where it should be in the end. var curStart = cm.getCursor(); var repeat = motionArgs.repeat; cm.moveV((motionArgs.forward ? repeat : -repeat), 'page'); var curEnd = cm.getCursor(); cm.setCursor(curStart); return curEnd; }, moveByParagraph: function(cm, motionArgs) { var line = cm.getCursor().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 { line: line, ch: 0 }; }, moveByScroll: function(cm, motionArgs, vim) { var globalState = getVimGlobalState(); var scrollbox = cm.getScrollInfo(); var curEnd = null; var repeat = motionArgs.repeat; if (!repeat) { repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight()); } var orig = cm.charCoords(cm.getCursor(), 'local'); motionArgs.repeat = repeat; var curEnd = motions.moveByDisplayLines(cm, motionArgs, vim); if (!curEnd) { return null; } var dest = cm.charCoords(curEnd, 'local'); cm.scrollTo(null, scrollbox.top + dest.top - orig.top); return curEnd; }, moveByWords: function(cm, motionArgs) { return moveToWord(cm, motionArgs.repeat, !!motionArgs.forward, !!motionArgs.wordEnd, !!motionArgs.bigWord); }, moveTillCharacter: function(cm, motionArgs) { var repeat = motionArgs.repeat; var curEnd = moveToCharacter(cm, repeat, motionArgs.forward, motionArgs.selectedCharacter); var increment = motionArgs.forward ? -1 : 1; recordLastCharacterSearch(increment, motionArgs); if(!curEnd)return cm.getCursor(); curEnd.ch += increment; return curEnd; }, moveToCharacter: function(cm, motionArgs) { var repeat = motionArgs.repeat; recordLastCharacterSearch(0, motionArgs); return moveToCharacter(cm, repeat, motionArgs.forward, motionArgs.selectedCharacter) || cm.getCursor(); }, moveToSymbol: function(cm, motionArgs) { var repeat = motionArgs.repeat; return findSymbol(cm, repeat, motionArgs.forward, motionArgs.selectedCharacter) || cm.getCursor(); }, moveToColumn: function(cm, motionArgs, vim) { var repeat = motionArgs.repeat; // repeat is equivalent to which column we want to move to! vim.lastHPos = repeat - 1; vim.lastHSPos = cm.charCoords(cm.getCursor(),"div").left; return moveToColumn(cm, repeat); }, moveToEol: function(cm, motionArgs, vim) { var cur = cm.getCursor(); vim.lastHPos = Infinity; var retval={ line: cur.line + motionArgs.repeat - 1, ch: Infinity } var end=cm.clipPos(retval); end.ch--; vim.lastHSPos = cm.charCoords(end,"div").left; return retval; }, moveToFirstNonWhiteSpaceCharacter: function(cm) { // Go to the start of the line where the text begins, or the end for // whitespace-only lines var cursor = cm.getCursor(); return { line: cursor.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line)) }; }, moveToMatchedSymbol: function(cm, motionArgs) {