michael@0: /** michael@0: * Supported keybindings: michael@0: * michael@0: * Motion: michael@0: * h, j, k, l michael@0: * gj, gk michael@0: * e, E, w, W, b, B, ge, gE michael@0: * f, F, t, T michael@0: * $, ^, 0, -, +, _ michael@0: * gg, G michael@0: * % michael@0: * ', ` michael@0: * michael@0: * Operator: michael@0: * d, y, c michael@0: * dd, yy, cc michael@0: * g~, g~g~ michael@0: * >, <, >>, << michael@0: * michael@0: * Operator-Motion: michael@0: * x, X, D, Y, C, ~ michael@0: * michael@0: * Action: michael@0: * a, i, s, A, I, S, o, O michael@0: * zz, z., z, zt, zb, z- michael@0: * J michael@0: * u, Ctrl-r michael@0: * m michael@0: * r michael@0: * michael@0: * Modes: michael@0: * ESC - leave insert mode, visual mode, and clear input state. michael@0: * Ctrl-[, Ctrl-c - same as ESC. michael@0: * michael@0: * Registers: unnamed, -, a-z, A-Z, 0-9 michael@0: * (Does not respect the special case for number registers when delete michael@0: * operator is made with these commands: %, (, ), , /, ?, n, N, {, } ) michael@0: * TODO: Implement the remaining registers. michael@0: * Marks: a-z, A-Z, and 0-9 michael@0: * TODO: Implement the remaining special marks. They have more complex michael@0: * behavior. michael@0: * michael@0: * Events: michael@0: * 'vim-mode-change' - raised on the editor anytime the current mode changes, michael@0: * Event object: {mode: "visual", subMode: "linewise"} michael@0: * michael@0: * Code structure: michael@0: * 1. Default keymap michael@0: * 2. Variable declarations and short basic helpers michael@0: * 3. Instance (External API) implementation michael@0: * 4. Internal state tracking objects (input state, counter) implementation michael@0: * and instanstiation michael@0: * 5. Key handler (the main command dispatcher) implementation michael@0: * 6. Motion, operator, and action implementations michael@0: * 7. Helper functions for the key handler, motions, operators, and actions michael@0: * 8. Set up Vim to work as a keymap for CodeMirror. michael@0: */ michael@0: michael@0: (function(mod) { michael@0: if (typeof exports == "object" && typeof module == "object") // CommonJS michael@0: mod(require("../lib/codemirror"), require("../addon/search/searchcursor"), require("../addon/dialog/dialog")); michael@0: else if (typeof define == "function" && define.amd) // AMD michael@0: define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/dialog/dialog"], mod); michael@0: else // Plain browser env michael@0: mod(CodeMirror); michael@0: })(function(CodeMirror) { michael@0: 'use strict'; michael@0: michael@0: var defaultKeymap = [ michael@0: // Key to key mapping. This goes first to make it possible to override michael@0: // existing mappings. michael@0: { keys: [''], type: 'keyToKey', toKeys: ['h'] }, michael@0: { keys: [''], type: 'keyToKey', toKeys: ['l'] }, michael@0: { keys: [''], type: 'keyToKey', toKeys: ['k'] }, michael@0: { keys: [''], type: 'keyToKey', toKeys: ['j'] }, michael@0: { keys: [''], type: 'keyToKey', toKeys: ['l'] }, michael@0: { keys: [''], type: 'keyToKey', toKeys: ['h'] }, michael@0: { keys: [''], type: 'keyToKey', toKeys: ['W'] }, michael@0: { keys: [''], type: 'keyToKey', toKeys: ['B'] }, michael@0: { keys: [''], type: 'keyToKey', toKeys: ['w'] }, michael@0: { keys: [''], type: 'keyToKey', toKeys: ['b'] }, michael@0: { keys: [''], type: 'keyToKey', toKeys: ['j'] }, michael@0: { keys: [''], type: 'keyToKey', toKeys: ['k'] }, michael@0: { keys: [''], type: 'keyToKey', toKeys: [''] }, michael@0: { keys: [''], type: 'keyToKey', toKeys: [''] }, michael@0: { keys: ['s'], type: 'keyToKey', toKeys: ['c', 'l'], context: 'normal' }, michael@0: { keys: ['s'], type: 'keyToKey', toKeys: ['x', 'i'], context: 'visual'}, michael@0: { keys: ['S'], type: 'keyToKey', toKeys: ['c', 'c'], context: 'normal' }, michael@0: { keys: ['S'], type: 'keyToKey', toKeys: ['d', 'c', 'c'], context: 'visual' }, michael@0: { keys: [''], type: 'keyToKey', toKeys: ['0'] }, michael@0: { keys: [''], type: 'keyToKey', toKeys: ['$'] }, michael@0: { keys: [''], type: 'keyToKey', toKeys: [''] }, michael@0: { keys: [''], type: 'keyToKey', toKeys: [''] }, michael@0: { keys: [''], type: 'keyToKey', toKeys: ['j', '^'], context: 'normal' }, michael@0: // Motions michael@0: { keys: ['H'], type: 'motion', michael@0: motion: 'moveToTopLine', michael@0: motionArgs: { linewise: true, toJumplist: true }}, michael@0: { keys: ['M'], type: 'motion', michael@0: motion: 'moveToMiddleLine', michael@0: motionArgs: { linewise: true, toJumplist: true }}, michael@0: { keys: ['L'], type: 'motion', michael@0: motion: 'moveToBottomLine', michael@0: motionArgs: { linewise: true, toJumplist: true }}, michael@0: { keys: ['h'], type: 'motion', michael@0: motion: 'moveByCharacters', michael@0: motionArgs: { forward: false }}, michael@0: { keys: ['l'], type: 'motion', michael@0: motion: 'moveByCharacters', michael@0: motionArgs: { forward: true }}, michael@0: { keys: ['j'], type: 'motion', michael@0: motion: 'moveByLines', michael@0: motionArgs: { forward: true, linewise: true }}, michael@0: { keys: ['k'], type: 'motion', michael@0: motion: 'moveByLines', michael@0: motionArgs: { forward: false, linewise: true }}, michael@0: { keys: ['g','j'], type: 'motion', michael@0: motion: 'moveByDisplayLines', michael@0: motionArgs: { forward: true }}, michael@0: { keys: ['g','k'], type: 'motion', michael@0: motion: 'moveByDisplayLines', michael@0: motionArgs: { forward: false }}, michael@0: { keys: ['w'], type: 'motion', michael@0: motion: 'moveByWords', michael@0: motionArgs: { forward: true, wordEnd: false }}, michael@0: { keys: ['W'], type: 'motion', michael@0: motion: 'moveByWords', michael@0: motionArgs: { forward: true, wordEnd: false, bigWord: true }}, michael@0: { keys: ['e'], type: 'motion', michael@0: motion: 'moveByWords', michael@0: motionArgs: { forward: true, wordEnd: true, inclusive: true }}, michael@0: { keys: ['E'], type: 'motion', michael@0: motion: 'moveByWords', michael@0: motionArgs: { forward: true, wordEnd: true, bigWord: true, michael@0: inclusive: true }}, michael@0: { keys: ['b'], type: 'motion', michael@0: motion: 'moveByWords', michael@0: motionArgs: { forward: false, wordEnd: false }}, michael@0: { keys: ['B'], type: 'motion', michael@0: motion: 'moveByWords', michael@0: motionArgs: { forward: false, wordEnd: false, bigWord: true }}, michael@0: { keys: ['g', 'e'], type: 'motion', michael@0: motion: 'moveByWords', michael@0: motionArgs: { forward: false, wordEnd: true, inclusive: true }}, michael@0: { keys: ['g', 'E'], type: 'motion', michael@0: motion: 'moveByWords', michael@0: motionArgs: { forward: false, wordEnd: true, bigWord: true, michael@0: inclusive: true }}, michael@0: { keys: ['{'], type: 'motion', motion: 'moveByParagraph', michael@0: motionArgs: { forward: false, toJumplist: true }}, michael@0: { keys: ['}'], type: 'motion', motion: 'moveByParagraph', michael@0: motionArgs: { forward: true, toJumplist: true }}, michael@0: { keys: [''], type: 'motion', michael@0: motion: 'moveByPage', motionArgs: { forward: true }}, michael@0: { keys: [''], type: 'motion', michael@0: motion: 'moveByPage', motionArgs: { forward: false }}, michael@0: { keys: [''], type: 'motion', michael@0: motion: 'moveByScroll', michael@0: motionArgs: { forward: true, explicitRepeat: true }}, michael@0: { keys: [''], type: 'motion', michael@0: motion: 'moveByScroll', michael@0: motionArgs: { forward: false, explicitRepeat: true }}, michael@0: { keys: ['g', 'g'], type: 'motion', michael@0: motion: 'moveToLineOrEdgeOfDocument', michael@0: motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }}, michael@0: { keys: ['G'], type: 'motion', michael@0: motion: 'moveToLineOrEdgeOfDocument', michael@0: motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }}, michael@0: { keys: ['0'], type: 'motion', motion: 'moveToStartOfLine' }, michael@0: { keys: ['^'], type: 'motion', michael@0: motion: 'moveToFirstNonWhiteSpaceCharacter' }, michael@0: { keys: ['+'], type: 'motion', michael@0: motion: 'moveByLines', michael@0: motionArgs: { forward: true, toFirstChar:true }}, michael@0: { keys: ['-'], type: 'motion', michael@0: motion: 'moveByLines', michael@0: motionArgs: { forward: false, toFirstChar:true }}, michael@0: { keys: ['_'], type: 'motion', michael@0: motion: 'moveByLines', michael@0: motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }}, michael@0: { keys: ['$'], type: 'motion', michael@0: motion: 'moveToEol', michael@0: motionArgs: { inclusive: true }}, michael@0: { keys: ['%'], type: 'motion', michael@0: motion: 'moveToMatchedSymbol', michael@0: motionArgs: { inclusive: true, toJumplist: true }}, michael@0: { keys: ['f', 'character'], type: 'motion', michael@0: motion: 'moveToCharacter', michael@0: motionArgs: { forward: true , inclusive: true }}, michael@0: { keys: ['F', 'character'], type: 'motion', michael@0: motion: 'moveToCharacter', michael@0: motionArgs: { forward: false }}, michael@0: { keys: ['t', 'character'], type: 'motion', michael@0: motion: 'moveTillCharacter', michael@0: motionArgs: { forward: true, inclusive: true }}, michael@0: { keys: ['T', 'character'], type: 'motion', michael@0: motion: 'moveTillCharacter', michael@0: motionArgs: { forward: false }}, michael@0: { keys: [';'], type: 'motion', motion: 'repeatLastCharacterSearch', michael@0: motionArgs: { forward: true }}, michael@0: { keys: [','], type: 'motion', motion: 'repeatLastCharacterSearch', michael@0: motionArgs: { forward: false }}, michael@0: { keys: ['\'', 'character'], type: 'motion', motion: 'goToMark', michael@0: motionArgs: {toJumplist: true}}, michael@0: { keys: ['`', 'character'], type: 'motion', motion: 'goToMark', michael@0: motionArgs: {toJumplist: true}}, michael@0: { keys: [']', '`'], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } }, michael@0: { keys: ['[', '`'], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } }, michael@0: { keys: [']', '\''], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } }, michael@0: { keys: ['[', '\''], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } }, michael@0: { keys: [']', 'character'], type: 'motion', michael@0: motion: 'moveToSymbol', michael@0: motionArgs: { forward: true, toJumplist: true}}, michael@0: { keys: ['[', 'character'], type: 'motion', michael@0: motion: 'moveToSymbol', michael@0: motionArgs: { forward: false, toJumplist: true}}, michael@0: { keys: ['|'], type: 'motion', michael@0: motion: 'moveToColumn', michael@0: motionArgs: { }}, michael@0: { keys: ['o'], type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: { },context:'visual'}, michael@0: // Operators michael@0: { keys: ['d'], type: 'operator', operator: 'delete' }, michael@0: { keys: ['y'], type: 'operator', operator: 'yank' }, michael@0: { keys: ['c'], type: 'operator', operator: 'change' }, michael@0: { keys: ['>'], type: 'operator', operator: 'indent', michael@0: operatorArgs: { indentRight: true }}, michael@0: { keys: ['<'], type: 'operator', operator: 'indent', michael@0: operatorArgs: { indentRight: false }}, michael@0: { keys: ['g', '~'], type: 'operator', operator: 'swapcase' }, michael@0: { keys: ['n'], type: 'motion', motion: 'findNext', michael@0: motionArgs: { forward: true, toJumplist: true }}, michael@0: { keys: ['N'], type: 'motion', motion: 'findNext', michael@0: motionArgs: { forward: false, toJumplist: true }}, michael@0: // Operator-Motion dual commands michael@0: { keys: ['x'], type: 'operatorMotion', operator: 'delete', michael@0: motion: 'moveByCharacters', motionArgs: { forward: true }, michael@0: operatorMotionArgs: { visualLine: false }}, michael@0: { keys: ['X'], type: 'operatorMotion', operator: 'delete', michael@0: motion: 'moveByCharacters', motionArgs: { forward: false }, michael@0: operatorMotionArgs: { visualLine: true }}, michael@0: { keys: ['D'], type: 'operatorMotion', operator: 'delete', michael@0: motion: 'moveToEol', motionArgs: { inclusive: true }, michael@0: operatorMotionArgs: { visualLine: true }}, michael@0: { keys: ['Y'], type: 'operatorMotion', operator: 'yank', michael@0: motion: 'moveToEol', motionArgs: { inclusive: true }, michael@0: operatorMotionArgs: { visualLine: true }}, michael@0: { keys: ['C'], type: 'operatorMotion', michael@0: operator: 'change', michael@0: motion: 'moveToEol', motionArgs: { inclusive: true }, michael@0: operatorMotionArgs: { visualLine: true }}, michael@0: { keys: ['~'], type: 'operatorMotion', michael@0: operator: 'swapcase', operatorArgs: { shouldMoveCursor: true }, michael@0: motion: 'moveByCharacters', motionArgs: { forward: true }}, michael@0: // Actions michael@0: { keys: [''], type: 'action', action: 'jumpListWalk', michael@0: actionArgs: { forward: true }}, michael@0: { keys: [''], type: 'action', action: 'jumpListWalk', michael@0: actionArgs: { forward: false }}, michael@0: { keys: [''], type: 'action', michael@0: action: 'scroll', michael@0: actionArgs: { forward: true, linewise: true }}, michael@0: { keys: [''], type: 'action', michael@0: action: 'scroll', michael@0: actionArgs: { forward: false, linewise: true }}, michael@0: { keys: ['a'], type: 'action', action: 'enterInsertMode', isEdit: true, michael@0: actionArgs: { insertAt: 'charAfter' }}, michael@0: { keys: ['A'], type: 'action', action: 'enterInsertMode', isEdit: true, michael@0: actionArgs: { insertAt: 'eol' }}, michael@0: { keys: ['i'], type: 'action', action: 'enterInsertMode', isEdit: true, michael@0: actionArgs: { insertAt: 'inplace' }}, michael@0: { keys: ['I'], type: 'action', action: 'enterInsertMode', isEdit: true, michael@0: actionArgs: { insertAt: 'firstNonBlank' }}, michael@0: { keys: ['o'], type: 'action', action: 'newLineAndEnterInsertMode', michael@0: isEdit: true, interlaceInsertRepeat: true, michael@0: actionArgs: { after: true }}, michael@0: { keys: ['O'], type: 'action', action: 'newLineAndEnterInsertMode', michael@0: isEdit: true, interlaceInsertRepeat: true, michael@0: actionArgs: { after: false }}, michael@0: { keys: ['v'], type: 'action', action: 'toggleVisualMode' }, michael@0: { keys: ['V'], type: 'action', action: 'toggleVisualMode', michael@0: actionArgs: { linewise: true }}, michael@0: { keys: ['g', 'v'], type: 'action', action: 'reselectLastSelection' }, michael@0: { keys: ['J'], type: 'action', action: 'joinLines', isEdit: true }, michael@0: { keys: ['p'], type: 'action', action: 'paste', isEdit: true, michael@0: actionArgs: { after: true, isEdit: true }}, michael@0: { keys: ['P'], type: 'action', action: 'paste', isEdit: true, michael@0: actionArgs: { after: false, isEdit: true }}, michael@0: { keys: ['r', 'character'], type: 'action', action: 'replace', isEdit: true }, michael@0: { keys: ['@', 'character'], type: 'action', action: 'replayMacro' }, michael@0: { keys: ['q', 'character'], type: 'action', action: 'enterMacroRecordMode' }, michael@0: // Handle Replace-mode as a special case of insert mode. michael@0: { keys: ['R'], type: 'action', action: 'enterInsertMode', isEdit: true, michael@0: actionArgs: { replace: true }}, michael@0: { keys: ['u'], type: 'action', action: 'undo' }, michael@0: { keys: [''], type: 'action', action: 'redo' }, michael@0: { keys: ['m', 'character'], type: 'action', action: 'setMark' }, michael@0: { keys: ['"', 'character'], type: 'action', action: 'setRegister' }, michael@0: { keys: ['z', 'z'], type: 'action', action: 'scrollToCursor', michael@0: actionArgs: { position: 'center' }}, michael@0: { keys: ['z', '.'], type: 'action', action: 'scrollToCursor', michael@0: actionArgs: { position: 'center' }, michael@0: motion: 'moveToFirstNonWhiteSpaceCharacter' }, michael@0: { keys: ['z', 't'], type: 'action', action: 'scrollToCursor', michael@0: actionArgs: { position: 'top' }}, michael@0: { keys: ['z', ''], type: 'action', action: 'scrollToCursor', michael@0: actionArgs: { position: 'top' }, michael@0: motion: 'moveToFirstNonWhiteSpaceCharacter' }, michael@0: { keys: ['z', '-'], type: 'action', action: 'scrollToCursor', michael@0: actionArgs: { position: 'bottom' }}, michael@0: { keys: ['z', 'b'], type: 'action', action: 'scrollToCursor', michael@0: actionArgs: { position: 'bottom' }, michael@0: motion: 'moveToFirstNonWhiteSpaceCharacter' }, michael@0: { keys: ['.'], type: 'action', action: 'repeatLastEdit' }, michael@0: { keys: [''], type: 'action', action: 'incrementNumberToken', michael@0: isEdit: true, michael@0: actionArgs: {increase: true, backtrack: false}}, michael@0: { keys: [''], type: 'action', action: 'incrementNumberToken', michael@0: isEdit: true, michael@0: actionArgs: {increase: false, backtrack: false}}, michael@0: // Text object motions michael@0: { keys: ['a', 'character'], type: 'motion', michael@0: motion: 'textObjectManipulation' }, michael@0: { keys: ['i', 'character'], type: 'motion', michael@0: motion: 'textObjectManipulation', michael@0: motionArgs: { textObjectInner: true }}, michael@0: // Search michael@0: { keys: ['/'], type: 'search', michael@0: searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }}, michael@0: { keys: ['?'], type: 'search', michael@0: searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }}, michael@0: { keys: ['*'], type: 'search', michael@0: searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }}, michael@0: { keys: ['#'], type: 'search', michael@0: searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }}, michael@0: // Ex command michael@0: { keys: [':'], type: 'ex' } michael@0: ]; michael@0: michael@0: var Pos = CodeMirror.Pos; michael@0: michael@0: var Vim = function() { michael@0: CodeMirror.defineOption('vimMode', false, function(cm, val) { michael@0: if (val) { michael@0: cm.setOption('keyMap', 'vim'); michael@0: cm.setOption('disableInput', true); michael@0: CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); michael@0: cm.on('beforeSelectionChange', beforeSelectionChange); michael@0: maybeInitVimState(cm); michael@0: CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm)); michael@0: } else if (cm.state.vim) { michael@0: cm.setOption('keyMap', 'default'); michael@0: cm.setOption('disableInput', false); michael@0: cm.off('beforeSelectionChange', beforeSelectionChange); michael@0: CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm)); michael@0: cm.state.vim = null; michael@0: } michael@0: }); michael@0: function beforeSelectionChange(cm, obj) { michael@0: var vim = cm.state.vim; michael@0: if (vim.insertMode || vim.exMode) return; michael@0: michael@0: var head = obj.ranges[0].head; michael@0: var anchor = obj.ranges[0].anchor; michael@0: if (head.ch && head.ch == cm.doc.getLine(head.line).length) { michael@0: var pos = Pos(head.line, head.ch - 1); michael@0: obj.update([{anchor: cursorEqual(head, anchor) ? pos : anchor, michael@0: head: pos}]); michael@0: } michael@0: } michael@0: function getOnPasteFn(cm) { michael@0: var vim = cm.state.vim; michael@0: if (!vim.onPasteFn) { michael@0: vim.onPasteFn = function() { michael@0: if (!vim.insertMode) { michael@0: cm.setCursor(offsetCursor(cm.getCursor(), 0, 1)); michael@0: actions.enterInsertMode(cm, {}, vim); michael@0: } michael@0: }; michael@0: } michael@0: return vim.onPasteFn; michael@0: } michael@0: michael@0: var numberRegex = /[\d]/; michael@0: var wordRegexp = [(/\w/), (/[^\w\s]/)], bigWordRegexp = [(/\S/)]; michael@0: function makeKeyRange(start, size) { michael@0: var keys = []; michael@0: for (var i = start; i < start + size; i++) { michael@0: keys.push(String.fromCharCode(i)); michael@0: } michael@0: return keys; michael@0: } michael@0: var upperCaseAlphabet = makeKeyRange(65, 26); michael@0: var lowerCaseAlphabet = makeKeyRange(97, 26); michael@0: var numbers = makeKeyRange(48, 10); michael@0: var specialSymbols = '~`!@#$%^&*()_-+=[{}]\\|/?.,<>:;"\''.split(''); michael@0: var specialKeys = ['Left', 'Right', 'Up', 'Down', 'Space', 'Backspace', michael@0: 'Esc', 'Home', 'End', 'PageUp', 'PageDown', 'Enter']; michael@0: var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']); michael@0: var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"']); michael@0: michael@0: function isLine(cm, line) { michael@0: return line >= cm.firstLine() && line <= cm.lastLine(); michael@0: } michael@0: function isLowerCase(k) { michael@0: return (/^[a-z]$/).test(k); michael@0: } michael@0: function isMatchableSymbol(k) { michael@0: return '()[]{}'.indexOf(k) != -1; michael@0: } michael@0: function isNumber(k) { michael@0: return numberRegex.test(k); michael@0: } michael@0: function isUpperCase(k) { michael@0: return (/^[A-Z]$/).test(k); michael@0: } michael@0: function isWhiteSpaceString(k) { michael@0: return (/^\s*$/).test(k); michael@0: } michael@0: function inArray(val, arr) { michael@0: for (var i = 0; i < arr.length; i++) { michael@0: if (arr[i] == val) { michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: var options = {}; michael@0: function defineOption(name, defaultValue, type) { michael@0: if (defaultValue === undefined) { throw Error('defaultValue is required'); } michael@0: if (!type) { type = 'string'; } michael@0: options[name] = { michael@0: type: type, michael@0: defaultValue: defaultValue michael@0: }; michael@0: setOption(name, defaultValue); michael@0: } michael@0: michael@0: function setOption(name, value) { michael@0: var option = options[name]; michael@0: if (!option) { michael@0: throw Error('Unknown option: ' + name); michael@0: } michael@0: if (option.type == 'boolean') { michael@0: if (value && value !== true) { michael@0: throw Error('Invalid argument: ' + name + '=' + value); michael@0: } else if (value !== false) { michael@0: // Boolean options are set to true if value is not defined. michael@0: value = true; michael@0: } michael@0: } michael@0: option.value = option.type == 'boolean' ? !!value : value; michael@0: } michael@0: michael@0: function getOption(name) { michael@0: var option = options[name]; michael@0: if (!option) { michael@0: throw Error('Unknown option: ' + name); michael@0: } michael@0: return option.value; michael@0: } michael@0: michael@0: var createCircularJumpList = function() { michael@0: var size = 100; michael@0: var pointer = -1; michael@0: var head = 0; michael@0: var tail = 0; michael@0: var buffer = new Array(size); michael@0: function add(cm, oldCur, newCur) { michael@0: var current = pointer % size; michael@0: var curMark = buffer[current]; michael@0: function useNextSlot(cursor) { michael@0: var next = ++pointer % size; michael@0: var trashMark = buffer[next]; michael@0: if (trashMark) { michael@0: trashMark.clear(); michael@0: } michael@0: buffer[next] = cm.setBookmark(cursor); michael@0: } michael@0: if (curMark) { michael@0: var markPos = curMark.find(); michael@0: // avoid recording redundant cursor position michael@0: if (markPos && !cursorEqual(markPos, oldCur)) { michael@0: useNextSlot(oldCur); michael@0: } michael@0: } else { michael@0: useNextSlot(oldCur); michael@0: } michael@0: useNextSlot(newCur); michael@0: head = pointer; michael@0: tail = pointer - size + 1; michael@0: if (tail < 0) { michael@0: tail = 0; michael@0: } michael@0: } michael@0: function move(cm, offset) { michael@0: pointer += offset; michael@0: if (pointer > head) { michael@0: pointer = head; michael@0: } else if (pointer < tail) { michael@0: pointer = tail; michael@0: } michael@0: var mark = buffer[(size + pointer) % size]; michael@0: // skip marks that are temporarily removed from text buffer michael@0: if (mark && !mark.find()) { michael@0: var inc = offset > 0 ? 1 : -1; michael@0: var newCur; michael@0: var oldCur = cm.getCursor(); michael@0: do { michael@0: pointer += inc; michael@0: mark = buffer[(size + pointer) % size]; michael@0: // skip marks that are the same as current position michael@0: if (mark && michael@0: (newCur = mark.find()) && michael@0: !cursorEqual(oldCur, newCur)) { michael@0: break; michael@0: } michael@0: } while (pointer < head && pointer > tail); michael@0: } michael@0: return mark; michael@0: } michael@0: return { michael@0: cachedCursor: undefined, //used for # and * jumps michael@0: add: add, michael@0: move: move michael@0: }; michael@0: }; michael@0: michael@0: // Returns an object to track the changes associated insert mode. It michael@0: // clones the object that is passed in, or creates an empty object one if michael@0: // none is provided. michael@0: var createInsertModeChanges = function(c) { michael@0: if (c) { michael@0: // Copy construction michael@0: return { michael@0: changes: c.changes, michael@0: expectCursorActivityForChange: c.expectCursorActivityForChange michael@0: }; michael@0: } michael@0: return { michael@0: // Change list michael@0: changes: [], michael@0: // Set to true on change, false on cursorActivity. michael@0: expectCursorActivityForChange: false michael@0: }; michael@0: }; michael@0: michael@0: function MacroModeState() { michael@0: this.latestRegister = undefined; michael@0: this.isPlaying = false; michael@0: this.isRecording = false; michael@0: this.onRecordingDone = undefined; michael@0: this.lastInsertModeChanges = createInsertModeChanges(); michael@0: } michael@0: MacroModeState.prototype = { michael@0: exitMacroRecordMode: function() { michael@0: var macroModeState = vimGlobalState.macroModeState; michael@0: macroModeState.onRecordingDone(); // close dialog michael@0: macroModeState.onRecordingDone = undefined; michael@0: macroModeState.isRecording = false; michael@0: }, michael@0: enterMacroRecordMode: function(cm, registerName) { michael@0: var register = michael@0: vimGlobalState.registerController.getRegister(registerName); michael@0: if (register) { michael@0: register.clear(); michael@0: this.latestRegister = registerName; michael@0: this.onRecordingDone = cm.openDialog( michael@0: '(recording)['+registerName+']', null, {bottom:true}); michael@0: this.isRecording = true; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: function maybeInitVimState(cm) { michael@0: if (!cm.state.vim) { michael@0: // Store instance state in the CodeMirror object. michael@0: cm.state.vim = { michael@0: inputState: new InputState(), michael@0: // Vim's input state that triggered the last edit, used to repeat michael@0: // motions and operators with '.'. michael@0: lastEditInputState: undefined, michael@0: // Vim's action command before the last edit, used to repeat actions michael@0: // with '.' and insert mode repeat. michael@0: lastEditActionCommand: undefined, michael@0: // When using jk for navigation, if you move from a longer line to a michael@0: // shorter line, the cursor may clip to the end of the shorter line. michael@0: // If j is pressed again and cursor goes to the next line, the michael@0: // cursor should go back to its horizontal position on the longer michael@0: // line if it can. This is to keep track of the horizontal position. michael@0: lastHPos: -1, michael@0: // Doing the same with screen-position for gj/gk michael@0: lastHSPos: -1, michael@0: // The last motion command run. Cleared if a non-motion command gets michael@0: // executed in between. michael@0: lastMotion: null, michael@0: marks: {}, michael@0: insertMode: false, michael@0: // Repeat count for changes made in insert mode, triggered by key michael@0: // sequences like 3,i. Only exists when insertMode is true. michael@0: insertModeRepeat: undefined, michael@0: visualMode: false, michael@0: // If we are in visual line mode. No effect if visualMode is false. michael@0: visualLine: false, michael@0: lastSelection: null michael@0: }; michael@0: } michael@0: return cm.state.vim; michael@0: } michael@0: var vimGlobalState; michael@0: function resetVimGlobalState() { michael@0: vimGlobalState = { michael@0: // The current search query. michael@0: searchQuery: null, michael@0: // Whether we are searching backwards. michael@0: searchIsReversed: false, michael@0: jumpList: createCircularJumpList(), michael@0: macroModeState: new MacroModeState, michael@0: // Recording latest f, t, F or T motion command. michael@0: lastChararacterSearch: {increment:0, forward:true, selectedCharacter:''}, michael@0: registerController: new RegisterController({}) michael@0: }; michael@0: for (var optionName in options) { michael@0: var option = options[optionName]; michael@0: option.value = option.defaultValue; michael@0: } michael@0: } michael@0: michael@0: var vimApi= { michael@0: buildKeyMap: function() { michael@0: // TODO: Convert keymap into dictionary format for fast lookup. michael@0: }, michael@0: // Testing hook, though it might be useful to expose the register michael@0: // controller anyways. michael@0: getRegisterController: function() { michael@0: return vimGlobalState.registerController; michael@0: }, michael@0: // Testing hook. michael@0: resetVimGlobalState_: resetVimGlobalState, michael@0: michael@0: // Testing hook. michael@0: getVimGlobalState_: function() { michael@0: return vimGlobalState; michael@0: }, michael@0: michael@0: // Testing hook. michael@0: maybeInitVimState_: maybeInitVimState, michael@0: michael@0: InsertModeKey: InsertModeKey, michael@0: map: function(lhs, rhs, ctx) { michael@0: // Add user defined key bindings. michael@0: exCommandDispatcher.map(lhs, rhs, ctx); michael@0: }, michael@0: setOption: setOption, michael@0: getOption: getOption, michael@0: defineOption: defineOption, michael@0: defineEx: function(name, prefix, func){ michael@0: if (name.indexOf(prefix) !== 0) { michael@0: throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered'); michael@0: } michael@0: exCommands[name]=func; michael@0: exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'}; michael@0: }, michael@0: // This is the outermost function called by CodeMirror, after keys have michael@0: // been mapped to their Vim equivalents. michael@0: handleKey: function(cm, key) { michael@0: var command; michael@0: var vim = maybeInitVimState(cm); michael@0: var macroModeState = vimGlobalState.macroModeState; michael@0: if (macroModeState.isRecording) { michael@0: if (key == 'q') { michael@0: macroModeState.exitMacroRecordMode(); michael@0: vim.inputState = new InputState(); michael@0: return; michael@0: } michael@0: } michael@0: if (key == '') { michael@0: // Clear input state and get back to normal mode. michael@0: vim.inputState = new InputState(); michael@0: if (vim.visualMode) { michael@0: exitVisualMode(cm); michael@0: } michael@0: return; michael@0: } michael@0: // Enter visual mode when the mouse selects text. michael@0: if (!vim.visualMode && michael@0: !cursorEqual(cm.getCursor('head'), cm.getCursor('anchor'))) { michael@0: vim.visualMode = true; michael@0: vim.visualLine = false; michael@0: CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"}); michael@0: cm.on('mousedown', exitVisualMode); michael@0: } michael@0: if (key != '0' || (key == '0' && vim.inputState.getRepeat() === 0)) { michael@0: // Have to special case 0 since it's both a motion and a number. michael@0: command = commandDispatcher.matchCommand(key, defaultKeymap, vim); michael@0: } michael@0: if (!command) { michael@0: if (isNumber(key)) { michael@0: // Increment count unless count is 0 and key is 0. michael@0: vim.inputState.pushRepeatDigit(key); michael@0: } michael@0: if (macroModeState.isRecording) { michael@0: logKey(macroModeState, key); michael@0: } michael@0: return; michael@0: } michael@0: if (command.type == 'keyToKey') { michael@0: // TODO: prevent infinite recursion. michael@0: for (var i = 0; i < command.toKeys.length; i++) { michael@0: this.handleKey(cm, command.toKeys[i]); michael@0: } michael@0: } else { michael@0: if (macroModeState.isRecording) { michael@0: logKey(macroModeState, key); michael@0: } michael@0: commandDispatcher.processCommand(cm, vim, command); michael@0: } michael@0: }, michael@0: handleEx: function(cm, input) { michael@0: exCommandDispatcher.processCommand(cm, input); michael@0: } michael@0: }; michael@0: michael@0: // Represents the current input state. michael@0: function InputState() { michael@0: this.prefixRepeat = []; michael@0: this.motionRepeat = []; michael@0: michael@0: this.operator = null; michael@0: this.operatorArgs = null; michael@0: this.motion = null; michael@0: this.motionArgs = null; michael@0: this.keyBuffer = []; // For matching multi-key commands. michael@0: this.registerName = null; // Defaults to the unnamed register. michael@0: } michael@0: InputState.prototype.pushRepeatDigit = function(n) { michael@0: if (!this.operator) { michael@0: this.prefixRepeat = this.prefixRepeat.concat(n); michael@0: } else { michael@0: this.motionRepeat = this.motionRepeat.concat(n); michael@0: } michael@0: }; michael@0: InputState.prototype.getRepeat = function() { michael@0: var repeat = 0; michael@0: if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) { michael@0: repeat = 1; michael@0: if (this.prefixRepeat.length > 0) { michael@0: repeat *= parseInt(this.prefixRepeat.join(''), 10); michael@0: } michael@0: if (this.motionRepeat.length > 0) { michael@0: repeat *= parseInt(this.motionRepeat.join(''), 10); michael@0: } michael@0: } michael@0: return repeat; michael@0: }; michael@0: michael@0: /* michael@0: * Register stores information about copy and paste registers. Besides michael@0: * text, a register must store whether it is linewise (i.e., when it is michael@0: * pasted, should it insert itself into a new line, or should the text be michael@0: * inserted at the cursor position.) michael@0: */ michael@0: function Register(text, linewise) { michael@0: this.clear(); michael@0: this.keyBuffer = [text || '']; michael@0: this.insertModeChanges = []; michael@0: this.linewise = !!linewise; michael@0: } michael@0: Register.prototype = { michael@0: setText: function(text, linewise) { michael@0: this.keyBuffer = [text || '']; michael@0: this.linewise = !!linewise; michael@0: }, michael@0: pushText: function(text, linewise) { michael@0: // if this register has ever been set to linewise, use linewise. michael@0: if (linewise || this.linewise) { michael@0: this.keyBuffer.push('\n'); michael@0: this.linewise = true; michael@0: } michael@0: this.keyBuffer.push(text); michael@0: }, michael@0: pushInsertModeChanges: function(changes) { michael@0: this.insertModeChanges.push(createInsertModeChanges(changes)); michael@0: }, michael@0: clear: function() { michael@0: this.keyBuffer = []; michael@0: this.insertModeChanges = []; michael@0: this.linewise = false; michael@0: }, michael@0: toString: function() { michael@0: return this.keyBuffer.join(''); michael@0: } michael@0: }; michael@0: michael@0: /* michael@0: * vim registers allow you to keep many independent copy and paste buffers. michael@0: * See http://usevim.com/2012/04/13/registers/ for an introduction. michael@0: * michael@0: * RegisterController keeps the state of all the registers. An initial michael@0: * state may be passed in. The unnamed register '"' will always be michael@0: * overridden. michael@0: */ michael@0: function RegisterController(registers) { michael@0: this.registers = registers; michael@0: this.unnamedRegister = registers['"'] = new Register(); michael@0: } michael@0: RegisterController.prototype = { michael@0: pushText: function(registerName, operator, text, linewise) { michael@0: if (linewise && text.charAt(0) == '\n') { michael@0: text = text.slice(1) + '\n'; michael@0: } michael@0: if (linewise && text.charAt(text.length - 1) !== '\n'){ michael@0: text += '\n'; michael@0: } michael@0: // Lowercase and uppercase registers refer to the same register. michael@0: // Uppercase just means append. michael@0: var register = this.isValidRegister(registerName) ? michael@0: this.getRegister(registerName) : null; michael@0: // if no register/an invalid register was specified, things go to the michael@0: // default registers michael@0: if (!register) { michael@0: switch (operator) { michael@0: case 'yank': michael@0: // The 0 register contains the text from the most recent yank. michael@0: this.registers['0'] = new Register(text, linewise); michael@0: break; michael@0: case 'delete': michael@0: case 'change': michael@0: if (text.indexOf('\n') == -1) { michael@0: // Delete less than 1 line. Update the small delete register. michael@0: this.registers['-'] = new Register(text, linewise); michael@0: } else { michael@0: // Shift down the contents of the numbered registers and put the michael@0: // deleted text into register 1. michael@0: this.shiftNumericRegisters_(); michael@0: this.registers['1'] = new Register(text, linewise); michael@0: } michael@0: break; michael@0: } michael@0: // Make sure the unnamed register is set to what just happened michael@0: this.unnamedRegister.setText(text, linewise); michael@0: return; michael@0: } michael@0: michael@0: // If we've gotten to this point, we've actually specified a register michael@0: var append = isUpperCase(registerName); michael@0: if (append) { michael@0: register.append(text, linewise); michael@0: // The unnamed register always has the same value as the last used michael@0: // register. michael@0: this.unnamedRegister.append(text, linewise); michael@0: } else { michael@0: register.setText(text, linewise); michael@0: this.unnamedRegister.setText(text, linewise); michael@0: } michael@0: }, michael@0: // Gets the register named @name. If one of @name doesn't already exist, michael@0: // create it. If @name is invalid, return the unnamedRegister. michael@0: getRegister: function(name) { michael@0: if (!this.isValidRegister(name)) { michael@0: return this.unnamedRegister; michael@0: } michael@0: name = name.toLowerCase(); michael@0: if (!this.registers[name]) { michael@0: this.registers[name] = new Register(); michael@0: } michael@0: return this.registers[name]; michael@0: }, michael@0: isValidRegister: function(name) { michael@0: return name && inArray(name, validRegisters); michael@0: }, michael@0: shiftNumericRegisters_: function() { michael@0: for (var i = 9; i >= 2; i--) { michael@0: this.registers[i] = this.getRegister('' + (i - 1)); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: var commandDispatcher = { michael@0: matchCommand: function(key, keyMap, vim) { michael@0: var inputState = vim.inputState; michael@0: var keys = inputState.keyBuffer.concat(key); michael@0: var matchedCommands = []; michael@0: var selectedCharacter; michael@0: for (var i = 0; i < keyMap.length; i++) { michael@0: var command = keyMap[i]; michael@0: if (matchKeysPartial(keys, command.keys)) { michael@0: if (inputState.operator && command.type == 'action') { michael@0: // Ignore matched action commands after an operator. Operators michael@0: // only operate on motions. This check is really for text michael@0: // objects since aW, a[ etcs conflicts with a. michael@0: continue; michael@0: } michael@0: // Match commands that take as an argument. michael@0: if (command.keys[keys.length - 1] == 'character') { michael@0: selectedCharacter = keys[keys.length - 1]; michael@0: if (selectedCharacter.length>1){ michael@0: switch(selectedCharacter){ michael@0: case '': michael@0: selectedCharacter='\n'; michael@0: break; michael@0: case '': michael@0: selectedCharacter=' '; michael@0: break; michael@0: default: michael@0: continue; michael@0: } michael@0: } michael@0: } michael@0: // Add the command to the list of matched commands. Choose the best michael@0: // command later. michael@0: matchedCommands.push(command); michael@0: } michael@0: } michael@0: michael@0: // Returns the command if it is a full match, or null if not. michael@0: function getFullyMatchedCommandOrNull(command) { michael@0: if (keys.length < command.keys.length) { michael@0: // Matches part of a multi-key command. Buffer and wait for next michael@0: // stroke. michael@0: inputState.keyBuffer.push(key); michael@0: return null; michael@0: } else { michael@0: if (command.keys[keys.length - 1] == 'character') { michael@0: inputState.selectedCharacter = selectedCharacter; michael@0: } michael@0: // Clear the buffer since a full match was found. michael@0: inputState.keyBuffer = []; michael@0: return command; michael@0: } michael@0: } michael@0: michael@0: if (!matchedCommands.length) { michael@0: // Clear the buffer since there were no matches. michael@0: inputState.keyBuffer = []; michael@0: return null; michael@0: } else if (matchedCommands.length == 1) { michael@0: return getFullyMatchedCommandOrNull(matchedCommands[0]); michael@0: } else { michael@0: // Find the best match in the list of matchedCommands. michael@0: var context = vim.visualMode ? 'visual' : 'normal'; michael@0: var bestMatch; // Default to first in the list. michael@0: for (var i = 0; i < matchedCommands.length; i++) { michael@0: var current = matchedCommands[i]; michael@0: if (current.context == context) { michael@0: bestMatch = current; michael@0: break; michael@0: } else if (!bestMatch && !current.context) { michael@0: // Only set an imperfect match to best match if no best match is michael@0: // set and the imperfect match is not restricted to another michael@0: // context. michael@0: bestMatch = current; michael@0: } michael@0: } michael@0: return getFullyMatchedCommandOrNull(bestMatch); michael@0: } michael@0: }, michael@0: processCommand: function(cm, vim, command) { michael@0: vim.inputState.repeatOverride = command.repeatOverride; michael@0: switch (command.type) { michael@0: case 'motion': michael@0: this.processMotion(cm, vim, command); michael@0: break; michael@0: case 'operator': michael@0: this.processOperator(cm, vim, command); michael@0: break; michael@0: case 'operatorMotion': michael@0: this.processOperatorMotion(cm, vim, command); michael@0: break; michael@0: case 'action': michael@0: this.processAction(cm, vim, command); michael@0: break; michael@0: case 'search': michael@0: this.processSearch(cm, vim, command); michael@0: break; michael@0: case 'ex': michael@0: case 'keyToEx': michael@0: this.processEx(cm, vim, command); michael@0: break; michael@0: default: michael@0: break; michael@0: } michael@0: }, michael@0: processMotion: function(cm, vim, command) { michael@0: vim.inputState.motion = command.motion; michael@0: vim.inputState.motionArgs = copyArgs(command.motionArgs); michael@0: this.evalInput(cm, vim); michael@0: }, michael@0: processOperator: function(cm, vim, command) { michael@0: var inputState = vim.inputState; michael@0: if (inputState.operator) { michael@0: if (inputState.operator == command.operator) { michael@0: // Typing an operator twice like 'dd' makes the operator operate michael@0: // linewise michael@0: inputState.motion = 'expandToLine'; michael@0: inputState.motionArgs = { linewise: true }; michael@0: this.evalInput(cm, vim); michael@0: return; michael@0: } else { michael@0: // 2 different operators in a row doesn't make sense. michael@0: vim.inputState = new InputState(); michael@0: } michael@0: } michael@0: inputState.operator = command.operator; michael@0: inputState.operatorArgs = copyArgs(command.operatorArgs); michael@0: if (vim.visualMode) { michael@0: // Operating on a selection in visual mode. We don't need a motion. michael@0: this.evalInput(cm, vim); michael@0: } michael@0: }, michael@0: processOperatorMotion: function(cm, vim, command) { michael@0: var visualMode = vim.visualMode; michael@0: var operatorMotionArgs = copyArgs(command.operatorMotionArgs); michael@0: if (operatorMotionArgs) { michael@0: // Operator motions may have special behavior in visual mode. michael@0: if (visualMode && operatorMotionArgs.visualLine) { michael@0: vim.visualLine = true; michael@0: } michael@0: } michael@0: this.processOperator(cm, vim, command); michael@0: if (!visualMode) { michael@0: this.processMotion(cm, vim, command); michael@0: } michael@0: }, michael@0: processAction: function(cm, vim, command) { michael@0: var inputState = vim.inputState; michael@0: var repeat = inputState.getRepeat(); michael@0: var repeatIsExplicit = !!repeat; michael@0: var actionArgs = copyArgs(command.actionArgs) || {}; michael@0: if (inputState.selectedCharacter) { michael@0: actionArgs.selectedCharacter = inputState.selectedCharacter; michael@0: } michael@0: // Actions may or may not have motions and operators. Do these first. michael@0: if (command.operator) { michael@0: this.processOperator(cm, vim, command); michael@0: } michael@0: if (command.motion) { michael@0: this.processMotion(cm, vim, command); michael@0: } michael@0: if (command.motion || command.operator) { michael@0: this.evalInput(cm, vim); michael@0: } michael@0: actionArgs.repeat = repeat || 1; michael@0: actionArgs.repeatIsExplicit = repeatIsExplicit; michael@0: actionArgs.registerName = inputState.registerName; michael@0: vim.inputState = new InputState(); michael@0: vim.lastMotion = null; michael@0: if (command.isEdit) { michael@0: this.recordLastEdit(vim, inputState, command); michael@0: } michael@0: actions[command.action](cm, actionArgs, vim); michael@0: }, michael@0: processSearch: function(cm, vim, command) { michael@0: if (!cm.getSearchCursor) { michael@0: // Search depends on SearchCursor. michael@0: return; michael@0: } michael@0: var forward = command.searchArgs.forward; michael@0: getSearchState(cm).setReversed(!forward); michael@0: var promptPrefix = (forward) ? '/' : '?'; michael@0: var originalQuery = getSearchState(cm).getQuery(); michael@0: var originalScrollPos = cm.getScrollInfo(); michael@0: function handleQuery(query, ignoreCase, smartCase) { michael@0: try { michael@0: updateSearchQuery(cm, query, ignoreCase, smartCase); michael@0: } catch (e) { michael@0: showConfirm(cm, 'Invalid regex: ' + query); michael@0: return; michael@0: } michael@0: commandDispatcher.processMotion(cm, vim, { michael@0: type: 'motion', michael@0: motion: 'findNext', michael@0: motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist } michael@0: }); michael@0: } michael@0: function onPromptClose(query) { michael@0: cm.scrollTo(originalScrollPos.left, originalScrollPos.top); michael@0: handleQuery(query, true /** ignoreCase */, true /** smartCase */); michael@0: } michael@0: function onPromptKeyUp(_e, query) { michael@0: var parsedQuery; michael@0: try { michael@0: parsedQuery = updateSearchQuery(cm, query, michael@0: true /** ignoreCase */, true /** smartCase */); michael@0: } catch (e) { michael@0: // Swallow bad regexes for incremental search. michael@0: } michael@0: if (parsedQuery) { michael@0: cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30); michael@0: } else { michael@0: clearSearchHighlight(cm); michael@0: cm.scrollTo(originalScrollPos.left, originalScrollPos.top); michael@0: } michael@0: } michael@0: function onPromptKeyDown(e, _query, close) { michael@0: var keyName = CodeMirror.keyName(e); michael@0: if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') { michael@0: updateSearchQuery(cm, originalQuery); michael@0: clearSearchHighlight(cm); michael@0: cm.scrollTo(originalScrollPos.left, originalScrollPos.top); michael@0: michael@0: CodeMirror.e_stop(e); michael@0: close(); michael@0: cm.focus(); michael@0: } michael@0: } michael@0: switch (command.searchArgs.querySrc) { michael@0: case 'prompt': michael@0: showPrompt(cm, { michael@0: onClose: onPromptClose, michael@0: prefix: promptPrefix, michael@0: desc: searchPromptDesc, michael@0: onKeyUp: onPromptKeyUp, michael@0: onKeyDown: onPromptKeyDown michael@0: }); michael@0: break; michael@0: case 'wordUnderCursor': michael@0: var word = expandWordUnderCursor(cm, false /** inclusive */, michael@0: true /** forward */, false /** bigWord */, michael@0: true /** noSymbol */); michael@0: var isKeyword = true; michael@0: if (!word) { michael@0: word = expandWordUnderCursor(cm, false /** inclusive */, michael@0: true /** forward */, false /** bigWord */, michael@0: false /** noSymbol */); michael@0: isKeyword = false; michael@0: } michael@0: if (!word) { michael@0: return; michael@0: } michael@0: var query = cm.getLine(word.start.line).substring(word.start.ch, michael@0: word.end.ch); michael@0: if (isKeyword) { michael@0: query = '\\b' + query + '\\b'; michael@0: } else { michael@0: query = escapeRegex(query); michael@0: } michael@0: michael@0: // cachedCursor is used to save the old position of the cursor michael@0: // when * or # causes vim to seek for the nearest word and shift michael@0: // the cursor before entering the motion. michael@0: vimGlobalState.jumpList.cachedCursor = cm.getCursor(); michael@0: cm.setCursor(word.start); michael@0: michael@0: handleQuery(query, true /** ignoreCase */, false /** smartCase */); michael@0: break; michael@0: } michael@0: }, michael@0: processEx: function(cm, vim, command) { michael@0: function onPromptClose(input) { michael@0: // Give the prompt some time to close so that if processCommand shows michael@0: // an error, the elements don't overlap. michael@0: exCommandDispatcher.processCommand(cm, input); michael@0: } michael@0: function onPromptKeyDown(e, _input, close) { michael@0: var keyName = CodeMirror.keyName(e); michael@0: if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') { michael@0: CodeMirror.e_stop(e); michael@0: close(); michael@0: cm.focus(); michael@0: } michael@0: } michael@0: if (command.type == 'keyToEx') { michael@0: // Handle user defined Ex to Ex mappings michael@0: exCommandDispatcher.processCommand(cm, command.exArgs.input); michael@0: } else { michael@0: if (vim.visualMode) { michael@0: showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>', michael@0: onKeyDown: onPromptKeyDown}); michael@0: } else { michael@0: showPrompt(cm, { onClose: onPromptClose, prefix: ':', michael@0: onKeyDown: onPromptKeyDown}); michael@0: } michael@0: } michael@0: }, michael@0: evalInput: function(cm, vim) { michael@0: // If the motion comand is set, execute both the operator and motion. michael@0: // Otherwise return. michael@0: var inputState = vim.inputState; michael@0: var motion = inputState.motion; michael@0: var motionArgs = inputState.motionArgs || {}; michael@0: var operator = inputState.operator; michael@0: var operatorArgs = inputState.operatorArgs || {}; michael@0: var registerName = inputState.registerName; michael@0: var selectionEnd = copyCursor(cm.getCursor('head')); michael@0: var selectionStart = copyCursor(cm.getCursor('anchor')); michael@0: // The difference between cur and selection cursors are that cur is michael@0: // being operated on and ignores that there is a selection. michael@0: var curStart = copyCursor(selectionEnd); michael@0: var curOriginal = copyCursor(curStart); michael@0: var curEnd; michael@0: var repeat; michael@0: if (operator) { michael@0: this.recordLastEdit(vim, inputState); michael@0: } michael@0: if (inputState.repeatOverride !== undefined) { michael@0: // If repeatOverride is specified, that takes precedence over the michael@0: // input state's repeat. Used by Ex mode and can be user defined. michael@0: repeat = inputState.repeatOverride; michael@0: } else { michael@0: repeat = inputState.getRepeat(); michael@0: } michael@0: if (repeat > 0 && motionArgs.explicitRepeat) { michael@0: motionArgs.repeatIsExplicit = true; michael@0: } else if (motionArgs.noRepeat || michael@0: (!motionArgs.explicitRepeat && repeat === 0)) { michael@0: repeat = 1; michael@0: motionArgs.repeatIsExplicit = false; michael@0: } michael@0: if (inputState.selectedCharacter) { michael@0: // If there is a character input, stick it in all of the arg arrays. michael@0: motionArgs.selectedCharacter = operatorArgs.selectedCharacter = michael@0: inputState.selectedCharacter; michael@0: } michael@0: motionArgs.repeat = repeat; michael@0: vim.inputState = new InputState(); michael@0: if (motion) { michael@0: var motionResult = motions[motion](cm, motionArgs, vim); michael@0: vim.lastMotion = motions[motion]; michael@0: if (!motionResult) { michael@0: return; michael@0: } michael@0: if (motionArgs.toJumplist) { michael@0: var jumpList = vimGlobalState.jumpList; michael@0: // if the current motion is # or *, use cachedCursor michael@0: var cachedCursor = jumpList.cachedCursor; michael@0: if (cachedCursor) { michael@0: recordJumpPosition(cm, cachedCursor, motionResult); michael@0: delete jumpList.cachedCursor; michael@0: } else { michael@0: recordJumpPosition(cm, curOriginal, motionResult); michael@0: } michael@0: } michael@0: if (motionResult instanceof Array) { michael@0: curStart = motionResult[0]; michael@0: curEnd = motionResult[1]; michael@0: } else { michael@0: curEnd = motionResult; michael@0: } michael@0: // TODO: Handle null returns from motion commands better. michael@0: if (!curEnd) { michael@0: curEnd = Pos(curStart.line, curStart.ch); michael@0: } michael@0: if (vim.visualMode) { michael@0: // Check if the selection crossed over itself. Will need to shift michael@0: // the start point if that happened. michael@0: if (cursorIsBefore(selectionStart, selectionEnd) && michael@0: (cursorEqual(selectionStart, curEnd) || michael@0: cursorIsBefore(curEnd, selectionStart))) { michael@0: // The end of the selection has moved from after the start to michael@0: // before the start. We will shift the start right by 1. michael@0: selectionStart.ch += 1; michael@0: } else if (cursorIsBefore(selectionEnd, selectionStart) && michael@0: (cursorEqual(selectionStart, curEnd) || michael@0: cursorIsBefore(selectionStart, curEnd))) { michael@0: // The opposite happened. We will shift the start left by 1. michael@0: selectionStart.ch -= 1; michael@0: } michael@0: selectionEnd = curEnd; michael@0: selectionStart = (motionResult instanceof Array) ? curStart : selectionStart; michael@0: if (vim.visualLine) { michael@0: if (cursorIsBefore(selectionStart, selectionEnd)) { michael@0: selectionStart.ch = 0; michael@0: michael@0: var lastLine = cm.lastLine(); michael@0: if (selectionEnd.line > lastLine) { michael@0: selectionEnd.line = lastLine; michael@0: } michael@0: selectionEnd.ch = lineLength(cm, selectionEnd.line); michael@0: } else { michael@0: selectionEnd.ch = 0; michael@0: selectionStart.ch = lineLength(cm, selectionStart.line); michael@0: } michael@0: } michael@0: cm.setSelection(selectionStart, selectionEnd); michael@0: updateMark(cm, vim, '<', michael@0: cursorIsBefore(selectionStart, selectionEnd) ? selectionStart michael@0: : selectionEnd); michael@0: updateMark(cm, vim, '>', michael@0: cursorIsBefore(selectionStart, selectionEnd) ? selectionEnd michael@0: : selectionStart); michael@0: } else if (!operator) { michael@0: curEnd = clipCursorToContent(cm, curEnd); michael@0: cm.setCursor(curEnd.line, curEnd.ch); michael@0: } michael@0: } michael@0: michael@0: if (operator) { michael@0: var inverted = false; michael@0: vim.lastMotion = null; michael@0: operatorArgs.repeat = repeat; // Indent in visual mode needs this. michael@0: if (vim.visualMode) { michael@0: curStart = selectionStart; michael@0: curEnd = selectionEnd; michael@0: motionArgs.inclusive = true; michael@0: } michael@0: // Swap start and end if motion was backward. michael@0: if (cursorIsBefore(curEnd, curStart)) { michael@0: var tmp = curStart; michael@0: curStart = curEnd; michael@0: curEnd = tmp; michael@0: inverted = true; michael@0: } michael@0: if (motionArgs.inclusive && !(vim.visualMode && inverted)) { michael@0: // Move the selection end one to the right to include the last michael@0: // character. michael@0: curEnd.ch++; michael@0: } michael@0: var linewise = motionArgs.linewise || michael@0: (vim.visualMode && vim.visualLine); michael@0: if (linewise) { michael@0: // Expand selection to entire line. michael@0: expandSelectionToLine(cm, curStart, curEnd); michael@0: } else if (motionArgs.forward) { michael@0: // Clip to trailing newlines only if the motion goes forward. michael@0: clipToLine(cm, curStart, curEnd); michael@0: } michael@0: operatorArgs.registerName = registerName; michael@0: // Keep track of linewise as it affects how paste and change behave. michael@0: operatorArgs.linewise = linewise; michael@0: operators[operator](cm, operatorArgs, vim, curStart, michael@0: curEnd, curOriginal); michael@0: if (vim.visualMode) { michael@0: exitVisualMode(cm); michael@0: } michael@0: } michael@0: }, michael@0: recordLastEdit: function(vim, inputState, actionCommand) { michael@0: var macroModeState = vimGlobalState.macroModeState; michael@0: if (macroModeState.isPlaying) { return; } michael@0: vim.lastEditInputState = inputState; michael@0: vim.lastEditActionCommand = actionCommand; michael@0: macroModeState.lastInsertModeChanges.changes = []; michael@0: macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * typedef {Object{line:number,ch:number}} Cursor An object containing the michael@0: * position of the cursor. michael@0: */ michael@0: // All of the functions below return Cursor objects. michael@0: var motions = { michael@0: moveToTopLine: function(cm, motionArgs) { michael@0: var line = getUserVisibleLines(cm).top + motionArgs.repeat -1; michael@0: return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); michael@0: }, michael@0: moveToMiddleLine: function(cm) { michael@0: var range = getUserVisibleLines(cm); michael@0: var line = Math.floor((range.top + range.bottom) * 0.5); michael@0: return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); michael@0: }, michael@0: moveToBottomLine: function(cm, motionArgs) { michael@0: var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1; michael@0: return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); michael@0: }, michael@0: expandToLine: function(cm, motionArgs) { michael@0: // Expands forward to end of line, and then to next line if repeat is michael@0: // >1. Does not handle backward motion! michael@0: var cur = cm.getCursor(); michael@0: return Pos(cur.line + motionArgs.repeat - 1, Infinity); michael@0: }, michael@0: findNext: function(cm, motionArgs) { michael@0: var state = getSearchState(cm); michael@0: var query = state.getQuery(); michael@0: if (!query) { michael@0: return; michael@0: } michael@0: var prev = !motionArgs.forward; michael@0: // If search is initiated with ? instead of /, negate direction. michael@0: prev = (state.isReversed()) ? !prev : prev; michael@0: highlightSearchMatches(cm, query); michael@0: return findNext(cm, prev/** prev */, query, motionArgs.repeat); michael@0: }, michael@0: goToMark: function(_cm, motionArgs, vim) { michael@0: var mark = vim.marks[motionArgs.selectedCharacter]; michael@0: if (mark) { michael@0: return mark.find(); michael@0: } michael@0: return null; michael@0: }, michael@0: moveToOtherHighlightedEnd: function(cm) { michael@0: var curEnd = copyCursor(cm.getCursor('head')); michael@0: var curStart = copyCursor(cm.getCursor('anchor')); michael@0: if (cursorIsBefore(curStart, curEnd)) { michael@0: curEnd.ch += 1; michael@0: } else if (cursorIsBefore(curEnd, curStart)) { michael@0: curStart.ch -= 1; michael@0: } michael@0: return ([curEnd,curStart]); michael@0: }, michael@0: jumpToMark: function(cm, motionArgs, vim) { michael@0: var best = cm.getCursor(); michael@0: for (var i = 0; i < motionArgs.repeat; i++) { michael@0: var cursor = best; michael@0: for (var key in vim.marks) { michael@0: if (!isLowerCase(key)) { michael@0: continue; michael@0: } michael@0: var mark = vim.marks[key].find(); michael@0: var isWrongDirection = (motionArgs.forward) ? michael@0: cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark); michael@0: michael@0: if (isWrongDirection) { michael@0: continue; michael@0: } michael@0: if (motionArgs.linewise && (mark.line == cursor.line)) { michael@0: continue; michael@0: } michael@0: michael@0: var equal = cursorEqual(cursor, best); michael@0: var between = (motionArgs.forward) ? michael@0: cusrorIsBetween(cursor, mark, best) : michael@0: cusrorIsBetween(best, mark, cursor); michael@0: michael@0: if (equal || between) { michael@0: best = mark; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (motionArgs.linewise) { michael@0: // Vim places the cursor on the first non-whitespace character of michael@0: // the line if there is one, else it places the cursor at the end michael@0: // of the line, regardless of whether a mark was found. michael@0: best = Pos(best.line, findFirstNonWhiteSpaceCharacter(cm.getLine(best.line))); michael@0: } michael@0: return best; michael@0: }, michael@0: moveByCharacters: function(cm, motionArgs) { michael@0: var cur = cm.getCursor(); michael@0: var repeat = motionArgs.repeat; michael@0: var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat; michael@0: return Pos(cur.line, ch); michael@0: }, michael@0: moveByLines: function(cm, motionArgs, vim) { michael@0: var cur = cm.getCursor(); michael@0: var endCh = cur.ch; michael@0: // Depending what our last motion was, we may want to do different michael@0: // things. If our last motion was moving vertically, we want to michael@0: // preserve the HPos from our last horizontal move. If our last motion michael@0: // was going to the end of a line, moving vertically we should go to michael@0: // the end of the line, etc. michael@0: switch (vim.lastMotion) { michael@0: case this.moveByLines: michael@0: case this.moveByDisplayLines: michael@0: case this.moveByScroll: michael@0: case this.moveToColumn: michael@0: case this.moveToEol: michael@0: endCh = vim.lastHPos; michael@0: break; michael@0: default: michael@0: vim.lastHPos = endCh; michael@0: } michael@0: var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0); michael@0: var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat; michael@0: var first = cm.firstLine(); michael@0: var last = cm.lastLine(); michael@0: // Vim cancels linewise motions that start on an edge and move beyond michael@0: // that edge. It does not cancel motions that do not start on an edge. michael@0: if ((line < first && cur.line == first) || michael@0: (line > last && cur.line == last)) { michael@0: return; michael@0: } michael@0: if (motionArgs.toFirstChar){ michael@0: endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line)); michael@0: vim.lastHPos = endCh; michael@0: } michael@0: vim.lastHSPos = cm.charCoords(Pos(line, endCh),'div').left; michael@0: return Pos(line, endCh); michael@0: }, michael@0: moveByDisplayLines: function(cm, motionArgs, vim) { michael@0: var cur = cm.getCursor(); michael@0: switch (vim.lastMotion) { michael@0: case this.moveByDisplayLines: michael@0: case this.moveByScroll: michael@0: case this.moveByLines: michael@0: case this.moveToColumn: michael@0: case this.moveToEol: michael@0: break; michael@0: default: michael@0: vim.lastHSPos = cm.charCoords(cur,'div').left; michael@0: } michael@0: var repeat = motionArgs.repeat; michael@0: var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),'line',vim.lastHSPos); michael@0: if (res.hitSide) { michael@0: if (motionArgs.forward) { michael@0: var lastCharCoords = cm.charCoords(res, 'div'); michael@0: var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos }; michael@0: var res = cm.coordsChar(goalCoords, 'div'); michael@0: } else { michael@0: var resCoords = cm.charCoords(Pos(cm.firstLine(), 0), 'div'); michael@0: resCoords.left = vim.lastHSPos; michael@0: res = cm.coordsChar(resCoords, 'div'); michael@0: } michael@0: } michael@0: vim.lastHPos = res.ch; michael@0: return res; michael@0: }, michael@0: moveByPage: function(cm, motionArgs) { michael@0: // CodeMirror only exposes functions that move the cursor page down, so michael@0: // doing this bad hack to move the cursor and move it back. evalInput michael@0: // will move the cursor to where it should be in the end. michael@0: var curStart = cm.getCursor(); michael@0: var repeat = motionArgs.repeat; michael@0: cm.moveV((motionArgs.forward ? repeat : -repeat), 'page'); michael@0: var curEnd = cm.getCursor(); michael@0: cm.setCursor(curStart); michael@0: return curEnd; michael@0: }, michael@0: moveByParagraph: function(cm, motionArgs) { michael@0: var line = cm.getCursor().line; michael@0: var repeat = motionArgs.repeat; michael@0: var inc = motionArgs.forward ? 1 : -1; michael@0: for (var i = 0; i < repeat; i++) { michael@0: if ((!motionArgs.forward && line === cm.firstLine() ) || michael@0: (motionArgs.forward && line == cm.lastLine())) { michael@0: break; michael@0: } michael@0: line += inc; michael@0: while (line !== cm.firstLine() && line != cm.lastLine() && cm.getLine(line)) { michael@0: line += inc; michael@0: } michael@0: } michael@0: return Pos(line, 0); michael@0: }, michael@0: moveByScroll: function(cm, motionArgs, vim) { michael@0: var scrollbox = cm.getScrollInfo(); michael@0: var curEnd = null; michael@0: var repeat = motionArgs.repeat; michael@0: if (!repeat) { michael@0: repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight()); michael@0: } michael@0: var orig = cm.charCoords(cm.getCursor(), 'local'); michael@0: motionArgs.repeat = repeat; michael@0: var curEnd = motions.moveByDisplayLines(cm, motionArgs, vim); michael@0: if (!curEnd) { michael@0: return null; michael@0: } michael@0: var dest = cm.charCoords(curEnd, 'local'); michael@0: cm.scrollTo(null, scrollbox.top + dest.top - orig.top); michael@0: return curEnd; michael@0: }, michael@0: moveByWords: function(cm, motionArgs) { michael@0: return moveToWord(cm, motionArgs.repeat, !!motionArgs.forward, michael@0: !!motionArgs.wordEnd, !!motionArgs.bigWord); michael@0: }, michael@0: moveTillCharacter: function(cm, motionArgs) { michael@0: var repeat = motionArgs.repeat; michael@0: var curEnd = moveToCharacter(cm, repeat, motionArgs.forward, michael@0: motionArgs.selectedCharacter); michael@0: var increment = motionArgs.forward ? -1 : 1; michael@0: recordLastCharacterSearch(increment, motionArgs); michael@0: if (!curEnd) return null; michael@0: curEnd.ch += increment; michael@0: return curEnd; michael@0: }, michael@0: moveToCharacter: function(cm, motionArgs) { michael@0: var repeat = motionArgs.repeat; michael@0: recordLastCharacterSearch(0, motionArgs); michael@0: return moveToCharacter(cm, repeat, motionArgs.forward, michael@0: motionArgs.selectedCharacter) || cm.getCursor(); michael@0: }, michael@0: moveToSymbol: function(cm, motionArgs) { michael@0: var repeat = motionArgs.repeat; michael@0: return findSymbol(cm, repeat, motionArgs.forward, michael@0: motionArgs.selectedCharacter) || cm.getCursor(); michael@0: }, michael@0: moveToColumn: function(cm, motionArgs, vim) { michael@0: var repeat = motionArgs.repeat; michael@0: // repeat is equivalent to which column we want to move to! michael@0: vim.lastHPos = repeat - 1; michael@0: vim.lastHSPos = cm.charCoords(cm.getCursor(),'div').left; michael@0: return moveToColumn(cm, repeat); michael@0: }, michael@0: moveToEol: function(cm, motionArgs, vim) { michael@0: var cur = cm.getCursor(); michael@0: vim.lastHPos = Infinity; michael@0: var retval= Pos(cur.line + motionArgs.repeat - 1, Infinity); michael@0: var end=cm.clipPos(retval); michael@0: end.ch--; michael@0: vim.lastHSPos = cm.charCoords(end,'div').left; michael@0: return retval; michael@0: }, michael@0: moveToFirstNonWhiteSpaceCharacter: function(cm) { michael@0: // Go to the start of the line where the text begins, or the end for michael@0: // whitespace-only lines michael@0: var cursor = cm.getCursor(); michael@0: return Pos(cursor.line, michael@0: findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line))); michael@0: }, michael@0: moveToMatchedSymbol: function(cm) { michael@0: var cursor = cm.getCursor(); michael@0: var line = cursor.line; michael@0: var ch = cursor.ch; michael@0: var lineText = cm.getLine(line); michael@0: var symbol; michael@0: var startContext = cm.getTokenAt(cursor).type; michael@0: var startCtxLevel = getContextLevel(startContext); michael@0: do { michael@0: symbol = lineText.charAt(ch++); michael@0: if (symbol && isMatchableSymbol(symbol)) { michael@0: var endContext = cm.getTokenAt(Pos(line, ch)).type; michael@0: var endCtxLevel = getContextLevel(endContext); michael@0: if (startCtxLevel >= endCtxLevel) { michael@0: break; michael@0: } michael@0: } michael@0: } while (symbol); michael@0: if (symbol) { michael@0: return findMatchedSymbol(cm, Pos(line, ch-1), symbol); michael@0: } else { michael@0: return cursor; michael@0: } michael@0: }, michael@0: moveToStartOfLine: function(cm) { michael@0: var cursor = cm.getCursor(); michael@0: return Pos(cursor.line, 0); michael@0: }, michael@0: moveToLineOrEdgeOfDocument: function(cm, motionArgs) { michael@0: var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine(); michael@0: if (motionArgs.repeatIsExplicit) { michael@0: lineNum = motionArgs.repeat - cm.getOption('firstLineNumber'); michael@0: } michael@0: return Pos(lineNum, michael@0: findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum))); michael@0: }, michael@0: textObjectManipulation: function(cm, motionArgs) { michael@0: // TODO: lots of possible exceptions that can be thrown here. Try da( michael@0: // outside of a () block. michael@0: michael@0: // TODO: adding <> >< to this map doesn't work, presumably because michael@0: // they're operators michael@0: var mirroredPairs = {'(': ')', ')': '(', michael@0: '{': '}', '}': '{', michael@0: '[': ']', ']': '['}; michael@0: var selfPaired = {'\'': true, '"': true}; michael@0: michael@0: var character = motionArgs.selectedCharacter; michael@0: michael@0: // Inclusive is the difference between a and i michael@0: // TODO: Instead of using the additional text object map to perform text michael@0: // object operations, merge the map into the defaultKeyMap and use michael@0: // motionArgs to define behavior. Define separate entries for 'aw', michael@0: // 'iw', 'a[', 'i[', etc. michael@0: var inclusive = !motionArgs.textObjectInner; michael@0: michael@0: var tmp; michael@0: if (mirroredPairs[character]) { michael@0: tmp = selectCompanionObject(cm, mirroredPairs[character], inclusive); michael@0: } else if (selfPaired[character]) { michael@0: tmp = findBeginningAndEnd(cm, character, inclusive); michael@0: } else if (character === 'W') { michael@0: tmp = expandWordUnderCursor(cm, inclusive, true /** forward */, michael@0: true /** bigWord */); michael@0: } else if (character === 'w') { michael@0: tmp = expandWordUnderCursor(cm, inclusive, true /** forward */, michael@0: false /** bigWord */); michael@0: } else { michael@0: // No text object defined for this, don't move. michael@0: return null; michael@0: } michael@0: michael@0: return [tmp.start, tmp.end]; michael@0: }, michael@0: michael@0: repeatLastCharacterSearch: function(cm, motionArgs) { michael@0: var lastSearch = vimGlobalState.lastChararacterSearch; michael@0: var repeat = motionArgs.repeat; michael@0: var forward = motionArgs.forward === lastSearch.forward; michael@0: var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1); michael@0: cm.moveH(-increment, 'char'); michael@0: motionArgs.inclusive = forward ? true : false; michael@0: var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter); michael@0: if (!curEnd) { michael@0: cm.moveH(increment, 'char'); michael@0: return cm.getCursor(); michael@0: } michael@0: curEnd.ch += increment; michael@0: return curEnd; michael@0: } michael@0: }; michael@0: michael@0: var operators = { michael@0: change: function(cm, operatorArgs, _vim, curStart, curEnd) { michael@0: vimGlobalState.registerController.pushText( michael@0: operatorArgs.registerName, 'change', cm.getRange(curStart, curEnd), michael@0: operatorArgs.linewise); michael@0: if (operatorArgs.linewise) { michael@0: // Push the next line back down, if there is a next line. michael@0: var replacement = curEnd.line > cm.lastLine() ? '' : '\n'; michael@0: cm.replaceRange(replacement, curStart, curEnd); michael@0: cm.indentLine(curStart.line, 'smart'); michael@0: // null ch so setCursor moves to end of line. michael@0: curStart.ch = null; michael@0: } else { michael@0: // Exclude trailing whitespace if the range is not all whitespace. michael@0: var text = cm.getRange(curStart, curEnd); michael@0: if (!isWhiteSpaceString(text)) { michael@0: var match = (/\s+$/).exec(text); michael@0: if (match) { michael@0: curEnd = offsetCursor(curEnd, 0, - match[0].length); michael@0: } michael@0: } michael@0: cm.replaceRange('', curStart, curEnd); michael@0: } michael@0: actions.enterInsertMode(cm, {}, cm.state.vim); michael@0: cm.setCursor(curStart); michael@0: }, michael@0: // delete is a javascript keyword. michael@0: 'delete': function(cm, operatorArgs, _vim, curStart, curEnd) { michael@0: // If the ending line is past the last line, inclusive, instead of michael@0: // including the trailing \n, include the \n before the starting line michael@0: if (operatorArgs.linewise && michael@0: curEnd.line > cm.lastLine() && curStart.line > cm.firstLine()) { michael@0: curStart.line--; michael@0: curStart.ch = lineLength(cm, curStart.line); michael@0: } michael@0: vimGlobalState.registerController.pushText( michael@0: operatorArgs.registerName, 'delete', cm.getRange(curStart, curEnd), michael@0: operatorArgs.linewise); michael@0: cm.replaceRange('', curStart, curEnd); michael@0: if (operatorArgs.linewise) { michael@0: cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm)); michael@0: } else { michael@0: cm.setCursor(curStart); michael@0: } michael@0: }, michael@0: indent: function(cm, operatorArgs, vim, curStart, curEnd) { michael@0: var startLine = curStart.line; michael@0: var endLine = curEnd.line; michael@0: // In visual mode, n> shifts the selection right n times, instead of michael@0: // shifting n lines right once. michael@0: var repeat = (vim.visualMode) ? operatorArgs.repeat : 1; michael@0: if (operatorArgs.linewise) { michael@0: // The only way to delete a newline is to delete until the start of michael@0: // the next line, so in linewise mode evalInput will include the next michael@0: // line. We don't want this in indent, so we go back a line. michael@0: endLine--; michael@0: } michael@0: for (var i = startLine; i <= endLine; i++) { michael@0: for (var j = 0; j < repeat; j++) { michael@0: cm.indentLine(i, operatorArgs.indentRight); michael@0: } michael@0: } michael@0: cm.setCursor(curStart); michael@0: cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm)); michael@0: }, michael@0: swapcase: function(cm, operatorArgs, _vim, curStart, curEnd, curOriginal) { michael@0: var toSwap = cm.getRange(curStart, curEnd); michael@0: var swapped = ''; michael@0: for (var i = 0; i < toSwap.length; i++) { michael@0: var character = toSwap.charAt(i); michael@0: swapped += isUpperCase(character) ? character.toLowerCase() : michael@0: character.toUpperCase(); michael@0: } michael@0: cm.replaceRange(swapped, curStart, curEnd); michael@0: if (!operatorArgs.shouldMoveCursor) { michael@0: cm.setCursor(curOriginal); michael@0: } michael@0: }, michael@0: yank: function(cm, operatorArgs, _vim, curStart, curEnd, curOriginal) { michael@0: vimGlobalState.registerController.pushText( michael@0: operatorArgs.registerName, 'yank', michael@0: cm.getRange(curStart, curEnd), operatorArgs.linewise); michael@0: cm.setCursor(curOriginal); michael@0: } michael@0: }; michael@0: michael@0: var actions = { michael@0: jumpListWalk: function(cm, actionArgs, vim) { michael@0: if (vim.visualMode) { michael@0: return; michael@0: } michael@0: var repeat = actionArgs.repeat; michael@0: var forward = actionArgs.forward; michael@0: var jumpList = vimGlobalState.jumpList; michael@0: michael@0: var mark = jumpList.move(cm, forward ? repeat : -repeat); michael@0: var markPos = mark ? mark.find() : undefined; michael@0: markPos = markPos ? markPos : cm.getCursor(); michael@0: cm.setCursor(markPos); michael@0: }, michael@0: scroll: function(cm, actionArgs, vim) { michael@0: if (vim.visualMode) { michael@0: return; michael@0: } michael@0: var repeat = actionArgs.repeat || 1; michael@0: var lineHeight = cm.defaultTextHeight(); michael@0: var top = cm.getScrollInfo().top; michael@0: var delta = lineHeight * repeat; michael@0: var newPos = actionArgs.forward ? top + delta : top - delta; michael@0: var cursor = copyCursor(cm.getCursor()); michael@0: var cursorCoords = cm.charCoords(cursor, 'local'); michael@0: if (actionArgs.forward) { michael@0: if (newPos > cursorCoords.top) { michael@0: cursor.line += (newPos - cursorCoords.top) / lineHeight; michael@0: cursor.line = Math.ceil(cursor.line); michael@0: cm.setCursor(cursor); michael@0: cursorCoords = cm.charCoords(cursor, 'local'); michael@0: cm.scrollTo(null, cursorCoords.top); michael@0: } else { michael@0: // Cursor stays within bounds. Just reposition the scroll window. michael@0: cm.scrollTo(null, newPos); michael@0: } michael@0: } else { michael@0: var newBottom = newPos + cm.getScrollInfo().clientHeight; michael@0: if (newBottom < cursorCoords.bottom) { michael@0: cursor.line -= (cursorCoords.bottom - newBottom) / lineHeight; michael@0: cursor.line = Math.floor(cursor.line); michael@0: cm.setCursor(cursor); michael@0: cursorCoords = cm.charCoords(cursor, 'local'); michael@0: cm.scrollTo( michael@0: null, cursorCoords.bottom - cm.getScrollInfo().clientHeight); michael@0: } else { michael@0: // Cursor stays within bounds. Just reposition the scroll window. michael@0: cm.scrollTo(null, newPos); michael@0: } michael@0: } michael@0: }, michael@0: scrollToCursor: function(cm, actionArgs) { michael@0: var lineNum = cm.getCursor().line; michael@0: var charCoords = cm.charCoords(Pos(lineNum, 0), 'local'); michael@0: var height = cm.getScrollInfo().clientHeight; michael@0: var y = charCoords.top; michael@0: var lineHeight = charCoords.bottom - y; michael@0: switch (actionArgs.position) { michael@0: case 'center': y = y - (height / 2) + lineHeight; michael@0: break; michael@0: case 'bottom': y = y - height + lineHeight*1.4; michael@0: break; michael@0: case 'top': y = y + lineHeight*0.4; michael@0: break; michael@0: } michael@0: cm.scrollTo(null, y); michael@0: }, michael@0: replayMacro: function(cm, actionArgs, vim) { michael@0: var registerName = actionArgs.selectedCharacter; michael@0: var repeat = actionArgs.repeat; michael@0: var macroModeState = vimGlobalState.macroModeState; michael@0: if (registerName == '@') { michael@0: registerName = macroModeState.latestRegister; michael@0: } michael@0: while(repeat--){ michael@0: executeMacroRegister(cm, vim, macroModeState, registerName); michael@0: } michael@0: }, michael@0: enterMacroRecordMode: function(cm, actionArgs) { michael@0: var macroModeState = vimGlobalState.macroModeState; michael@0: var registerName = actionArgs.selectedCharacter; michael@0: macroModeState.enterMacroRecordMode(cm, registerName); michael@0: }, michael@0: enterInsertMode: function(cm, actionArgs, vim) { michael@0: if (cm.getOption('readOnly')) { return; } michael@0: vim.insertMode = true; michael@0: vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1; michael@0: var insertAt = (actionArgs) ? actionArgs.insertAt : null; michael@0: if (insertAt == 'eol') { michael@0: var cursor = cm.getCursor(); michael@0: cursor = Pos(cursor.line, lineLength(cm, cursor.line)); michael@0: cm.setCursor(cursor); michael@0: } else if (insertAt == 'charAfter') { michael@0: cm.setCursor(offsetCursor(cm.getCursor(), 0, 1)); michael@0: } else if (insertAt == 'firstNonBlank') { michael@0: cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm)); michael@0: } michael@0: cm.setOption('keyMap', 'vim-insert'); michael@0: cm.setOption('disableInput', false); michael@0: if (actionArgs && actionArgs.replace) { michael@0: // Handle Replace-mode as a special case of insert mode. michael@0: cm.toggleOverwrite(true); michael@0: cm.setOption('keyMap', 'vim-replace'); michael@0: CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"}); michael@0: } else { michael@0: cm.setOption('keyMap', 'vim-insert'); michael@0: CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"}); michael@0: } michael@0: if (!vimGlobalState.macroModeState.isPlaying) { michael@0: // Only record if not replaying. michael@0: cm.on('change', onChange); michael@0: cm.on('cursorActivity', onCursorActivity); michael@0: CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); michael@0: } michael@0: }, michael@0: toggleVisualMode: function(cm, actionArgs, vim) { michael@0: var repeat = actionArgs.repeat; michael@0: var curStart = cm.getCursor(); michael@0: var curEnd; michael@0: // TODO: The repeat should actually select number of characters/lines michael@0: // equal to the repeat times the size of the previous visual michael@0: // operation. michael@0: if (!vim.visualMode) { michael@0: cm.on('mousedown', exitVisualMode); michael@0: vim.visualMode = true; michael@0: vim.visualLine = !!actionArgs.linewise; michael@0: if (vim.visualLine) { michael@0: curStart.ch = 0; michael@0: curEnd = clipCursorToContent( michael@0: cm, Pos(curStart.line + repeat - 1, lineLength(cm, curStart.line)), michael@0: true /** includeLineBreak */); michael@0: } else { michael@0: curEnd = clipCursorToContent( michael@0: cm, Pos(curStart.line, curStart.ch + repeat), michael@0: true /** includeLineBreak */); michael@0: } michael@0: // Make the initial selection. michael@0: if (!actionArgs.repeatIsExplicit && !vim.visualLine) { michael@0: // This is a strange case. Here the implicit repeat is 1. The michael@0: // following commands lets the cursor hover over the 1 character michael@0: // selection. michael@0: cm.setCursor(curEnd); michael@0: cm.setSelection(curEnd, curStart); michael@0: } else { michael@0: cm.setSelection(curStart, curEnd); michael@0: } michael@0: CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : ""}); michael@0: } else { michael@0: curStart = cm.getCursor('anchor'); michael@0: curEnd = cm.getCursor('head'); michael@0: if (!vim.visualLine && actionArgs.linewise) { michael@0: // Shift-V pressed in characterwise visual mode. Switch to linewise michael@0: // visual mode instead of exiting visual mode. michael@0: vim.visualLine = true; michael@0: curStart.ch = cursorIsBefore(curStart, curEnd) ? 0 : michael@0: lineLength(cm, curStart.line); michael@0: curEnd.ch = cursorIsBefore(curStart, curEnd) ? michael@0: lineLength(cm, curEnd.line) : 0; michael@0: cm.setSelection(curStart, curEnd); michael@0: CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: "linewise"}); michael@0: } else if (vim.visualLine && !actionArgs.linewise) { michael@0: // v pressed in linewise visual mode. Switch to characterwise visual michael@0: // mode instead of exiting visual mode. michael@0: vim.visualLine = false; michael@0: CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"}); michael@0: } else { michael@0: exitVisualMode(cm); michael@0: } michael@0: } michael@0: updateMark(cm, vim, '<', cursorIsBefore(curStart, curEnd) ? curStart michael@0: : curEnd); michael@0: updateMark(cm, vim, '>', cursorIsBefore(curStart, curEnd) ? curEnd michael@0: : curStart); michael@0: }, michael@0: reselectLastSelection: function(cm, _actionArgs, vim) { michael@0: if (vim.lastSelection) { michael@0: var lastSelection = vim.lastSelection; michael@0: cm.setSelection(lastSelection.curStart, lastSelection.curEnd); michael@0: if (lastSelection.visualLine) { michael@0: vim.visualMode = true; michael@0: vim.visualLine = true; michael@0: } michael@0: else { michael@0: vim.visualMode = true; michael@0: vim.visualLine = false; michael@0: } michael@0: CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : ""}); michael@0: } michael@0: }, michael@0: joinLines: function(cm, actionArgs, vim) { michael@0: var curStart, curEnd; michael@0: if (vim.visualMode) { michael@0: curStart = cm.getCursor('anchor'); michael@0: curEnd = cm.getCursor('head'); michael@0: curEnd.ch = lineLength(cm, curEnd.line) - 1; michael@0: } else { michael@0: // Repeat is the number of lines to join. Minimum 2 lines. michael@0: var repeat = Math.max(actionArgs.repeat, 2); michael@0: curStart = cm.getCursor(); michael@0: curEnd = clipCursorToContent(cm, Pos(curStart.line + repeat - 1, michael@0: Infinity)); michael@0: } michael@0: var finalCh = 0; michael@0: cm.operation(function() { michael@0: for (var i = curStart.line; i < curEnd.line; i++) { michael@0: finalCh = lineLength(cm, curStart.line); michael@0: var tmp = Pos(curStart.line + 1, michael@0: lineLength(cm, curStart.line + 1)); michael@0: var text = cm.getRange(curStart, tmp); michael@0: text = text.replace(/\n\s*/g, ' '); michael@0: cm.replaceRange(text, curStart, tmp); michael@0: } michael@0: var curFinalPos = Pos(curStart.line, finalCh); michael@0: cm.setCursor(curFinalPos); michael@0: }); michael@0: }, michael@0: newLineAndEnterInsertMode: function(cm, actionArgs, vim) { michael@0: vim.insertMode = true; michael@0: var insertAt = copyCursor(cm.getCursor()); michael@0: if (insertAt.line === cm.firstLine() && !actionArgs.after) { michael@0: // Special case for inserting newline before start of document. michael@0: cm.replaceRange('\n', Pos(cm.firstLine(), 0)); michael@0: cm.setCursor(cm.firstLine(), 0); michael@0: } else { michael@0: insertAt.line = (actionArgs.after) ? insertAt.line : michael@0: insertAt.line - 1; michael@0: insertAt.ch = lineLength(cm, insertAt.line); michael@0: cm.setCursor(insertAt); michael@0: var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment || michael@0: CodeMirror.commands.newlineAndIndent; michael@0: newlineFn(cm); michael@0: } michael@0: this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim); michael@0: }, michael@0: paste: function(cm, actionArgs) { michael@0: var cur = copyCursor(cm.getCursor()); michael@0: var register = vimGlobalState.registerController.getRegister( michael@0: actionArgs.registerName); michael@0: var text = register.toString(); michael@0: if (!text) { michael@0: return; michael@0: } michael@0: if (actionArgs.repeat > 1) { michael@0: var text = Array(actionArgs.repeat + 1).join(text); michael@0: } michael@0: var linewise = register.linewise; michael@0: if (linewise) { michael@0: if (actionArgs.after) { michael@0: // Move the newline at the end to the start instead, and paste just michael@0: // before the newline character of the line we are on right now. michael@0: text = '\n' + text.slice(0, text.length - 1); michael@0: cur.ch = lineLength(cm, cur.line); michael@0: } else { michael@0: cur.ch = 0; michael@0: } michael@0: } else { michael@0: cur.ch += actionArgs.after ? 1 : 0; michael@0: } michael@0: cm.replaceRange(text, cur); michael@0: // Now fine tune the cursor to where we want it. michael@0: var curPosFinal; michael@0: var idx; michael@0: if (linewise && actionArgs.after) { michael@0: curPosFinal = Pos( michael@0: cur.line + 1, michael@0: findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1))); michael@0: } else if (linewise && !actionArgs.after) { michael@0: curPosFinal = Pos( michael@0: cur.line, michael@0: findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line))); michael@0: } else if (!linewise && actionArgs.after) { michael@0: idx = cm.indexFromPos(cur); michael@0: curPosFinal = cm.posFromIndex(idx + text.length - 1); michael@0: } else { michael@0: idx = cm.indexFromPos(cur); michael@0: curPosFinal = cm.posFromIndex(idx + text.length); michael@0: } michael@0: cm.setCursor(curPosFinal); michael@0: }, michael@0: undo: function(cm, actionArgs) { michael@0: cm.operation(function() { michael@0: repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)(); michael@0: cm.setCursor(cm.getCursor('anchor')); michael@0: }); michael@0: }, michael@0: redo: function(cm, actionArgs) { michael@0: repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)(); michael@0: }, michael@0: setRegister: function(_cm, actionArgs, vim) { michael@0: vim.inputState.registerName = actionArgs.selectedCharacter; michael@0: }, michael@0: setMark: function(cm, actionArgs, vim) { michael@0: var markName = actionArgs.selectedCharacter; michael@0: updateMark(cm, vim, markName, cm.getCursor()); michael@0: }, michael@0: replace: function(cm, actionArgs, vim) { michael@0: var replaceWith = actionArgs.selectedCharacter; michael@0: var curStart = cm.getCursor(); michael@0: var replaceTo; michael@0: var curEnd; michael@0: if (vim.visualMode){ michael@0: curStart=cm.getCursor('start'); michael@0: curEnd=cm.getCursor('end'); michael@0: // workaround to catch the character under the cursor michael@0: // existing workaround doesn't cover actions michael@0: curEnd=cm.clipPos(Pos(curEnd.line, curEnd.ch+1)); michael@0: }else{ michael@0: var line = cm.getLine(curStart.line); michael@0: replaceTo = curStart.ch + actionArgs.repeat; michael@0: if (replaceTo > line.length) { michael@0: replaceTo=line.length; michael@0: } michael@0: curEnd = Pos(curStart.line, replaceTo); michael@0: } michael@0: if (replaceWith=='\n'){ michael@0: if (!vim.visualMode) cm.replaceRange('', curStart, curEnd); michael@0: // special case, where vim help says to replace by just one line-break michael@0: (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm); michael@0: }else { michael@0: var replaceWithStr=cm.getRange(curStart, curEnd); michael@0: //replace all characters in range by selected, but keep linebreaks michael@0: replaceWithStr=replaceWithStr.replace(/[^\n]/g,replaceWith); michael@0: cm.replaceRange(replaceWithStr, curStart, curEnd); michael@0: if (vim.visualMode){ michael@0: cm.setCursor(curStart); michael@0: exitVisualMode(cm); michael@0: }else{ michael@0: cm.setCursor(offsetCursor(curEnd, 0, -1)); michael@0: } michael@0: } michael@0: }, michael@0: incrementNumberToken: function(cm, actionArgs) { michael@0: var cur = cm.getCursor(); michael@0: var lineStr = cm.getLine(cur.line); michael@0: var re = /-?\d+/g; michael@0: var match; michael@0: var start; michael@0: var end; michael@0: var numberStr; michael@0: var token; michael@0: while ((match = re.exec(lineStr)) !== null) { michael@0: token = match[0]; michael@0: start = match.index; michael@0: end = start + token.length; michael@0: if (cur.ch < end)break; michael@0: } michael@0: if (!actionArgs.backtrack && (end <= cur.ch))return; michael@0: if (token) { michael@0: var increment = actionArgs.increase ? 1 : -1; michael@0: var number = parseInt(token) + (increment * actionArgs.repeat); michael@0: var from = Pos(cur.line, start); michael@0: var to = Pos(cur.line, end); michael@0: numberStr = number.toString(); michael@0: cm.replaceRange(numberStr, from, to); michael@0: } else { michael@0: return; michael@0: } michael@0: cm.setCursor(Pos(cur.line, start + numberStr.length - 1)); michael@0: }, michael@0: repeatLastEdit: function(cm, actionArgs, vim) { michael@0: var lastEditInputState = vim.lastEditInputState; michael@0: if (!lastEditInputState) { return; } michael@0: var repeat = actionArgs.repeat; michael@0: if (repeat && actionArgs.repeatIsExplicit) { michael@0: vim.lastEditInputState.repeatOverride = repeat; michael@0: } else { michael@0: repeat = vim.lastEditInputState.repeatOverride || repeat; michael@0: } michael@0: repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */); michael@0: } michael@0: }; michael@0: michael@0: /* michael@0: * Below are miscellaneous utility functions used by vim.js michael@0: */ michael@0: michael@0: /** michael@0: * Clips cursor to ensure that line is within the buffer's range michael@0: * If includeLineBreak is true, then allow cur.ch == lineLength. michael@0: */ michael@0: function clipCursorToContent(cm, cur, includeLineBreak) { michael@0: var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() ); michael@0: var maxCh = lineLength(cm, line) - 1; michael@0: maxCh = (includeLineBreak) ? maxCh + 1 : maxCh; michael@0: var ch = Math.min(Math.max(0, cur.ch), maxCh); michael@0: return Pos(line, ch); michael@0: } michael@0: function copyArgs(args) { michael@0: var ret = {}; michael@0: for (var prop in args) { michael@0: if (args.hasOwnProperty(prop)) { michael@0: ret[prop] = args[prop]; michael@0: } michael@0: } michael@0: return ret; michael@0: } michael@0: function offsetCursor(cur, offsetLine, offsetCh) { michael@0: return Pos(cur.line + offsetLine, cur.ch + offsetCh); michael@0: } michael@0: function matchKeysPartial(pressed, mapped) { michael@0: for (var i = 0; i < pressed.length; i++) { michael@0: // 'character' means any character. For mark, register commads, etc. michael@0: if (pressed[i] != mapped[i] && mapped[i] != 'character') { michael@0: return false; michael@0: } michael@0: } michael@0: return true; michael@0: } michael@0: function repeatFn(cm, fn, repeat) { michael@0: return function() { michael@0: for (var i = 0; i < repeat; i++) { michael@0: fn(cm); michael@0: } michael@0: }; michael@0: } michael@0: function copyCursor(cur) { michael@0: return Pos(cur.line, cur.ch); michael@0: } michael@0: function cursorEqual(cur1, cur2) { michael@0: return cur1.ch == cur2.ch && cur1.line == cur2.line; michael@0: } michael@0: function cursorIsBefore(cur1, cur2) { michael@0: if (cur1.line < cur2.line) { michael@0: return true; michael@0: } michael@0: if (cur1.line == cur2.line && cur1.ch < cur2.ch) { michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: function cusrorIsBetween(cur1, cur2, cur3) { michael@0: // returns true if cur2 is between cur1 and cur3. michael@0: var cur1before2 = cursorIsBefore(cur1, cur2); michael@0: var cur2before3 = cursorIsBefore(cur2, cur3); michael@0: return cur1before2 && cur2before3; michael@0: } michael@0: function lineLength(cm, lineNum) { michael@0: return cm.getLine(lineNum).length; michael@0: } michael@0: function reverse(s){ michael@0: return s.split('').reverse().join(''); michael@0: } michael@0: function trim(s) { michael@0: if (s.trim) { michael@0: return s.trim(); michael@0: } michael@0: return s.replace(/^\s+|\s+$/g, ''); michael@0: } michael@0: function escapeRegex(s) { michael@0: return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1'); michael@0: } michael@0: michael@0: function exitVisualMode(cm) { michael@0: cm.off('mousedown', exitVisualMode); michael@0: var vim = cm.state.vim; michael@0: // can't use selection state here because yank has already reset its cursor michael@0: vim.lastSelection = {'curStart': vim.marks['<'].find(), michael@0: 'curEnd': vim.marks['>'].find(), 'visualMode': vim.visualMode, michael@0: 'visualLine': vim.visualLine}; michael@0: vim.visualMode = false; michael@0: vim.visualLine = false; michael@0: var selectionStart = cm.getCursor('anchor'); michael@0: var selectionEnd = cm.getCursor('head'); michael@0: if (!cursorEqual(selectionStart, selectionEnd)) { michael@0: // Clear the selection and set the cursor only if the selection has not michael@0: // already been cleared. Otherwise we risk moving the cursor somewhere michael@0: // it's not supposed to be. michael@0: cm.setCursor(clipCursorToContent(cm, selectionEnd)); michael@0: } michael@0: CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); michael@0: } michael@0: michael@0: // Remove any trailing newlines from the selection. For michael@0: // example, with the caret at the start of the last word on the line, michael@0: // 'dw' should word, but not the newline, while 'w' should advance the michael@0: // caret to the first character of the next line. michael@0: function clipToLine(cm, curStart, curEnd) { michael@0: var selection = cm.getRange(curStart, curEnd); michael@0: // Only clip if the selection ends with trailing newline + whitespace michael@0: if (/\n\s*$/.test(selection)) { michael@0: var lines = selection.split('\n'); michael@0: // We know this is all whitepsace. michael@0: lines.pop(); michael@0: michael@0: // Cases: michael@0: // 1. Last word is an empty line - do not clip the trailing '\n' michael@0: // 2. Last word is not an empty line - clip the trailing '\n' michael@0: var line; michael@0: // Find the line containing the last word, and clip all whitespace up michael@0: // to it. michael@0: for (var line = lines.pop(); lines.length > 0 && line && isWhiteSpaceString(line); line = lines.pop()) { michael@0: curEnd.line--; michael@0: curEnd.ch = 0; michael@0: } michael@0: // If the last word is not an empty line, clip an additional newline michael@0: if (line) { michael@0: curEnd.line--; michael@0: curEnd.ch = lineLength(cm, curEnd.line); michael@0: } else { michael@0: curEnd.ch = 0; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Expand the selection to line ends. michael@0: function expandSelectionToLine(_cm, curStart, curEnd) { michael@0: curStart.ch = 0; michael@0: curEnd.ch = 0; michael@0: curEnd.line++; michael@0: } michael@0: michael@0: function findFirstNonWhiteSpaceCharacter(text) { michael@0: if (!text) { michael@0: return 0; michael@0: } michael@0: var firstNonWS = text.search(/\S/); michael@0: return firstNonWS == -1 ? text.length : firstNonWS; michael@0: } michael@0: michael@0: function expandWordUnderCursor(cm, inclusive, _forward, bigWord, noSymbol) { michael@0: var cur = cm.getCursor(); michael@0: var line = cm.getLine(cur.line); michael@0: var idx = cur.ch; michael@0: michael@0: // Seek to first word or non-whitespace character, depending on if michael@0: // noSymbol is true. michael@0: var textAfterIdx = line.substring(idx); michael@0: var firstMatchedChar; michael@0: if (noSymbol) { michael@0: firstMatchedChar = textAfterIdx.search(/\w/); michael@0: } else { michael@0: firstMatchedChar = textAfterIdx.search(/\S/); michael@0: } michael@0: if (firstMatchedChar == -1) { michael@0: return null; michael@0: } michael@0: idx += firstMatchedChar; michael@0: textAfterIdx = line.substring(idx); michael@0: var textBeforeIdx = line.substring(0, idx); michael@0: michael@0: var matchRegex; michael@0: // Greedy matchers for the "word" we are trying to expand. michael@0: if (bigWord) { michael@0: matchRegex = /^\S+/; michael@0: } else { michael@0: if ((/\w/).test(line.charAt(idx))) { michael@0: matchRegex = /^\w+/; michael@0: } else { michael@0: matchRegex = /^[^\w\s]+/; michael@0: } michael@0: } michael@0: michael@0: var wordAfterRegex = matchRegex.exec(textAfterIdx); michael@0: var wordStart = idx; michael@0: var wordEnd = idx + wordAfterRegex[0].length; michael@0: // TODO: Find a better way to do this. It will be slow on very long lines. michael@0: var revTextBeforeIdx = reverse(textBeforeIdx); michael@0: var wordBeforeRegex = matchRegex.exec(revTextBeforeIdx); michael@0: if (wordBeforeRegex) { michael@0: wordStart -= wordBeforeRegex[0].length; michael@0: } michael@0: michael@0: if (inclusive) { michael@0: // If present, trim all whitespace after word. michael@0: // Otherwise, trim all whitespace before word. michael@0: var textAfterWordEnd = line.substring(wordEnd); michael@0: var whitespacesAfterWord = textAfterWordEnd.match(/^\s*/)[0].length; michael@0: if (whitespacesAfterWord > 0) { michael@0: wordEnd += whitespacesAfterWord; michael@0: } else { michael@0: var revTrim = revTextBeforeIdx.length - wordStart; michael@0: var textBeforeWordStart = revTextBeforeIdx.substring(revTrim); michael@0: var whitespacesBeforeWord = textBeforeWordStart.match(/^\s*/)[0].length; michael@0: wordStart -= whitespacesBeforeWord; michael@0: } michael@0: } michael@0: michael@0: return { start: Pos(cur.line, wordStart), michael@0: end: Pos(cur.line, wordEnd) }; michael@0: } michael@0: michael@0: function recordJumpPosition(cm, oldCur, newCur) { michael@0: if (!cursorEqual(oldCur, newCur)) { michael@0: vimGlobalState.jumpList.add(cm, oldCur, newCur); michael@0: } michael@0: } michael@0: michael@0: function recordLastCharacterSearch(increment, args) { michael@0: vimGlobalState.lastChararacterSearch.increment = increment; michael@0: vimGlobalState.lastChararacterSearch.forward = args.forward; michael@0: vimGlobalState.lastChararacterSearch.selectedCharacter = args.selectedCharacter; michael@0: } michael@0: michael@0: var symbolToMode = { michael@0: '(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket', michael@0: '[': 'section', ']': 'section', michael@0: '*': 'comment', '/': 'comment', michael@0: 'm': 'method', 'M': 'method', michael@0: '#': 'preprocess' michael@0: }; michael@0: var findSymbolModes = { michael@0: bracket: { michael@0: isComplete: function(state) { michael@0: if (state.nextCh === state.symb) { michael@0: state.depth++; michael@0: if (state.depth >= 1)return true; michael@0: } else if (state.nextCh === state.reverseSymb) { michael@0: state.depth--; michael@0: } michael@0: return false; michael@0: } michael@0: }, michael@0: section: { michael@0: init: function(state) { michael@0: state.curMoveThrough = true; michael@0: state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}'; michael@0: }, michael@0: isComplete: function(state) { michael@0: return state.index === 0 && state.nextCh === state.symb; michael@0: } michael@0: }, michael@0: comment: { michael@0: isComplete: function(state) { michael@0: var found = state.lastCh === '*' && state.nextCh === '/'; michael@0: state.lastCh = state.nextCh; michael@0: return found; michael@0: } michael@0: }, michael@0: // TODO: The original Vim implementation only operates on level 1 and 2. michael@0: // The current implementation doesn't check for code block level and michael@0: // therefore it operates on any levels. michael@0: method: { michael@0: init: function(state) { michael@0: state.symb = (state.symb === 'm' ? '{' : '}'); michael@0: state.reverseSymb = state.symb === '{' ? '}' : '{'; michael@0: }, michael@0: isComplete: function(state) { michael@0: if (state.nextCh === state.symb)return true; michael@0: return false; michael@0: } michael@0: }, michael@0: preprocess: { michael@0: init: function(state) { michael@0: state.index = 0; michael@0: }, michael@0: isComplete: function(state) { michael@0: if (state.nextCh === '#') { michael@0: var token = state.lineText.match(/#(\w+)/)[1]; michael@0: if (token === 'endif') { michael@0: if (state.forward && state.depth === 0) { michael@0: return true; michael@0: } michael@0: state.depth++; michael@0: } else if (token === 'if') { michael@0: if (!state.forward && state.depth === 0) { michael@0: return true; michael@0: } michael@0: state.depth--; michael@0: } michael@0: if (token === 'else' && state.depth === 0)return true; michael@0: } michael@0: return false; michael@0: } michael@0: } michael@0: }; michael@0: function findSymbol(cm, repeat, forward, symb) { michael@0: var cur = copyCursor(cm.getCursor()); michael@0: var increment = forward ? 1 : -1; michael@0: var endLine = forward ? cm.lineCount() : -1; michael@0: var curCh = cur.ch; michael@0: var line = cur.line; michael@0: var lineText = cm.getLine(line); michael@0: var state = { michael@0: lineText: lineText, michael@0: nextCh: lineText.charAt(curCh), michael@0: lastCh: null, michael@0: index: curCh, michael@0: symb: symb, michael@0: reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb], michael@0: forward: forward, michael@0: depth: 0, michael@0: curMoveThrough: false michael@0: }; michael@0: var mode = symbolToMode[symb]; michael@0: if (!mode)return cur; michael@0: var init = findSymbolModes[mode].init; michael@0: var isComplete = findSymbolModes[mode].isComplete; michael@0: if (init) { init(state); } michael@0: while (line !== endLine && repeat) { michael@0: state.index += increment; michael@0: state.nextCh = state.lineText.charAt(state.index); michael@0: if (!state.nextCh) { michael@0: line += increment; michael@0: state.lineText = cm.getLine(line) || ''; michael@0: if (increment > 0) { michael@0: state.index = 0; michael@0: } else { michael@0: var lineLen = state.lineText.length; michael@0: state.index = (lineLen > 0) ? (lineLen-1) : 0; michael@0: } michael@0: state.nextCh = state.lineText.charAt(state.index); michael@0: } michael@0: if (isComplete(state)) { michael@0: cur.line = line; michael@0: cur.ch = state.index; michael@0: repeat--; michael@0: } michael@0: } michael@0: if (state.nextCh || state.curMoveThrough) { michael@0: return Pos(line, state.index); michael@0: } michael@0: return cur; michael@0: } michael@0: michael@0: /* michael@0: * Returns the boundaries of the next word. If the cursor in the middle of michael@0: * the word, then returns the boundaries of the current word, starting at michael@0: * the cursor. If the cursor is at the start/end of a word, and we are going michael@0: * forward/backward, respectively, find the boundaries of the next word. michael@0: * michael@0: * @param {CodeMirror} cm CodeMirror object. michael@0: * @param {Cursor} cur The cursor position. michael@0: * @param {boolean} forward True to search forward. False to search michael@0: * backward. michael@0: * @param {boolean} bigWord True if punctuation count as part of the word. michael@0: * False if only [a-zA-Z0-9] characters count as part of the word. michael@0: * @param {boolean} emptyLineIsWord True if empty lines should be treated michael@0: * as words. michael@0: * @return {Object{from:number, to:number, line: number}} The boundaries of michael@0: * the word, or null if there are no more words. michael@0: */ michael@0: function findWord(cm, cur, forward, bigWord, emptyLineIsWord) { michael@0: var lineNum = cur.line; michael@0: var pos = cur.ch; michael@0: var line = cm.getLine(lineNum); michael@0: var dir = forward ? 1 : -1; michael@0: var regexps = bigWord ? bigWordRegexp : wordRegexp; michael@0: michael@0: if (emptyLineIsWord && line == '') { michael@0: lineNum += dir; michael@0: line = cm.getLine(lineNum); michael@0: if (!isLine(cm, lineNum)) { michael@0: return null; michael@0: } michael@0: pos = (forward) ? 0 : line.length; michael@0: } michael@0: michael@0: while (true) { michael@0: if (emptyLineIsWord && line == '') { michael@0: return { from: 0, to: 0, line: lineNum }; michael@0: } michael@0: var stop = (dir > 0) ? line.length : -1; michael@0: var wordStart = stop, wordEnd = stop; michael@0: // Find bounds of next word. michael@0: while (pos != stop) { michael@0: var foundWord = false; michael@0: for (var i = 0; i < regexps.length && !foundWord; ++i) { michael@0: if (regexps[i].test(line.charAt(pos))) { michael@0: wordStart = pos; michael@0: // Advance to end of word. michael@0: while (pos != stop && regexps[i].test(line.charAt(pos))) { michael@0: pos += dir; michael@0: } michael@0: wordEnd = pos; michael@0: foundWord = wordStart != wordEnd; michael@0: if (wordStart == cur.ch && lineNum == cur.line && michael@0: wordEnd == wordStart + dir) { michael@0: // We started at the end of a word. Find the next one. michael@0: continue; michael@0: } else { michael@0: return { michael@0: from: Math.min(wordStart, wordEnd + 1), michael@0: to: Math.max(wordStart, wordEnd), michael@0: line: lineNum }; michael@0: } michael@0: } michael@0: } michael@0: if (!foundWord) { michael@0: pos += dir; michael@0: } michael@0: } michael@0: // Advance to next/prev line. michael@0: lineNum += dir; michael@0: if (!isLine(cm, lineNum)) { michael@0: return null; michael@0: } michael@0: line = cm.getLine(lineNum); michael@0: pos = (dir > 0) ? 0 : line.length; michael@0: } michael@0: // Should never get here. michael@0: throw new Error('The impossible happened.'); michael@0: } michael@0: michael@0: /** michael@0: * @param {CodeMirror} cm CodeMirror object. michael@0: * @param {int} repeat Number of words to move past. michael@0: * @param {boolean} forward True to search forward. False to search michael@0: * backward. michael@0: * @param {boolean} wordEnd True to move to end of word. False to move to michael@0: * beginning of word. michael@0: * @param {boolean} bigWord True if punctuation count as part of the word. michael@0: * False if only alphabet characters count as part of the word. michael@0: * @return {Cursor} The position the cursor should move to. michael@0: */ michael@0: function moveToWord(cm, repeat, forward, wordEnd, bigWord) { michael@0: var cur = cm.getCursor(); michael@0: var curStart = copyCursor(cur); michael@0: var words = []; michael@0: if (forward && !wordEnd || !forward && wordEnd) { michael@0: repeat++; michael@0: } michael@0: // For 'e', empty lines are not considered words, go figure. michael@0: var emptyLineIsWord = !(forward && wordEnd); michael@0: for (var i = 0; i < repeat; i++) { michael@0: var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord); michael@0: if (!word) { michael@0: var eodCh = lineLength(cm, cm.lastLine()); michael@0: words.push(forward michael@0: ? {line: cm.lastLine(), from: eodCh, to: eodCh} michael@0: : {line: 0, from: 0, to: 0}); michael@0: break; michael@0: } michael@0: words.push(word); michael@0: cur = Pos(word.line, forward ? (word.to - 1) : word.from); michael@0: } michael@0: var shortCircuit = words.length != repeat; michael@0: var firstWord = words[0]; michael@0: var lastWord = words.pop(); michael@0: if (forward && !wordEnd) { michael@0: // w michael@0: if (!shortCircuit && (firstWord.from != curStart.ch || firstWord.line != curStart.line)) { michael@0: // We did not start in the middle of a word. Discard the extra word at the end. michael@0: lastWord = words.pop(); michael@0: } michael@0: return Pos(lastWord.line, lastWord.from); michael@0: } else if (forward && wordEnd) { michael@0: return Pos(lastWord.line, lastWord.to - 1); michael@0: } else if (!forward && wordEnd) { michael@0: // ge michael@0: if (!shortCircuit && (firstWord.to != curStart.ch || firstWord.line != curStart.line)) { michael@0: // We did not start in the middle of a word. Discard the extra word at the end. michael@0: lastWord = words.pop(); michael@0: } michael@0: return Pos(lastWord.line, lastWord.to); michael@0: } else { michael@0: // b michael@0: return Pos(lastWord.line, lastWord.from); michael@0: } michael@0: } michael@0: michael@0: function moveToCharacter(cm, repeat, forward, character) { michael@0: var cur = cm.getCursor(); michael@0: var start = cur.ch; michael@0: var idx; michael@0: for (var i = 0; i < repeat; i ++) { michael@0: var line = cm.getLine(cur.line); michael@0: idx = charIdxInLine(start, line, character, forward, true); michael@0: if (idx == -1) { michael@0: return null; michael@0: } michael@0: start = idx; michael@0: } michael@0: return Pos(cm.getCursor().line, idx); michael@0: } michael@0: michael@0: function moveToColumn(cm, repeat) { michael@0: // repeat is always >= 1, so repeat - 1 always corresponds michael@0: // to the column we want to go to. michael@0: var line = cm.getCursor().line; michael@0: return clipCursorToContent(cm, Pos(line, repeat - 1)); michael@0: } michael@0: michael@0: function updateMark(cm, vim, markName, pos) { michael@0: if (!inArray(markName, validMarks)) { michael@0: return; michael@0: } michael@0: if (vim.marks[markName]) { michael@0: vim.marks[markName].clear(); michael@0: } michael@0: vim.marks[markName] = cm.setBookmark(pos); michael@0: } michael@0: michael@0: function charIdxInLine(start, line, character, forward, includeChar) { michael@0: // Search for char in line. michael@0: // motion_options: {forward, includeChar} michael@0: // If includeChar = true, include it too. michael@0: // If forward = true, search forward, else search backwards. michael@0: // If char is not found on this line, do nothing michael@0: var idx; michael@0: if (forward) { michael@0: idx = line.indexOf(character, start + 1); michael@0: if (idx != -1 && !includeChar) { michael@0: idx -= 1; michael@0: } michael@0: } else { michael@0: idx = line.lastIndexOf(character, start - 1); michael@0: if (idx != -1 && !includeChar) { michael@0: idx += 1; michael@0: } michael@0: } michael@0: return idx; michael@0: } michael@0: michael@0: function getContextLevel(ctx) { michael@0: return (ctx === 'string' || ctx === 'comment') ? 1 : 0; michael@0: } michael@0: michael@0: function findMatchedSymbol(cm, cur, symb) { michael@0: var line = cur.line; michael@0: var ch = cur.ch; michael@0: symb = symb ? symb : cm.getLine(line).charAt(ch); michael@0: michael@0: var symbContext = cm.getTokenAt(Pos(line, ch + 1)).type; michael@0: var symbCtxLevel = getContextLevel(symbContext); michael@0: michael@0: var reverseSymb = ({ michael@0: '(': ')', ')': '(', michael@0: '[': ']', ']': '[', michael@0: '{': '}', '}': '{'})[symb]; michael@0: michael@0: // Couldn't find a matching symbol, abort michael@0: if (!reverseSymb) { michael@0: return cur; michael@0: } michael@0: michael@0: // set our increment to move forward (+1) or backwards (-1) michael@0: // depending on which bracket we're matching michael@0: var increment = ({'(': 1, '{': 1, '[': 1})[symb] || -1; michael@0: var endLine = increment === 1 ? cm.lineCount() : -1; michael@0: var depth = 1, nextCh = symb, index = ch, lineText = cm.getLine(line); michael@0: // Simple search for closing paren--just count openings and closings till michael@0: // we find our match michael@0: // TODO: use info from CodeMirror to ignore closing brackets in comments michael@0: // and quotes, etc. michael@0: while (line !== endLine && depth > 0) { michael@0: index += increment; michael@0: nextCh = lineText.charAt(index); michael@0: if (!nextCh) { michael@0: line += increment; michael@0: lineText = cm.getLine(line) || ''; michael@0: if (increment > 0) { michael@0: index = 0; michael@0: } else { michael@0: var lineLen = lineText.length; michael@0: index = (lineLen > 0) ? (lineLen-1) : 0; michael@0: } michael@0: nextCh = lineText.charAt(index); michael@0: } michael@0: var revSymbContext = cm.getTokenAt(Pos(line, index + 1)).type; michael@0: var revSymbCtxLevel = getContextLevel(revSymbContext); michael@0: if (symbCtxLevel >= revSymbCtxLevel) { michael@0: if (nextCh === symb) { michael@0: depth++; michael@0: } else if (nextCh === reverseSymb) { michael@0: depth--; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (nextCh) { michael@0: return Pos(line, index); michael@0: } michael@0: return cur; michael@0: } michael@0: michael@0: // TODO: perhaps this finagling of start and end positions belonds michael@0: // in codmirror/replaceRange? michael@0: function selectCompanionObject(cm, revSymb, inclusive) { michael@0: var cur = copyCursor(cm.getCursor()); michael@0: var end = findMatchedSymbol(cm, cur, revSymb); michael@0: var start = findMatchedSymbol(cm, end); michael@0: michael@0: if ((start.line == end.line && start.ch > end.ch) michael@0: || (start.line > end.line)) { michael@0: var tmp = start; michael@0: start = end; michael@0: end = tmp; michael@0: } michael@0: michael@0: if (inclusive) { michael@0: end.ch += 1; michael@0: } else { michael@0: start.ch += 1; michael@0: } michael@0: michael@0: return { start: start, end: end }; michael@0: } michael@0: michael@0: // Takes in a symbol and a cursor and tries to simulate text objects that michael@0: // have identical opening and closing symbols michael@0: // TODO support across multiple lines michael@0: function findBeginningAndEnd(cm, symb, inclusive) { michael@0: var cur = copyCursor(cm.getCursor()); michael@0: var line = cm.getLine(cur.line); michael@0: var chars = line.split(''); michael@0: var start, end, i, len; michael@0: var firstIndex = chars.indexOf(symb); michael@0: michael@0: // the decision tree is to always look backwards for the beginning first, michael@0: // but if the cursor is in front of the first instance of the symb, michael@0: // then move the cursor forward michael@0: if (cur.ch < firstIndex) { michael@0: cur.ch = firstIndex; michael@0: // Why is this line even here??? michael@0: // cm.setCursor(cur.line, firstIndex+1); michael@0: } michael@0: // otherwise if the cursor is currently on the closing symbol michael@0: else if (firstIndex < cur.ch && chars[cur.ch] == symb) { michael@0: end = cur.ch; // assign end to the current cursor michael@0: --cur.ch; // make sure to look backwards michael@0: } michael@0: michael@0: // if we're currently on the symbol, we've got a start michael@0: if (chars[cur.ch] == symb && !end) { michael@0: start = cur.ch + 1; // assign start to ahead of the cursor michael@0: } else { michael@0: // go backwards to find the start michael@0: for (i = cur.ch; i > -1 && !start; i--) { michael@0: if (chars[i] == symb) { michael@0: start = i + 1; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // look forwards for the end symbol michael@0: if (start && !end) { michael@0: for (i = start, len = chars.length; i < len && !end; i++) { michael@0: if (chars[i] == symb) { michael@0: end = i; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // nothing found michael@0: if (!start || !end) { michael@0: return { start: cur, end: cur }; michael@0: } michael@0: michael@0: // include the symbols michael@0: if (inclusive) { michael@0: --start; ++end; michael@0: } michael@0: michael@0: return { michael@0: start: Pos(cur.line, start), michael@0: end: Pos(cur.line, end) michael@0: }; michael@0: } michael@0: michael@0: // Search functions michael@0: defineOption('pcre', true, 'boolean'); michael@0: function SearchState() {} michael@0: SearchState.prototype = { michael@0: getQuery: function() { michael@0: return vimGlobalState.query; michael@0: }, michael@0: setQuery: function(query) { michael@0: vimGlobalState.query = query; michael@0: }, michael@0: getOverlay: function() { michael@0: return this.searchOverlay; michael@0: }, michael@0: setOverlay: function(overlay) { michael@0: this.searchOverlay = overlay; michael@0: }, michael@0: isReversed: function() { michael@0: return vimGlobalState.isReversed; michael@0: }, michael@0: setReversed: function(reversed) { michael@0: vimGlobalState.isReversed = reversed; michael@0: } michael@0: }; michael@0: function getSearchState(cm) { michael@0: var vim = cm.state.vim; michael@0: return vim.searchState_ || (vim.searchState_ = new SearchState()); michael@0: } michael@0: function dialog(cm, template, shortText, onClose, options) { michael@0: if (cm.openDialog) { michael@0: cm.openDialog(template, onClose, { bottom: true, value: options.value, michael@0: onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp }); michael@0: } michael@0: else { michael@0: onClose(prompt(shortText, '')); michael@0: } michael@0: } michael@0: michael@0: function findUnescapedSlashes(str) { michael@0: var escapeNextChar = false; michael@0: var slashes = []; michael@0: for (var i = 0; i < str.length; i++) { michael@0: var c = str.charAt(i); michael@0: if (!escapeNextChar && c == '/') { michael@0: slashes.push(i); michael@0: } michael@0: escapeNextChar = !escapeNextChar && (c == '\\'); michael@0: } michael@0: return slashes; michael@0: } michael@0: michael@0: // Translates a search string from ex (vim) syntax into javascript form. michael@0: function translateRegex(str) { michael@0: // When these match, add a '\' if unescaped or remove one if escaped. michael@0: var specials = ['|', '(', ')', '{']; michael@0: // Remove, but never add, a '\' for these. michael@0: var unescape = ['}']; michael@0: var escapeNextChar = false; michael@0: var out = []; michael@0: for (var i = -1; i < str.length; i++) { michael@0: var c = str.charAt(i) || ''; michael@0: var n = str.charAt(i+1) || ''; michael@0: var specialComesNext = (specials.indexOf(n) != -1); michael@0: if (escapeNextChar) { michael@0: if (c !== '\\' || !specialComesNext) { michael@0: out.push(c); michael@0: } michael@0: escapeNextChar = false; michael@0: } else { michael@0: if (c === '\\') { michael@0: escapeNextChar = true; michael@0: // Treat the unescape list as special for removing, but not adding '\'. michael@0: if (unescape.indexOf(n) != -1) { michael@0: specialComesNext = true; michael@0: } michael@0: // Not passing this test means removing a '\'. michael@0: if (!specialComesNext || n === '\\') { michael@0: out.push(c); michael@0: } michael@0: } else { michael@0: out.push(c); michael@0: if (specialComesNext && n !== '\\') { michael@0: out.push('\\'); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: return out.join(''); michael@0: } michael@0: michael@0: // Translates the replace part of a search and replace from ex (vim) syntax into michael@0: // javascript form. Similar to translateRegex, but additionally fixes back references michael@0: // (translates '\[0..9]' to '$[0..9]') and follows different rules for escaping '$'. michael@0: function translateRegexReplace(str) { michael@0: var escapeNextChar = false; michael@0: var out = []; michael@0: for (var i = -1; i < str.length; i++) { michael@0: var c = str.charAt(i) || ''; michael@0: var n = str.charAt(i+1) || ''; michael@0: if (escapeNextChar) { michael@0: // At any point in the loop, escapeNextChar is true if the previous michael@0: // character was a '\' and was not escaped. michael@0: out.push(c); michael@0: escapeNextChar = false; michael@0: } else { michael@0: if (c === '\\') { michael@0: escapeNextChar = true; michael@0: if ((isNumber(n) || n === '$')) { michael@0: out.push('$'); michael@0: } else if (n !== '/' && n !== '\\') { michael@0: out.push('\\'); michael@0: } michael@0: } else { michael@0: if (c === '$') { michael@0: out.push('$'); michael@0: } michael@0: out.push(c); michael@0: if (n === '/') { michael@0: out.push('\\'); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: return out.join(''); michael@0: } michael@0: michael@0: // Unescape \ and / in the replace part, for PCRE mode. michael@0: function unescapeRegexReplace(str) { michael@0: var stream = new CodeMirror.StringStream(str); michael@0: var output = []; michael@0: while (!stream.eol()) { michael@0: // Search for \. michael@0: while (stream.peek() && stream.peek() != '\\') { michael@0: output.push(stream.next()); michael@0: } michael@0: if (stream.match('\\/', true)) { michael@0: // \/ => / michael@0: output.push('/'); michael@0: } else if (stream.match('\\\\', true)) { michael@0: // \\ => \ michael@0: output.push('\\'); michael@0: } else { michael@0: // Don't change anything michael@0: output.push(stream.next()); michael@0: } michael@0: } michael@0: return output.join(''); michael@0: } michael@0: michael@0: /** michael@0: * Extract the regular expression from the query and return a Regexp object. michael@0: * Returns null if the query is blank. michael@0: * If ignoreCase is passed in, the Regexp object will have the 'i' flag set. michael@0: * If smartCase is passed in, and the query contains upper case letters, michael@0: * then ignoreCase is overridden, and the 'i' flag will not be set. michael@0: * If the query contains the /i in the flag part of the regular expression, michael@0: * then both ignoreCase and smartCase are ignored, and 'i' will be passed michael@0: * through to the Regex object. michael@0: */ michael@0: function parseQuery(query, ignoreCase, smartCase) { michael@0: // Check if the query is already a regex. michael@0: if (query instanceof RegExp) { return query; } michael@0: // First try to extract regex + flags from the input. If no flags found, michael@0: // extract just the regex. IE does not accept flags directly defined in michael@0: // the regex string in the form /regex/flags michael@0: var slashes = findUnescapedSlashes(query); michael@0: var regexPart; michael@0: var forceIgnoreCase; michael@0: if (!slashes.length) { michael@0: // Query looks like 'regexp' michael@0: regexPart = query; michael@0: } else { michael@0: // Query looks like 'regexp/...' michael@0: regexPart = query.substring(0, slashes[0]); michael@0: var flagsPart = query.substring(slashes[0]); michael@0: forceIgnoreCase = (flagsPart.indexOf('i') != -1); michael@0: } michael@0: if (!regexPart) { michael@0: return null; michael@0: } michael@0: if (!getOption('pcre')) { michael@0: regexPart = translateRegex(regexPart); michael@0: } michael@0: if (smartCase) { michael@0: ignoreCase = (/^[^A-Z]*$/).test(regexPart); michael@0: } michael@0: var regexp = new RegExp(regexPart, michael@0: (ignoreCase || forceIgnoreCase) ? 'i' : undefined); michael@0: return regexp; michael@0: } michael@0: function showConfirm(cm, text) { michael@0: if (cm.openNotification) { michael@0: cm.openNotification('' + text + '', michael@0: {bottom: true, duration: 5000}); michael@0: } else { michael@0: alert(text); michael@0: } michael@0: } michael@0: function makePrompt(prefix, desc) { michael@0: var raw = ''; michael@0: if (prefix) { michael@0: raw += '' + prefix + ''; michael@0: } michael@0: raw += ' ' + michael@0: ''; michael@0: if (desc) { michael@0: raw += ''; michael@0: raw += desc; michael@0: raw += ''; michael@0: } michael@0: return raw; michael@0: } michael@0: var searchPromptDesc = '(Javascript regexp)'; michael@0: function showPrompt(cm, options) { michael@0: var shortText = (options.prefix || '') + ' ' + (options.desc || ''); michael@0: var prompt = makePrompt(options.prefix, options.desc); michael@0: dialog(cm, prompt, shortText, options.onClose, options); michael@0: } michael@0: function regexEqual(r1, r2) { michael@0: if (r1 instanceof RegExp && r2 instanceof RegExp) { michael@0: var props = ['global', 'multiline', 'ignoreCase', 'source']; michael@0: for (var i = 0; i < props.length; i++) { michael@0: var prop = props[i]; michael@0: if (r1[prop] !== r2[prop]) { michael@0: return false; michael@0: } michael@0: } michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: // Returns true if the query is valid. michael@0: function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) { michael@0: if (!rawQuery) { michael@0: return; michael@0: } michael@0: var state = getSearchState(cm); michael@0: var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase); michael@0: if (!query) { michael@0: return; michael@0: } michael@0: highlightSearchMatches(cm, query); michael@0: if (regexEqual(query, state.getQuery())) { michael@0: return query; michael@0: } michael@0: state.setQuery(query); michael@0: return query; michael@0: } michael@0: function searchOverlay(query) { michael@0: if (query.source.charAt(0) == '^') { michael@0: var matchSol = true; michael@0: } michael@0: return { michael@0: token: function(stream) { michael@0: if (matchSol && !stream.sol()) { michael@0: stream.skipToEnd(); michael@0: return; michael@0: } michael@0: var match = stream.match(query, false); michael@0: if (match) { michael@0: if (match[0].length == 0) { michael@0: // Matched empty string, skip to next. michael@0: stream.next(); michael@0: return 'searching'; michael@0: } michael@0: if (!stream.sol()) { michael@0: // Backtrack 1 to match \b michael@0: stream.backUp(1); michael@0: if (!query.exec(stream.next() + match[0])) { michael@0: stream.next(); michael@0: return null; michael@0: } michael@0: } michael@0: stream.match(query); michael@0: return 'searching'; michael@0: } michael@0: while (!stream.eol()) { michael@0: stream.next(); michael@0: if (stream.match(query, false)) break; michael@0: } michael@0: }, michael@0: query: query michael@0: }; michael@0: } michael@0: function highlightSearchMatches(cm, query) { michael@0: var overlay = getSearchState(cm).getOverlay(); michael@0: if (!overlay || query != overlay.query) { michael@0: if (overlay) { michael@0: cm.removeOverlay(overlay); michael@0: } michael@0: overlay = searchOverlay(query); michael@0: cm.addOverlay(overlay); michael@0: getSearchState(cm).setOverlay(overlay); michael@0: } michael@0: } michael@0: function findNext(cm, prev, query, repeat) { michael@0: if (repeat === undefined) { repeat = 1; } michael@0: return cm.operation(function() { michael@0: var pos = cm.getCursor(); michael@0: var cursor = cm.getSearchCursor(query, pos); michael@0: for (var i = 0; i < repeat; i++) { michael@0: var found = cursor.find(prev); michael@0: if (i == 0 && found && cursorEqual(cursor.from(), pos)) { found = cursor.find(prev); } michael@0: if (!found) { michael@0: // SearchCursor may have returned null because it hit EOF, wrap michael@0: // around and try again. michael@0: cursor = cm.getSearchCursor(query, michael@0: (prev) ? Pos(cm.lastLine()) : Pos(cm.firstLine(), 0) ); michael@0: if (!cursor.find(prev)) { michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: return cursor.from(); michael@0: }); michael@0: } michael@0: function clearSearchHighlight(cm) { michael@0: cm.removeOverlay(getSearchState(cm).getOverlay()); michael@0: getSearchState(cm).setOverlay(null); michael@0: } michael@0: /** michael@0: * Check if pos is in the specified range, INCLUSIVE. michael@0: * Range can be specified with 1 or 2 arguments. michael@0: * If the first range argument is an array, treat it as an array of line michael@0: * numbers. Match pos against any of the lines. michael@0: * If the first range argument is a number, michael@0: * if there is only 1 range argument, check if pos has the same line michael@0: * number michael@0: * if there are 2 range arguments, then check if pos is in between the two michael@0: * range arguments. michael@0: */ michael@0: function isInRange(pos, start, end) { michael@0: if (typeof pos != 'number') { michael@0: // Assume it is a cursor position. Get the line number. michael@0: pos = pos.line; michael@0: } michael@0: if (start instanceof Array) { michael@0: return inArray(pos, start); michael@0: } else { michael@0: if (end) { michael@0: return (pos >= start && pos <= end); michael@0: } else { michael@0: return pos == start; michael@0: } michael@0: } michael@0: } michael@0: function getUserVisibleLines(cm) { michael@0: var scrollInfo = cm.getScrollInfo(); michael@0: var occludeToleranceTop = 6; michael@0: var occludeToleranceBottom = 10; michael@0: var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local'); michael@0: var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top; michael@0: var to = cm.coordsChar({left:0, top: bottomY}, 'local'); michael@0: return {top: from.line, bottom: to.line}; michael@0: } michael@0: michael@0: // Ex command handling michael@0: // Care must be taken when adding to the default Ex command map. For any michael@0: // pair of commands that have a shared prefix, at least one of their michael@0: // shortNames must not match the prefix of the other command. michael@0: var defaultExCommandMap = [ michael@0: { name: 'map' }, michael@0: { name: 'nmap', shortName: 'nm' }, michael@0: { name: 'vmap', shortName: 'vm' }, michael@0: { name: 'unmap' }, michael@0: { name: 'write', shortName: 'w' }, michael@0: { name: 'undo', shortName: 'u' }, michael@0: { name: 'redo', shortName: 'red' }, michael@0: { name: 'set', shortName: 'set' }, michael@0: { name: 'sort', shortName: 'sor' }, michael@0: { name: 'substitute', shortName: 's' }, michael@0: { name: 'nohlsearch', shortName: 'noh' }, michael@0: { name: 'delmarks', shortName: 'delm' }, michael@0: { name: 'registers', shortName: 'reg' } michael@0: ]; michael@0: Vim.ExCommandDispatcher = function() { michael@0: this.buildCommandMap_(); michael@0: }; michael@0: Vim.ExCommandDispatcher.prototype = { michael@0: processCommand: function(cm, input) { michael@0: var vim = cm.state.vim; michael@0: if (vim.visualMode) { michael@0: exitVisualMode(cm); michael@0: } michael@0: var inputStream = new CodeMirror.StringStream(input); michael@0: var params = {}; michael@0: params.input = input; michael@0: try { michael@0: this.parseInput_(cm, inputStream, params); michael@0: } catch(e) { michael@0: showConfirm(cm, e); michael@0: throw e; michael@0: } michael@0: var commandName; michael@0: if (!params.commandName) { michael@0: // If only a line range is defined, move to the line. michael@0: if (params.line !== undefined) { michael@0: commandName = 'move'; michael@0: } michael@0: } else { michael@0: var command = this.matchCommand_(params.commandName); michael@0: if (command) { michael@0: commandName = command.name; michael@0: this.parseCommandArgs_(inputStream, params, command); michael@0: if (command.type == 'exToKey') { michael@0: // Handle Ex to Key mapping. michael@0: for (var i = 0; i < command.toKeys.length; i++) { michael@0: CodeMirror.Vim.handleKey(cm, command.toKeys[i]); michael@0: } michael@0: return; michael@0: } else if (command.type == 'exToEx') { michael@0: // Handle Ex to Ex mapping. michael@0: this.processCommand(cm, command.toInput); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: if (!commandName) { michael@0: showConfirm(cm, 'Not an editor command ":' + input + '"'); michael@0: return; michael@0: } michael@0: try { michael@0: exCommands[commandName](cm, params); michael@0: } catch(e) { michael@0: showConfirm(cm, e); michael@0: throw e; michael@0: } michael@0: }, michael@0: parseInput_: function(cm, inputStream, result) { michael@0: inputStream.eatWhile(':'); michael@0: // Parse range. michael@0: if (inputStream.eat('%')) { michael@0: result.line = cm.firstLine(); michael@0: result.lineEnd = cm.lastLine(); michael@0: } else { michael@0: result.line = this.parseLineSpec_(cm, inputStream); michael@0: if (result.line !== undefined && inputStream.eat(',')) { michael@0: result.lineEnd = this.parseLineSpec_(cm, inputStream); michael@0: } michael@0: } michael@0: michael@0: // Parse command name. michael@0: var commandMatch = inputStream.match(/^(\w+)/); michael@0: if (commandMatch) { michael@0: result.commandName = commandMatch[1]; michael@0: } else { michael@0: result.commandName = inputStream.match(/.*/)[0]; michael@0: } michael@0: michael@0: return result; michael@0: }, michael@0: parseLineSpec_: function(cm, inputStream) { michael@0: var numberMatch = inputStream.match(/^(\d+)/); michael@0: if (numberMatch) { michael@0: return parseInt(numberMatch[1], 10) - 1; michael@0: } michael@0: switch (inputStream.next()) { michael@0: case '.': michael@0: return cm.getCursor().line; michael@0: case '$': michael@0: return cm.lastLine(); michael@0: case '\'': michael@0: var mark = cm.state.vim.marks[inputStream.next()]; michael@0: if (mark && mark.find()) { michael@0: return mark.find().line; michael@0: } michael@0: throw new Error('Mark not set'); michael@0: default: michael@0: inputStream.backUp(1); michael@0: return undefined; michael@0: } michael@0: }, michael@0: parseCommandArgs_: function(inputStream, params, command) { michael@0: if (inputStream.eol()) { michael@0: return; michael@0: } michael@0: params.argString = inputStream.match(/.*/)[0]; michael@0: // Parse command-line arguments michael@0: var delim = command.argDelimiter || /\s+/; michael@0: var args = trim(params.argString).split(delim); michael@0: if (args.length && args[0]) { michael@0: params.args = args; michael@0: } michael@0: }, michael@0: matchCommand_: function(commandName) { michael@0: // Return the command in the command map that matches the shortest michael@0: // prefix of the passed in command name. The match is guaranteed to be michael@0: // unambiguous if the defaultExCommandMap's shortNames are set up michael@0: // correctly. (see @code{defaultExCommandMap}). michael@0: for (var i = commandName.length; i > 0; i--) { michael@0: var prefix = commandName.substring(0, i); michael@0: if (this.commandMap_[prefix]) { michael@0: var command = this.commandMap_[prefix]; michael@0: if (command.name.indexOf(commandName) === 0) { michael@0: return command; michael@0: } michael@0: } michael@0: } michael@0: return null; michael@0: }, michael@0: buildCommandMap_: function() { michael@0: this.commandMap_ = {}; michael@0: for (var i = 0; i < defaultExCommandMap.length; i++) { michael@0: var command = defaultExCommandMap[i]; michael@0: var key = command.shortName || command.name; michael@0: this.commandMap_[key] = command; michael@0: } michael@0: }, michael@0: map: function(lhs, rhs, ctx) { michael@0: if (lhs != ':' && lhs.charAt(0) == ':') { michael@0: if (ctx) { throw Error('Mode not supported for ex mappings'); } michael@0: var commandName = lhs.substring(1); michael@0: if (rhs != ':' && rhs.charAt(0) == ':') { michael@0: // Ex to Ex mapping michael@0: this.commandMap_[commandName] = { michael@0: name: commandName, michael@0: type: 'exToEx', michael@0: toInput: rhs.substring(1), michael@0: user: true michael@0: }; michael@0: } else { michael@0: // Ex to key mapping michael@0: this.commandMap_[commandName] = { michael@0: name: commandName, michael@0: type: 'exToKey', michael@0: toKeys: parseKeyString(rhs), michael@0: user: true michael@0: }; michael@0: } michael@0: } else { michael@0: if (rhs != ':' && rhs.charAt(0) == ':') { michael@0: // Key to Ex mapping. michael@0: var mapping = { michael@0: keys: parseKeyString(lhs), michael@0: type: 'keyToEx', michael@0: exArgs: { input: rhs.substring(1) }, michael@0: user: true}; michael@0: if (ctx) { mapping.context = ctx; } michael@0: defaultKeymap.unshift(mapping); michael@0: } else { michael@0: // Key to key mapping michael@0: var mapping = { michael@0: keys: parseKeyString(lhs), michael@0: type: 'keyToKey', michael@0: toKeys: parseKeyString(rhs), michael@0: user: true michael@0: }; michael@0: if (ctx) { mapping.context = ctx; } michael@0: defaultKeymap.unshift(mapping); michael@0: } michael@0: } michael@0: }, michael@0: unmap: function(lhs, ctx) { michael@0: var arrayEquals = function(a, b) { michael@0: if (a === b) return true; michael@0: if (a == null || b == null) return true; michael@0: if (a.length != b.length) return false; michael@0: for (var i = 0; i < a.length; i++) { michael@0: if (a[i] !== b[i]) return false; michael@0: } michael@0: return true; michael@0: }; michael@0: if (lhs != ':' && lhs.charAt(0) == ':') { michael@0: // Ex to Ex or Ex to key mapping michael@0: if (ctx) { throw Error('Mode not supported for ex mappings'); } michael@0: var commandName = lhs.substring(1); michael@0: if (this.commandMap_[commandName] && this.commandMap_[commandName].user) { michael@0: delete this.commandMap_[commandName]; michael@0: return; michael@0: } michael@0: } else { michael@0: // Key to Ex or key to key mapping michael@0: var keys = parseKeyString(lhs); michael@0: for (var i = 0; i < defaultKeymap.length; i++) { michael@0: if (arrayEquals(keys, defaultKeymap[i].keys) michael@0: && defaultKeymap[i].context === ctx michael@0: && defaultKeymap[i].user) { michael@0: defaultKeymap.splice(i, 1); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: throw Error('No such mapping.'); michael@0: } michael@0: }; michael@0: michael@0: // Converts a key string sequence of the form abd into Vim's michael@0: // keymap representation. michael@0: function parseKeyString(str) { michael@0: var key, match; michael@0: var keys = []; michael@0: while (str) { michael@0: match = (/<\w+-.+?>|<\w+>|./).exec(str); michael@0: if (match === null)break; michael@0: key = match[0]; michael@0: str = str.substring(match.index + key.length); michael@0: keys.push(key); michael@0: } michael@0: return keys; michael@0: } michael@0: michael@0: var exCommands = { michael@0: map: function(cm, params, ctx) { michael@0: var mapArgs = params.args; michael@0: if (!mapArgs || mapArgs.length < 2) { michael@0: if (cm) { michael@0: showConfirm(cm, 'Invalid mapping: ' + params.input); michael@0: } michael@0: return; michael@0: } michael@0: exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx); michael@0: }, michael@0: nmap: function(cm, params) { this.map(cm, params, 'normal'); }, michael@0: vmap: function(cm, params) { this.map(cm, params, 'visual'); }, michael@0: unmap: function(cm, params, ctx) { michael@0: var mapArgs = params.args; michael@0: if (!mapArgs || mapArgs.length < 1) { michael@0: if (cm) { michael@0: showConfirm(cm, 'No such mapping: ' + params.input); michael@0: } michael@0: return; michael@0: } michael@0: exCommandDispatcher.unmap(mapArgs[0], ctx); michael@0: }, michael@0: move: function(cm, params) { michael@0: commandDispatcher.processCommand(cm, cm.state.vim, { michael@0: type: 'motion', michael@0: motion: 'moveToLineOrEdgeOfDocument', michael@0: motionArgs: { forward: false, explicitRepeat: true, michael@0: linewise: true }, michael@0: repeatOverride: params.line+1}); michael@0: }, michael@0: set: function(cm, params) { michael@0: var setArgs = params.args; michael@0: if (!setArgs || setArgs.length < 1) { michael@0: if (cm) { michael@0: showConfirm(cm, 'Invalid mapping: ' + params.input); michael@0: } michael@0: return; michael@0: } michael@0: var expr = setArgs[0].split('='); michael@0: var optionName = expr[0]; michael@0: var value = expr[1]; michael@0: var forceGet = false; michael@0: michael@0: if (optionName.charAt(optionName.length - 1) == '?') { michael@0: // If post-fixed with ?, then the set is actually a get. michael@0: if (value) { throw Error('Trailing characters: ' + params.argString); } michael@0: optionName = optionName.substring(0, optionName.length - 1); michael@0: forceGet = true; michael@0: } michael@0: if (value === undefined && optionName.substring(0, 2) == 'no') { michael@0: // To set boolean options to false, the option name is prefixed with michael@0: // 'no'. michael@0: optionName = optionName.substring(2); michael@0: value = false; michael@0: } michael@0: var optionIsBoolean = options[optionName] && options[optionName].type == 'boolean'; michael@0: if (optionIsBoolean && value == undefined) { michael@0: // Calling set with a boolean option sets it to true. michael@0: value = true; michael@0: } michael@0: if (!optionIsBoolean && !value || forceGet) { michael@0: var oldValue = getOption(optionName); michael@0: // If no value is provided, then we assume this is a get. michael@0: if (oldValue === true || oldValue === false) { michael@0: showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName); michael@0: } else { michael@0: showConfirm(cm, ' ' + optionName + '=' + oldValue); michael@0: } michael@0: } else { michael@0: setOption(optionName, value); michael@0: } michael@0: }, michael@0: registers: function(cm,params) { michael@0: var regArgs = params.args; michael@0: var registers = vimGlobalState.registerController.registers; michael@0: var regInfo = '----------Registers----------

'; michael@0: if (!regArgs) { michael@0: for (var registerName in registers) { michael@0: var text = registers[registerName].toString(); michael@0: if (text.length) { michael@0: regInfo += '"' + registerName + ' ' + text + '
'; michael@0: } michael@0: } michael@0: } else { michael@0: var registerName; michael@0: regArgs = regArgs.join(''); michael@0: for (var i = 0; i < regArgs.length; i++) { michael@0: registerName = regArgs.charAt(i); michael@0: if (!vimGlobalState.registerController.isValidRegister(registerName)) { michael@0: continue; michael@0: } michael@0: var register = registers[registerName] || new Register(); michael@0: regInfo += '"' + registerName + ' ' + register.text + '
'; michael@0: } michael@0: } michael@0: showConfirm(cm, regInfo); michael@0: }, michael@0: sort: function(cm, params) { michael@0: var reverse, ignoreCase, unique, number; michael@0: function parseArgs() { michael@0: if (params.argString) { michael@0: var args = new CodeMirror.StringStream(params.argString); michael@0: if (args.eat('!')) { reverse = true; } michael@0: if (args.eol()) { return; } michael@0: if (!args.eatSpace()) { return 'Invalid arguments'; } michael@0: var opts = args.match(/[a-z]+/); michael@0: if (opts) { michael@0: opts = opts[0]; michael@0: ignoreCase = opts.indexOf('i') != -1; michael@0: unique = opts.indexOf('u') != -1; michael@0: var decimal = opts.indexOf('d') != -1 && 1; michael@0: var hex = opts.indexOf('x') != -1 && 1; michael@0: var octal = opts.indexOf('o') != -1 && 1; michael@0: if (decimal + hex + octal > 1) { return 'Invalid arguments'; } michael@0: number = decimal && 'decimal' || hex && 'hex' || octal && 'octal'; michael@0: } michael@0: if (args.eatSpace() && args.match(/\/.*\//)) { 'patterns not supported'; } michael@0: } michael@0: } michael@0: var err = parseArgs(); michael@0: if (err) { michael@0: showConfirm(cm, err + ': ' + params.argString); michael@0: return; michael@0: } michael@0: var lineStart = params.line || cm.firstLine(); michael@0: var lineEnd = params.lineEnd || params.line || cm.lastLine(); michael@0: if (lineStart == lineEnd) { return; } michael@0: var curStart = Pos(lineStart, 0); michael@0: var curEnd = Pos(lineEnd, lineLength(cm, lineEnd)); michael@0: var text = cm.getRange(curStart, curEnd).split('\n'); michael@0: var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ : michael@0: (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i : michael@0: (number == 'octal') ? /([0-7]+)/ : null; michael@0: var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null; michael@0: var numPart = [], textPart = []; michael@0: if (number) { michael@0: for (var i = 0; i < text.length; i++) { michael@0: if (numberRegex.exec(text[i])) { michael@0: numPart.push(text[i]); michael@0: } else { michael@0: textPart.push(text[i]); michael@0: } michael@0: } michael@0: } else { michael@0: textPart = text; michael@0: } michael@0: function compareFn(a, b) { michael@0: if (reverse) { var tmp; tmp = a; a = b; b = tmp; } michael@0: if (ignoreCase) { a = a.toLowerCase(); b = b.toLowerCase(); } michael@0: var anum = number && numberRegex.exec(a); michael@0: var bnum = number && numberRegex.exec(b); michael@0: if (!anum) { return a < b ? -1 : 1; } michael@0: anum = parseInt((anum[1] + anum[2]).toLowerCase(), radix); michael@0: bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix); michael@0: return anum - bnum; michael@0: } michael@0: numPart.sort(compareFn); michael@0: textPart.sort(compareFn); michael@0: text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart); michael@0: if (unique) { // Remove duplicate lines michael@0: var textOld = text; michael@0: var lastLine; michael@0: text = []; michael@0: for (var i = 0; i < textOld.length; i++) { michael@0: if (textOld[i] != lastLine) { michael@0: text.push(textOld[i]); michael@0: } michael@0: lastLine = textOld[i]; michael@0: } michael@0: } michael@0: cm.replaceRange(text.join('\n'), curStart, curEnd); michael@0: }, michael@0: substitute: function(cm, params) { michael@0: if (!cm.getSearchCursor) { michael@0: throw new Error('Search feature not available. Requires searchcursor.js or ' + michael@0: 'any other getSearchCursor implementation.'); michael@0: } michael@0: var argString = params.argString; michael@0: var slashes = findUnescapedSlashes(argString); michael@0: if (slashes[0] !== 0) { michael@0: showConfirm(cm, 'Substitutions should be of the form ' + michael@0: ':s/pattern/replace/'); michael@0: return; michael@0: } michael@0: var regexPart = argString.substring(slashes[0] + 1, slashes[1]); michael@0: var replacePart = ''; michael@0: var flagsPart; michael@0: var count; michael@0: var confirm = false; // Whether to confirm each replace. michael@0: if (slashes[1]) { michael@0: replacePart = argString.substring(slashes[1] + 1, slashes[2]); michael@0: if (getOption('pcre')) { michael@0: replacePart = unescapeRegexReplace(replacePart); michael@0: } else { michael@0: replacePart = translateRegexReplace(replacePart); michael@0: } michael@0: } michael@0: if (slashes[2]) { michael@0: // After the 3rd slash, we can have flags followed by a space followed michael@0: // by count. michael@0: var trailing = argString.substring(slashes[2] + 1).split(' '); michael@0: flagsPart = trailing[0]; michael@0: count = parseInt(trailing[1]); michael@0: } michael@0: if (flagsPart) { michael@0: if (flagsPart.indexOf('c') != -1) { michael@0: confirm = true; michael@0: flagsPart.replace('c', ''); michael@0: } michael@0: regexPart = regexPart + '/' + flagsPart; michael@0: } michael@0: if (regexPart) { michael@0: // If regex part is empty, then use the previous query. Otherwise use michael@0: // the regex part as the new query. michael@0: try { michael@0: updateSearchQuery(cm, regexPart, true /** ignoreCase */, michael@0: true /** smartCase */); michael@0: } catch (e) { michael@0: showConfirm(cm, 'Invalid regex: ' + regexPart); michael@0: return; michael@0: } michael@0: } michael@0: var state = getSearchState(cm); michael@0: var query = state.getQuery(); michael@0: var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line; michael@0: var lineEnd = params.lineEnd || lineStart; michael@0: if (count) { michael@0: lineStart = lineEnd; michael@0: lineEnd = lineStart + count - 1; michael@0: } michael@0: var startPos = clipCursorToContent(cm, Pos(lineStart, 0)); michael@0: var cursor = cm.getSearchCursor(query, startPos); michael@0: doReplace(cm, confirm, lineStart, lineEnd, cursor, query, replacePart); michael@0: }, michael@0: redo: CodeMirror.commands.redo, michael@0: undo: CodeMirror.commands.undo, michael@0: write: function(cm) { michael@0: if (CodeMirror.commands.save) { michael@0: // If a save command is defined, call it. michael@0: CodeMirror.commands.save(cm); michael@0: } else { michael@0: // Saves to text area if no save command is defined. michael@0: cm.save(); michael@0: } michael@0: }, michael@0: nohlsearch: function(cm) { michael@0: clearSearchHighlight(cm); michael@0: }, michael@0: delmarks: function(cm, params) { michael@0: if (!params.argString || !trim(params.argString)) { michael@0: showConfirm(cm, 'Argument required'); michael@0: return; michael@0: } michael@0: michael@0: var state = cm.state.vim; michael@0: var stream = new CodeMirror.StringStream(trim(params.argString)); michael@0: while (!stream.eol()) { michael@0: stream.eatSpace(); michael@0: michael@0: // Record the streams position at the beginning of the loop for use michael@0: // in error messages. michael@0: var count = stream.pos; michael@0: michael@0: if (!stream.match(/[a-zA-Z]/, false)) { michael@0: showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); michael@0: return; michael@0: } michael@0: michael@0: var sym = stream.next(); michael@0: // Check if this symbol is part of a range michael@0: if (stream.match('-', true)) { michael@0: // This symbol is part of a range. michael@0: michael@0: // The range must terminate at an alphabetic character. michael@0: if (!stream.match(/[a-zA-Z]/, false)) { michael@0: showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); michael@0: return; michael@0: } michael@0: michael@0: var startMark = sym; michael@0: var finishMark = stream.next(); michael@0: // The range must terminate at an alphabetic character which michael@0: // shares the same case as the start of the range. michael@0: if (isLowerCase(startMark) && isLowerCase(finishMark) || michael@0: isUpperCase(startMark) && isUpperCase(finishMark)) { michael@0: var start = startMark.charCodeAt(0); michael@0: var finish = finishMark.charCodeAt(0); michael@0: if (start >= finish) { michael@0: showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); michael@0: return; michael@0: } michael@0: michael@0: // Because marks are always ASCII values, and we have michael@0: // determined that they are the same case, we can use michael@0: // their char codes to iterate through the defined range. michael@0: for (var j = 0; j <= finish - start; j++) { michael@0: var mark = String.fromCharCode(start + j); michael@0: delete state.marks[mark]; michael@0: } michael@0: } else { michael@0: showConfirm(cm, 'Invalid argument: ' + startMark + '-'); michael@0: return; michael@0: } michael@0: } else { michael@0: // This symbol is a valid mark, and is not part of a range. michael@0: delete state.marks[sym]; michael@0: } michael@0: } michael@0: } michael@0: }; michael@0: michael@0: var exCommandDispatcher = new Vim.ExCommandDispatcher(); michael@0: michael@0: /** michael@0: * @param {CodeMirror} cm CodeMirror instance we are in. michael@0: * @param {boolean} confirm Whether to confirm each replace. michael@0: * @param {Cursor} lineStart Line to start replacing from. michael@0: * @param {Cursor} lineEnd Line to stop replacing at. michael@0: * @param {RegExp} query Query for performing matches with. michael@0: * @param {string} replaceWith Text to replace matches with. May contain $1, michael@0: * $2, etc for replacing captured groups using Javascript replace. michael@0: */ michael@0: function doReplace(cm, confirm, lineStart, lineEnd, searchCursor, query, michael@0: replaceWith) { michael@0: // Set up all the functions. michael@0: cm.state.vim.exMode = true; michael@0: var done = false; michael@0: var lastPos = searchCursor.from(); michael@0: function replaceAll() { michael@0: cm.operation(function() { michael@0: while (!done) { michael@0: replace(); michael@0: next(); michael@0: } michael@0: stop(); michael@0: }); michael@0: } michael@0: function replace() { michael@0: var text = cm.getRange(searchCursor.from(), searchCursor.to()); michael@0: var newText = text.replace(query, replaceWith); michael@0: searchCursor.replace(newText); michael@0: } michael@0: function next() { michael@0: var found = searchCursor.findNext(); michael@0: if (!found) { michael@0: done = true; michael@0: } else if (isInRange(searchCursor.from(), lineStart, lineEnd)) { michael@0: cm.scrollIntoView(searchCursor.from(), 30); michael@0: cm.setSelection(searchCursor.from(), searchCursor.to()); michael@0: lastPos = searchCursor.from(); michael@0: done = false; michael@0: } else { michael@0: done = true; michael@0: } michael@0: } michael@0: function stop(close) { michael@0: if (close) { close(); } michael@0: cm.focus(); michael@0: if (lastPos) { michael@0: cm.setCursor(lastPos); michael@0: var vim = cm.state.vim; michael@0: vim.exMode = false; michael@0: vim.lastHPos = vim.lastHSPos = lastPos.ch; michael@0: } michael@0: } michael@0: function onPromptKeyDown(e, _value, close) { michael@0: // Swallow all keys. michael@0: CodeMirror.e_stop(e); michael@0: var keyName = CodeMirror.keyName(e); michael@0: switch (keyName) { michael@0: case 'Y': michael@0: replace(); next(); break; michael@0: case 'N': michael@0: next(); break; michael@0: case 'A': michael@0: cm.operation(replaceAll); break; michael@0: case 'L': michael@0: replace(); michael@0: // fall through and exit. michael@0: case 'Q': michael@0: case 'Esc': michael@0: case 'Ctrl-C': michael@0: case 'Ctrl-[': michael@0: stop(close); michael@0: break; michael@0: } michael@0: if (done) { stop(close); } michael@0: } michael@0: michael@0: // Actually do replace. michael@0: next(); michael@0: if (done) { michael@0: showConfirm(cm, 'No matches for ' + query.source); michael@0: return; michael@0: } michael@0: if (!confirm) { michael@0: replaceAll(); michael@0: return; michael@0: } michael@0: showPrompt(cm, { michael@0: prefix: 'replace with ' + replaceWith + ' (y/n/a/q/l)', michael@0: onKeyDown: onPromptKeyDown michael@0: }); michael@0: } michael@0: michael@0: // Register Vim with CodeMirror michael@0: function buildVimKeyMap() { michael@0: /** michael@0: * Handle the raw key event from CodeMirror. Translate the michael@0: * Shift + key modifier to the resulting letter, while preserving other michael@0: * modifers. michael@0: */ michael@0: function cmKeyToVimKey(key, modifier) { michael@0: var vimKey = key; michael@0: if (isUpperCase(vimKey) && modifier == 'Ctrl') { michael@0: vimKey = vimKey.toLowerCase(); michael@0: } michael@0: if (modifier) { michael@0: // Vim will parse modifier+key combination as a single key. michael@0: vimKey = modifier.charAt(0) + '-' + vimKey; michael@0: } michael@0: var specialKey = ({Enter:'CR',Backspace:'BS',Delete:'Del'})[vimKey]; michael@0: vimKey = specialKey ? specialKey : vimKey; michael@0: vimKey = vimKey.length > 1 ? '<'+ vimKey + '>' : vimKey; michael@0: return vimKey; michael@0: } michael@0: michael@0: // Closure to bind CodeMirror, key, modifier. michael@0: function keyMapper(vimKey) { michael@0: return function(cm) { michael@0: CodeMirror.Vim.handleKey(cm, vimKey); michael@0: }; michael@0: } michael@0: michael@0: var cmToVimKeymap = { michael@0: 'nofallthrough': true, michael@0: 'style': 'fat-cursor' michael@0: }; michael@0: function bindKeys(keys, modifier) { michael@0: for (var i = 0; i < keys.length; i++) { michael@0: var key = keys[i]; michael@0: if (!modifier && key.length == 1) { michael@0: // Wrap all keys without modifiers with '' to identify them by their michael@0: // key characters instead of key identifiers. michael@0: key = "'" + key + "'"; michael@0: } michael@0: var vimKey = cmKeyToVimKey(keys[i], modifier); michael@0: var cmKey = modifier ? modifier + '-' + key : key; michael@0: cmToVimKeymap[cmKey] = keyMapper(vimKey); michael@0: } michael@0: } michael@0: bindKeys(upperCaseAlphabet); michael@0: bindKeys(lowerCaseAlphabet); michael@0: bindKeys(upperCaseAlphabet, 'Ctrl'); michael@0: bindKeys(specialSymbols); michael@0: bindKeys(specialSymbols, 'Ctrl'); michael@0: bindKeys(numbers); michael@0: bindKeys(numbers, 'Ctrl'); michael@0: bindKeys(specialKeys); michael@0: bindKeys(specialKeys, 'Ctrl'); michael@0: return cmToVimKeymap; michael@0: } michael@0: CodeMirror.keyMap.vim = buildVimKeyMap(); michael@0: michael@0: function exitInsertMode(cm) { michael@0: var vim = cm.state.vim; michael@0: var macroModeState = vimGlobalState.macroModeState; michael@0: var isPlaying = macroModeState.isPlaying; michael@0: if (!isPlaying) { michael@0: cm.off('change', onChange); michael@0: cm.off('cursorActivity', onCursorActivity); michael@0: CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); michael@0: } michael@0: if (!isPlaying && vim.insertModeRepeat > 1) { michael@0: // Perform insert mode repeat for commands like 3,a and 3,o. michael@0: repeatLastEdit(cm, vim, vim.insertModeRepeat - 1, michael@0: true /** repeatForInsert */); michael@0: vim.lastEditInputState.repeatOverride = vim.insertModeRepeat; michael@0: } michael@0: delete vim.insertModeRepeat; michael@0: cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1); michael@0: vim.insertMode = false; michael@0: cm.setOption('keyMap', 'vim'); michael@0: cm.setOption('disableInput', true); michael@0: cm.toggleOverwrite(false); // exit replace mode if we were in it. michael@0: CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); michael@0: if (macroModeState.isRecording) { michael@0: logInsertModeChange(macroModeState); michael@0: } michael@0: } michael@0: michael@0: CodeMirror.keyMap['vim-insert'] = { michael@0: // TODO: override navigation keys so that Esc will cancel automatic michael@0: // indentation from o, O, i_ michael@0: 'Esc': exitInsertMode, michael@0: 'Ctrl-[': exitInsertMode, michael@0: 'Ctrl-C': exitInsertMode, michael@0: 'Ctrl-N': 'autocomplete', michael@0: 'Ctrl-P': 'autocomplete', michael@0: 'Enter': function(cm) { michael@0: var fn = CodeMirror.commands.newlineAndIndentContinueComment || michael@0: CodeMirror.commands.newlineAndIndent; michael@0: fn(cm); michael@0: }, michael@0: fallthrough: ['default'] michael@0: }; michael@0: michael@0: CodeMirror.keyMap['vim-replace'] = { michael@0: 'Backspace': 'goCharLeft', michael@0: fallthrough: ['vim-insert'] michael@0: }; michael@0: michael@0: function executeMacroRegister(cm, vim, macroModeState, registerName) { michael@0: var register = vimGlobalState.registerController.getRegister(registerName); michael@0: var keyBuffer = register.keyBuffer; michael@0: var imc = 0; michael@0: macroModeState.isPlaying = true; michael@0: for (var i = 0; i < keyBuffer.length; i++) { michael@0: var text = keyBuffer[i]; michael@0: var match, key; michael@0: while (text) { michael@0: // Pull off one command key, which is either a single character michael@0: // or a special sequence wrapped in '<' and '>', e.g. ''. michael@0: match = (/<\w+-.+?>|<\w+>|./).exec(text); michael@0: key = match[0]; michael@0: text = text.substring(match.index + key.length); michael@0: CodeMirror.Vim.handleKey(cm, key); michael@0: if (vim.insertMode) { michael@0: repeatInsertModeChanges( michael@0: cm, register.insertModeChanges[imc++].changes, 1); michael@0: exitInsertMode(cm); michael@0: } michael@0: } michael@0: }; michael@0: macroModeState.isPlaying = false; michael@0: } michael@0: michael@0: function logKey(macroModeState, key) { michael@0: if (macroModeState.isPlaying) { return; } michael@0: var registerName = macroModeState.latestRegister; michael@0: var register = vimGlobalState.registerController.getRegister(registerName); michael@0: if (register) { michael@0: register.pushText(key); michael@0: } michael@0: } michael@0: michael@0: function logInsertModeChange(macroModeState) { michael@0: if (macroModeState.isPlaying) { return; } michael@0: var registerName = macroModeState.latestRegister; michael@0: var register = vimGlobalState.registerController.getRegister(registerName); michael@0: if (register) { michael@0: register.pushInsertModeChanges(macroModeState.lastInsertModeChanges); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Listens for changes made in insert mode. michael@0: * Should only be active in insert mode. michael@0: */ michael@0: function onChange(_cm, changeObj) { michael@0: var macroModeState = vimGlobalState.macroModeState; michael@0: var lastChange = macroModeState.lastInsertModeChanges; michael@0: if (!macroModeState.isPlaying) { michael@0: while(changeObj) { michael@0: lastChange.expectCursorActivityForChange = true; michael@0: if (changeObj.origin == '+input' || changeObj.origin == 'paste' michael@0: || changeObj.origin === undefined /* only in testing */) { michael@0: var text = changeObj.text.join('\n'); michael@0: lastChange.changes.push(text); michael@0: } michael@0: // Change objects may be chained with next. michael@0: changeObj = changeObj.next; michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Listens for any kind of cursor activity on CodeMirror. michael@0: * - For tracking cursor activity in insert mode. michael@0: * - Should only be active in insert mode. michael@0: */ michael@0: function onCursorActivity() { michael@0: var macroModeState = vimGlobalState.macroModeState; michael@0: if (macroModeState.isPlaying) { return; } michael@0: var lastChange = macroModeState.lastInsertModeChanges; michael@0: if (lastChange.expectCursorActivityForChange) { michael@0: lastChange.expectCursorActivityForChange = false; michael@0: } else { michael@0: // Cursor moved outside the context of an edit. Reset the change. michael@0: lastChange.changes = []; michael@0: } michael@0: } michael@0: michael@0: /** Wrapper for special keys pressed in insert mode */ michael@0: function InsertModeKey(keyName) { michael@0: this.keyName = keyName; michael@0: } michael@0: michael@0: /** michael@0: * Handles raw key down events from the text area. michael@0: * - Should only be active in insert mode. michael@0: * - For recording deletes in insert mode. michael@0: */ michael@0: function onKeyEventTargetKeyDown(e) { michael@0: var macroModeState = vimGlobalState.macroModeState; michael@0: var lastChange = macroModeState.lastInsertModeChanges; michael@0: var keyName = CodeMirror.keyName(e); michael@0: function onKeyFound() { michael@0: lastChange.changes.push(new InsertModeKey(keyName)); michael@0: return true; michael@0: } michael@0: if (keyName.indexOf('Delete') != -1 || keyName.indexOf('Backspace') != -1) { michael@0: CodeMirror.lookupKey(keyName, ['vim-insert'], onKeyFound); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Repeats the last edit, which includes exactly 1 command and at most 1 michael@0: * insert. Operator and motion commands are read from lastEditInputState, michael@0: * while action commands are read from lastEditActionCommand. michael@0: * michael@0: * If repeatForInsert is true, then the function was called by michael@0: * exitInsertMode to repeat the insert mode changes the user just made. The michael@0: * corresponding enterInsertMode call was made with a count. michael@0: */ michael@0: function repeatLastEdit(cm, vim, repeat, repeatForInsert) { michael@0: var macroModeState = vimGlobalState.macroModeState; michael@0: macroModeState.isPlaying = true; michael@0: var isAction = !!vim.lastEditActionCommand; michael@0: var cachedInputState = vim.inputState; michael@0: function repeatCommand() { michael@0: if (isAction) { michael@0: commandDispatcher.processAction(cm, vim, vim.lastEditActionCommand); michael@0: } else { michael@0: commandDispatcher.evalInput(cm, vim); michael@0: } michael@0: } michael@0: function repeatInsert(repeat) { michael@0: if (macroModeState.lastInsertModeChanges.changes.length > 0) { michael@0: // For some reason, repeat cw in desktop VIM does not repeat michael@0: // insert mode changes. Will conform to that behavior. michael@0: repeat = !vim.lastEditActionCommand ? 1 : repeat; michael@0: var changeObject = macroModeState.lastInsertModeChanges; michael@0: // This isn't strictly necessary, but since lastInsertModeChanges is michael@0: // supposed to be immutable during replay, this helps catch bugs. michael@0: macroModeState.lastInsertModeChanges = {}; michael@0: repeatInsertModeChanges(cm, changeObject.changes, repeat); michael@0: macroModeState.lastInsertModeChanges = changeObject; michael@0: } michael@0: } michael@0: vim.inputState = vim.lastEditInputState; michael@0: if (isAction && vim.lastEditActionCommand.interlaceInsertRepeat) { michael@0: // o and O repeat have to be interlaced with insert repeats so that the michael@0: // insertions appear on separate lines instead of the last line. michael@0: for (var i = 0; i < repeat; i++) { michael@0: repeatCommand(); michael@0: repeatInsert(1); michael@0: } michael@0: } else { michael@0: if (!repeatForInsert) { michael@0: // Hack to get the cursor to end up at the right place. If I is michael@0: // repeated in insert mode repeat, cursor will be 1 insert michael@0: // change set left of where it should be. michael@0: repeatCommand(); michael@0: } michael@0: repeatInsert(repeat); michael@0: } michael@0: vim.inputState = cachedInputState; michael@0: if (vim.insertMode && !repeatForInsert) { michael@0: // Don't exit insert mode twice. If repeatForInsert is set, then we michael@0: // were called by an exitInsertMode call lower on the stack. michael@0: exitInsertMode(cm); michael@0: } michael@0: macroModeState.isPlaying = false; michael@0: }; michael@0: michael@0: function repeatInsertModeChanges(cm, changes, repeat) { michael@0: function keyHandler(binding) { michael@0: if (typeof binding == 'string') { michael@0: CodeMirror.commands[binding](cm); michael@0: } else { michael@0: binding(cm); michael@0: } michael@0: return true; michael@0: } michael@0: for (var i = 0; i < repeat; i++) { michael@0: for (var j = 0; j < changes.length; j++) { michael@0: var change = changes[j]; michael@0: if (change instanceof InsertModeKey) { michael@0: CodeMirror.lookupKey(change.keyName, ['vim-insert'], keyHandler); michael@0: } else { michael@0: var cur = cm.getCursor(); michael@0: cm.replaceRange(change, cur, cur); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: resetVimGlobalState(); michael@0: return vimApi; michael@0: }; michael@0: // Initialize Vim and make it available as an API. michael@0: CodeMirror.Vim = Vim(); michael@0: });