/** * Supported keybindings: * * Motion: * h, j, k, l * gj, gk * e, E, w, W, b, B, ge, gE * f, F, t, T * $, ^, 0, -, +, _ * gg, G * % * ', ` * * 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, zt, zb, z- * J * u, Ctrl-r * m * r * * 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: [''], type: 'keyToKey', toKeys: ['h'] }, { keys: [''], type: 'keyToKey', toKeys: ['l'] }, { keys: [''], type: 'keyToKey', toKeys: ['k'] }, { keys: [''], type: 'keyToKey', toKeys: ['j'] }, { keys: [''], type: 'keyToKey', toKeys: ['l'] }, { keys: [''], type: 'keyToKey', toKeys: ['h'] }, { keys: [''], type: 'keyToKey', toKeys: ['W'] }, { keys: [''], type: 'keyToKey', toKeys: ['B'] }, { keys: [''], type: 'keyToKey', toKeys: ['w'] }, { keys: [''], type: 'keyToKey', toKeys: ['b'] }, { keys: [''], type: 'keyToKey', toKeys: ['j'] }, { keys: [''], type: 'keyToKey', toKeys: ['k'] }, { keys: ['C-['], type: 'keyToKey', toKeys: [''] }, { keys: [''], type: 'keyToKey', toKeys: [''] }, { keys: ['s'], type: 'keyToKey', toKeys: ['c', 'l'] }, { keys: ['S'], type: 'keyToKey', toKeys: ['c', 'c'] }, { keys: [''], type: 'keyToKey', toKeys: ['0'] }, { keys: [''], type: 'keyToKey', toKeys: ['$'] }, { keys: [''], type: 'keyToKey', toKeys: [''] }, { keys: [''], type: 'keyToKey', toKeys: [''] }, // 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: [''], type: 'motion', motion: 'moveByPage', motionArgs: { forward: true }}, { keys: [''], type: 'motion', motion: 'moveByPage', motionArgs: { forward: false }}, { keys: [''], type: 'motion', motion: 'moveByScroll', motionArgs: { forward: true, explicitRepeat: true }}, { keys: [''], 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}}, { 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: { }}, // 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, 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', operatorArgs: { enterInsertMode: true }, motion: 'moveToEol', motionArgs: { inclusive: true }, operatorMotionArgs: { visualLine: true }}, { keys: ['~'], type: 'operatorMotion', operator: 'swapcase', motion: 'moveByCharacters', motionArgs: { forward: true }}, // Actions { keys: [''], type: 'action', action: 'jumpListWalk', actionArgs: { forward: true }}, { keys: [''], type: 'action', action: 'jumpListWalk', actionArgs: { forward: false }}, { 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: ['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: [''], 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', ''], 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: [''], type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: true, backtrack: false}}, { keys: [''], 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', toJumplist: true }}, { keys: ['#'], type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }}, // Ex command { keys: [':'], type: 'ex' } ]; var Vim = function() { 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 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 }; }; var createMacroState = function() { return { macroKeyBuffer: [], latestRegister: undefined, inReplay: false, lastInsertModeChanges: { changes: [], // Change list expectCursorActivityForChange: false // Set to true on change, false on cursorActivity. }, enteredMacroMode: undefined, isMacroPlaying: false, toggle: function(cm, registerName) { if (this.enteredMacroMode) { //onExit this.enteredMacroMode(); // close dialog this.enteredMacroMode = undefined; } else { //onEnter this.latestRegister = registerName; this.enteredMacroMode = cm.openDialog( '(recording)['+registerName+']', null, {bottom:true}); } } }; }; // 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, jumpList: createCircularJumpList(), macroModeState: createMacroState(), // 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(), // 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 }; } 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; }, // Testing hook. getVimGlobalState_: function() { return vimGlobalState; }, InsertModeKey: InsertModeKey, map: function(lhs, rhs) { // Add user defined key bindings. exCommandDispatcher.map(lhs, rhs); }, 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'}; }, // 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); var macroModeState = getVimGlobalState().macroModeState; if (macroModeState.enteredMacroMode) { if (key == 'q') { actions.exitMacroRecordMode(); vim.inputState = new InputState(); return; } } if (key == '') { // 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; 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); } 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.enteredMacroMode) { logKey(macroModeState, key); } 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) { if (linewise && text.charAt(0) == '\n') { text = text.slice(1) + '\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.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); } }, setRegisterText: function(name, text, linewise) { this.getRegister(name).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; } 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 '': inputState.selectedCharacter='\n'; break; case '': 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; 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; 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 */); } 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); if (isKeyword) { 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. getVimGlobalState().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 = getVimGlobalState().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 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 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); } if (operatorArgs.enterInsertMode) { actions.enterInsertMode(cm, {}, vim); } } }, recordLastEdit: function(vim, inputState, actionCommand) { var macroModeState = getVimGlobalState().macroModeState; if (macroModeState.inReplay) { return; } vim.lastEditInputState = inputState; vim.lastEditActionCommand = actionCommand; macroModeState.lastInsertModeChanges.changes = []; macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false; } }; /** * 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) { 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; var first = cm.firstLine(); var last = cm.lastLine(); // Vim cancels linewise motions that start on an edge and move beyond // that edge. It does not cancel motions that do not start on an edge. if ((line < first && cur.line == first) || (line > last && cur.line == last)) { return; } 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 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) { var cursor = cm.getCursor(); var line = cursor.line; var ch = cursor.ch; var lineText = cm.getLine(line); var symbol; var startContext = cm.getTokenAt(cursor).type; var startCtxLevel = getContextLevel(startContext); do { symbol = lineText.charAt(ch++); if (symbol && isMatchableSymbol(symbol)) { var endContext = cm.getTokenAt({line:line, ch:ch}).type; var endCtxLevel = getContextLevel(endContext); if (startCtxLevel >= endCtxLevel) { break; } } } while (symbol); if (symbol) { return findMatchedSymbol(cm, {line:line, ch:ch-1}, symbol); } else { return cursor; } }, moveToStartOfLine: function(cm) { var cursor = cm.getCursor(); return { line: cursor.line, ch: 0 }; }, moveToLineOrEdgeOfDocument: function(cm, motionArgs) { var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine(); if (motionArgs.repeatIsExplicit) { lineNum = motionArgs.repeat - cm.getOption('firstLineNumber'); } return { line: lineNum, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum)) }; }, textObjectManipulation: function(cm, motionArgs) { var character = motionArgs.selectedCharacter; // Inclusive is the difference between a and i // TODO: Instead of using the additional text object map to perform text // object operations, merge the map into the defaultKeyMap and use // motionArgs to define behavior. Define separate entries for 'aw', // 'iw', 'a[', 'i[', etc. var inclusive = !motionArgs.textObjectInner; if (!textObjects[character]) { // No text object defined for this, don't move. return null; } var tmp = textObjects[character](cm, inclusive); var start = tmp.start; var end = tmp.end; return [start, end]; }, repeatLastCharacterSearch: function(cm, motionArgs) { var lastSearch = getVimGlobalState().lastChararacterSearch; var repeat = motionArgs.repeat; var forward = motionArgs.forward === lastSearch.forward; var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1); cm.moveH(-increment, 'char'); motionArgs.inclusive = forward ? true : false; var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter); if (!curEnd) { cm.moveH(increment, 'char'); return cm.getCursor(); } curEnd.ch += increment; return curEnd; } }; var operators = { change: function(cm, operatorArgs, _vim, curStart, curEnd) { getVimGlobalState().registerController.pushText( operatorArgs.registerName, 'change', cm.getRange(curStart, curEnd), operatorArgs.linewise); if (operatorArgs.linewise) { // Push the next line back down, if there is a next line. var replacement = curEnd.line > cm.lastLine() ? '' : '\n'; cm.replaceRange(replacement, curStart, curEnd); cm.indentLine(curStart.line, 'smart'); // null ch so setCursor moves to end of line. curStart.ch = null; } else { // Exclude trailing whitespace if the range is not all whitespace. var text = cm.getRange(curStart, curEnd); if (!isWhiteSpaceString(text)) { var match = (/\s+$/).exec(text); if (match) { curEnd = offsetCursor(curEnd, 0, - match[0].length); } } cm.replaceRange('', curStart, curEnd); } cm.setCursor(curStart); }, // delete is a javascript keyword. 'delete': function(cm, operatorArgs, _vim, curStart, curEnd) { // If the ending line is past the last line, inclusive, instead of // including the trailing \n, include the \n before the starting line if (operatorArgs.linewise && curEnd.line > cm.lastLine() && curStart.line > cm.firstLine()) { curStart.line--; curStart.ch = lineLength(cm, curStart.line); } getVimGlobalState().registerController.pushText( operatorArgs.registerName, 'delete', cm.getRange(curStart, curEnd), operatorArgs.linewise); cm.replaceRange('', curStart, curEnd); if (operatorArgs.linewise) { cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm)); } else { cm.setCursor(curStart); } }, indent: function(cm, operatorArgs, vim, curStart, curEnd) { var startLine = curStart.line; var endLine = curEnd.line; // In visual mode, n> shifts the selection right n times, instead of // shifting n lines right once. var repeat = (vim.visualMode) ? operatorArgs.repeat : 1; if (operatorArgs.linewise) { // The only way to delete a newline is to delete until the start of // the next line, so in linewise mode evalInput will include the next // line. We don't want this in indent, so we go back a line. endLine--; } for (var i = startLine; i <= endLine; i++) { for (var j = 0; j < repeat; j++) { cm.indentLine(i, operatorArgs.indentRight); } } cm.setCursor(curStart); cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm)); }, swapcase: function(cm, _operatorArgs, _vim, curStart, curEnd, curOriginal) { var toSwap = cm.getRange(curStart, curEnd); var swapped = ''; for (var i = 0; i < toSwap.length; i++) { var character = toSwap.charAt(i); swapped += isUpperCase(character) ? character.toLowerCase() : character.toUpperCase(); } cm.replaceRange(swapped, curStart, curEnd); cm.setCursor(curOriginal); }, yank: function(cm, operatorArgs, _vim, curStart, curEnd, curOriginal) { getVimGlobalState().registerController.pushText( operatorArgs.registerName, 'yank', cm.getRange(curStart, curEnd), operatorArgs.linewise); cm.setCursor(curOriginal); } }; var actions = { jumpListWalk: function(cm, actionArgs, vim) { if (vim.visualMode) { return; } var repeat = actionArgs.repeat; var forward = actionArgs.forward; var jumpList = getVimGlobalState().jumpList; var mark = jumpList.move(cm, forward ? repeat : -repeat); var markPos = mark ? mark.find() : undefined; markPos = markPos ? markPos : cm.getCursor(); cm.setCursor(markPos); }, scrollToCursor: function(cm, actionArgs) { var lineNum = cm.getCursor().line; var charCoords = cm.charCoords({line: lineNum, ch: 0}, 'local'); var height = cm.getScrollInfo().clientHeight; var y = charCoords.top; var lineHeight = charCoords.bottom - y; switch (actionArgs.position) { case 'center': y = y - (height / 2) + lineHeight; break; case 'bottom': y = y - height + lineHeight*1.4; break; case 'top': y = y + lineHeight*0.4; break; } cm.scrollTo(null, y); }, replayMacro: function(cm, actionArgs) { var registerName = actionArgs.selectedCharacter; var repeat = actionArgs.repeat; var macroModeState = getVimGlobalState().macroModeState; if (registerName == '@') { registerName = macroModeState.latestRegister; } var keyBuffer = parseRegisterToKeyBuffer(macroModeState, registerName); while(repeat--){ executeMacroKeyBuffer(cm, macroModeState, keyBuffer); } }, exitMacroRecordMode: function() { var macroModeState = getVimGlobalState().macroModeState; macroModeState.toggle(); parseKeyBufferToRegister(macroModeState.latestRegister, macroModeState.macroKeyBuffer); }, enterMacroRecordMode: function(cm, actionArgs) { var macroModeState = getVimGlobalState().macroModeState; var registerName = actionArgs.selectedCharacter; macroModeState.toggle(cm, registerName); emptyMacroKeyBuffer(macroModeState); }, enterInsertMode: function(cm, actionArgs, vim) { vim.insertMode = true; vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1; var insertAt = (actionArgs) ? actionArgs.insertAt : null; if (insertAt == 'eol') { var cursor = cm.getCursor(); cursor = { line: cursor.line, ch: lineLength(cm, cursor.line) }; cm.setCursor(cursor); } else if (insertAt == 'charAfter') { cm.setCursor(offsetCursor(cm.getCursor(), 0, 1)); } else if (insertAt == 'firstNonBlank') { cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm)); } cm.setOption('keyMap', 'vim-insert'); if (actionArgs && actionArgs.replace) { // Handle Replace-mode as a special case of insert mode. cm.toggleOverwrite(true); cm.setOption('keyMap', 'vim-replace'); } else { cm.setOption('keyMap', 'vim-insert'); } if (!getVimGlobalState().macroModeState.inReplay) { // Only record if not replaying. cm.on('change', onChange); cm.on('cursorActivity', onCursorActivity); CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); } }, toggleVisualMode: function(cm, actionArgs, vim) { var repeat = actionArgs.repeat; var curStart = cm.getCursor(); var curEnd; // TODO: The repeat should actually select number of characters/lines // equal to the repeat times the size of the previous visual // operation. if (!vim.visualMode) { cm.on('mousedown', exitVisualMode); vim.visualMode = true; vim.visualLine = !!actionArgs.linewise; if (vim.visualLine) { curStart.ch = 0; curEnd = clipCursorToContent(cm, { line: curStart.line + repeat - 1, ch: lineLength(cm, curStart.line) }, true /** includeLineBreak */); } else { curEnd = clipCursorToContent(cm, { line: curStart.line, ch: curStart.ch + repeat }, true /** includeLineBreak */); } // Make the initial selection. if (!actionArgs.repeatIsExplicit && !vim.visualLine) { // This is a strange case. Here the implicit repeat is 1. The // following commands lets the cursor hover over the 1 character // selection. cm.setCursor(curEnd); cm.setSelection(curEnd, curStart); } else { cm.setSelection(curStart, curEnd); } } else { curStart = cm.getCursor('anchor'); curEnd = cm.getCursor('head'); if (!vim.visualLine && actionArgs.linewise) { // Shift-V pressed in characterwise visual mode. Switch to linewise // visual mode instead of exiting visual mode. vim.visualLine = true; curStart.ch = cursorIsBefore(curStart, curEnd) ? 0 : lineLength(cm, curStart.line); curEnd.ch = cursorIsBefore(curStart, curEnd) ? lineLength(cm, curEnd.line) : 0; cm.setSelection(curStart, curEnd); } else if (vim.visualLine && !actionArgs.linewise) { // v pressed in linewise visual mode. Switch to characterwise visual // mode instead of exiting visual mode. vim.visualLine = false; } else { exitVisualMode(cm); } } updateMark(cm, vim, '<', cursorIsBefore(curStart, curEnd) ? curStart : curEnd); updateMark(cm, vim, '>', cursorIsBefore(curStart, curEnd) ? curEnd : curStart); }, joinLines: function(cm, actionArgs, vim) { var curStart, curEnd; if (vim.visualMode) { curStart = cm.getCursor('anchor'); curEnd = cm.getCursor('head'); curEnd.ch = lineLength(cm, curEnd.line) - 1; } else { // Repeat is the number of lines to join. Minimum 2 lines. var repeat = Math.max(actionArgs.repeat, 2); curStart = cm.getCursor(); curEnd = clipCursorToContent(cm, { line: curStart.line + repeat - 1, ch: Infinity }); } var finalCh = 0; cm.operation(function() { for (var i = curStart.line; i < curEnd.line; i++) { finalCh = lineLength(cm, curStart.line); var tmp = { line: curStart.line + 1, ch: lineLength(cm, curStart.line + 1) }; var text = cm.getRange(curStart, tmp); text = text.replace(/\n\s*/g, ' '); cm.replaceRange(text, curStart, tmp); } var curFinalPos = { line: curStart.line, ch: finalCh }; cm.setCursor(curFinalPos); }); }, newLineAndEnterInsertMode: function(cm, actionArgs, vim) { var insertAt = cm.getCursor(); if (insertAt.line === cm.firstLine() && !actionArgs.after) { // Special case for inserting newline before start of document. cm.replaceRange('\n', { line: cm.firstLine(), ch: 0 }); cm.setCursor(cm.firstLine(), 0); } else { insertAt.line = (actionArgs.after) ? insertAt.line : insertAt.line - 1; insertAt.ch = lineLength(cm, insertAt.line); cm.setCursor(insertAt); var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent; newlineFn(cm); } this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim); }, paste: function(cm, actionArgs) { var cur = cm.getCursor(); var register = getVimGlobalState().registerController.getRegister( actionArgs.registerName); if (!register.text) { return; } for (var text = '', i = 0; i < actionArgs.repeat; i++) { text += register.text; } var linewise = register.linewise; if (linewise) { if (actionArgs.after) { // Move the newline at the end to the start instead, and paste just // before the newline character of the line we are on right now. text = '\n' + text.slice(0, text.length - 1); cur.ch = lineLength(cm, cur.line); } else { cur.ch = 0; } } else { cur.ch += actionArgs.after ? 1 : 0; } cm.replaceRange(text, cur); // Now fine tune the cursor to where we want it. var curPosFinal; var idx; if (linewise && actionArgs.after) { curPosFinal = { line: cur.line + 1, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1)) }; } else if (linewise && !actionArgs.after) { curPosFinal = { line: cur.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line)) }; } else if (!linewise && actionArgs.after) { idx = cm.indexFromPos(cur); curPosFinal = cm.posFromIndex(idx + text.length - 1); } else { idx = cm.indexFromPos(cur); curPosFinal = cm.posFromIndex(idx + text.length); } cm.setCursor(curPosFinal); }, undo: function(cm, actionArgs) { cm.operation(function() { repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)(); cm.setCursor(cm.getCursor('anchor')); }); }, redo: function(cm, actionArgs) { repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)(); }, setRegister: function(_cm, actionArgs, vim) { vim.inputState.registerName = actionArgs.selectedCharacter; }, setMark: function(cm, actionArgs, vim) { var markName = actionArgs.selectedCharacter; updateMark(cm, vim, markName, cm.getCursor()); }, replace: function(cm, actionArgs, vim) { var replaceWith = actionArgs.selectedCharacter; var curStart = cm.getCursor(); var replaceTo; var curEnd; if(vim.visualMode){ curStart=cm.getCursor('start'); curEnd=cm.getCursor('end'); // workaround to catch the character under the cursor // existing workaround doesn't cover actions curEnd=cm.clipPos({line: curEnd.line, ch: curEnd.ch+1}); }else{ var line = cm.getLine(curStart.line); replaceTo = curStart.ch + actionArgs.repeat; if (replaceTo > line.length) { replaceTo=line.length; } curEnd = { line: curStart.line, ch: replaceTo }; } if(replaceWith=='\n'){ if(!vim.visualMode) cm.replaceRange('', curStart, curEnd); // special case, where vim help says to replace by just one line-break (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm); }else { var replaceWithStr=cm.getRange(curStart, curEnd); //replace all characters in range by selected, but keep linebreaks replaceWithStr=replaceWithStr.replace(/[^\n]/g,replaceWith); cm.replaceRange(replaceWithStr, curStart, curEnd); if(vim.visualMode){ cm.setCursor(curStart); exitVisualMode(cm); }else{ cm.setCursor(offsetCursor(curEnd, 0, -1)); } } }, incrementNumberToken: function(cm, actionArgs) { var cur = cm.getCursor(); var lineStr = cm.getLine(cur.line); var re = /-?\d+/g; var match; var start; var end; var numberStr; var token; while ((match = re.exec(lineStr)) !== null) { token = match[0]; start = match.index; end = start + token.length; if(cur.ch < end)break; } if(!actionArgs.backtrack && (end <= cur.ch))return; if (token) { var increment = actionArgs.increase ? 1 : -1; var number = parseInt(token) + (increment * actionArgs.repeat); var from = {ch:start, line:cur.line}; var to = {ch:end, line:cur.line}; numberStr = number.toString(); cm.replaceRange(numberStr, from, to); } else { return; } cm.setCursor({line: cur.line, ch: start + numberStr.length - 1}); }, repeatLastEdit: function(cm, actionArgs, vim) { var lastEditInputState = vim.lastEditInputState; if (!lastEditInputState) { return; } var repeat = actionArgs.repeat; if (repeat && actionArgs.repeatIsExplicit) { vim.lastEditInputState.repeatOverride = repeat; } else { repeat = vim.lastEditInputState.repeatOverride || repeat; } repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */); } }; var textObjects = { // TODO: lots of possible exceptions that can be thrown here. Try da( // outside of a () block. // TODO: implement text objects for the reverse like }. Should just be // an additional mapping after moving to the defaultKeyMap. 'w': function(cm, inclusive) { return expandWordUnderCursor(cm, inclusive, true /** forward */, false /** bigWord */); }, 'W': function(cm, inclusive) { return expandWordUnderCursor(cm, inclusive, true /** forward */, true /** bigWord */); }, '{': function(cm, inclusive) { return selectCompanionObject(cm, '}', inclusive); }, '(': function(cm, inclusive) { return selectCompanionObject(cm, ')', inclusive); }, '[': function(cm, inclusive) { return selectCompanionObject(cm, ']', inclusive); }, '\'': function(cm, inclusive) { return findBeginningAndEnd(cm, "'", inclusive); }, '"': function(cm, inclusive) { return findBeginningAndEnd(cm, '"', inclusive); } }; /* * Below are miscellaneous utility functions used by vim.js */ /** * Clips cursor to ensure that line is within the buffer's range * If includeLineBreak is true, then allow cur.ch == lineLength. */ function clipCursorToContent(cm, cur, includeLineBreak) { var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() ); var maxCh = lineLength(cm, line) - 1; maxCh = (includeLineBreak) ? maxCh + 1 : maxCh; var ch = Math.min(Math.max(0, cur.ch), maxCh); return { line: line, ch: ch }; } function copyArgs(args) { var ret = {}; for (var prop in args) { if (args.hasOwnProperty(prop)) { ret[prop] = args[prop]; } } return ret; } function offsetCursor(cur, offsetLine, offsetCh) { return { line: cur.line + offsetLine, ch: cur.ch + offsetCh }; } function matchKeysPartial(pressed, mapped) { for (var i = 0; i < pressed.length; i++) { // 'character' means any character. For mark, register commads, etc. if (pressed[i] != mapped[i] && mapped[i] != 'character') { return false; } } return true; } function repeatFn(cm, fn, repeat) { return function() { for (var i = 0; i < repeat; i++) { fn(cm); } }; } function copyCursor(cur) { return { line: cur.line, ch: cur.ch }; } function cursorEqual(cur1, cur2) { return cur1.ch == cur2.ch && cur1.line == cur2.line; } function cursorIsBefore(cur1, cur2) { if (cur1.line < cur2.line) { return true; } if (cur1.line == cur2.line && cur1.ch < cur2.ch) { return true; } return false; } function cusrorIsBetween(cur1, cur2, cur3) { // returns true if cur2 is between cur1 and cur3. var cur1before2 = cursorIsBefore(cur1, cur2); var cur2before3 = cursorIsBefore(cur2, cur3); return cur1before2 && cur2before3; } function lineLength(cm, lineNum) { return cm.getLine(lineNum).length; } function reverse(s){ return s.split('').reverse().join(''); } function trim(s) { if (s.trim) { return s.trim(); } return s.replace(/^\s+|\s+$/g, ''); } function escapeRegex(s) { return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1'); } function exitVisualMode(cm) { cm.off('mousedown', exitVisualMode); var vim = cm.vimState; vim.visualMode = false; vim.visualLine = false; var selectionStart = cm.getCursor('anchor'); var selectionEnd = cm.getCursor('head'); if (!cursorEqual(selectionStart, selectionEnd)) { // Clear the selection and set the cursor only if the selection has not // already been cleared. Otherwise we risk moving the cursor somewhere // it's not supposed to be. cm.setCursor(clipCursorToContent(cm, selectionEnd)); } } // Remove any trailing newlines from the selection. For // example, with the caret at the start of the last word on the line, // 'dw' should word, but not the newline, while 'w' should advance the // caret to the first character of the next line. function clipToLine(cm, curStart, curEnd) { var selection = cm.getRange(curStart, curEnd); // Only clip if the selection ends with trailing newline + whitespace if (/\n\s*$/.test(selection)) { var lines = selection.split('\n'); // We know this is all whitepsace. lines.pop(); // Cases: // 1. Last word is an empty line - do not clip the trailing '\n' // 2. Last word is not an empty line - clip the trailing '\n' var line; // Find the line containing the last word, and clip all whitespace up // to it. for (var line = lines.pop(); lines.length > 0 && line && isWhiteSpaceString(line); line = lines.pop()) { curEnd.line--; curEnd.ch = 0; } // If the last word is not an empty line, clip an additional newline if (line) { curEnd.line--; curEnd.ch = lineLength(cm, curEnd.line); } else { curEnd.ch = 0; } } } // Expand the selection to line ends. function expandSelectionToLine(_cm, curStart, curEnd) { curStart.ch = 0; curEnd.ch = 0; curEnd.line++; } function findFirstNonWhiteSpaceCharacter(text) { if (!text) { return 0; } var firstNonWS = text.search(/\S/); return firstNonWS == -1 ? text.length : firstNonWS; } function expandWordUnderCursor(cm, inclusive, _forward, bigWord, noSymbol) { var cur = cm.getCursor(); var line = cm.getLine(cur.line); var idx = cur.ch; // Seek to first word or non-whitespace character, depending on if // noSymbol is true. var textAfterIdx = line.substring(idx); var firstMatchedChar; if (noSymbol) { firstMatchedChar = textAfterIdx.search(/\w/); } else { firstMatchedChar = textAfterIdx.search(/\S/); } if (firstMatchedChar == -1) { return null; } idx += firstMatchedChar; textAfterIdx = line.substring(idx); var textBeforeIdx = line.substring(0, idx); var matchRegex; // Greedy matchers for the "word" we are trying to expand. if (bigWord) { matchRegex = /^\S+/; } else { if ((/\w/).test(line.charAt(idx))) { matchRegex = /^\w+/; } else { matchRegex = /^[^\w\s]+/; } } var wordAfterRegex = matchRegex.exec(textAfterIdx); var wordStart = idx; var wordEnd = idx + wordAfterRegex[0].length; // TODO: Find a better way to do this. It will be slow on very long lines. var revTextBeforeIdx = reverse(textBeforeIdx); var wordBeforeRegex = matchRegex.exec(revTextBeforeIdx); if (wordBeforeRegex) { wordStart -= wordBeforeRegex[0].length; } if (inclusive) { // If present, trim all whitespace after word. // Otherwise, trim all whitespace before word. var textAfterWordEnd = line.substring(wordEnd); var whitespacesAfterWord = textAfterWordEnd.match(/^\s*/)[0].length; if (whitespacesAfterWord > 0) { wordEnd += whitespacesAfterWord; } else { var revTrim = revTextBeforeIdx.length - wordStart; var textBeforeWordStart = revTextBeforeIdx.substring(revTrim); var whitespacesBeforeWord = textBeforeWordStart.match(/^\s*/)[0].length; wordStart -= whitespacesBeforeWord; } } return { start: { line: cur.line, ch: wordStart }, end: { line: cur.line, ch: wordEnd }}; } function recordJumpPosition(cm, oldCur, newCur) { if(!cursorEqual(oldCur, newCur)) { getVimGlobalState().jumpList.add(cm, oldCur, newCur); } } function recordLastCharacterSearch(increment, args) { var vimGlobalState = getVimGlobalState(); vimGlobalState.lastChararacterSearch.increment = increment; vimGlobalState.lastChararacterSearch.forward = args.forward; vimGlobalState.lastChararacterSearch.selectedCharacter = args.selectedCharacter; } var symbolToMode = { '(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket', '[': 'section', ']': 'section', '*': 'comment', '/': 'comment', 'm': 'method', 'M': 'method', '#': 'preprocess' }; var findSymbolModes = { bracket: { isComplete: function(state) { if (state.nextCh === state.symb) { state.depth++; if(state.depth >= 1)return true; } else if (state.nextCh === state.reverseSymb) { state.depth--; } return false; } }, section: { init: function(state) { state.curMoveThrough = true; state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}'; }, isComplete: function(state) { return state.index === 0 && state.nextCh === state.symb; } }, comment: { isComplete: function(state) { var found = state.lastCh === '*' && state.nextCh === '/'; state.lastCh = state.nextCh; return found; } }, // TODO: The original Vim implementation only operates on level 1 and 2. // The current implementation doesn't check for code block level and // therefore it operates on any levels. method: { init: function(state) { state.symb = (state.symb === 'm' ? '{' : '}'); state.reverseSymb = state.symb === '{' ? '}' : '{'; }, isComplete: function(state) { if(state.nextCh === state.symb)return true; return false; } }, preprocess: { init: function(state) { state.index = 0; }, isComplete: function(state) { if (state.nextCh === '#') { var token = state.lineText.match(/#(\w+)/)[1]; if (token === 'endif') { if (state.forward && state.depth === 0) { return true; } state.depth++; } else if (token === 'if') { if (!state.forward && state.depth === 0) { return true; } state.depth--; } if(token === 'else' && state.depth === 0)return true; } return false; } } }; function findSymbol(cm, repeat, forward, symb) { var cur = cm.getCursor(); var increment = forward ? 1 : -1; var endLine = forward ? cm.lineCount() : -1; var curCh = cur.ch; var line = cur.line; var lineText = cm.getLine(line); var state = { lineText: lineText, nextCh: lineText.charAt(curCh), lastCh: null, index: curCh, symb: symb, reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb], forward: forward, depth: 0, curMoveThrough: false }; var mode = symbolToMode[symb]; if(!mode)return cur; var init = findSymbolModes[mode].init; var isComplete = findSymbolModes[mode].isComplete; if(init)init(state); while (line !== endLine && repeat) { state.index += increment; state.nextCh = state.lineText.charAt(state.index); if (!state.nextCh) { line += increment; state.lineText = cm.getLine(line) || ''; if (increment > 0) { state.index = 0; } else { var lineLen = state.lineText.length; state.index = (lineLen > 0) ? (lineLen-1) : 0; } state.nextCh = state.lineText.charAt(state.index); } if (isComplete(state)) { cur.line = line; cur.ch = state.index; repeat--; } } if (state.nextCh || state.curMoveThrough) { return { line: line, ch: state.index }; } return cur; } /* * Returns the boundaries of the next word. If the cursor in the middle of * the word, then returns the boundaries of the current word, starting at * the cursor. If the cursor is at the start/end of a word, and we are going * forward/backward, respectively, find the boundaries of the next word. * * @param {CodeMirror} cm CodeMirror object. * @param {Cursor} cur The cursor position. * @param {boolean} forward True to search forward. False to search * backward. * @param {boolean} bigWord True if punctuation count as part of the word. * False if only [a-zA-Z0-9] characters count as part of the word. * @param {boolean} emptyLineIsWord True if empty lines should be treated * as words. * @return {Object{from:number, to:number, line: number}} The boundaries of * the word, or null if there are no more words. */ function findWord(cm, cur, forward, bigWord, emptyLineIsWord) { var lineNum = cur.line; var pos = cur.ch; var line = cm.getLine(lineNum); var dir = forward ? 1 : -1; var regexps = bigWord ? bigWordRegexp : wordRegexp; if (emptyLineIsWord && line == '') { lineNum += dir; line = cm.getLine(lineNum); if (!isLine(cm, lineNum)) { return null; } pos = (forward) ? 0 : line.length; } while (true) { if (emptyLineIsWord && line == '') { return { from: 0, to: 0, line: lineNum }; } var stop = (dir > 0) ? line.length : -1; var wordStart = stop, wordEnd = stop; // Find bounds of next word. while (pos != stop) { var foundWord = false; for (var i = 0; i < regexps.length && !foundWord; ++i) { if (regexps[i].test(line.charAt(pos))) { wordStart = pos; // Advance to end of word. while (pos != stop && regexps[i].test(line.charAt(pos))) { pos += dir; } wordEnd = pos; foundWord = wordStart != wordEnd; if (wordStart == cur.ch && lineNum == cur.line && wordEnd == wordStart + dir) { // We started at the end of a word. Find the next one. continue; } else { return { from: Math.min(wordStart, wordEnd + 1), to: Math.max(wordStart, wordEnd), line: lineNum }; } } } if (!foundWord) { pos += dir; } } // Advance to next/prev line. lineNum += dir; if (!isLine(cm, lineNum)) { return null; } line = cm.getLine(lineNum); pos = (dir > 0) ? 0 : line.length; } // Should never get here. throw new Error('The impossible happened.'); } /** * @param {CodeMirror} cm CodeMirror object. * @param {int} repeat Number of words to move past. * @param {boolean} forward True to search forward. False to search * backward. * @param {boolean} wordEnd True to move to end of word. False to move to * beginning of word. * @param {boolean} bigWord True if punctuation count as part of the word. * False if only alphabet characters count as part of the word. * @return {Cursor} The position the cursor should move to. */ function moveToWord(cm, repeat, forward, wordEnd, bigWord) { var cur = cm.getCursor(); var curStart = copyCursor(cur); var words = []; if (forward && !wordEnd || !forward && wordEnd) { repeat++; } // For 'e', empty lines are not considered words, go figure. var emptyLineIsWord = !(forward && wordEnd); for (var i = 0; i < repeat; i++) { var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord); if (!word) { var eodCh = lineLength(cm, cm.lastLine()); words.push(forward ? {line: cm.lastLine(), from: eodCh, to: eodCh} : {line: 0, from: 0, to: 0}); break; } words.push(word); cur = {line: word.line, ch: forward ? (word.to - 1) : word.from}; } var shortCircuit = words.length != repeat; var firstWord = words[0]; var lastWord = words.pop(); if (forward && !wordEnd) { // w if (!shortCircuit && (firstWord.from != curStart.ch || firstWord.line != curStart.line)) { // We did not start in the middle of a word. Discard the extra word at the end. lastWord = words.pop(); } return {line: lastWord.line, ch: lastWord.from}; } else if (forward && wordEnd) { return {line: lastWord.line, ch: lastWord.to - 1}; } else if (!forward && wordEnd) { // ge if (!shortCircuit && (firstWord.to != curStart.ch || firstWord.line != curStart.line)) { // We did not start in the middle of a word. Discard the extra word at the end. lastWord = words.pop(); } return {line: lastWord.line, ch: lastWord.to}; } else { // b return {line: lastWord.line, ch: lastWord.from}; } } function moveToCharacter(cm, repeat, forward, character) { var cur = cm.getCursor(); var start = cur.ch; var idx; for (var i = 0; i < repeat; i ++) { var line = cm.getLine(cur.line); idx = charIdxInLine(start, line, character, forward, true); if (idx == -1) { return null; } start = idx; } return { line: cm.getCursor().line, ch: idx }; } function moveToColumn(cm, repeat) { // repeat is always >= 1, so repeat - 1 always corresponds // to the column we want to go to. var line = cm.getCursor().line; return clipCursorToContent(cm, { line: line, ch: repeat - 1 }); } function updateMark(cm, vim, markName, pos) { if (!inArray(markName, validMarks)) { return; } if (vim.marks[markName]) { vim.marks[markName].clear(); } vim.marks[markName] = cm.setBookmark(pos); } function charIdxInLine(start, line, character, forward, includeChar) { // Search for char in line. // motion_options: {forward, includeChar} // If includeChar = true, include it too. // If forward = true, search forward, else search backwards. // If char is not found on this line, do nothing var idx; if (forward) { idx = line.indexOf(character, start + 1); if (idx != -1 && !includeChar) { idx -= 1; } } else { idx = line.lastIndexOf(character, start - 1); if (idx != -1 && !includeChar) { idx += 1; } } return idx; } function getContextLevel(ctx) { return (ctx === 'string' || ctx === 'comment') ? 1 : 0; } function findMatchedSymbol(cm, cur, symb) { var line = cur.line; var ch = cur.ch; symb = symb ? symb : cm.getLine(line).charAt(ch); var symbContext = cm.getTokenAt({line:line, ch:ch+1}).type; var symbCtxLevel = getContextLevel(symbContext); var reverseSymb = ({ '(': ')', ')': '(', '[': ']', ']': '[', '{': '}', '}': '{'})[symb]; // Couldn't find a matching symbol, abort if (!reverseSymb) { return cur; } // set our increment to move forward (+1) or backwards (-1) // depending on which bracket we're matching var increment = ({'(': 1, '{': 1, '[': 1})[symb] || -1; var endLine = increment === 1 ? cm.lineCount() : -1; var depth = 1, nextCh = symb, index = ch, lineText = cm.getLine(line); // Simple search for closing paren--just count openings and closings till // we find our match // TODO: use info from CodeMirror to ignore closing brackets in comments // and quotes, etc. while (line !== endLine && depth > 0) { index += increment; nextCh = lineText.charAt(index); if (!nextCh) { line += increment; lineText = cm.getLine(line) || ''; if (increment > 0) { index = 0; } else { var lineLen = lineText.length; index = (lineLen > 0) ? (lineLen-1) : 0; } nextCh = lineText.charAt(index); } var revSymbContext = cm.getTokenAt({line:line, ch:index+1}).type; var revSymbCtxLevel = getContextLevel(revSymbContext); if (symbCtxLevel >= revSymbCtxLevel) { if (nextCh === symb) { depth++; } else if (nextCh === reverseSymb) { depth--; } } } if (nextCh) { return { line: line, ch: index }; } return cur; } function selectCompanionObject(cm, revSymb, inclusive) { var cur = cm.getCursor(); var end = findMatchedSymbol(cm, cur, revSymb); var start = findMatchedSymbol(cm, end); start.ch += inclusive ? 1 : 0; end.ch += inclusive ? 0 : 1; return { start: start, end: end }; } // Takes in a symbol and a cursor and tries to simulate text objects that // have identical opening and closing symbols // TODO support across multiple lines function findBeginningAndEnd(cm, symb, inclusive) { var cur = cm.getCursor(); var line = cm.getLine(cur.line); var chars = line.split(''); var start, end, i, len; var firstIndex = chars.indexOf(symb); // the decision tree is to always look backwards for the beginning first, // but if the cursor is in front of the first instance of the symb, // then move the cursor forward if (cur.ch < firstIndex) { cur.ch = firstIndex; // Why is this line even here??? // cm.setCursor(cur.line, firstIndex+1); } // otherwise if the cursor is currently on the closing symbol else if (firstIndex < cur.ch && chars[cur.ch] == symb) { end = cur.ch; // assign end to the current cursor --cur.ch; // make sure to look backwards } // if we're currently on the symbol, we've got a start if (chars[cur.ch] == symb && !end) { start = cur.ch + 1; // assign start to ahead of the cursor } else { // go backwards to find the start for (i = cur.ch; i > -1 && !start; i--) { if (chars[i] == symb) { start = i + 1; } } } // look forwards for the end symbol if (start && !end) { for (i = start, len = chars.length; i < len && !end; i++) { if (chars[i] == symb) { end = i; } } } // nothing found if (!start || !end) { return { start: cur, end: cur }; } // include the symbols if (inclusive) { --start; ++end; } return { start: { line: cur.line, ch: start }, end: { line: cur.line, ch: end } }; } // Search functions function SearchState() {} SearchState.prototype = { getQuery: function() { return getVimGlobalState().query; }, setQuery: function(query) { getVimGlobalState().query = query; }, getOverlay: function() { return this.searchOverlay; }, setOverlay: function(overlay) { this.searchOverlay = overlay; }, isReversed: function() { return getVimGlobalState().isReversed; }, setReversed: function(reversed) { getVimGlobalState().isReversed = reversed; } }; function getSearchState(cm) { var vim = getVimState(cm); return vim.searchState_ || (vim.searchState_ = new SearchState()); } function dialog(cm, template, shortText, onClose, options) { if (cm.openDialog) { cm.openDialog(template, onClose, { bottom: true, value: options.value, onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp }); } else { onClose(prompt(shortText, '')); } } function findUnescapedSlashes(str) { var escapeNextChar = false; var slashes = []; for (var i = 0; i < str.length; i++) { var c = str.charAt(i); if (!escapeNextChar && c == '/') { slashes.push(i); } escapeNextChar = (c == '\\'); } return slashes; } /** * Extract the regular expression from the query and return a Regexp object. * Returns null if the query is blank. * If ignoreCase is passed in, the Regexp object will have the 'i' flag set. * If smartCase is passed in, and the query contains upper case letters, * then ignoreCase is overridden, and the 'i' flag will not be set. * If the query contains the /i in the flag part of the regular expression, * then both ignoreCase and smartCase are ignored, and 'i' will be passed * through to the Regex object. */ function parseQuery(query, ignoreCase, smartCase) { // Check if the query is already a regex. if (query instanceof RegExp) { return query; } // First try to extract regex + flags from the input. If no flags found, // extract just the regex. IE does not accept flags directly defined in // the regex string in the form /regex/flags var slashes = findUnescapedSlashes(query); var regexPart; var forceIgnoreCase; if (!slashes.length) { // Query looks like 'regexp' regexPart = query; } else { // Query looks like 'regexp/...' regexPart = query.substring(0, slashes[0]); var flagsPart = query.substring(slashes[0]); forceIgnoreCase = (flagsPart.indexOf('i') != -1); } if (!regexPart) { return null; } if (smartCase) { ignoreCase = (/^[^A-Z]*$/).test(regexPart); } var regexp = new RegExp(regexPart, (ignoreCase || forceIgnoreCase) ? 'i' : undefined); return regexp; } function showConfirm(cm, text) { if (cm.openConfirm) { cm.openConfirm('' + text + ' ', function() {}, {bottom: true}); } else { alert(text); } } function makePrompt(prefix, desc) { var raw = ''; if (prefix) { raw += '' + prefix + ''; } raw += ' ' + ''; if (desc) { raw += ''; raw += desc; raw += ''; } return raw; } var searchPromptDesc = '(Javascript regexp)'; function showPrompt(cm, options) { var shortText = (options.prefix || '') + ' ' + (options.desc || ''); var prompt = makePrompt(options.prefix, options.desc); dialog(cm, prompt, shortText, options.onClose, options); } function regexEqual(r1, r2) { if (r1 instanceof RegExp && r2 instanceof RegExp) { var props = ['global', 'multiline', 'ignoreCase', 'source']; for (var i = 0; i < props.length; i++) { var prop = props[i]; if (r1[prop] !== r2[prop]) { return false; } } return true; } return false; } // Returns true if the query is valid. function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) { if (!rawQuery) { return; } var state = getSearchState(cm); var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase); if (!query) { return; } highlightSearchMatches(cm, query); if (regexEqual(query, state.getQuery())) { return query; } state.setQuery(query); return query; } function searchOverlay(query) { if (query.source.charAt(0) == '^') { var matchSol = true; } return { token: function(stream) { if (matchSol && !stream.sol()) { stream.skipToEnd(); return; } var match = stream.match(query, false); if (match) { if (match[0].length == 0) { // Matched empty string, skip to next. stream.next(); return 'searching'; } if (!stream.sol()) { // Backtrack 1 to match \b stream.backUp(1); if (!query.exec(stream.next() + match[0])) { stream.next(); return null; } } stream.match(query); return 'searching'; } while (!stream.eol()) { stream.next(); if (stream.match(query, false)) break; } }, query: query }; } function highlightSearchMatches(cm, query) { var overlay = getSearchState(cm).getOverlay(); if (!overlay || query != overlay.query) { if (overlay) { cm.removeOverlay(overlay); } overlay = searchOverlay(query); cm.addOverlay(overlay); getSearchState(cm).setOverlay(overlay); } } function findNext(cm, prev, query, repeat) { if (repeat === undefined) { repeat = 1; } return cm.operation(function() { var pos = cm.getCursor(); var cursor = cm.getSearchCursor(query, pos); for (var i = 0; i < repeat; i++) { var found = cursor.find(prev); if (i == 0 && found && cursorEqual(cursor.from(), pos)) { found = cursor.find(prev); } if (!found) { // SearchCursor may have returned null because it hit EOF, wrap // around and try again. cursor = cm.getSearchCursor(query, (prev) ? { line: cm.lastLine() } : {line: cm.firstLine(), ch: 0} ); if (!cursor.find(prev)) { return; } } } return cursor.from(); }); } function clearSearchHighlight(cm) { cm.removeOverlay(getSearchState(cm).getOverlay()); getSearchState(cm).setOverlay(null); } /** * Check if pos is in the specified range, INCLUSIVE. * Range can be specified with 1 or 2 arguments. * If the first range argument is an array, treat it as an array of line * numbers. Match pos against any of the lines. * If the first range argument is a number, * if there is only 1 range argument, check if pos has the same line * number * if there are 2 range arguments, then check if pos is in between the two * range arguments. */ function isInRange(pos, start, end) { if (typeof pos != 'number') { // Assume it is a cursor position. Get the line number. pos = pos.line; } if (start instanceof Array) { return inArray(pos, start); } else { if (end) { return (pos >= start && pos <= end); } else { return pos == start; } } } function getUserVisibleLines(cm) { var scrollInfo = cm.getScrollInfo(); var occludeToleranceTop = 6; var occludeToleranceBottom = 10; var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local'); var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top; var to = cm.coordsChar({left:0, top: bottomY}, 'local'); return {top: from.line, bottom: to.line}; } // Ex command handling // 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: 'map', type: 'builtIn' }, { name: 'write', shortName: 'w', type: 'builtIn' }, { name: 'undo', shortName: 'u', type: 'builtIn' }, { name: 'redo', shortName: 'red', type: 'builtIn' }, { name: 'sort', shortName: 'sor', type: 'builtIn'}, { name: 'substitute', shortName: 's', type: 'builtIn'}, { name: 'nohlsearch', shortName: 'noh', type: 'builtIn'}, { name: 'delmarks', shortName: 'delm', type: 'builtin'} ]; Vim.ExCommandDispatcher = function() { this.buildCommandMap_(); }; Vim.ExCommandDispatcher.prototype = { processCommand: function(cm, input) { var vim = getVimState(cm); if (vim.visualMode) { exitVisualMode(cm); } var inputStream = new CodeMirror.StringStream(input); var params = {}; params.input = input; try { this.parseInput_(cm, inputStream, params); } catch(e) { showConfirm(cm, e); return; } var commandName; if (!params.commandName) { // If only a line range is defined, move to the line. if (params.line !== undefined) { commandName = 'move'; } } else { var command = this.matchCommand_(params.commandName); if (command) { commandName = command.name; this.parseCommandArgs_(inputStream, params, command); if (command.type == 'exToKey') { // Handle Ex to Key mapping. for (var i = 0; i < command.toKeys.length; i++) { CodeMirror.Vim.handleKey(cm, command.toKeys[i]); } return; } else if (command.type == 'exToEx') { // Handle Ex to Ex mapping. this.processCommand(cm, command.toInput); return; } } } if (!commandName) { showConfirm(cm, 'Not an editor command ":' + input + '"'); return; } try { exCommands[commandName](cm, params); } catch(e) { showConfirm(cm, e); } }, parseInput_: function(cm, inputStream, result) { inputStream.eatWhile(':'); // Parse range. if (inputStream.eat('%')) { result.line = cm.firstLine(); result.lineEnd = cm.lastLine(); } else { result.line = this.parseLineSpec_(cm, inputStream); if (result.line !== undefined && inputStream.eat(',')) { result.lineEnd = this.parseLineSpec_(cm, inputStream); } } // Parse command name. var commandMatch = inputStream.match(/^(\w+)/); if (commandMatch) { result.commandName = commandMatch[1]; } else { result.commandName = inputStream.match(/.*/)[0]; } return result; }, parseLineSpec_: function(cm, inputStream) { var numberMatch = inputStream.match(/^(\d+)/); if (numberMatch) { return parseInt(numberMatch[1], 10) - 1; } switch (inputStream.next()) { case '.': return cm.getCursor().line; case '$': return cm.lastLine(); case '\'': var mark = getVimState(cm).marks[inputStream.next()]; if (mark && mark.find()) { return mark.find().line; } throw new Error('Mark not set'); default: inputStream.backUp(1); return undefined; } }, parseCommandArgs_: function(inputStream, params, command) { if (inputStream.eol()) { return; } params.argString = inputStream.match(/.*/)[0]; // Parse command-line arguments var delim = command.argDelimiter || /\s+/; var args = trim(params.argString).split(delim); if (args.length && args[0]) { params.args = args; } }, matchCommand_: function(commandName) { // Return the command in the command map that matches the shortest // prefix of the passed in command name. The match is guaranteed to be // unambiguous if the defaultExCommandMap's shortNames are set up // correctly. (see @code{defaultExCommandMap}). for (var i = commandName.length; i > 0; i--) { var prefix = commandName.substring(0, i); if (this.commandMap_[prefix]) { var command = this.commandMap_[prefix]; if (command.name.indexOf(commandName) === 0) { return command; } } } return null; }, buildCommandMap_: function() { this.commandMap_ = {}; for (var i = 0; i < defaultExCommandMap.length; i++) { var command = defaultExCommandMap[i]; var key = command.shortName || command.name; this.commandMap_[key] = command; } }, map: function(lhs, rhs) { if (lhs != ':' && lhs.charAt(0) == ':') { var commandName = lhs.substring(1); if (rhs != ':' && rhs.charAt(0) == ':') { // Ex to Ex mapping this.commandMap_[commandName] = { name: commandName, type: 'exToEx', toInput: rhs.substring(1) }; } else { // Ex to key mapping this.commandMap_[commandName] = { name: commandName, type: 'exToKey', toKeys: parseKeyString(rhs) }; } } else { if (rhs != ':' && rhs.charAt(0) == ':') { // Key to Ex mapping. defaultKeymap.unshift({ keys: parseKeyString(lhs), type: 'keyToEx', exArgs: { input: rhs.substring(1) }}); } else { // Key to key mapping defaultKeymap.unshift({ keys: parseKeyString(lhs), type: 'keyToKey', toKeys: parseKeyString(rhs) }); } } } }; // Converts a key string sequence of the form abd into Vim's // keymap representation. function parseKeyString(str) { var key, match; var keys = []; while (str) { match = (/<\w+-.+?>|<\w+>|./).exec(str); if(match === null)break; key = match[0]; str = str.substring(match.index + key.length); keys.push(key); } return keys; } var exCommands = { map: function(cm, params) { var mapArgs = params.args; if (!mapArgs || mapArgs.length < 2) { if (cm) { showConfirm(cm, 'Invalid mapping: ' + params.input); } return; } exCommandDispatcher.map(mapArgs[0], mapArgs[1], cm); }, move: function(cm, params) { commandDispatcher.processCommand(cm, getVimState(cm), { type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: false, explicitRepeat: true, linewise: true }, repeatOverride: params.line+1}); }, sort: function(cm, params) { var reverse, ignoreCase, unique, number; function parseArgs() { if (params.argString) { var args = new CodeMirror.StringStream(params.argString); if (args.eat('!')) { reverse = true; } if (args.eol()) { return; } if (!args.eatSpace()) { throw new Error('invalid arguments ' + args.match(/.*/)[0]); } var opts = args.match(/[a-z]+/); if (opts) { opts = opts[0]; ignoreCase = opts.indexOf('i') != -1; unique = opts.indexOf('u') != -1; var decimal = opts.indexOf('d') != -1 && 1; var hex = opts.indexOf('x') != -1 && 1; var octal = opts.indexOf('o') != -1 && 1; if (decimal + hex + octal > 1) { throw new Error('invalid arguments'); } number = decimal && 'decimal' || hex && 'hex' || octal && 'octal'; } if (args.eatSpace() && args.match(/\/.*\//)) { throw new Error('patterns not supported'); } } } parseArgs(); var lineStart = params.line || cm.firstLine(); var lineEnd = params.lineEnd || params.line || cm.lastLine(); if (lineStart == lineEnd) { return; } var curStart = { line: lineStart, ch: 0 }; var curEnd = { line: lineEnd, ch: lineLength(cm, lineEnd) }; var text = cm.getRange(curStart, curEnd).split('\n'); var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ : (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i : (number == 'octal') ? /([0-7]+)/ : null; var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null; var numPart = [], textPart = []; if (number) { for (var i = 0; i < text.length; i++) { if (numberRegex.exec(text[i])) { numPart.push(text[i]); } else { textPart.push(text[i]); } } } else { textPart = text; } function compareFn(a, b) { if (reverse) { var tmp; tmp = a; a = b; b = tmp; } if (ignoreCase) { a = a.toLowerCase(); b = b.toLowerCase(); } var anum = number && numberRegex.exec(a); var bnum = number && numberRegex.exec(b); if (!anum) { return a < b ? -1 : 1; } anum = parseInt((anum[1] + anum[2]).toLowerCase(), radix); bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix); return anum - bnum; } numPart.sort(compareFn); textPart.sort(compareFn); text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart); if (unique) { // Remove duplicate lines var textOld = text; var lastLine; text = []; for (var i = 0; i < textOld.length; i++) { if (textOld[i] != lastLine) { text.push(textOld[i]); } lastLine = textOld[i]; } } cm.replaceRange(text.join('\n'), curStart, curEnd); }, substitute: function(cm, params) { if (!cm.getSearchCursor) { throw new Error('Search feature not available. Requires searchcursor.js or ' + 'any other getSearchCursor implementation.'); } var argString = params.argString; var slashes = findUnescapedSlashes(argString); if (slashes[0] !== 0) { showConfirm(cm, 'Substitutions should be of the form ' + ':s/pattern/replace/'); return; } var regexPart = argString.substring(slashes[0] + 1, slashes[1]); var replacePart = ''; var flagsPart; var count; var confirm = false; // Whether to confirm each replace. if (slashes[1]) { replacePart = argString.substring(slashes[1] + 1, slashes[2]); } if (slashes[2]) { // After the 3rd slash, we can have flags followed by a space followed // by count. var trailing = argString.substring(slashes[2] + 1).split(' '); flagsPart = trailing[0]; count = parseInt(trailing[1]); } if (flagsPart) { if (flagsPart.indexOf('c') != -1) { confirm = true; flagsPart.replace('c', ''); } regexPart = regexPart + '/' + flagsPart; } if (regexPart) { // If regex part is empty, then use the previous query. Otherwise use // the regex part as the new query. try { updateSearchQuery(cm, regexPart, true /** ignoreCase */, true /** smartCase */); } catch (e) { showConfirm(cm, 'Invalid regex: ' + regexPart); return; } } var state = getSearchState(cm); var query = state.getQuery(); var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line; var lineEnd = params.lineEnd || lineStart; if (count) { lineStart = lineEnd; lineEnd = lineStart + count - 1; } var startPos = clipCursorToContent(cm, { line: lineStart, ch: 0 }); var cursor = cm.getSearchCursor(query, startPos); doReplace(cm, confirm, lineStart, lineEnd, cursor, query, replacePart); }, redo: CodeMirror.commands.redo, undo: CodeMirror.commands.undo, write: function(cm) { if (CodeMirror.commands.save) { // If a save command is defined, call it. CodeMirror.commands.save(cm); } else { // Saves to text area if no save command is defined. cm.save(); } }, nohlsearch: function(cm) { clearSearchHighlight(cm); }, delmarks: function(cm, params) { if (!params.argString || !params.argString.trim()) { showConfirm(cm, 'Argument required'); return; } var state = getVimState(cm); var stream = new CodeMirror.StringStream(params.argString.trim()); while (!stream.eol()) { stream.eatSpace(); // Record the streams position at the beginning of the loop for use // in error messages. var count = stream.pos; if (!stream.match(/[a-zA-Z]/, false)) { showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); return; } var sym = stream.next(); // Check if this symbol is part of a range if (stream.match('-', true)) { // This symbol is part of a range. // The range must terminate at an alphabetic character. if (!stream.match(/[a-zA-Z]/, false)) { showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); return; } var startMark = sym; var finishMark = stream.next(); // The range must terminate at an alphabetic character which // shares the same case as the start of the range. if (isLowerCase(startMark) && isLowerCase(finishMark) || isUpperCase(startMark) && isUpperCase(finishMark)) { var start = startMark.charCodeAt(0); var finish = finishMark.charCodeAt(0); if (start >= finish) { showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); return; } // Because marks are always ASCII values, and we have // determined that they are the same case, we can use // their char codes to iterate through the defined range. for (var j = 0; j <= finish - start; j++) { var mark = String.fromCharCode(start + j); delete state.marks[mark]; } } else { showConfirm(cm, 'Invalid argument: ' + startMark + '-'); return; } } else { // This symbol is a valid mark, and is not part of a range. delete state.marks[sym]; } } } }; var exCommandDispatcher = new Vim.ExCommandDispatcher(); /** * @param {CodeMirror} cm CodeMirror instance we are in. * @param {boolean} confirm Whether to confirm each replace. * @param {Cursor} lineStart Line to start replacing from. * @param {Cursor} lineEnd Line to stop replacing at. * @param {RegExp} query Query for performing matches with. * @param {string} replaceWith Text to replace matches with. May contain $1, * $2, etc for replacing captured groups using Javascript replace. */ function doReplace(cm, confirm, lineStart, lineEnd, searchCursor, query, replaceWith) { // Set up all the functions. var done = false; var lastPos = searchCursor.from(); function replaceAll() { cm.operation(function() { while (!done) { replace(); next(); } stop(); }); } function replace() { var text = cm.getRange(searchCursor.from(), searchCursor.to()); var newText = text.replace(query, replaceWith); searchCursor.replace(newText); } function next() { var found = searchCursor.findNext(); if (!found) { done = true; } else if (isInRange(searchCursor.from(), lineStart, lineEnd)) { cm.scrollIntoView(searchCursor.from(), 30); cm.setSelection(searchCursor.from(), searchCursor.to()); lastPos = searchCursor.from(); done = false; } else { done = true; } } function stop(close) { if (close) { close(); } cm.focus(); if (lastPos) { cm.setCursor(lastPos); var vim = getVimState(cm); vim.lastHPos = vim.lastHSPos = lastPos.ch; } } function onPromptKeyDown(e, _value, close) { // Swallow all keys. CodeMirror.e_stop(e); var keyName = CodeMirror.keyName(e); switch (keyName) { case 'Y': replace(); next(); break; case 'N': next(); break; case 'A': cm.operation(replaceAll); break; case 'L': replace(); // fall through and exit. case 'Q': case 'Esc': case 'Ctrl-C': case 'Ctrl-[': stop(close); break; } if (done) { stop(close); } } // Actually do replace. next(); if (done) { throw new Error('No matches for ' + query.source); } if (!confirm) { replaceAll(); return; } showPrompt(cm, { prefix: 'replace with ' + replaceWith + ' (y/n/a/q/l)', onKeyDown: onPromptKeyDown }); } // Register Vim with CodeMirror function buildVimKeyMap() { /** * Handle the raw key event from CodeMirror. Translate the * Shift + key modifier to the resulting letter, while preserving other * modifers. */ // TODO: Figure out a way to catch capslock. function cmKeyToVimKey(key, modifier) { var vimKey = key; if (isUpperCase(vimKey)) { // Convert to lower case if shift is not the modifier since the key // we get from CodeMirror is always upper case. if (modifier == 'Shift') { modifier = null; } else { vimKey = vimKey.toLowerCase(); } } if (modifier) { // Vim will parse modifier+key combination as a single key. vimKey = modifier.charAt(0) + '-' + vimKey; } var specialKey = ({Enter:'CR',Backspace:'BS',Delete:'Del'})[vimKey]; vimKey = specialKey ? specialKey : vimKey; vimKey = vimKey.length > 1 ? '<'+ vimKey + '>' : vimKey; return vimKey; } // Closure to bind CodeMirror, key, modifier. function keyMapper(vimKey) { return function(cm) { CodeMirror.Vim.handleKey(cm, vimKey); }; } var cmToVimKeymap = { 'nofallthrough': true, 'disableInput': true, 'style': 'fat-cursor' }; function bindKeys(keys, modifier) { for (var i = 0; i < keys.length; i++) { var key = keys[i]; if (!modifier && inArray(key, specialSymbols)) { // Wrap special symbols with '' because that's how CodeMirror binds // them. key = "'" + key + "'"; } var vimKey = cmKeyToVimKey(keys[i], modifier); var cmKey = modifier ? modifier + '-' + key : key; cmToVimKeymap[cmKey] = keyMapper(vimKey); } } bindKeys(upperCaseAlphabet); bindKeys(upperCaseAlphabet, 'Shift'); bindKeys(upperCaseAlphabet, 'Ctrl'); bindKeys(specialSymbols); bindKeys(specialSymbols, 'Ctrl'); bindKeys(numbers); bindKeys(numbers, 'Ctrl'); bindKeys(specialKeys); bindKeys(specialKeys, 'Ctrl'); return cmToVimKeymap; } CodeMirror.keyMap.vim = buildVimKeyMap(); function exitInsertMode(cm) { var vim = getVimState(cm); vim.insertMode = false; var inReplay = getVimGlobalState().macroModeState.inReplay; if (!inReplay) { cm.off('change', onChange); cm.off('cursorActivity', onCursorActivity); CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); } if (!inReplay && vim.insertModeRepeat > 1) { // Perform insert mode repeat for commands like 3,a and 3,o. repeatLastEdit(cm, vim, vim.insertModeRepeat - 1, true /** repeatForInsert */); vim.lastEditInputState.repeatOverride = vim.insertModeRepeat; } delete vim.insertModeRepeat; cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1, true); cm.setOption('keyMap', 'vim'); cm.toggleOverwrite(false); // exit replace mode if we were in it. } CodeMirror.keyMap['vim-insert'] = { // TODO: override navigation keys so that Esc will cancel automatic // indentation from o, O, i_ 'Esc': exitInsertMode, 'Ctrl-[': exitInsertMode, 'Ctrl-C': exitInsertMode, 'Ctrl-N': 'autocomplete', 'Ctrl-P': 'autocomplete', 'Enter': function(cm) { var fn = CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent; fn(cm); }, fallthrough: ['default'] }; CodeMirror.keyMap['vim-replace'] = { 'Backspace': 'goCharLeft', fallthrough: ['vim-insert'] }; function parseRegisterToKeyBuffer(macroModeState, registerName) { var match, key; var register = getVimGlobalState().registerController.getRegister(registerName); var text = register.toString(); var macroKeyBuffer = macroModeState.macroKeyBuffer; emptyMacroKeyBuffer(macroModeState); do { match = (/<\w+-.+?>|<\w+>|./).exec(text); if(match === null)break; key = match[0]; text = text.substring(match.index + key.length); macroKeyBuffer.push(key); } while (text); return macroKeyBuffer; } function parseKeyBufferToRegister(registerName, keyBuffer) { var text = keyBuffer.join(''); getVimGlobalState().registerController.setRegisterText(registerName, text); } function emptyMacroKeyBuffer(macroModeState) { if(macroModeState.isMacroPlaying)return; var macroKeyBuffer = macroModeState.macroKeyBuffer; macroKeyBuffer.length = 0; } function executeMacroKeyBuffer(cm, macroModeState, keyBuffer) { macroModeState.isMacroPlaying = true; for (var i = 0, len = keyBuffer.length; i < len; i++) { CodeMirror.Vim.handleKey(cm, keyBuffer[i]); }; macroModeState.isMacroPlaying = false; } function logKey(macroModeState, key) { if(macroModeState.isMacroPlaying)return; var macroKeyBuffer = macroModeState.macroKeyBuffer; macroKeyBuffer.push(key); } /** * Listens for changes made in insert mode. * Should only be active in insert mode. */ function onChange(_cm, changeObj) { var macroModeState = getVimGlobalState().macroModeState; var lastChange = macroModeState.lastInsertModeChanges; while (changeObj) { lastChange.expectCursorActivityForChange = true; if (changeObj.origin == '+input' || changeObj.origin == 'paste' || changeObj.origin === undefined /* only in testing */) { var text = changeObj.text.join('\n'); lastChange.changes.push(text); } // Change objects may be chained with next. changeObj = changeObj.next; } } /** * Listens for any kind of cursor activity on CodeMirror. * - For tracking cursor activity in insert mode. * - Should only be active in insert mode. */ function onCursorActivity() { var macroModeState = getVimGlobalState().macroModeState; var lastChange = macroModeState.lastInsertModeChanges; if (lastChange.expectCursorActivityForChange) { lastChange.expectCursorActivityForChange = false; } else { // Cursor moved outside the context of an edit. Reset the change. lastChange.changes = []; } } /** Wrapper for special keys pressed in insert mode */ function InsertModeKey(keyName) { this.keyName = keyName; } /** * Handles raw key down events from the text area. * - Should only be active in insert mode. * - For recording deletes in insert mode. */ function onKeyEventTargetKeyDown(e) { var macroModeState = getVimGlobalState().macroModeState; var lastChange = macroModeState.lastInsertModeChanges; var keyName = CodeMirror.keyName(e); function onKeyFound() { lastChange.changes.push(new InsertModeKey(keyName)); return true; } if (keyName.indexOf('Delete') != -1 || keyName.indexOf('Backspace') != -1) { CodeMirror.lookupKey(keyName, ['vim-insert'], onKeyFound); } } /** * Repeats the last edit, which includes exactly 1 command and at most 1 * insert. Operator and motion commands are read from lastEditInputState, * while action commands are read from lastEditActionCommand. * * If repeatForInsert is true, then the function was called by * exitInsertMode to repeat the insert mode changes the user just made. The * corresponding enterInsertMode call was made with a count. */ function repeatLastEdit(cm, vim, repeat, repeatForInsert) { var macroModeState = getVimGlobalState().macroModeState; macroModeState.inReplay = true; var isAction = !!vim.lastEditActionCommand; var cachedInputState = vim.inputState; function repeatCommand() { if (isAction) { commandDispatcher.processAction(cm, vim, vim.lastEditActionCommand); } else { commandDispatcher.evalInput(cm, vim); } } function repeatInsert(repeat) { if (macroModeState.lastInsertModeChanges.changes.length > 0) { // For some reason, repeat cw in desktop VIM will does not repeat // insert mode changes. Will conform to that behavior. repeat = !vim.lastEditActionCommand ? 1 : repeat; repeatLastInsertModeChanges(cm, repeat, macroModeState); } } vim.inputState = vim.lastEditInputState; if (isAction && vim.lastEditActionCommand.interlaceInsertRepeat) { // o and O repeat have to be interlaced with insert repeats so that the // insertions appear on separate lines instead of the last line. for (var i = 0; i < repeat; i++) { repeatCommand(); repeatInsert(1); } } else { if (!repeatForInsert) { // Hack to get the cursor to end up at the right place. If I is // repeated in insert mode repeat, cursor will be 1 insert // change set left of where it should be. repeatCommand(); } repeatInsert(repeat); } vim.inputState = cachedInputState; if (vim.insertMode && !repeatForInsert) { // Don't exit insert mode twice. If repeatForInsert is set, then we // were called by an exitInsertMode call lower on the stack. exitInsertMode(cm); } macroModeState.inReplay = false; }; function repeatLastInsertModeChanges(cm, repeat, macroModeState) { var lastChange = macroModeState.lastInsertModeChanges; function keyHandler(binding) { if (typeof binding == 'string') { CodeMirror.commands[binding](cm); } else { binding(cm); } return true; } for (var i = 0; i < repeat; i++) { for (var j = 0; j < lastChange.changes.length; j++) { var change = lastChange.changes[j]; if (change instanceof InsertModeKey) { CodeMirror.lookupKey(change.keyName, ['vim-insert'], keyHandler); } else { var cur = cm.getCursor(); cm.replaceRange(change, cur, cur); } } } } return vimApi; }; // Initialize Vim and make it available as an API. CodeMirror.Vim = Vim(); } )();