Path: blob/master/web-gui/buildyourownbotnet/assets/js/codemirror/keymap/vim.js
1293 views
/**1* Supported keybindings:2*3* Motion:4* h, j, k, l5* gj, gk6* e, E, w, W, b, B, ge, gE7* f<character>, F<character>, t<character>, T<character>8* $, ^, 0, -, +, _9* gg, G10* %11* '<character>, `<character>12*13* Operator:14* d, y, c15* dd, yy, cc16* g~, g~g~17* >, <, >>, <<18*19* Operator-Motion:20* x, X, D, Y, C, ~21*22* Action:23* a, i, s, A, I, S, o, O24* zz, z., z<CR>, zt, zb, z-25* J26* u, Ctrl-r27* m<character>28* r<character>29*30* Modes:31* ESC - leave insert mode, visual mode, and clear input state.32* Ctrl-[, Ctrl-c - same as ESC.33*34* Registers: unamed, -, a-z, A-Z, 0-935* (Does not respect the special case for number registers when delete36* operator is made with these commands: %, (, ), , /, ?, n, N, {, } )37* TODO: Implement the remaining registers.38* Marks: a-z, A-Z, and 0-939* TODO: Implement the remaining special marks. They have more complex40* behavior.41*42* Events:43* 'vim-mode-change' - raised on the editor anytime the current mode changes,44* Event object: {mode: "visual", subMode: "linewise"}45*46* Code structure:47* 1. Default keymap48* 2. Variable declarations and short basic helpers49* 3. Instance (External API) implementation50* 4. Internal state tracking objects (input state, counter) implementation51* and instanstiation52* 5. Key handler (the main command dispatcher) implementation53* 6. Motion, operator, and action implementations54* 7. Helper functions for the key handler, motions, operators, and actions55* 8. Set up Vim to work as a keymap for CodeMirror.56*/5758(function() {59'use strict';6061var defaultKeymap = [62// Key to key mapping. This goes first to make it possible to override63// existing mappings.64{ keys: ['<Left>'], type: 'keyToKey', toKeys: ['h'] },65{ keys: ['<Right>'], type: 'keyToKey', toKeys: ['l'] },66{ keys: ['<Up>'], type: 'keyToKey', toKeys: ['k'] },67{ keys: ['<Down>'], type: 'keyToKey', toKeys: ['j'] },68{ keys: ['<Space>'], type: 'keyToKey', toKeys: ['l'] },69{ keys: ['<BS>'], type: 'keyToKey', toKeys: ['h'] },70{ keys: ['<C-Space>'], type: 'keyToKey', toKeys: ['W'] },71{ keys: ['<C-BS>'], type: 'keyToKey', toKeys: ['B'] },72{ keys: ['<S-Space>'], type: 'keyToKey', toKeys: ['w'] },73{ keys: ['<S-BS>'], type: 'keyToKey', toKeys: ['b'] },74{ keys: ['<C-n>'], type: 'keyToKey', toKeys: ['j'] },75{ keys: ['<C-p>'], type: 'keyToKey', toKeys: ['k'] },76{ keys: ['C-['], type: 'keyToKey', toKeys: ['<Esc>'] },77{ keys: ['<C-c>'], type: 'keyToKey', toKeys: ['<Esc>'] },78{ keys: ['s'], type: 'keyToKey', toKeys: ['c', 'l'], context: 'normal' },79{ keys: ['s'], type: 'keyToKey', toKeys: ['x', 'i'], context: 'visual'},80{ keys: ['S'], type: 'keyToKey', toKeys: ['c', 'c'], context: 'normal' },81{ keys: ['S'], type: 'keyToKey', toKeys: ['d', 'c', 'c'], context: 'visual' },82{ keys: ['<Home>'], type: 'keyToKey', toKeys: ['0'] },83{ keys: ['<End>'], type: 'keyToKey', toKeys: ['$'] },84{ keys: ['<PageUp>'], type: 'keyToKey', toKeys: ['<C-b>'] },85{ keys: ['<PageDown>'], type: 'keyToKey', toKeys: ['<C-f>'] },86{ keys: ['<CR>'], type: 'keyToKey', toKeys: ['j', '^'], context: 'normal' },87// Motions88{ keys: ['H'], type: 'motion',89motion: 'moveToTopLine',90motionArgs: { linewise: true, toJumplist: true }},91{ keys: ['M'], type: 'motion',92motion: 'moveToMiddleLine',93motionArgs: { linewise: true, toJumplist: true }},94{ keys: ['L'], type: 'motion',95motion: 'moveToBottomLine',96motionArgs: { linewise: true, toJumplist: true }},97{ keys: ['h'], type: 'motion',98motion: 'moveByCharacters',99motionArgs: { forward: false }},100{ keys: ['l'], type: 'motion',101motion: 'moveByCharacters',102motionArgs: { forward: true }},103{ keys: ['j'], type: 'motion',104motion: 'moveByLines',105motionArgs: { forward: true, linewise: true }},106{ keys: ['k'], type: 'motion',107motion: 'moveByLines',108motionArgs: { forward: false, linewise: true }},109{ keys: ['g','j'], type: 'motion',110motion: 'moveByDisplayLines',111motionArgs: { forward: true }},112{ keys: ['g','k'], type: 'motion',113motion: 'moveByDisplayLines',114motionArgs: { forward: false }},115{ keys: ['w'], type: 'motion',116motion: 'moveByWords',117motionArgs: { forward: true, wordEnd: false }},118{ keys: ['W'], type: 'motion',119motion: 'moveByWords',120motionArgs: { forward: true, wordEnd: false, bigWord: true }},121{ keys: ['e'], type: 'motion',122motion: 'moveByWords',123motionArgs: { forward: true, wordEnd: true, inclusive: true }},124{ keys: ['E'], type: 'motion',125motion: 'moveByWords',126motionArgs: { forward: true, wordEnd: true, bigWord: true,127inclusive: true }},128{ keys: ['b'], type: 'motion',129motion: 'moveByWords',130motionArgs: { forward: false, wordEnd: false }},131{ keys: ['B'], type: 'motion',132motion: 'moveByWords',133motionArgs: { forward: false, wordEnd: false, bigWord: true }},134{ keys: ['g', 'e'], type: 'motion',135motion: 'moveByWords',136motionArgs: { forward: false, wordEnd: true, inclusive: true }},137{ keys: ['g', 'E'], type: 'motion',138motion: 'moveByWords',139motionArgs: { forward: false, wordEnd: true, bigWord: true,140inclusive: true }},141{ keys: ['{'], type: 'motion', motion: 'moveByParagraph',142motionArgs: { forward: false, toJumplist: true }},143{ keys: ['}'], type: 'motion', motion: 'moveByParagraph',144motionArgs: { forward: true, toJumplist: true }},145{ keys: ['<C-f>'], type: 'motion',146motion: 'moveByPage', motionArgs: { forward: true }},147{ keys: ['<C-b>'], type: 'motion',148motion: 'moveByPage', motionArgs: { forward: false }},149{ keys: ['<C-d>'], type: 'motion',150motion: 'moveByScroll',151motionArgs: { forward: true, explicitRepeat: true }},152{ keys: ['<C-u>'], type: 'motion',153motion: 'moveByScroll',154motionArgs: { forward: false, explicitRepeat: true }},155{ keys: ['g', 'g'], type: 'motion',156motion: 'moveToLineOrEdgeOfDocument',157motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }},158{ keys: ['G'], type: 'motion',159motion: 'moveToLineOrEdgeOfDocument',160motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }},161{ keys: ['0'], type: 'motion', motion: 'moveToStartOfLine' },162{ keys: ['^'], type: 'motion',163motion: 'moveToFirstNonWhiteSpaceCharacter' },164{ keys: ['+'], type: 'motion',165motion: 'moveByLines',166motionArgs: { forward: true, toFirstChar:true }},167{ keys: ['-'], type: 'motion',168motion: 'moveByLines',169motionArgs: { forward: false, toFirstChar:true }},170{ keys: ['_'], type: 'motion',171motion: 'moveByLines',172motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }},173{ keys: ['$'], type: 'motion',174motion: 'moveToEol',175motionArgs: { inclusive: true }},176{ keys: ['%'], type: 'motion',177motion: 'moveToMatchedSymbol',178motionArgs: { inclusive: true, toJumplist: true }},179{ keys: ['f', 'character'], type: 'motion',180motion: 'moveToCharacter',181motionArgs: { forward: true , inclusive: true }},182{ keys: ['F', 'character'], type: 'motion',183motion: 'moveToCharacter',184motionArgs: { forward: false }},185{ keys: ['t', 'character'], type: 'motion',186motion: 'moveTillCharacter',187motionArgs: { forward: true, inclusive: true }},188{ keys: ['T', 'character'], type: 'motion',189motion: 'moveTillCharacter',190motionArgs: { forward: false }},191{ keys: [';'], type: 'motion', motion: 'repeatLastCharacterSearch',192motionArgs: { forward: true }},193{ keys: [','], type: 'motion', motion: 'repeatLastCharacterSearch',194motionArgs: { forward: false }},195{ keys: ['\'', 'character'], type: 'motion', motion: 'goToMark',196motionArgs: {toJumplist: true}},197{ keys: ['`', 'character'], type: 'motion', motion: 'goToMark',198motionArgs: {toJumplist: true}},199{ keys: [']', '`'], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } },200{ keys: ['[', '`'], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } },201{ keys: [']', '\''], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } },202{ keys: ['[', '\''], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } },203{ keys: [']', 'character'], type: 'motion',204motion: 'moveToSymbol',205motionArgs: { forward: true, toJumplist: true}},206{ keys: ['[', 'character'], type: 'motion',207motion: 'moveToSymbol',208motionArgs: { forward: false, toJumplist: true}},209{ keys: ['|'], type: 'motion',210motion: 'moveToColumn',211motionArgs: { }},212// Operators213{ keys: ['d'], type: 'operator', operator: 'delete' },214{ keys: ['y'], type: 'operator', operator: 'yank' },215{ keys: ['c'], type: 'operator', operator: 'change' },216{ keys: ['>'], type: 'operator', operator: 'indent',217operatorArgs: { indentRight: true }},218{ keys: ['<'], type: 'operator', operator: 'indent',219operatorArgs: { indentRight: false }},220{ keys: ['g', '~'], type: 'operator', operator: 'swapcase' },221{ keys: ['n'], type: 'motion', motion: 'findNext',222motionArgs: { forward: true, toJumplist: true }},223{ keys: ['N'], type: 'motion', motion: 'findNext',224motionArgs: { forward: false, toJumplist: true }},225// Operator-Motion dual commands226{ keys: ['x'], type: 'operatorMotion', operator: 'delete',227motion: 'moveByCharacters', motionArgs: { forward: true },228operatorMotionArgs: { visualLine: false }},229{ keys: ['X'], type: 'operatorMotion', operator: 'delete',230motion: 'moveByCharacters', motionArgs: { forward: false },231operatorMotionArgs: { visualLine: true }},232{ keys: ['D'], type: 'operatorMotion', operator: 'delete',233motion: 'moveToEol', motionArgs: { inclusive: true },234operatorMotionArgs: { visualLine: true }},235{ keys: ['Y'], type: 'operatorMotion', operator: 'yank',236motion: 'moveToEol', motionArgs: { inclusive: true },237operatorMotionArgs: { visualLine: true }},238{ keys: ['C'], type: 'operatorMotion',239operator: 'change',240motion: 'moveToEol', motionArgs: { inclusive: true },241operatorMotionArgs: { visualLine: true }},242{ keys: ['~'], type: 'operatorMotion',243operator: 'swapcase', operatorArgs: { shouldMoveCursor: true },244motion: 'moveByCharacters', motionArgs: { forward: true }},245// Actions246{ keys: ['<C-i>'], type: 'action', action: 'jumpListWalk',247actionArgs: { forward: true }},248{ keys: ['<C-o>'], type: 'action', action: 'jumpListWalk',249actionArgs: { forward: false }},250{ keys: ['<C-e>'], type: 'action',251action: 'scroll',252actionArgs: { forward: true, linewise: true }},253{ keys: ['<C-y>'], type: 'action',254action: 'scroll',255actionArgs: { forward: false, linewise: true }},256{ keys: ['a'], type: 'action', action: 'enterInsertMode', isEdit: true,257actionArgs: { insertAt: 'charAfter' }},258{ keys: ['A'], type: 'action', action: 'enterInsertMode', isEdit: true,259actionArgs: { insertAt: 'eol' }},260{ keys: ['i'], type: 'action', action: 'enterInsertMode', isEdit: true,261actionArgs: { insertAt: 'inplace' }},262{ keys: ['I'], type: 'action', action: 'enterInsertMode', isEdit: true,263actionArgs: { insertAt: 'firstNonBlank' }},264{ keys: ['o'], type: 'action', action: 'newLineAndEnterInsertMode',265isEdit: true, interlaceInsertRepeat: true,266actionArgs: { after: true }},267{ keys: ['O'], type: 'action', action: 'newLineAndEnterInsertMode',268isEdit: true, interlaceInsertRepeat: true,269actionArgs: { after: false }},270{ keys: ['v'], type: 'action', action: 'toggleVisualMode' },271{ keys: ['V'], type: 'action', action: 'toggleVisualMode',272actionArgs: { linewise: true }},273{ keys: ['J'], type: 'action', action: 'joinLines', isEdit: true },274{ keys: ['p'], type: 'action', action: 'paste', isEdit: true,275actionArgs: { after: true, isEdit: true }},276{ keys: ['P'], type: 'action', action: 'paste', isEdit: true,277actionArgs: { after: false, isEdit: true }},278{ keys: ['r', 'character'], type: 'action', action: 'replace', isEdit: true },279{ keys: ['@', 'character'], type: 'action', action: 'replayMacro' },280{ keys: ['q', 'character'], type: 'action', action: 'enterMacroRecordMode' },281// Handle Replace-mode as a special case of insert mode.282{ keys: ['R'], type: 'action', action: 'enterInsertMode', isEdit: true,283actionArgs: { replace: true }},284{ keys: ['u'], type: 'action', action: 'undo' },285{ keys: ['<C-r>'], type: 'action', action: 'redo' },286{ keys: ['m', 'character'], type: 'action', action: 'setMark' },287{ keys: ['"', 'character'], type: 'action', action: 'setRegister' },288{ keys: ['z', 'z'], type: 'action', action: 'scrollToCursor',289actionArgs: { position: 'center' }},290{ keys: ['z', '.'], type: 'action', action: 'scrollToCursor',291actionArgs: { position: 'center' },292motion: 'moveToFirstNonWhiteSpaceCharacter' },293{ keys: ['z', 't'], type: 'action', action: 'scrollToCursor',294actionArgs: { position: 'top' }},295{ keys: ['z', '<CR>'], type: 'action', action: 'scrollToCursor',296actionArgs: { position: 'top' },297motion: 'moveToFirstNonWhiteSpaceCharacter' },298{ keys: ['z', '-'], type: 'action', action: 'scrollToCursor',299actionArgs: { position: 'bottom' }},300{ keys: ['z', 'b'], type: 'action', action: 'scrollToCursor',301actionArgs: { position: 'bottom' },302motion: 'moveToFirstNonWhiteSpaceCharacter' },303{ keys: ['.'], type: 'action', action: 'repeatLastEdit' },304{ keys: ['<C-a>'], type: 'action', action: 'incrementNumberToken',305isEdit: true,306actionArgs: {increase: true, backtrack: false}},307{ keys: ['<C-x>'], type: 'action', action: 'incrementNumberToken',308isEdit: true,309actionArgs: {increase: false, backtrack: false}},310// Text object motions311{ keys: ['a', 'character'], type: 'motion',312motion: 'textObjectManipulation' },313{ keys: ['i', 'character'], type: 'motion',314motion: 'textObjectManipulation',315motionArgs: { textObjectInner: true }},316// Search317{ keys: ['/'], type: 'search',318searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }},319{ keys: ['?'], type: 'search',320searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }},321{ keys: ['*'], type: 'search',322searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }},323{ keys: ['#'], type: 'search',324searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }},325// Ex command326{ keys: [':'], type: 'ex' }327];328329var Vim = function() {330CodeMirror.defineOption('vimMode', false, function(cm, val) {331if (val) {332cm.setOption('keyMap', 'vim');333cm.setOption('disableInput', true);334CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});335cm.on('beforeSelectionChange', beforeSelectionChange);336maybeInitVimState(cm);337CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm));338} else if (cm.state.vim) {339cm.setOption('keyMap', 'default');340cm.setOption('disableInput', false);341cm.off('beforeSelectionChange', beforeSelectionChange);342CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm));343cm.state.vim = null;344}345});346function beforeSelectionChange(cm, cur) {347var vim = cm.state.vim;348if (vim.insertMode || vim.exMode) return;349350var head = cur.head;351if (head.ch && head.ch == cm.doc.getLine(head.line).length) {352head.ch--;353}354}355function getOnPasteFn(cm) {356var vim = cm.state.vim;357if (!vim.onPasteFn) {358vim.onPasteFn = function() {359if (!vim.insertMode) {360cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));361actions.enterInsertMode(cm, {}, vim);362}363};364}365return vim.onPasteFn;366}367368var numberRegex = /[\d]/;369var wordRegexp = [(/\w/), (/[^\w\s]/)], bigWordRegexp = [(/\S/)];370function makeKeyRange(start, size) {371var keys = [];372for (var i = start; i < start + size; i++) {373keys.push(String.fromCharCode(i));374}375return keys;376}377var upperCaseAlphabet = makeKeyRange(65, 26);378var lowerCaseAlphabet = makeKeyRange(97, 26);379var numbers = makeKeyRange(48, 10);380var specialSymbols = '~`!@#$%^&*()_-+=[{}]\\|/?.,<>:;"\''.split('');381var specialKeys = ['Left', 'Right', 'Up', 'Down', 'Space', 'Backspace',382'Esc', 'Home', 'End', 'PageUp', 'PageDown', 'Enter'];383var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']);384var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"']);385386function isLine(cm, line) {387return line >= cm.firstLine() && line <= cm.lastLine();388}389function isLowerCase(k) {390return (/^[a-z]$/).test(k);391}392function isMatchableSymbol(k) {393return '()[]{}'.indexOf(k) != -1;394}395function isNumber(k) {396return numberRegex.test(k);397}398function isUpperCase(k) {399return (/^[A-Z]$/).test(k);400}401function isWhiteSpaceString(k) {402return (/^\s*$/).test(k);403}404function inArray(val, arr) {405for (var i = 0; i < arr.length; i++) {406if (arr[i] == val) {407return true;408}409}410return false;411}412413var createCircularJumpList = function() {414var size = 100;415var pointer = -1;416var head = 0;417var tail = 0;418var buffer = new Array(size);419function add(cm, oldCur, newCur) {420var current = pointer % size;421var curMark = buffer[current];422function useNextSlot(cursor) {423var next = ++pointer % size;424var trashMark = buffer[next];425if (trashMark) {426trashMark.clear();427}428buffer[next] = cm.setBookmark(cursor);429}430if (curMark) {431var markPos = curMark.find();432// avoid recording redundant cursor position433if (markPos && !cursorEqual(markPos, oldCur)) {434useNextSlot(oldCur);435}436} else {437useNextSlot(oldCur);438}439useNextSlot(newCur);440head = pointer;441tail = pointer - size + 1;442if (tail < 0) {443tail = 0;444}445}446function move(cm, offset) {447pointer += offset;448if (pointer > head) {449pointer = head;450} else if (pointer < tail) {451pointer = tail;452}453var mark = buffer[(size + pointer) % size];454// skip marks that are temporarily removed from text buffer455if (mark && !mark.find()) {456var inc = offset > 0 ? 1 : -1;457var newCur;458var oldCur = cm.getCursor();459do {460pointer += inc;461mark = buffer[(size + pointer) % size];462// skip marks that are the same as current position463if (mark &&464(newCur = mark.find()) &&465!cursorEqual(oldCur, newCur)) {466break;467}468} while (pointer < head && pointer > tail);469}470return mark;471}472return {473cachedCursor: undefined, //used for # and * jumps474add: add,475move: move476};477};478479var createMacroState = function() {480return {481macroKeyBuffer: [],482latestRegister: undefined,483inReplay: false,484lastInsertModeChanges: {485changes: [], // Change list486expectCursorActivityForChange: false // Set to true on change, false on cursorActivity.487},488enteredMacroMode: undefined,489isMacroPlaying: false,490toggle: function(cm, registerName) {491if (this.enteredMacroMode) { //onExit492this.enteredMacroMode(); // close dialog493this.enteredMacroMode = undefined;494} else { //onEnter495this.latestRegister = registerName;496this.enteredMacroMode = cm.openDialog(497'(recording)['+registerName+']', null, {bottom:true});498}499}500};501};502503504function maybeInitVimState(cm) {505if (!cm.state.vim) {506// Store instance state in the CodeMirror object.507cm.state.vim = {508inputState: new InputState(),509// Vim's input state that triggered the last edit, used to repeat510// motions and operators with '.'.511lastEditInputState: undefined,512// Vim's action command before the last edit, used to repeat actions513// with '.' and insert mode repeat.514lastEditActionCommand: undefined,515// When using jk for navigation, if you move from a longer line to a516// shorter line, the cursor may clip to the end of the shorter line.517// If j is pressed again and cursor goes to the next line, the518// cursor should go back to its horizontal position on the longer519// line if it can. This is to keep track of the horizontal position.520lastHPos: -1,521// Doing the same with screen-position for gj/gk522lastHSPos: -1,523// The last motion command run. Cleared if a non-motion command gets524// executed in between.525lastMotion: null,526marks: {},527insertMode: false,528// Repeat count for changes made in insert mode, triggered by key529// sequences like 3,i. Only exists when insertMode is true.530insertModeRepeat: undefined,531visualMode: false,532// If we are in visual line mode. No effect if visualMode is false.533visualLine: false534};535}536return cm.state.vim;537}538var vimGlobalState;539function resetVimGlobalState() {540vimGlobalState = {541// The current search query.542searchQuery: null,543// Whether we are searching backwards.544searchIsReversed: false,545jumpList: createCircularJumpList(),546macroModeState: createMacroState(),547// Recording latest f, t, F or T motion command.548lastChararacterSearch: {increment:0, forward:true, selectedCharacter:''},549registerController: new RegisterController({})550};551}552553var vimApi= {554buildKeyMap: function() {555// TODO: Convert keymap into dictionary format for fast lookup.556},557// Testing hook, though it might be useful to expose the register558// controller anyways.559getRegisterController: function() {560return vimGlobalState.registerController;561},562// Testing hook.563resetVimGlobalState_: resetVimGlobalState,564565// Testing hook.566getVimGlobalState_: function() {567return vimGlobalState;568},569570// Testing hook.571maybeInitVimState_: maybeInitVimState,572573InsertModeKey: InsertModeKey,574map: function(lhs, rhs, ctx) {575// Add user defined key bindings.576exCommandDispatcher.map(lhs, rhs, ctx);577},578defineEx: function(name, prefix, func){579if (name.indexOf(prefix) !== 0) {580throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered');581}582exCommands[name]=func;583exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'};584},585// This is the outermost function called by CodeMirror, after keys have586// been mapped to their Vim equivalents.587handleKey: function(cm, key) {588var command;589var vim = maybeInitVimState(cm);590var macroModeState = vimGlobalState.macroModeState;591if (macroModeState.enteredMacroMode) {592if (key == 'q') {593actions.exitMacroRecordMode();594vim.inputState = new InputState();595return;596}597}598if (key == '<Esc>') {599// Clear input state and get back to normal mode.600vim.inputState = new InputState();601if (vim.visualMode) {602exitVisualMode(cm);603}604return;605}606// Enter visual mode when the mouse selects text.607if (!vim.visualMode &&608!cursorEqual(cm.getCursor('head'), cm.getCursor('anchor'))) {609vim.visualMode = true;610vim.visualLine = false;611CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"});612cm.on('mousedown', exitVisualMode);613}614if (key != '0' || (key == '0' && vim.inputState.getRepeat() === 0)) {615// Have to special case 0 since it's both a motion and a number.616command = commandDispatcher.matchCommand(key, defaultKeymap, vim);617}618if (!command) {619if (isNumber(key)) {620// Increment count unless count is 0 and key is 0.621vim.inputState.pushRepeatDigit(key);622}623return;624}625if (command.type == 'keyToKey') {626// TODO: prevent infinite recursion.627for (var i = 0; i < command.toKeys.length; i++) {628this.handleKey(cm, command.toKeys[i]);629}630} else {631if (macroModeState.enteredMacroMode) {632logKey(macroModeState, key);633}634commandDispatcher.processCommand(cm, vim, command);635}636},637handleEx: function(cm, input) {638exCommandDispatcher.processCommand(cm, input);639}640};641642// Represents the current input state.643function InputState() {644this.prefixRepeat = [];645this.motionRepeat = [];646647this.operator = null;648this.operatorArgs = null;649this.motion = null;650this.motionArgs = null;651this.keyBuffer = []; // For matching multi-key commands.652this.registerName = null; // Defaults to the unamed register.653}654InputState.prototype.pushRepeatDigit = function(n) {655if (!this.operator) {656this.prefixRepeat = this.prefixRepeat.concat(n);657} else {658this.motionRepeat = this.motionRepeat.concat(n);659}660};661InputState.prototype.getRepeat = function() {662var repeat = 0;663if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) {664repeat = 1;665if (this.prefixRepeat.length > 0) {666repeat *= parseInt(this.prefixRepeat.join(''), 10);667}668if (this.motionRepeat.length > 0) {669repeat *= parseInt(this.motionRepeat.join(''), 10);670}671}672return repeat;673};674675/*676* Register stores information about copy and paste registers. Besides677* text, a register must store whether it is linewise (i.e., when it is678* pasted, should it insert itself into a new line, or should the text be679* inserted at the cursor position.)680*/681function Register(text, linewise) {682this.clear();683if (text) {684this.set(text, linewise);685}686}687Register.prototype = {688set: function(text, linewise) {689this.text = text;690this.linewise = !!linewise;691},692append: function(text, linewise) {693// if this register has ever been set to linewise, use linewise.694if (linewise || this.linewise) {695this.text += '\n' + text;696this.linewise = true;697} else {698this.text += text;699}700},701clear: function() {702this.text = '';703this.linewise = false;704},705toString: function() { return this.text; }706};707708/*709* vim registers allow you to keep many independent copy and paste buffers.710* See http://usevim.com/2012/04/13/registers/ for an introduction.711*712* RegisterController keeps the state of all the registers. An initial713* state may be passed in. The unnamed register '"' will always be714* overridden.715*/716function RegisterController(registers) {717this.registers = registers;718this.unamedRegister = registers['"'] = new Register();719}720RegisterController.prototype = {721pushText: function(registerName, operator, text, linewise) {722if (linewise && text.charAt(0) == '\n') {723text = text.slice(1) + '\n';724}725if(linewise && text.charAt(text.length - 1) !== '\n'){726text += '\n';727}728// Lowercase and uppercase registers refer to the same register.729// Uppercase just means append.730var register = this.isValidRegister(registerName) ?731this.getRegister(registerName) : null;732// if no register/an invalid register was specified, things go to the733// default registers734if (!register) {735switch (operator) {736case 'yank':737// The 0 register contains the text from the most recent yank.738this.registers['0'] = new Register(text, linewise);739break;740case 'delete':741case 'change':742if (text.indexOf('\n') == -1) {743// Delete less than 1 line. Update the small delete register.744this.registers['-'] = new Register(text, linewise);745} else {746// Shift down the contents of the numbered registers and put the747// deleted text into register 1.748this.shiftNumericRegisters_();749this.registers['1'] = new Register(text, linewise);750}751break;752}753// Make sure the unnamed register is set to what just happened754this.unamedRegister.set(text, linewise);755return;756}757758// If we've gotten to this point, we've actually specified a register759var append = isUpperCase(registerName);760if (append) {761register.append(text, linewise);762// The unamed register always has the same value as the last used763// register.764this.unamedRegister.append(text, linewise);765} else {766register.set(text, linewise);767this.unamedRegister.set(text, linewise);768}769},770setRegisterText: function(name, text, linewise) {771this.getRegister(name).set(text, linewise);772},773// Gets the register named @name. If one of @name doesn't already exist,774// create it. If @name is invalid, return the unamedRegister.775getRegister: function(name) {776if (!this.isValidRegister(name)) {777return this.unamedRegister;778}779name = name.toLowerCase();780if (!this.registers[name]) {781this.registers[name] = new Register();782}783return this.registers[name];784},785isValidRegister: function(name) {786return name && inArray(name, validRegisters);787},788shiftNumericRegisters_: function() {789for (var i = 9; i >= 2; i--) {790this.registers[i] = this.getRegister('' + (i - 1));791}792}793};794795var commandDispatcher = {796matchCommand: function(key, keyMap, vim) {797var inputState = vim.inputState;798var keys = inputState.keyBuffer.concat(key);799var matchedCommands = [];800var selectedCharacter;801for (var i = 0; i < keyMap.length; i++) {802var command = keyMap[i];803if (matchKeysPartial(keys, command.keys)) {804if (inputState.operator && command.type == 'action') {805// Ignore matched action commands after an operator. Operators806// only operate on motions. This check is really for text807// objects since aW, a[ etcs conflicts with a.808continue;809}810// Match commands that take <character> as an argument.811if (command.keys[keys.length - 1] == 'character') {812selectedCharacter = keys[keys.length - 1];813if(selectedCharacter.length>1){814switch(selectedCharacter){815case '<CR>':816selectedCharacter='\n';817break;818case '<Space>':819selectedCharacter=' ';820break;821default:822continue;823}824}825}826// Add the command to the list of matched commands. Choose the best827// command later.828matchedCommands.push(command);829}830}831832// Returns the command if it is a full match, or null if not.833function getFullyMatchedCommandOrNull(command) {834if (keys.length < command.keys.length) {835// Matches part of a multi-key command. Buffer and wait for next836// stroke.837inputState.keyBuffer.push(key);838return null;839} else {840if (command.keys[keys.length - 1] == 'character') {841inputState.selectedCharacter = selectedCharacter;842}843// Clear the buffer since a full match was found.844inputState.keyBuffer = [];845return command;846}847}848849if (!matchedCommands.length) {850// Clear the buffer since there were no matches.851inputState.keyBuffer = [];852return null;853} else if (matchedCommands.length == 1) {854return getFullyMatchedCommandOrNull(matchedCommands[0]);855} else {856// Find the best match in the list of matchedCommands.857var context = vim.visualMode ? 'visual' : 'normal';858var bestMatch; // Default to first in the list.859for (var i = 0; i < matchedCommands.length; i++) {860var current = matchedCommands[i];861if (current.context == context) {862bestMatch = current;863break;864} else if (!bestMatch && !current.context) {865// Only set an imperfect match to best match if no best match is866// set and the imperfect match is not restricted to another867// context.868bestMatch = current;869}870}871return getFullyMatchedCommandOrNull(bestMatch);872}873},874processCommand: function(cm, vim, command) {875vim.inputState.repeatOverride = command.repeatOverride;876switch (command.type) {877case 'motion':878this.processMotion(cm, vim, command);879break;880case 'operator':881this.processOperator(cm, vim, command);882break;883case 'operatorMotion':884this.processOperatorMotion(cm, vim, command);885break;886case 'action':887this.processAction(cm, vim, command);888break;889case 'search':890this.processSearch(cm, vim, command);891break;892case 'ex':893case 'keyToEx':894this.processEx(cm, vim, command);895break;896default:897break;898}899},900processMotion: function(cm, vim, command) {901vim.inputState.motion = command.motion;902vim.inputState.motionArgs = copyArgs(command.motionArgs);903this.evalInput(cm, vim);904},905processOperator: function(cm, vim, command) {906var inputState = vim.inputState;907if (inputState.operator) {908if (inputState.operator == command.operator) {909// Typing an operator twice like 'dd' makes the operator operate910// linewise911inputState.motion = 'expandToLine';912inputState.motionArgs = { linewise: true };913this.evalInput(cm, vim);914return;915} else {916// 2 different operators in a row doesn't make sense.917vim.inputState = new InputState();918}919}920inputState.operator = command.operator;921inputState.operatorArgs = copyArgs(command.operatorArgs);922if (vim.visualMode) {923// Operating on a selection in visual mode. We don't need a motion.924this.evalInput(cm, vim);925}926},927processOperatorMotion: function(cm, vim, command) {928var visualMode = vim.visualMode;929var operatorMotionArgs = copyArgs(command.operatorMotionArgs);930if (operatorMotionArgs) {931// Operator motions may have special behavior in visual mode.932if (visualMode && operatorMotionArgs.visualLine) {933vim.visualLine = true;934}935}936this.processOperator(cm, vim, command);937if (!visualMode) {938this.processMotion(cm, vim, command);939}940},941processAction: function(cm, vim, command) {942var inputState = vim.inputState;943var repeat = inputState.getRepeat();944var repeatIsExplicit = !!repeat;945var actionArgs = copyArgs(command.actionArgs) || {};946if (inputState.selectedCharacter) {947actionArgs.selectedCharacter = inputState.selectedCharacter;948}949// Actions may or may not have motions and operators. Do these first.950if (command.operator) {951this.processOperator(cm, vim, command);952}953if (command.motion) {954this.processMotion(cm, vim, command);955}956if (command.motion || command.operator) {957this.evalInput(cm, vim);958}959actionArgs.repeat = repeat || 1;960actionArgs.repeatIsExplicit = repeatIsExplicit;961actionArgs.registerName = inputState.registerName;962vim.inputState = new InputState();963vim.lastMotion = null;964if (command.isEdit) {965this.recordLastEdit(vim, inputState, command);966}967actions[command.action](cm, actionArgs, vim);968},969processSearch: function(cm, vim, command) {970if (!cm.getSearchCursor) {971// Search depends on SearchCursor.972return;973}974var forward = command.searchArgs.forward;975getSearchState(cm).setReversed(!forward);976var promptPrefix = (forward) ? '/' : '?';977var originalQuery = getSearchState(cm).getQuery();978var originalScrollPos = cm.getScrollInfo();979function handleQuery(query, ignoreCase, smartCase) {980try {981updateSearchQuery(cm, query, ignoreCase, smartCase);982} catch (e) {983showConfirm(cm, 'Invalid regex: ' + query);984return;985}986commandDispatcher.processMotion(cm, vim, {987type: 'motion',988motion: 'findNext',989motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist }990});991}992function onPromptClose(query) {993cm.scrollTo(originalScrollPos.left, originalScrollPos.top);994handleQuery(query, true /** ignoreCase */, true /** smartCase */);995}996function onPromptKeyUp(_e, query) {997var parsedQuery;998try {999parsedQuery = updateSearchQuery(cm, query,1000true /** ignoreCase */, true /** smartCase */);1001} catch (e) {1002// Swallow bad regexes for incremental search.1003}1004if (parsedQuery) {1005cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30);1006} else {1007clearSearchHighlight(cm);1008cm.scrollTo(originalScrollPos.left, originalScrollPos.top);1009}1010}1011function onPromptKeyDown(e, _query, close) {1012var keyName = CodeMirror.keyName(e);1013if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') {1014updateSearchQuery(cm, originalQuery);1015clearSearchHighlight(cm);1016cm.scrollTo(originalScrollPos.left, originalScrollPos.top);10171018CodeMirror.e_stop(e);1019close();1020cm.focus();1021}1022}1023switch (command.searchArgs.querySrc) {1024case 'prompt':1025showPrompt(cm, {1026onClose: onPromptClose,1027prefix: promptPrefix,1028desc: searchPromptDesc,1029onKeyUp: onPromptKeyUp,1030onKeyDown: onPromptKeyDown1031});1032break;1033case 'wordUnderCursor':1034var word = expandWordUnderCursor(cm, false /** inclusive */,1035true /** forward */, false /** bigWord */,1036true /** noSymbol */);1037var isKeyword = true;1038if (!word) {1039word = expandWordUnderCursor(cm, false /** inclusive */,1040true /** forward */, false /** bigWord */,1041false /** noSymbol */);1042isKeyword = false;1043}1044if (!word) {1045return;1046}1047var query = cm.getLine(word.start.line).substring(word.start.ch,1048word.end.ch);1049if (isKeyword) {1050query = '\\b' + query + '\\b';1051} else {1052query = escapeRegex(query);1053}10541055// cachedCursor is used to save the old position of the cursor1056// when * or # causes vim to seek for the nearest word and shift1057// the cursor before entering the motion.1058vimGlobalState.jumpList.cachedCursor = cm.getCursor();1059cm.setCursor(word.start);10601061handleQuery(query, true /** ignoreCase */, false /** smartCase */);1062break;1063}1064},1065processEx: function(cm, vim, command) {1066function onPromptClose(input) {1067// Give the prompt some time to close so that if processCommand shows1068// an error, the elements don't overlap.1069exCommandDispatcher.processCommand(cm, input);1070}1071function onPromptKeyDown(e, _input, close) {1072var keyName = CodeMirror.keyName(e);1073if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') {1074CodeMirror.e_stop(e);1075close();1076cm.focus();1077}1078}1079if (command.type == 'keyToEx') {1080// Handle user defined Ex to Ex mappings1081exCommandDispatcher.processCommand(cm, command.exArgs.input);1082} else {1083if (vim.visualMode) {1084showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>',1085onKeyDown: onPromptKeyDown});1086} else {1087showPrompt(cm, { onClose: onPromptClose, prefix: ':',1088onKeyDown: onPromptKeyDown});1089}1090}1091},1092evalInput: function(cm, vim) {1093// If the motion comand is set, execute both the operator and motion.1094// Otherwise return.1095var inputState = vim.inputState;1096var motion = inputState.motion;1097var motionArgs = inputState.motionArgs || {};1098var operator = inputState.operator;1099var operatorArgs = inputState.operatorArgs || {};1100var registerName = inputState.registerName;1101var selectionEnd = cm.getCursor('head');1102var selectionStart = cm.getCursor('anchor');1103// The difference between cur and selection cursors are that cur is1104// being operated on and ignores that there is a selection.1105var curStart = copyCursor(selectionEnd);1106var curOriginal = copyCursor(curStart);1107var curEnd;1108var repeat;1109if (operator) {1110this.recordLastEdit(vim, inputState);1111}1112if (inputState.repeatOverride !== undefined) {1113// If repeatOverride is specified, that takes precedence over the1114// input state's repeat. Used by Ex mode and can be user defined.1115repeat = inputState.repeatOverride;1116} else {1117repeat = inputState.getRepeat();1118}1119if (repeat > 0 && motionArgs.explicitRepeat) {1120motionArgs.repeatIsExplicit = true;1121} else if (motionArgs.noRepeat ||1122(!motionArgs.explicitRepeat && repeat === 0)) {1123repeat = 1;1124motionArgs.repeatIsExplicit = false;1125}1126if (inputState.selectedCharacter) {1127// If there is a character input, stick it in all of the arg arrays.1128motionArgs.selectedCharacter = operatorArgs.selectedCharacter =1129inputState.selectedCharacter;1130}1131motionArgs.repeat = repeat;1132vim.inputState = new InputState();1133if (motion) {1134var motionResult = motions[motion](cm, motionArgs, vim);1135vim.lastMotion = motions[motion];1136if (!motionResult) {1137return;1138}1139if (motionArgs.toJumplist) {1140var jumpList = vimGlobalState.jumpList;1141// if the current motion is # or *, use cachedCursor1142var cachedCursor = jumpList.cachedCursor;1143if (cachedCursor) {1144recordJumpPosition(cm, cachedCursor, motionResult);1145delete jumpList.cachedCursor;1146} else {1147recordJumpPosition(cm, curOriginal, motionResult);1148}1149}1150if (motionResult instanceof Array) {1151curStart = motionResult[0];1152curEnd = motionResult[1];1153} else {1154curEnd = motionResult;1155}1156// TODO: Handle null returns from motion commands better.1157if (!curEnd) {1158curEnd = { ch: curStart.ch, line: curStart.line };1159}1160if (vim.visualMode) {1161// Check if the selection crossed over itself. Will need to shift1162// the start point if that happened.1163if (cursorIsBefore(selectionStart, selectionEnd) &&1164(cursorEqual(selectionStart, curEnd) ||1165cursorIsBefore(curEnd, selectionStart))) {1166// The end of the selection has moved from after the start to1167// before the start. We will shift the start right by 1.1168selectionStart.ch += 1;1169} else if (cursorIsBefore(selectionEnd, selectionStart) &&1170(cursorEqual(selectionStart, curEnd) ||1171cursorIsBefore(selectionStart, curEnd))) {1172// The opposite happened. We will shift the start left by 1.1173selectionStart.ch -= 1;1174}1175selectionEnd = curEnd;1176if (vim.visualLine) {1177if (cursorIsBefore(selectionStart, selectionEnd)) {1178selectionStart.ch = 0;11791180var lastLine = cm.lastLine();1181if (selectionEnd.line > lastLine) {1182selectionEnd.line = lastLine;1183}1184selectionEnd.ch = lineLength(cm, selectionEnd.line);1185} else {1186selectionEnd.ch = 0;1187selectionStart.ch = lineLength(cm, selectionStart.line);1188}1189}1190cm.setSelection(selectionStart, selectionEnd);1191updateMark(cm, vim, '<',1192cursorIsBefore(selectionStart, selectionEnd) ? selectionStart1193: selectionEnd);1194updateMark(cm, vim, '>',1195cursorIsBefore(selectionStart, selectionEnd) ? selectionEnd1196: selectionStart);1197} else if (!operator) {1198curEnd = clipCursorToContent(cm, curEnd);1199cm.setCursor(curEnd.line, curEnd.ch);1200}1201}12021203if (operator) {1204var inverted = false;1205vim.lastMotion = null;1206operatorArgs.repeat = repeat; // Indent in visual mode needs this.1207if (vim.visualMode) {1208curStart = selectionStart;1209curEnd = selectionEnd;1210motionArgs.inclusive = true;1211}1212// Swap start and end if motion was backward.1213if (cursorIsBefore(curEnd, curStart)) {1214var tmp = curStart;1215curStart = curEnd;1216curEnd = tmp;1217inverted = true;1218}1219if (motionArgs.inclusive && !(vim.visualMode && inverted)) {1220// Move the selection end one to the right to include the last1221// character.1222curEnd.ch++;1223}1224var linewise = motionArgs.linewise ||1225(vim.visualMode && vim.visualLine);1226if (linewise) {1227// Expand selection to entire line.1228expandSelectionToLine(cm, curStart, curEnd);1229} else if (motionArgs.forward) {1230// Clip to trailing newlines only if the motion goes forward.1231clipToLine(cm, curStart, curEnd);1232}1233operatorArgs.registerName = registerName;1234// Keep track of linewise as it affects how paste and change behave.1235operatorArgs.linewise = linewise;1236operators[operator](cm, operatorArgs, vim, curStart,1237curEnd, curOriginal);1238if (vim.visualMode) {1239exitVisualMode(cm);1240}1241}1242},1243recordLastEdit: function(vim, inputState, actionCommand) {1244var macroModeState = vimGlobalState.macroModeState;1245if (macroModeState.inReplay) { return; }1246vim.lastEditInputState = inputState;1247vim.lastEditActionCommand = actionCommand;1248macroModeState.lastInsertModeChanges.changes = [];1249macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false;1250}1251};12521253/**1254* typedef {Object{line:number,ch:number}} Cursor An object containing the1255* position of the cursor.1256*/1257// All of the functions below return Cursor objects.1258var motions = {1259moveToTopLine: function(cm, motionArgs) {1260var line = getUserVisibleLines(cm).top + motionArgs.repeat -1;1261return { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) };1262},1263moveToMiddleLine: function(cm) {1264var range = getUserVisibleLines(cm);1265var line = Math.floor((range.top + range.bottom) * 0.5);1266return { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) };1267},1268moveToBottomLine: function(cm, motionArgs) {1269var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1;1270return { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) };1271},1272expandToLine: function(cm, motionArgs) {1273// Expands forward to end of line, and then to next line if repeat is1274// >1. Does not handle backward motion!1275var cur = cm.getCursor();1276return { line: cur.line + motionArgs.repeat - 1, ch: Infinity };1277},1278findNext: function(cm, motionArgs) {1279var state = getSearchState(cm);1280var query = state.getQuery();1281if (!query) {1282return;1283}1284var prev = !motionArgs.forward;1285// If search is initiated with ? instead of /, negate direction.1286prev = (state.isReversed()) ? !prev : prev;1287highlightSearchMatches(cm, query);1288return findNext(cm, prev/** prev */, query, motionArgs.repeat);1289},1290goToMark: function(_cm, motionArgs, vim) {1291var mark = vim.marks[motionArgs.selectedCharacter];1292if (mark) {1293return mark.find();1294}1295return null;1296},1297jumpToMark: function(cm, motionArgs, vim) {1298var best = cm.getCursor();1299for (var i = 0; i < motionArgs.repeat; i++) {1300var cursor = best;1301for (var key in vim.marks) {1302if (!isLowerCase(key)) {1303continue;1304}1305var mark = vim.marks[key].find();1306var isWrongDirection = (motionArgs.forward) ?1307cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark);13081309if (isWrongDirection) {1310continue;1311}1312if (motionArgs.linewise && (mark.line == cursor.line)) {1313continue;1314}13151316var equal = cursorEqual(cursor, best);1317var between = (motionArgs.forward) ?1318cusrorIsBetween(cursor, mark, best) :1319cusrorIsBetween(best, mark, cursor);13201321if (equal || between) {1322best = mark;1323}1324}1325}13261327if (motionArgs.linewise) {1328// Vim places the cursor on the first non-whitespace character of1329// the line if there is one, else it places the cursor at the end1330// of the line, regardless of whether a mark was found.1331best.ch = findFirstNonWhiteSpaceCharacter(cm.getLine(best.line));1332}1333return best;1334},1335moveByCharacters: function(cm, motionArgs) {1336var cur = cm.getCursor();1337var repeat = motionArgs.repeat;1338var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat;1339return { line: cur.line, ch: ch };1340},1341moveByLines: function(cm, motionArgs, vim) {1342var cur = cm.getCursor();1343var endCh = cur.ch;1344// Depending what our last motion was, we may want to do different1345// things. If our last motion was moving vertically, we want to1346// preserve the HPos from our last horizontal move. If our last motion1347// was going to the end of a line, moving vertically we should go to1348// the end of the line, etc.1349switch (vim.lastMotion) {1350case this.moveByLines:1351case this.moveByDisplayLines:1352case this.moveByScroll:1353case this.moveToColumn:1354case this.moveToEol:1355endCh = vim.lastHPos;1356break;1357default:1358vim.lastHPos = endCh;1359}1360var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0);1361var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat;1362var first = cm.firstLine();1363var last = cm.lastLine();1364// Vim cancels linewise motions that start on an edge and move beyond1365// that edge. It does not cancel motions that do not start on an edge.1366if ((line < first && cur.line == first) ||1367(line > last && cur.line == last)) {1368return;1369}1370if(motionArgs.toFirstChar){1371endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line));1372vim.lastHPos = endCh;1373}1374vim.lastHSPos = cm.charCoords({line:line, ch:endCh},'div').left;1375return { line: line, ch: endCh };1376},1377moveByDisplayLines: function(cm, motionArgs, vim) {1378var cur = cm.getCursor();1379switch (vim.lastMotion) {1380case this.moveByDisplayLines:1381case this.moveByScroll:1382case this.moveByLines:1383case this.moveToColumn:1384case this.moveToEol:1385break;1386default:1387vim.lastHSPos = cm.charCoords(cur,'div').left;1388}1389var repeat = motionArgs.repeat;1390var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),'line',vim.lastHSPos);1391if (res.hitSide) {1392if (motionArgs.forward) {1393var lastCharCoords = cm.charCoords(res, 'div');1394var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos };1395var res = cm.coordsChar(goalCoords, 'div');1396} else {1397var resCoords = cm.charCoords({ line: cm.firstLine(), ch: 0}, 'div');1398resCoords.left = vim.lastHSPos;1399res = cm.coordsChar(resCoords, 'div');1400}1401}1402vim.lastHPos = res.ch;1403return res;1404},1405moveByPage: function(cm, motionArgs) {1406// CodeMirror only exposes functions that move the cursor page down, so1407// doing this bad hack to move the cursor and move it back. evalInput1408// will move the cursor to where it should be in the end.1409var curStart = cm.getCursor();1410var repeat = motionArgs.repeat;1411cm.moveV((motionArgs.forward ? repeat : -repeat), 'page');1412var curEnd = cm.getCursor();1413cm.setCursor(curStart);1414return curEnd;1415},1416moveByParagraph: function(cm, motionArgs) {1417var line = cm.getCursor().line;1418var repeat = motionArgs.repeat;1419var inc = motionArgs.forward ? 1 : -1;1420for (var i = 0; i < repeat; i++) {1421if ((!motionArgs.forward && line === cm.firstLine() ) ||1422(motionArgs.forward && line == cm.lastLine())) {1423break;1424}1425line += inc;1426while (line !== cm.firstLine() && line != cm.lastLine() && cm.getLine(line)) {1427line += inc;1428}1429}1430return { line: line, ch: 0 };1431},1432moveByScroll: function(cm, motionArgs, vim) {1433var scrollbox = cm.getScrollInfo();1434var curEnd = null;1435var repeat = motionArgs.repeat;1436if (!repeat) {1437repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight());1438}1439var orig = cm.charCoords(cm.getCursor(), 'local');1440motionArgs.repeat = repeat;1441var curEnd = motions.moveByDisplayLines(cm, motionArgs, vim);1442if (!curEnd) {1443return null;1444}1445var dest = cm.charCoords(curEnd, 'local');1446cm.scrollTo(null, scrollbox.top + dest.top - orig.top);1447return curEnd;1448},1449moveByWords: function(cm, motionArgs) {1450return moveToWord(cm, motionArgs.repeat, !!motionArgs.forward,1451!!motionArgs.wordEnd, !!motionArgs.bigWord);1452},1453moveTillCharacter: function(cm, motionArgs) {1454var repeat = motionArgs.repeat;1455var curEnd = moveToCharacter(cm, repeat, motionArgs.forward,1456motionArgs.selectedCharacter);1457var increment = motionArgs.forward ? -1 : 1;1458recordLastCharacterSearch(increment, motionArgs);1459if(!curEnd)return cm.getCursor();1460curEnd.ch += increment;1461return curEnd;1462},1463moveToCharacter: function(cm, motionArgs) {1464var repeat = motionArgs.repeat;1465recordLastCharacterSearch(0, motionArgs);1466return moveToCharacter(cm, repeat, motionArgs.forward,1467motionArgs.selectedCharacter) || cm.getCursor();1468},1469moveToSymbol: function(cm, motionArgs) {1470var repeat = motionArgs.repeat;1471return findSymbol(cm, repeat, motionArgs.forward,1472motionArgs.selectedCharacter) || cm.getCursor();1473},1474moveToColumn: function(cm, motionArgs, vim) {1475var repeat = motionArgs.repeat;1476// repeat is equivalent to which column we want to move to!1477vim.lastHPos = repeat - 1;1478vim.lastHSPos = cm.charCoords(cm.getCursor(),'div').left;1479return moveToColumn(cm, repeat);1480},1481moveToEol: function(cm, motionArgs, vim) {1482var cur = cm.getCursor();1483vim.lastHPos = Infinity;1484var retval={ line: cur.line + motionArgs.repeat - 1, ch: Infinity };1485var end=cm.clipPos(retval);1486end.ch--;1487vim.lastHSPos = cm.charCoords(end,'div').left;1488return retval;1489},1490moveToFirstNonWhiteSpaceCharacter: function(cm) {1491// Go to the start of the line where the text begins, or the end for1492// whitespace-only lines1493var cursor = cm.getCursor();1494return { line: cursor.line,1495ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line)) };1496},1497moveToMatchedSymbol: function(cm) {1498var cursor = cm.getCursor();1499var line = cursor.line;1500var ch = cursor.ch;1501var lineText = cm.getLine(line);1502var symbol;1503var startContext = cm.getTokenAt(cursor).type;1504var startCtxLevel = getContextLevel(startContext);1505do {1506symbol = lineText.charAt(ch++);1507if (symbol && isMatchableSymbol(symbol)) {1508var endContext = cm.getTokenAt({line:line, ch:ch}).type;1509var endCtxLevel = getContextLevel(endContext);1510if (startCtxLevel >= endCtxLevel) {1511break;1512}1513}1514} while (symbol);1515if (symbol) {1516return findMatchedSymbol(cm, {line:line, ch:ch-1}, symbol);1517} else {1518return cursor;1519}1520},1521moveToStartOfLine: function(cm) {1522var cursor = cm.getCursor();1523return { line: cursor.line, ch: 0 };1524},1525moveToLineOrEdgeOfDocument: function(cm, motionArgs) {1526var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine();1527if (motionArgs.repeatIsExplicit) {1528lineNum = motionArgs.repeat - cm.getOption('firstLineNumber');1529}1530return { line: lineNum,1531ch: findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum)) };1532},1533textObjectManipulation: function(cm, motionArgs) {1534var character = motionArgs.selectedCharacter;1535// Inclusive is the difference between a and i1536// TODO: Instead of using the additional text object map to perform text1537// object operations, merge the map into the defaultKeyMap and use1538// motionArgs to define behavior. Define separate entries for 'aw',1539// 'iw', 'a[', 'i[', etc.1540var inclusive = !motionArgs.textObjectInner;1541if (!textObjects[character]) {1542// No text object defined for this, don't move.1543return null;1544}1545var tmp = textObjects[character](cm, inclusive);1546var start = tmp.start;1547var end = tmp.end;1548return [start, end];1549},1550repeatLastCharacterSearch: function(cm, motionArgs) {1551var lastSearch = vimGlobalState.lastChararacterSearch;1552var repeat = motionArgs.repeat;1553var forward = motionArgs.forward === lastSearch.forward;1554var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1);1555cm.moveH(-increment, 'char');1556motionArgs.inclusive = forward ? true : false;1557var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter);1558if (!curEnd) {1559cm.moveH(increment, 'char');1560return cm.getCursor();1561}1562curEnd.ch += increment;1563return curEnd;1564}1565};15661567var operators = {1568change: function(cm, operatorArgs, _vim, curStart, curEnd) {1569vimGlobalState.registerController.pushText(1570operatorArgs.registerName, 'change', cm.getRange(curStart, curEnd),1571operatorArgs.linewise);1572if (operatorArgs.linewise) {1573// Push the next line back down, if there is a next line.1574var replacement = curEnd.line > cm.lastLine() ? '' : '\n';1575cm.replaceRange(replacement, curStart, curEnd);1576cm.indentLine(curStart.line, 'smart');1577// null ch so setCursor moves to end of line.1578curStart.ch = null;1579} else {1580// Exclude trailing whitespace if the range is not all whitespace.1581var text = cm.getRange(curStart, curEnd);1582if (!isWhiteSpaceString(text)) {1583var match = (/\s+$/).exec(text);1584if (match) {1585curEnd = offsetCursor(curEnd, 0, - match[0].length);1586}1587}1588cm.replaceRange('', curStart, curEnd);1589}1590actions.enterInsertMode(cm, {}, cm.state.vim);1591cm.setCursor(curStart);1592},1593// delete is a javascript keyword.1594'delete': function(cm, operatorArgs, _vim, curStart, curEnd) {1595// If the ending line is past the last line, inclusive, instead of1596// including the trailing \n, include the \n before the starting line1597if (operatorArgs.linewise &&1598curEnd.line > cm.lastLine() && curStart.line > cm.firstLine()) {1599curStart.line--;1600curStart.ch = lineLength(cm, curStart.line);1601}1602vimGlobalState.registerController.pushText(1603operatorArgs.registerName, 'delete', cm.getRange(curStart, curEnd),1604operatorArgs.linewise);1605cm.replaceRange('', curStart, curEnd);1606if (operatorArgs.linewise) {1607cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));1608} else {1609cm.setCursor(curStart);1610}1611},1612indent: function(cm, operatorArgs, vim, curStart, curEnd) {1613var startLine = curStart.line;1614var endLine = curEnd.line;1615// In visual mode, n> shifts the selection right n times, instead of1616// shifting n lines right once.1617var repeat = (vim.visualMode) ? operatorArgs.repeat : 1;1618if (operatorArgs.linewise) {1619// The only way to delete a newline is to delete until the start of1620// the next line, so in linewise mode evalInput will include the next1621// line. We don't want this in indent, so we go back a line.1622endLine--;1623}1624for (var i = startLine; i <= endLine; i++) {1625for (var j = 0; j < repeat; j++) {1626cm.indentLine(i, operatorArgs.indentRight);1627}1628}1629cm.setCursor(curStart);1630cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));1631},1632swapcase: function(cm, operatorArgs, _vim, curStart, curEnd, curOriginal) {1633var toSwap = cm.getRange(curStart, curEnd);1634var swapped = '';1635for (var i = 0; i < toSwap.length; i++) {1636var character = toSwap.charAt(i);1637swapped += isUpperCase(character) ? character.toLowerCase() :1638character.toUpperCase();1639}1640cm.replaceRange(swapped, curStart, curEnd);1641if (!operatorArgs.shouldMoveCursor) {1642cm.setCursor(curOriginal);1643}1644},1645yank: function(cm, operatorArgs, _vim, curStart, curEnd, curOriginal) {1646vimGlobalState.registerController.pushText(1647operatorArgs.registerName, 'yank',1648cm.getRange(curStart, curEnd), operatorArgs.linewise);1649cm.setCursor(curOriginal);1650}1651};16521653var actions = {1654jumpListWalk: function(cm, actionArgs, vim) {1655if (vim.visualMode) {1656return;1657}1658var repeat = actionArgs.repeat;1659var forward = actionArgs.forward;1660var jumpList = vimGlobalState.jumpList;16611662var mark = jumpList.move(cm, forward ? repeat : -repeat);1663var markPos = mark ? mark.find() : undefined;1664markPos = markPos ? markPos : cm.getCursor();1665cm.setCursor(markPos);1666},1667scroll: function(cm, actionArgs, vim) {1668if (vim.visualMode) {1669return;1670}1671var repeat = actionArgs.repeat || 1;1672var lineHeight = cm.defaultTextHeight();1673var top = cm.getScrollInfo().top;1674var delta = lineHeight * repeat;1675var newPos = actionArgs.forward ? top + delta : top - delta;1676var cursor = cm.getCursor();1677var cursorCoords = cm.charCoords(cursor, 'local');1678if (actionArgs.forward) {1679if (newPos > cursorCoords.top) {1680cursor.line += (newPos - cursorCoords.top) / lineHeight;1681cursor.line = Math.ceil(cursor.line);1682cm.setCursor(cursor);1683cursorCoords = cm.charCoords(cursor, 'local');1684cm.scrollTo(null, cursorCoords.top);1685} else {1686// Cursor stays within bounds. Just reposition the scroll window.1687cm.scrollTo(null, newPos);1688}1689} else {1690var newBottom = newPos + cm.getScrollInfo().clientHeight;1691if (newBottom < cursorCoords.bottom) {1692cursor.line -= (cursorCoords.bottom - newBottom) / lineHeight;1693cursor.line = Math.floor(cursor.line);1694cm.setCursor(cursor);1695cursorCoords = cm.charCoords(cursor, 'local');1696cm.scrollTo(1697null, cursorCoords.bottom - cm.getScrollInfo().clientHeight);1698} else {1699// Cursor stays within bounds. Just reposition the scroll window.1700cm.scrollTo(null, newPos);1701}1702}1703},1704scrollToCursor: function(cm, actionArgs) {1705var lineNum = cm.getCursor().line;1706var charCoords = cm.charCoords({line: lineNum, ch: 0}, 'local');1707var height = cm.getScrollInfo().clientHeight;1708var y = charCoords.top;1709var lineHeight = charCoords.bottom - y;1710switch (actionArgs.position) {1711case 'center': y = y - (height / 2) + lineHeight;1712break;1713case 'bottom': y = y - height + lineHeight*1.4;1714break;1715case 'top': y = y + lineHeight*0.4;1716break;1717}1718cm.scrollTo(null, y);1719},1720replayMacro: function(cm, actionArgs) {1721var registerName = actionArgs.selectedCharacter;1722var repeat = actionArgs.repeat;1723var macroModeState = vimGlobalState.macroModeState;1724if (registerName == '@') {1725registerName = macroModeState.latestRegister;1726}1727var keyBuffer = parseRegisterToKeyBuffer(macroModeState, registerName);1728while(repeat--){1729executeMacroKeyBuffer(cm, macroModeState, keyBuffer);1730}1731},1732exitMacroRecordMode: function() {1733var macroModeState = vimGlobalState.macroModeState;1734macroModeState.toggle();1735parseKeyBufferToRegister(macroModeState.latestRegister,1736macroModeState.macroKeyBuffer);1737},1738enterMacroRecordMode: function(cm, actionArgs) {1739var macroModeState = vimGlobalState.macroModeState;1740var registerName = actionArgs.selectedCharacter;1741macroModeState.toggle(cm, registerName);1742emptyMacroKeyBuffer(macroModeState);1743},1744enterInsertMode: function(cm, actionArgs, vim) {1745if (cm.getOption('readOnly')) { return; }1746vim.insertMode = true;1747vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1;1748var insertAt = (actionArgs) ? actionArgs.insertAt : null;1749if (insertAt == 'eol') {1750var cursor = cm.getCursor();1751cursor = { line: cursor.line, ch: lineLength(cm, cursor.line) };1752cm.setCursor(cursor);1753} else if (insertAt == 'charAfter') {1754cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));1755} else if (insertAt == 'firstNonBlank') {1756cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));1757}1758cm.setOption('keyMap', 'vim-insert');1759cm.setOption('disableInput', false);1760if (actionArgs && actionArgs.replace) {1761// Handle Replace-mode as a special case of insert mode.1762cm.toggleOverwrite(true);1763cm.setOption('keyMap', 'vim-replace');1764CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"});1765} else {1766cm.setOption('keyMap', 'vim-insert');1767CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"});1768}1769if (!vimGlobalState.macroModeState.inReplay) {1770// Only record if not replaying.1771cm.on('change', onChange);1772cm.on('cursorActivity', onCursorActivity);1773CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown);1774}1775},1776toggleVisualMode: function(cm, actionArgs, vim) {1777var repeat = actionArgs.repeat;1778var curStart = cm.getCursor();1779var curEnd;1780// TODO: The repeat should actually select number of characters/lines1781// equal to the repeat times the size of the previous visual1782// operation.1783if (!vim.visualMode) {1784cm.on('mousedown', exitVisualMode);1785vim.visualMode = true;1786vim.visualLine = !!actionArgs.linewise;1787if (vim.visualLine) {1788curStart.ch = 0;1789curEnd = clipCursorToContent(cm, {1790line: curStart.line + repeat - 1,1791ch: lineLength(cm, curStart.line)1792}, true /** includeLineBreak */);1793} else {1794curEnd = clipCursorToContent(cm, {1795line: curStart.line,1796ch: curStart.ch + repeat1797}, true /** includeLineBreak */);1798}1799// Make the initial selection.1800if (!actionArgs.repeatIsExplicit && !vim.visualLine) {1801// This is a strange case. Here the implicit repeat is 1. The1802// following commands lets the cursor hover over the 1 character1803// selection.1804cm.setCursor(curEnd);1805cm.setSelection(curEnd, curStart);1806} else {1807cm.setSelection(curStart, curEnd);1808}1809CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : ""});1810} else {1811curStart = cm.getCursor('anchor');1812curEnd = cm.getCursor('head');1813if (!vim.visualLine && actionArgs.linewise) {1814// Shift-V pressed in characterwise visual mode. Switch to linewise1815// visual mode instead of exiting visual mode.1816vim.visualLine = true;1817curStart.ch = cursorIsBefore(curStart, curEnd) ? 0 :1818lineLength(cm, curStart.line);1819curEnd.ch = cursorIsBefore(curStart, curEnd) ?1820lineLength(cm, curEnd.line) : 0;1821cm.setSelection(curStart, curEnd);1822CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: "linewise"});1823} else if (vim.visualLine && !actionArgs.linewise) {1824// v pressed in linewise visual mode. Switch to characterwise visual1825// mode instead of exiting visual mode.1826vim.visualLine = false;1827CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"});1828} else {1829exitVisualMode(cm);1830}1831}1832updateMark(cm, vim, '<', cursorIsBefore(curStart, curEnd) ? curStart1833: curEnd);1834updateMark(cm, vim, '>', cursorIsBefore(curStart, curEnd) ? curEnd1835: curStart);1836},1837joinLines: function(cm, actionArgs, vim) {1838var curStart, curEnd;1839if (vim.visualMode) {1840curStart = cm.getCursor('anchor');1841curEnd = cm.getCursor('head');1842curEnd.ch = lineLength(cm, curEnd.line) - 1;1843} else {1844// Repeat is the number of lines to join. Minimum 2 lines.1845var repeat = Math.max(actionArgs.repeat, 2);1846curStart = cm.getCursor();1847curEnd = clipCursorToContent(cm, { line: curStart.line + repeat - 1,1848ch: Infinity });1849}1850var finalCh = 0;1851cm.operation(function() {1852for (var i = curStart.line; i < curEnd.line; i++) {1853finalCh = lineLength(cm, curStart.line);1854var tmp = { line: curStart.line + 1,1855ch: lineLength(cm, curStart.line + 1) };1856var text = cm.getRange(curStart, tmp);1857text = text.replace(/\n\s*/g, ' ');1858cm.replaceRange(text, curStart, tmp);1859}1860var curFinalPos = { line: curStart.line, ch: finalCh };1861cm.setCursor(curFinalPos);1862});1863},1864newLineAndEnterInsertMode: function(cm, actionArgs, vim) {1865vim.insertMode = true;1866var insertAt = cm.getCursor();1867if (insertAt.line === cm.firstLine() && !actionArgs.after) {1868// Special case for inserting newline before start of document.1869cm.replaceRange('\n', { line: cm.firstLine(), ch: 0 });1870cm.setCursor(cm.firstLine(), 0);1871} else {1872insertAt.line = (actionArgs.after) ? insertAt.line :1873insertAt.line - 1;1874insertAt.ch = lineLength(cm, insertAt.line);1875cm.setCursor(insertAt);1876var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment ||1877CodeMirror.commands.newlineAndIndent;1878newlineFn(cm);1879}1880this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim);1881},1882paste: function(cm, actionArgs) {1883var cur = cm.getCursor();1884var register = vimGlobalState.registerController.getRegister(1885actionArgs.registerName);1886if (!register.text) {1887return;1888}1889for (var text = '', i = 0; i < actionArgs.repeat; i++) {1890text += register.text;1891}1892var linewise = register.linewise;1893if (linewise) {1894if (actionArgs.after) {1895// Move the newline at the end to the start instead, and paste just1896// before the newline character of the line we are on right now.1897text = '\n' + text.slice(0, text.length - 1);1898cur.ch = lineLength(cm, cur.line);1899} else {1900cur.ch = 0;1901}1902} else {1903cur.ch += actionArgs.after ? 1 : 0;1904}1905cm.replaceRange(text, cur);1906// Now fine tune the cursor to where we want it.1907var curPosFinal;1908var idx;1909if (linewise && actionArgs.after) {1910curPosFinal = { line: cur.line + 1,1911ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1)) };1912} else if (linewise && !actionArgs.after) {1913curPosFinal = { line: cur.line,1914ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line)) };1915} else if (!linewise && actionArgs.after) {1916idx = cm.indexFromPos(cur);1917curPosFinal = cm.posFromIndex(idx + text.length - 1);1918} else {1919idx = cm.indexFromPos(cur);1920curPosFinal = cm.posFromIndex(idx + text.length);1921}1922cm.setCursor(curPosFinal);1923},1924undo: function(cm, actionArgs) {1925cm.operation(function() {1926repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)();1927cm.setCursor(cm.getCursor('anchor'));1928});1929},1930redo: function(cm, actionArgs) {1931repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)();1932},1933setRegister: function(_cm, actionArgs, vim) {1934vim.inputState.registerName = actionArgs.selectedCharacter;1935},1936setMark: function(cm, actionArgs, vim) {1937var markName = actionArgs.selectedCharacter;1938updateMark(cm, vim, markName, cm.getCursor());1939},1940replace: function(cm, actionArgs, vim) {1941var replaceWith = actionArgs.selectedCharacter;1942var curStart = cm.getCursor();1943var replaceTo;1944var curEnd;1945if(vim.visualMode){1946curStart=cm.getCursor('start');1947curEnd=cm.getCursor('end');1948// workaround to catch the character under the cursor1949// existing workaround doesn't cover actions1950curEnd=cm.clipPos({line: curEnd.line, ch: curEnd.ch+1});1951}else{1952var line = cm.getLine(curStart.line);1953replaceTo = curStart.ch + actionArgs.repeat;1954if (replaceTo > line.length) {1955replaceTo=line.length;1956}1957curEnd = { line: curStart.line, ch: replaceTo };1958}1959if(replaceWith=='\n'){1960if(!vim.visualMode) cm.replaceRange('', curStart, curEnd);1961// special case, where vim help says to replace by just one line-break1962(CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm);1963}else {1964var replaceWithStr=cm.getRange(curStart, curEnd);1965//replace all characters in range by selected, but keep linebreaks1966replaceWithStr=replaceWithStr.replace(/[^\n]/g,replaceWith);1967cm.replaceRange(replaceWithStr, curStart, curEnd);1968if(vim.visualMode){1969cm.setCursor(curStart);1970exitVisualMode(cm);1971}else{1972cm.setCursor(offsetCursor(curEnd, 0, -1));1973}1974}1975},1976incrementNumberToken: function(cm, actionArgs) {1977var cur = cm.getCursor();1978var lineStr = cm.getLine(cur.line);1979var re = /-?\d+/g;1980var match;1981var start;1982var end;1983var numberStr;1984var token;1985while ((match = re.exec(lineStr)) !== null) {1986token = match[0];1987start = match.index;1988end = start + token.length;1989if(cur.ch < end)break;1990}1991if(!actionArgs.backtrack && (end <= cur.ch))return;1992if (token) {1993var increment = actionArgs.increase ? 1 : -1;1994var number = parseInt(token) + (increment * actionArgs.repeat);1995var from = {ch:start, line:cur.line};1996var to = {ch:end, line:cur.line};1997numberStr = number.toString();1998cm.replaceRange(numberStr, from, to);1999} else {2000return;2001}2002cm.setCursor({line: cur.line, ch: start + numberStr.length - 1});2003},2004repeatLastEdit: function(cm, actionArgs, vim) {2005var lastEditInputState = vim.lastEditInputState;2006if (!lastEditInputState) { return; }2007var repeat = actionArgs.repeat;2008if (repeat && actionArgs.repeatIsExplicit) {2009vim.lastEditInputState.repeatOverride = repeat;2010} else {2011repeat = vim.lastEditInputState.repeatOverride || repeat;2012}2013repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */);2014}2015};20162017var textObjects = {2018// TODO: lots of possible exceptions that can be thrown here. Try da(2019// outside of a () block.2020// TODO: implement text objects for the reverse like }. Should just be2021// an additional mapping after moving to the defaultKeyMap.2022'w': function(cm, inclusive) {2023return expandWordUnderCursor(cm, inclusive, true /** forward */,2024false /** bigWord */);2025},2026'W': function(cm, inclusive) {2027return expandWordUnderCursor(cm, inclusive,2028true /** forward */, true /** bigWord */);2029},2030'{': function(cm, inclusive) {2031return selectCompanionObject(cm, '}', inclusive);2032},2033'(': function(cm, inclusive) {2034return selectCompanionObject(cm, ')', inclusive);2035},2036'[': function(cm, inclusive) {2037return selectCompanionObject(cm, ']', inclusive);2038},2039'\'': function(cm, inclusive) {2040return findBeginningAndEnd(cm, "'", inclusive);2041},2042'"': function(cm, inclusive) {2043return findBeginningAndEnd(cm, '"', inclusive);2044}2045};20462047/*2048* Below are miscellaneous utility functions used by vim.js2049*/20502051/**2052* Clips cursor to ensure that line is within the buffer's range2053* If includeLineBreak is true, then allow cur.ch == lineLength.2054*/2055function clipCursorToContent(cm, cur, includeLineBreak) {2056var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() );2057var maxCh = lineLength(cm, line) - 1;2058maxCh = (includeLineBreak) ? maxCh + 1 : maxCh;2059var ch = Math.min(Math.max(0, cur.ch), maxCh);2060return { line: line, ch: ch };2061}2062function copyArgs(args) {2063var ret = {};2064for (var prop in args) {2065if (args.hasOwnProperty(prop)) {2066ret[prop] = args[prop];2067}2068}2069return ret;2070}2071function offsetCursor(cur, offsetLine, offsetCh) {2072return { line: cur.line + offsetLine, ch: cur.ch + offsetCh };2073}2074function matchKeysPartial(pressed, mapped) {2075for (var i = 0; i < pressed.length; i++) {2076// 'character' means any character. For mark, register commads, etc.2077if (pressed[i] != mapped[i] && mapped[i] != 'character') {2078return false;2079}2080}2081return true;2082}2083function repeatFn(cm, fn, repeat) {2084return function() {2085for (var i = 0; i < repeat; i++) {2086fn(cm);2087}2088};2089}2090function copyCursor(cur) {2091return { line: cur.line, ch: cur.ch };2092}2093function cursorEqual(cur1, cur2) {2094return cur1.ch == cur2.ch && cur1.line == cur2.line;2095}2096function cursorIsBefore(cur1, cur2) {2097if (cur1.line < cur2.line) {2098return true;2099}2100if (cur1.line == cur2.line && cur1.ch < cur2.ch) {2101return true;2102}2103return false;2104}2105function cusrorIsBetween(cur1, cur2, cur3) {2106// returns true if cur2 is between cur1 and cur3.2107var cur1before2 = cursorIsBefore(cur1, cur2);2108var cur2before3 = cursorIsBefore(cur2, cur3);2109return cur1before2 && cur2before3;2110}2111function lineLength(cm, lineNum) {2112return cm.getLine(lineNum).length;2113}2114function reverse(s){2115return s.split('').reverse().join('');2116}2117function trim(s) {2118if (s.trim) {2119return s.trim();2120}2121return s.replace(/^\s+|\s+$/g, '');2122}2123function escapeRegex(s) {2124return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1');2125}21262127function exitVisualMode(cm) {2128cm.off('mousedown', exitVisualMode);2129var vim = cm.state.vim;2130vim.visualMode = false;2131vim.visualLine = false;2132var selectionStart = cm.getCursor('anchor');2133var selectionEnd = cm.getCursor('head');2134if (!cursorEqual(selectionStart, selectionEnd)) {2135// Clear the selection and set the cursor only if the selection has not2136// already been cleared. Otherwise we risk moving the cursor somewhere2137// it's not supposed to be.2138cm.setCursor(clipCursorToContent(cm, selectionEnd));2139}2140CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});2141}21422143// Remove any trailing newlines from the selection. For2144// example, with the caret at the start of the last word on the line,2145// 'dw' should word, but not the newline, while 'w' should advance the2146// caret to the first character of the next line.2147function clipToLine(cm, curStart, curEnd) {2148var selection = cm.getRange(curStart, curEnd);2149// Only clip if the selection ends with trailing newline + whitespace2150if (/\n\s*$/.test(selection)) {2151var lines = selection.split('\n');2152// We know this is all whitepsace.2153lines.pop();21542155// Cases:2156// 1. Last word is an empty line - do not clip the trailing '\n'2157// 2. Last word is not an empty line - clip the trailing '\n'2158var line;2159// Find the line containing the last word, and clip all whitespace up2160// to it.2161for (var line = lines.pop(); lines.length > 0 && line && isWhiteSpaceString(line); line = lines.pop()) {2162curEnd.line--;2163curEnd.ch = 0;2164}2165// If the last word is not an empty line, clip an additional newline2166if (line) {2167curEnd.line--;2168curEnd.ch = lineLength(cm, curEnd.line);2169} else {2170curEnd.ch = 0;2171}2172}2173}21742175// Expand the selection to line ends.2176function expandSelectionToLine(_cm, curStart, curEnd) {2177curStart.ch = 0;2178curEnd.ch = 0;2179curEnd.line++;2180}21812182function findFirstNonWhiteSpaceCharacter(text) {2183if (!text) {2184return 0;2185}2186var firstNonWS = text.search(/\S/);2187return firstNonWS == -1 ? text.length : firstNonWS;2188}21892190function expandWordUnderCursor(cm, inclusive, _forward, bigWord, noSymbol) {2191var cur = cm.getCursor();2192var line = cm.getLine(cur.line);2193var idx = cur.ch;21942195// Seek to first word or non-whitespace character, depending on if2196// noSymbol is true.2197var textAfterIdx = line.substring(idx);2198var firstMatchedChar;2199if (noSymbol) {2200firstMatchedChar = textAfterIdx.search(/\w/);2201} else {2202firstMatchedChar = textAfterIdx.search(/\S/);2203}2204if (firstMatchedChar == -1) {2205return null;2206}2207idx += firstMatchedChar;2208textAfterIdx = line.substring(idx);2209var textBeforeIdx = line.substring(0, idx);22102211var matchRegex;2212// Greedy matchers for the "word" we are trying to expand.2213if (bigWord) {2214matchRegex = /^\S+/;2215} else {2216if ((/\w/).test(line.charAt(idx))) {2217matchRegex = /^\w+/;2218} else {2219matchRegex = /^[^\w\s]+/;2220}2221}22222223var wordAfterRegex = matchRegex.exec(textAfterIdx);2224var wordStart = idx;2225var wordEnd = idx + wordAfterRegex[0].length;2226// TODO: Find a better way to do this. It will be slow on very long lines.2227var revTextBeforeIdx = reverse(textBeforeIdx);2228var wordBeforeRegex = matchRegex.exec(revTextBeforeIdx);2229if (wordBeforeRegex) {2230wordStart -= wordBeforeRegex[0].length;2231}22322233if (inclusive) {2234// If present, trim all whitespace after word.2235// Otherwise, trim all whitespace before word.2236var textAfterWordEnd = line.substring(wordEnd);2237var whitespacesAfterWord = textAfterWordEnd.match(/^\s*/)[0].length;2238if (whitespacesAfterWord > 0) {2239wordEnd += whitespacesAfterWord;2240} else {2241var revTrim = revTextBeforeIdx.length - wordStart;2242var textBeforeWordStart = revTextBeforeIdx.substring(revTrim);2243var whitespacesBeforeWord = textBeforeWordStart.match(/^\s*/)[0].length;2244wordStart -= whitespacesBeforeWord;2245}2246}22472248return { start: { line: cur.line, ch: wordStart },2249end: { line: cur.line, ch: wordEnd }};2250}22512252function recordJumpPosition(cm, oldCur, newCur) {2253if(!cursorEqual(oldCur, newCur)) {2254vimGlobalState.jumpList.add(cm, oldCur, newCur);2255}2256}22572258function recordLastCharacterSearch(increment, args) {2259vimGlobalState.lastChararacterSearch.increment = increment;2260vimGlobalState.lastChararacterSearch.forward = args.forward;2261vimGlobalState.lastChararacterSearch.selectedCharacter = args.selectedCharacter;2262}22632264var symbolToMode = {2265'(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket',2266'[': 'section', ']': 'section',2267'*': 'comment', '/': 'comment',2268'm': 'method', 'M': 'method',2269'#': 'preprocess'2270};2271var findSymbolModes = {2272bracket: {2273isComplete: function(state) {2274if (state.nextCh === state.symb) {2275state.depth++;2276if(state.depth >= 1)return true;2277} else if (state.nextCh === state.reverseSymb) {2278state.depth--;2279}2280return false;2281}2282},2283section: {2284init: function(state) {2285state.curMoveThrough = true;2286state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}';2287},2288isComplete: function(state) {2289return state.index === 0 && state.nextCh === state.symb;2290}2291},2292comment: {2293isComplete: function(state) {2294var found = state.lastCh === '*' && state.nextCh === '/';2295state.lastCh = state.nextCh;2296return found;2297}2298},2299// TODO: The original Vim implementation only operates on level 1 and 2.2300// The current implementation doesn't check for code block level and2301// therefore it operates on any levels.2302method: {2303init: function(state) {2304state.symb = (state.symb === 'm' ? '{' : '}');2305state.reverseSymb = state.symb === '{' ? '}' : '{';2306},2307isComplete: function(state) {2308if(state.nextCh === state.symb)return true;2309return false;2310}2311},2312preprocess: {2313init: function(state) {2314state.index = 0;2315},2316isComplete: function(state) {2317if (state.nextCh === '#') {2318var token = state.lineText.match(/#(\w+)/)[1];2319if (token === 'endif') {2320if (state.forward && state.depth === 0) {2321return true;2322}2323state.depth++;2324} else if (token === 'if') {2325if (!state.forward && state.depth === 0) {2326return true;2327}2328state.depth--;2329}2330if(token === 'else' && state.depth === 0)return true;2331}2332return false;2333}2334}2335};2336function findSymbol(cm, repeat, forward, symb) {2337var cur = cm.getCursor();2338var increment = forward ? 1 : -1;2339var endLine = forward ? cm.lineCount() : -1;2340var curCh = cur.ch;2341var line = cur.line;2342var lineText = cm.getLine(line);2343var state = {2344lineText: lineText,2345nextCh: lineText.charAt(curCh),2346lastCh: null,2347index: curCh,2348symb: symb,2349reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb],2350forward: forward,2351depth: 0,2352curMoveThrough: false2353};2354var mode = symbolToMode[symb];2355if(!mode)return cur;2356var init = findSymbolModes[mode].init;2357var isComplete = findSymbolModes[mode].isComplete;2358if(init)init(state);2359while (line !== endLine && repeat) {2360state.index += increment;2361state.nextCh = state.lineText.charAt(state.index);2362if (!state.nextCh) {2363line += increment;2364state.lineText = cm.getLine(line) || '';2365if (increment > 0) {2366state.index = 0;2367} else {2368var lineLen = state.lineText.length;2369state.index = (lineLen > 0) ? (lineLen-1) : 0;2370}2371state.nextCh = state.lineText.charAt(state.index);2372}2373if (isComplete(state)) {2374cur.line = line;2375cur.ch = state.index;2376repeat--;2377}2378}2379if (state.nextCh || state.curMoveThrough) {2380return { line: line, ch: state.index };2381}2382return cur;2383}23842385/*2386* Returns the boundaries of the next word. If the cursor in the middle of2387* the word, then returns the boundaries of the current word, starting at2388* the cursor. If the cursor is at the start/end of a word, and we are going2389* forward/backward, respectively, find the boundaries of the next word.2390*2391* @param {CodeMirror} cm CodeMirror object.2392* @param {Cursor} cur The cursor position.2393* @param {boolean} forward True to search forward. False to search2394* backward.2395* @param {boolean} bigWord True if punctuation count as part of the word.2396* False if only [a-zA-Z0-9] characters count as part of the word.2397* @param {boolean} emptyLineIsWord True if empty lines should be treated2398* as words.2399* @return {Object{from:number, to:number, line: number}} The boundaries of2400* the word, or null if there are no more words.2401*/2402function findWord(cm, cur, forward, bigWord, emptyLineIsWord) {2403var lineNum = cur.line;2404var pos = cur.ch;2405var line = cm.getLine(lineNum);2406var dir = forward ? 1 : -1;2407var regexps = bigWord ? bigWordRegexp : wordRegexp;24082409if (emptyLineIsWord && line == '') {2410lineNum += dir;2411line = cm.getLine(lineNum);2412if (!isLine(cm, lineNum)) {2413return null;2414}2415pos = (forward) ? 0 : line.length;2416}24172418while (true) {2419if (emptyLineIsWord && line == '') {2420return { from: 0, to: 0, line: lineNum };2421}2422var stop = (dir > 0) ? line.length : -1;2423var wordStart = stop, wordEnd = stop;2424// Find bounds of next word.2425while (pos != stop) {2426var foundWord = false;2427for (var i = 0; i < regexps.length && !foundWord; ++i) {2428if (regexps[i].test(line.charAt(pos))) {2429wordStart = pos;2430// Advance to end of word.2431while (pos != stop && regexps[i].test(line.charAt(pos))) {2432pos += dir;2433}2434wordEnd = pos;2435foundWord = wordStart != wordEnd;2436if (wordStart == cur.ch && lineNum == cur.line &&2437wordEnd == wordStart + dir) {2438// We started at the end of a word. Find the next one.2439continue;2440} else {2441return {2442from: Math.min(wordStart, wordEnd + 1),2443to: Math.max(wordStart, wordEnd),2444line: lineNum };2445}2446}2447}2448if (!foundWord) {2449pos += dir;2450}2451}2452// Advance to next/prev line.2453lineNum += dir;2454if (!isLine(cm, lineNum)) {2455return null;2456}2457line = cm.getLine(lineNum);2458pos = (dir > 0) ? 0 : line.length;2459}2460// Should never get here.2461throw new Error('The impossible happened.');2462}24632464/**2465* @param {CodeMirror} cm CodeMirror object.2466* @param {int} repeat Number of words to move past.2467* @param {boolean} forward True to search forward. False to search2468* backward.2469* @param {boolean} wordEnd True to move to end of word. False to move to2470* beginning of word.2471* @param {boolean} bigWord True if punctuation count as part of the word.2472* False if only alphabet characters count as part of the word.2473* @return {Cursor} The position the cursor should move to.2474*/2475function moveToWord(cm, repeat, forward, wordEnd, bigWord) {2476var cur = cm.getCursor();2477var curStart = copyCursor(cur);2478var words = [];2479if (forward && !wordEnd || !forward && wordEnd) {2480repeat++;2481}2482// For 'e', empty lines are not considered words, go figure.2483var emptyLineIsWord = !(forward && wordEnd);2484for (var i = 0; i < repeat; i++) {2485var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord);2486if (!word) {2487var eodCh = lineLength(cm, cm.lastLine());2488words.push(forward2489? {line: cm.lastLine(), from: eodCh, to: eodCh}2490: {line: 0, from: 0, to: 0});2491break;2492}2493words.push(word);2494cur = {line: word.line, ch: forward ? (word.to - 1) : word.from};2495}2496var shortCircuit = words.length != repeat;2497var firstWord = words[0];2498var lastWord = words.pop();2499if (forward && !wordEnd) {2500// w2501if (!shortCircuit && (firstWord.from != curStart.ch || firstWord.line != curStart.line)) {2502// We did not start in the middle of a word. Discard the extra word at the end.2503lastWord = words.pop();2504}2505return {line: lastWord.line, ch: lastWord.from};2506} else if (forward && wordEnd) {2507return {line: lastWord.line, ch: lastWord.to - 1};2508} else if (!forward && wordEnd) {2509// ge2510if (!shortCircuit && (firstWord.to != curStart.ch || firstWord.line != curStart.line)) {2511// We did not start in the middle of a word. Discard the extra word at the end.2512lastWord = words.pop();2513}2514return {line: lastWord.line, ch: lastWord.to};2515} else {2516// b2517return {line: lastWord.line, ch: lastWord.from};2518}2519}25202521function moveToCharacter(cm, repeat, forward, character) {2522var cur = cm.getCursor();2523var start = cur.ch;2524var idx;2525for (var i = 0; i < repeat; i ++) {2526var line = cm.getLine(cur.line);2527idx = charIdxInLine(start, line, character, forward, true);2528if (idx == -1) {2529return null;2530}2531start = idx;2532}2533return { line: cm.getCursor().line, ch: idx };2534}25352536function moveToColumn(cm, repeat) {2537// repeat is always >= 1, so repeat - 1 always corresponds2538// to the column we want to go to.2539var line = cm.getCursor().line;2540return clipCursorToContent(cm, { line: line, ch: repeat - 1 });2541}25422543function updateMark(cm, vim, markName, pos) {2544if (!inArray(markName, validMarks)) {2545return;2546}2547if (vim.marks[markName]) {2548vim.marks[markName].clear();2549}2550vim.marks[markName] = cm.setBookmark(pos);2551}25522553function charIdxInLine(start, line, character, forward, includeChar) {2554// Search for char in line.2555// motion_options: {forward, includeChar}2556// If includeChar = true, include it too.2557// If forward = true, search forward, else search backwards.2558// If char is not found on this line, do nothing2559var idx;2560if (forward) {2561idx = line.indexOf(character, start + 1);2562if (idx != -1 && !includeChar) {2563idx -= 1;2564}2565} else {2566idx = line.lastIndexOf(character, start - 1);2567if (idx != -1 && !includeChar) {2568idx += 1;2569}2570}2571return idx;2572}25732574function getContextLevel(ctx) {2575return (ctx === 'string' || ctx === 'comment') ? 1 : 0;2576}25772578function findMatchedSymbol(cm, cur, symb) {2579var line = cur.line;2580var ch = cur.ch;2581symb = symb ? symb : cm.getLine(line).charAt(ch);25822583var symbContext = cm.getTokenAt({line:line, ch:ch+1}).type;2584var symbCtxLevel = getContextLevel(symbContext);25852586var reverseSymb = ({2587'(': ')', ')': '(',2588'[': ']', ']': '[',2589'{': '}', '}': '{'})[symb];25902591// Couldn't find a matching symbol, abort2592if (!reverseSymb) {2593return cur;2594}25952596// set our increment to move forward (+1) or backwards (-1)2597// depending on which bracket we're matching2598var increment = ({'(': 1, '{': 1, '[': 1})[symb] || -1;2599var endLine = increment === 1 ? cm.lineCount() : -1;2600var depth = 1, nextCh = symb, index = ch, lineText = cm.getLine(line);2601// Simple search for closing paren--just count openings and closings till2602// we find our match2603// TODO: use info from CodeMirror to ignore closing brackets in comments2604// and quotes, etc.2605while (line !== endLine && depth > 0) {2606index += increment;2607nextCh = lineText.charAt(index);2608if (!nextCh) {2609line += increment;2610lineText = cm.getLine(line) || '';2611if (increment > 0) {2612index = 0;2613} else {2614var lineLen = lineText.length;2615index = (lineLen > 0) ? (lineLen-1) : 0;2616}2617nextCh = lineText.charAt(index);2618}2619var revSymbContext = cm.getTokenAt({line:line, ch:index+1}).type;2620var revSymbCtxLevel = getContextLevel(revSymbContext);2621if (symbCtxLevel >= revSymbCtxLevel) {2622if (nextCh === symb) {2623depth++;2624} else if (nextCh === reverseSymb) {2625depth--;2626}2627}2628}26292630if (nextCh) {2631return { line: line, ch: index };2632}2633return cur;2634}26352636function selectCompanionObject(cm, revSymb, inclusive) {2637var cur = cm.getCursor();26382639var end = findMatchedSymbol(cm, cur, revSymb);2640var start = findMatchedSymbol(cm, end);2641start.ch += inclusive ? 1 : 0;2642end.ch += inclusive ? 0 : 1;26432644return { start: start, end: end };2645}26462647// Takes in a symbol and a cursor and tries to simulate text objects that2648// have identical opening and closing symbols2649// TODO support across multiple lines2650function findBeginningAndEnd(cm, symb, inclusive) {2651var cur = cm.getCursor();2652var line = cm.getLine(cur.line);2653var chars = line.split('');2654var start, end, i, len;2655var firstIndex = chars.indexOf(symb);26562657// the decision tree is to always look backwards for the beginning first,2658// but if the cursor is in front of the first instance of the symb,2659// then move the cursor forward2660if (cur.ch < firstIndex) {2661cur.ch = firstIndex;2662// Why is this line even here???2663// cm.setCursor(cur.line, firstIndex+1);2664}2665// otherwise if the cursor is currently on the closing symbol2666else if (firstIndex < cur.ch && chars[cur.ch] == symb) {2667end = cur.ch; // assign end to the current cursor2668--cur.ch; // make sure to look backwards2669}26702671// if we're currently on the symbol, we've got a start2672if (chars[cur.ch] == symb && !end) {2673start = cur.ch + 1; // assign start to ahead of the cursor2674} else {2675// go backwards to find the start2676for (i = cur.ch; i > -1 && !start; i--) {2677if (chars[i] == symb) {2678start = i + 1;2679}2680}2681}26822683// look forwards for the end symbol2684if (start && !end) {2685for (i = start, len = chars.length; i < len && !end; i++) {2686if (chars[i] == symb) {2687end = i;2688}2689}2690}26912692// nothing found2693if (!start || !end) {2694return { start: cur, end: cur };2695}26962697// include the symbols2698if (inclusive) {2699--start; ++end;2700}27012702return {2703start: { line: cur.line, ch: start },2704end: { line: cur.line, ch: end }2705};2706}27072708// Search functions2709function SearchState() {}2710SearchState.prototype = {2711getQuery: function() {2712return vimGlobalState.query;2713},2714setQuery: function(query) {2715vimGlobalState.query = query;2716},2717getOverlay: function() {2718return this.searchOverlay;2719},2720setOverlay: function(overlay) {2721this.searchOverlay = overlay;2722},2723isReversed: function() {2724return vimGlobalState.isReversed;2725},2726setReversed: function(reversed) {2727vimGlobalState.isReversed = reversed;2728}2729};2730function getSearchState(cm) {2731var vim = cm.state.vim;2732return vim.searchState_ || (vim.searchState_ = new SearchState());2733}2734function dialog(cm, template, shortText, onClose, options) {2735if (cm.openDialog) {2736cm.openDialog(template, onClose, { bottom: true, value: options.value,2737onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp });2738}2739else {2740onClose(prompt(shortText, ''));2741}2742}27432744function findUnescapedSlashes(str) {2745var escapeNextChar = false;2746var slashes = [];2747for (var i = 0; i < str.length; i++) {2748var c = str.charAt(i);2749if (!escapeNextChar && c == '/') {2750slashes.push(i);2751}2752escapeNextChar = (c == '\\');2753}2754return slashes;2755}2756/**2757* Extract the regular expression from the query and return a Regexp object.2758* Returns null if the query is blank.2759* If ignoreCase is passed in, the Regexp object will have the 'i' flag set.2760* If smartCase is passed in, and the query contains upper case letters,2761* then ignoreCase is overridden, and the 'i' flag will not be set.2762* If the query contains the /i in the flag part of the regular expression,2763* then both ignoreCase and smartCase are ignored, and 'i' will be passed2764* through to the Regex object.2765*/2766function parseQuery(query, ignoreCase, smartCase) {2767// Check if the query is already a regex.2768if (query instanceof RegExp) { return query; }2769// First try to extract regex + flags from the input. If no flags found,2770// extract just the regex. IE does not accept flags directly defined in2771// the regex string in the form /regex/flags2772var slashes = findUnescapedSlashes(query);2773var regexPart;2774var forceIgnoreCase;2775if (!slashes.length) {2776// Query looks like 'regexp'2777regexPart = query;2778} else {2779// Query looks like 'regexp/...'2780regexPart = query.substring(0, slashes[0]);2781var flagsPart = query.substring(slashes[0]);2782forceIgnoreCase = (flagsPart.indexOf('i') != -1);2783}2784if (!regexPart) {2785return null;2786}2787if (smartCase) {2788ignoreCase = (/^[^A-Z]*$/).test(regexPart);2789}2790var regexp = new RegExp(regexPart,2791(ignoreCase || forceIgnoreCase) ? 'i' : undefined);2792return regexp;2793}2794function showConfirm(cm, text) {2795if (cm.openNotification) {2796cm.openNotification('<span style="color: red">' + text + '</span>',2797{bottom: true, duration: 5000});2798} else {2799alert(text);2800}2801}2802function makePrompt(prefix, desc) {2803var raw = '';2804if (prefix) {2805raw += '<span style="font-family: monospace">' + prefix + '</span>';2806}2807raw += '<input type="text"/> ' +2808'<span style="color: #888">';2809if (desc) {2810raw += '<span style="color: #888">';2811raw += desc;2812raw += '</span>';2813}2814return raw;2815}2816var searchPromptDesc = '(Javascript regexp)';2817function showPrompt(cm, options) {2818var shortText = (options.prefix || '') + ' ' + (options.desc || '');2819var prompt = makePrompt(options.prefix, options.desc);2820dialog(cm, prompt, shortText, options.onClose, options);2821}2822function regexEqual(r1, r2) {2823if (r1 instanceof RegExp && r2 instanceof RegExp) {2824var props = ['global', 'multiline', 'ignoreCase', 'source'];2825for (var i = 0; i < props.length; i++) {2826var prop = props[i];2827if (r1[prop] !== r2[prop]) {2828return false;2829}2830}2831return true;2832}2833return false;2834}2835// Returns true if the query is valid.2836function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) {2837if (!rawQuery) {2838return;2839}2840var state = getSearchState(cm);2841var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase);2842if (!query) {2843return;2844}2845highlightSearchMatches(cm, query);2846if (regexEqual(query, state.getQuery())) {2847return query;2848}2849state.setQuery(query);2850return query;2851}2852function searchOverlay(query) {2853if (query.source.charAt(0) == '^') {2854var matchSol = true;2855}2856return {2857token: function(stream) {2858if (matchSol && !stream.sol()) {2859stream.skipToEnd();2860return;2861}2862var match = stream.match(query, false);2863if (match) {2864if (match[0].length == 0) {2865// Matched empty string, skip to next.2866stream.next();2867return 'searching';2868}2869if (!stream.sol()) {2870// Backtrack 1 to match \b2871stream.backUp(1);2872if (!query.exec(stream.next() + match[0])) {2873stream.next();2874return null;2875}2876}2877stream.match(query);2878return 'searching';2879}2880while (!stream.eol()) {2881stream.next();2882if (stream.match(query, false)) break;2883}2884},2885query: query2886};2887}2888function highlightSearchMatches(cm, query) {2889var overlay = getSearchState(cm).getOverlay();2890if (!overlay || query != overlay.query) {2891if (overlay) {2892cm.removeOverlay(overlay);2893}2894overlay = searchOverlay(query);2895cm.addOverlay(overlay);2896getSearchState(cm).setOverlay(overlay);2897}2898}2899function findNext(cm, prev, query, repeat) {2900if (repeat === undefined) { repeat = 1; }2901return cm.operation(function() {2902var pos = cm.getCursor();2903var cursor = cm.getSearchCursor(query, pos);2904for (var i = 0; i < repeat; i++) {2905var found = cursor.find(prev);2906if (i == 0 && found && cursorEqual(cursor.from(), pos)) { found = cursor.find(prev); }2907if (!found) {2908// SearchCursor may have returned null because it hit EOF, wrap2909// around and try again.2910cursor = cm.getSearchCursor(query,2911(prev) ? { line: cm.lastLine() } : {line: cm.firstLine(), ch: 0} );2912if (!cursor.find(prev)) {2913return;2914}2915}2916}2917return cursor.from();2918});2919}2920function clearSearchHighlight(cm) {2921cm.removeOverlay(getSearchState(cm).getOverlay());2922getSearchState(cm).setOverlay(null);2923}2924/**2925* Check if pos is in the specified range, INCLUSIVE.2926* Range can be specified with 1 or 2 arguments.2927* If the first range argument is an array, treat it as an array of line2928* numbers. Match pos against any of the lines.2929* If the first range argument is a number,2930* if there is only 1 range argument, check if pos has the same line2931* number2932* if there are 2 range arguments, then check if pos is in between the two2933* range arguments.2934*/2935function isInRange(pos, start, end) {2936if (typeof pos != 'number') {2937// Assume it is a cursor position. Get the line number.2938pos = pos.line;2939}2940if (start instanceof Array) {2941return inArray(pos, start);2942} else {2943if (end) {2944return (pos >= start && pos <= end);2945} else {2946return pos == start;2947}2948}2949}2950function getUserVisibleLines(cm) {2951var scrollInfo = cm.getScrollInfo();2952var occludeToleranceTop = 6;2953var occludeToleranceBottom = 10;2954var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local');2955var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top;2956var to = cm.coordsChar({left:0, top: bottomY}, 'local');2957return {top: from.line, bottom: to.line};2958}29592960// Ex command handling2961// Care must be taken when adding to the default Ex command map. For any2962// pair of commands that have a shared prefix, at least one of their2963// shortNames must not match the prefix of the other command.2964var defaultExCommandMap = [2965{ name: 'map' },2966{ name: 'nmap', shortName: 'nm' },2967{ name: 'vmap', shortName: 'vm' },2968{ name: 'write', shortName: 'w' },2969{ name: 'undo', shortName: 'u' },2970{ name: 'redo', shortName: 'red' },2971{ name: 'sort', shortName: 'sor' },2972{ name: 'substitute', shortName: 's' },2973{ name: 'nohlsearch', shortName: 'noh' },2974{ name: 'delmarks', shortName: 'delm' }2975];2976Vim.ExCommandDispatcher = function() {2977this.buildCommandMap_();2978};2979Vim.ExCommandDispatcher.prototype = {2980processCommand: function(cm, input) {2981var vim = cm.state.vim;2982if (vim.visualMode) {2983exitVisualMode(cm);2984}2985var inputStream = new CodeMirror.StringStream(input);2986var params = {};2987params.input = input;2988try {2989this.parseInput_(cm, inputStream, params);2990} catch(e) {2991showConfirm(cm, e);2992return;2993}2994var commandName;2995if (!params.commandName) {2996// If only a line range is defined, move to the line.2997if (params.line !== undefined) {2998commandName = 'move';2999}3000} else {3001var command = this.matchCommand_(params.commandName);3002if (command) {3003commandName = command.name;3004this.parseCommandArgs_(inputStream, params, command);3005if (command.type == 'exToKey') {3006// Handle Ex to Key mapping.3007for (var i = 0; i < command.toKeys.length; i++) {3008CodeMirror.Vim.handleKey(cm, command.toKeys[i]);3009}3010return;3011} else if (command.type == 'exToEx') {3012// Handle Ex to Ex mapping.3013this.processCommand(cm, command.toInput);3014return;3015}3016}3017}3018if (!commandName) {3019showConfirm(cm, 'Not an editor command ":' + input + '"');3020return;3021}3022try {3023exCommands[commandName](cm, params);3024} catch(e) {3025showConfirm(cm, e);3026throw e;3027}3028},3029parseInput_: function(cm, inputStream, result) {3030inputStream.eatWhile(':');3031// Parse range.3032if (inputStream.eat('%')) {3033result.line = cm.firstLine();3034result.lineEnd = cm.lastLine();3035} else {3036result.line = this.parseLineSpec_(cm, inputStream);3037if (result.line !== undefined && inputStream.eat(',')) {3038result.lineEnd = this.parseLineSpec_(cm, inputStream);3039}3040}30413042// Parse command name.3043var commandMatch = inputStream.match(/^(\w+)/);3044if (commandMatch) {3045result.commandName = commandMatch[1];3046} else {3047result.commandName = inputStream.match(/.*/)[0];3048}30493050return result;3051},3052parseLineSpec_: function(cm, inputStream) {3053var numberMatch = inputStream.match(/^(\d+)/);3054if (numberMatch) {3055return parseInt(numberMatch[1], 10) - 1;3056}3057switch (inputStream.next()) {3058case '.':3059return cm.getCursor().line;3060case '$':3061return cm.lastLine();3062case '\'':3063var mark = cm.state.vim.marks[inputStream.next()];3064if (mark && mark.find()) {3065return mark.find().line;3066}3067throw new Error('Mark not set');3068default:3069inputStream.backUp(1);3070return undefined;3071}3072},3073parseCommandArgs_: function(inputStream, params, command) {3074if (inputStream.eol()) {3075return;3076}3077params.argString = inputStream.match(/.*/)[0];3078// Parse command-line arguments3079var delim = command.argDelimiter || /\s+/;3080var args = trim(params.argString).split(delim);3081if (args.length && args[0]) {3082params.args = args;3083}3084},3085matchCommand_: function(commandName) {3086// Return the command in the command map that matches the shortest3087// prefix of the passed in command name. The match is guaranteed to be3088// unambiguous if the defaultExCommandMap's shortNames are set up3089// correctly. (see @code{defaultExCommandMap}).3090for (var i = commandName.length; i > 0; i--) {3091var prefix = commandName.substring(0, i);3092if (this.commandMap_[prefix]) {3093var command = this.commandMap_[prefix];3094if (command.name.indexOf(commandName) === 0) {3095return command;3096}3097}3098}3099return null;3100},3101buildCommandMap_: function() {3102this.commandMap_ = {};3103for (var i = 0; i < defaultExCommandMap.length; i++) {3104var command = defaultExCommandMap[i];3105var key = command.shortName || command.name;3106this.commandMap_[key] = command;3107}3108},3109map: function(lhs, rhs, ctx) {3110if (lhs != ':' && lhs.charAt(0) == ':') {3111if (ctx) { throw Error('Mode not supported for ex mappings'); }3112var commandName = lhs.substring(1);3113if (rhs != ':' && rhs.charAt(0) == ':') {3114// Ex to Ex mapping3115this.commandMap_[commandName] = {3116name: commandName,3117type: 'exToEx',3118toInput: rhs.substring(1)3119};3120} else {3121// Ex to key mapping3122this.commandMap_[commandName] = {3123name: commandName,3124type: 'exToKey',3125toKeys: parseKeyString(rhs)3126};3127}3128} else {3129if (rhs != ':' && rhs.charAt(0) == ':') {3130// Key to Ex mapping.3131var mapping = {3132keys: parseKeyString(lhs),3133type: 'keyToEx',3134exArgs: { input: rhs.substring(1) }};3135if (ctx) { mapping.context = ctx; }3136defaultKeymap.unshift(mapping);3137} else {3138// Key to key mapping3139var mapping = {3140keys: parseKeyString(lhs),3141type: 'keyToKey',3142toKeys: parseKeyString(rhs)3143};3144if (ctx) { mapping.context = ctx; }3145defaultKeymap.unshift(mapping);3146}3147}3148}3149};31503151// Converts a key string sequence of the form a<C-w>bd<Left> into Vim's3152// keymap representation.3153function parseKeyString(str) {3154var key, match;3155var keys = [];3156while (str) {3157match = (/<\w+-.+?>|<\w+>|./).exec(str);3158if(match === null)break;3159key = match[0];3160str = str.substring(match.index + key.length);3161keys.push(key);3162}3163return keys;3164}31653166var exCommands = {3167map: function(cm, params, ctx) {3168var mapArgs = params.args;3169if (!mapArgs || mapArgs.length < 2) {3170if (cm) {3171showConfirm(cm, 'Invalid mapping: ' + params.input);3172}3173return;3174}3175exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx);3176},3177nmap: function(cm, params) { this.map(cm, params, 'normal'); },3178vmap: function(cm, params) { this.map(cm, params, 'visual'); },3179move: function(cm, params) {3180commandDispatcher.processCommand(cm, cm.state.vim, {3181type: 'motion',3182motion: 'moveToLineOrEdgeOfDocument',3183motionArgs: { forward: false, explicitRepeat: true,3184linewise: true },3185repeatOverride: params.line+1});3186},3187sort: function(cm, params) {3188var reverse, ignoreCase, unique, number;3189function parseArgs() {3190if (params.argString) {3191var args = new CodeMirror.StringStream(params.argString);3192if (args.eat('!')) { reverse = true; }3193if (args.eol()) { return; }3194if (!args.eatSpace()) { return 'Invalid arguments'; }3195var opts = args.match(/[a-z]+/);3196if (opts) {3197opts = opts[0];3198ignoreCase = opts.indexOf('i') != -1;3199unique = opts.indexOf('u') != -1;3200var decimal = opts.indexOf('d') != -1 && 1;3201var hex = opts.indexOf('x') != -1 && 1;3202var octal = opts.indexOf('o') != -1 && 1;3203if (decimal + hex + octal > 1) { return 'Invalid arguments'; }3204number = decimal && 'decimal' || hex && 'hex' || octal && 'octal';3205}3206if (args.eatSpace() && args.match(/\/.*\//)) { 'patterns not supported'; }3207}3208}3209var err = parseArgs();3210if (err) {3211showConfirm(cm, err + ': ' + params.argString);3212return;3213}3214var lineStart = params.line || cm.firstLine();3215var lineEnd = params.lineEnd || params.line || cm.lastLine();3216if (lineStart == lineEnd) { return; }3217var curStart = { line: lineStart, ch: 0 };3218var curEnd = { line: lineEnd, ch: lineLength(cm, lineEnd) };3219var text = cm.getRange(curStart, curEnd).split('\n');3220var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ :3221(number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i :3222(number == 'octal') ? /([0-7]+)/ : null;3223var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null;3224var numPart = [], textPart = [];3225if (number) {3226for (var i = 0; i < text.length; i++) {3227if (numberRegex.exec(text[i])) {3228numPart.push(text[i]);3229} else {3230textPart.push(text[i]);3231}3232}3233} else {3234textPart = text;3235}3236function compareFn(a, b) {3237if (reverse) { var tmp; tmp = a; a = b; b = tmp; }3238if (ignoreCase) { a = a.toLowerCase(); b = b.toLowerCase(); }3239var anum = number && numberRegex.exec(a);3240var bnum = number && numberRegex.exec(b);3241if (!anum) { return a < b ? -1 : 1; }3242anum = parseInt((anum[1] + anum[2]).toLowerCase(), radix);3243bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix);3244return anum - bnum;3245}3246numPart.sort(compareFn);3247textPart.sort(compareFn);3248text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart);3249if (unique) { // Remove duplicate lines3250var textOld = text;3251var lastLine;3252text = [];3253for (var i = 0; i < textOld.length; i++) {3254if (textOld[i] != lastLine) {3255text.push(textOld[i]);3256}3257lastLine = textOld[i];3258}3259}3260cm.replaceRange(text.join('\n'), curStart, curEnd);3261},3262substitute: function(cm, params) {3263if (!cm.getSearchCursor) {3264throw new Error('Search feature not available. Requires searchcursor.js or ' +3265'any other getSearchCursor implementation.');3266}3267var argString = params.argString;3268var slashes = findUnescapedSlashes(argString);3269if (slashes[0] !== 0) {3270showConfirm(cm, 'Substitutions should be of the form ' +3271':s/pattern/replace/');3272return;3273}3274var regexPart = argString.substring(slashes[0] + 1, slashes[1]);3275var replacePart = '';3276var flagsPart;3277var count;3278var confirm = false; // Whether to confirm each replace.3279if (slashes[1]) {3280replacePart = argString.substring(slashes[1] + 1, slashes[2]);3281}3282if (slashes[2]) {3283// After the 3rd slash, we can have flags followed by a space followed3284// by count.3285var trailing = argString.substring(slashes[2] + 1).split(' ');3286flagsPart = trailing[0];3287count = parseInt(trailing[1]);3288}3289if (flagsPart) {3290if (flagsPart.indexOf('c') != -1) {3291confirm = true;3292flagsPart.replace('c', '');3293}3294regexPart = regexPart + '/' + flagsPart;3295}3296if (regexPart) {3297// If regex part is empty, then use the previous query. Otherwise use3298// the regex part as the new query.3299try {3300updateSearchQuery(cm, regexPart, true /** ignoreCase */,3301true /** smartCase */);3302} catch (e) {3303showConfirm(cm, 'Invalid regex: ' + regexPart);3304return;3305}3306}3307var state = getSearchState(cm);3308var query = state.getQuery();3309var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line;3310var lineEnd = params.lineEnd || lineStart;3311if (count) {3312lineStart = lineEnd;3313lineEnd = lineStart + count - 1;3314}3315var startPos = clipCursorToContent(cm, { line: lineStart, ch: 0 });3316var cursor = cm.getSearchCursor(query, startPos);3317doReplace(cm, confirm, lineStart, lineEnd, cursor, query, replacePart);3318},3319redo: CodeMirror.commands.redo,3320undo: CodeMirror.commands.undo,3321write: function(cm) {3322if (CodeMirror.commands.save) {3323// If a save command is defined, call it.3324CodeMirror.commands.save(cm);3325} else {3326// Saves to text area if no save command is defined.3327cm.save();3328}3329},3330nohlsearch: function(cm) {3331clearSearchHighlight(cm);3332},3333delmarks: function(cm, params) {3334if (!params.argString || !trim(params.argString)) {3335showConfirm(cm, 'Argument required');3336return;3337}33383339var state = cm.state.vim;3340var stream = new CodeMirror.StringStream(trim(params.argString));3341while (!stream.eol()) {3342stream.eatSpace();33433344// Record the streams position at the beginning of the loop for use3345// in error messages.3346var count = stream.pos;33473348if (!stream.match(/[a-zA-Z]/, false)) {3349showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));3350return;3351}33523353var sym = stream.next();3354// Check if this symbol is part of a range3355if (stream.match('-', true)) {3356// This symbol is part of a range.33573358// The range must terminate at an alphabetic character.3359if (!stream.match(/[a-zA-Z]/, false)) {3360showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));3361return;3362}33633364var startMark = sym;3365var finishMark = stream.next();3366// The range must terminate at an alphabetic character which3367// shares the same case as the start of the range.3368if (isLowerCase(startMark) && isLowerCase(finishMark) ||3369isUpperCase(startMark) && isUpperCase(finishMark)) {3370var start = startMark.charCodeAt(0);3371var finish = finishMark.charCodeAt(0);3372if (start >= finish) {3373showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));3374return;3375}33763377// Because marks are always ASCII values, and we have3378// determined that they are the same case, we can use3379// their char codes to iterate through the defined range.3380for (var j = 0; j <= finish - start; j++) {3381var mark = String.fromCharCode(start + j);3382delete state.marks[mark];3383}3384} else {3385showConfirm(cm, 'Invalid argument: ' + startMark + '-');3386return;3387}3388} else {3389// This symbol is a valid mark, and is not part of a range.3390delete state.marks[sym];3391}3392}3393}3394};33953396var exCommandDispatcher = new Vim.ExCommandDispatcher();33973398/**3399* @param {CodeMirror} cm CodeMirror instance we are in.3400* @param {boolean} confirm Whether to confirm each replace.3401* @param {Cursor} lineStart Line to start replacing from.3402* @param {Cursor} lineEnd Line to stop replacing at.3403* @param {RegExp} query Query for performing matches with.3404* @param {string} replaceWith Text to replace matches with. May contain $1,3405* $2, etc for replacing captured groups using Javascript replace.3406*/3407function doReplace(cm, confirm, lineStart, lineEnd, searchCursor, query,3408replaceWith) {3409// Set up all the functions.3410cm.state.vim.exMode = true;3411var done = false;3412var lastPos = searchCursor.from();3413function replaceAll() {3414cm.operation(function() {3415while (!done) {3416replace();3417next();3418}3419stop();3420});3421}3422function replace() {3423var text = cm.getRange(searchCursor.from(), searchCursor.to());3424var newText = text.replace(query, replaceWith);3425searchCursor.replace(newText);3426}3427function next() {3428var found = searchCursor.findNext();3429if (!found) {3430done = true;3431} else if (isInRange(searchCursor.from(), lineStart, lineEnd)) {3432cm.scrollIntoView(searchCursor.from(), 30);3433cm.setSelection(searchCursor.from(), searchCursor.to());3434lastPos = searchCursor.from();3435done = false;3436} else {3437done = true;3438}3439}3440function stop(close) {3441if (close) { close(); }3442cm.focus();3443if (lastPos) {3444cm.setCursor(lastPos);3445var vim = cm.state.vim;3446vim.exMode = false;3447vim.lastHPos = vim.lastHSPos = lastPos.ch;3448}3449}3450function onPromptKeyDown(e, _value, close) {3451// Swallow all keys.3452CodeMirror.e_stop(e);3453var keyName = CodeMirror.keyName(e);3454switch (keyName) {3455case 'Y':3456replace(); next(); break;3457case 'N':3458next(); break;3459case 'A':3460cm.operation(replaceAll); break;3461case 'L':3462replace();3463// fall through and exit.3464case 'Q':3465case 'Esc':3466case 'Ctrl-C':3467case 'Ctrl-[':3468stop(close);3469break;3470}3471if (done) { stop(close); }3472}34733474// Actually do replace.3475next();3476if (done) {3477showConfirm(cm, 'No matches for ' + query.source);3478return;3479}3480if (!confirm) {3481replaceAll();3482return;3483}3484showPrompt(cm, {3485prefix: 'replace with <strong>' + replaceWith + '</strong> (y/n/a/q/l)',3486onKeyDown: onPromptKeyDown3487});3488}34893490// Register Vim with CodeMirror3491function buildVimKeyMap() {3492/**3493* Handle the raw key event from CodeMirror. Translate the3494* Shift + key modifier to the resulting letter, while preserving other3495* modifers.3496*/3497// TODO: Figure out a way to catch capslock.3498function cmKeyToVimKey(key, modifier) {3499var vimKey = key;3500if (isUpperCase(vimKey)) {3501// Convert to lower case if shift is not the modifier since the key3502// we get from CodeMirror is always upper case.3503if (modifier == 'Shift') {3504modifier = null;3505}3506else {3507vimKey = vimKey.toLowerCase();3508}3509}3510if (modifier) {3511// Vim will parse modifier+key combination as a single key.3512vimKey = modifier.charAt(0) + '-' + vimKey;3513}3514var specialKey = ({Enter:'CR',Backspace:'BS',Delete:'Del'})[vimKey];3515vimKey = specialKey ? specialKey : vimKey;3516vimKey = vimKey.length > 1 ? '<'+ vimKey + '>' : vimKey;3517return vimKey;3518}35193520// Closure to bind CodeMirror, key, modifier.3521function keyMapper(vimKey) {3522return function(cm) {3523CodeMirror.Vim.handleKey(cm, vimKey);3524};3525}35263527var cmToVimKeymap = {3528'nofallthrough': true,3529'style': 'fat-cursor'3530};3531function bindKeys(keys, modifier) {3532for (var i = 0; i < keys.length; i++) {3533var key = keys[i];3534if (!modifier && inArray(key, specialSymbols)) {3535// Wrap special symbols with '' because that's how CodeMirror binds3536// them.3537key = "'" + key + "'";3538}3539var vimKey = cmKeyToVimKey(keys[i], modifier);3540var cmKey = modifier ? modifier + '-' + key : key;3541cmToVimKeymap[cmKey] = keyMapper(vimKey);3542}3543}3544bindKeys(upperCaseAlphabet);3545bindKeys(upperCaseAlphabet, 'Shift');3546bindKeys(upperCaseAlphabet, 'Ctrl');3547bindKeys(specialSymbols);3548bindKeys(specialSymbols, 'Ctrl');3549bindKeys(numbers);3550bindKeys(numbers, 'Ctrl');3551bindKeys(specialKeys);3552bindKeys(specialKeys, 'Ctrl');3553return cmToVimKeymap;3554}3555CodeMirror.keyMap.vim = buildVimKeyMap();35563557function exitInsertMode(cm) {3558var vim = cm.state.vim;3559var inReplay = vimGlobalState.macroModeState.inReplay;3560if (!inReplay) {3561cm.off('change', onChange);3562cm.off('cursorActivity', onCursorActivity);3563CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown);3564}3565if (!inReplay && vim.insertModeRepeat > 1) {3566// Perform insert mode repeat for commands like 3,a and 3,o.3567repeatLastEdit(cm, vim, vim.insertModeRepeat - 1,3568true /** repeatForInsert */);3569vim.lastEditInputState.repeatOverride = vim.insertModeRepeat;3570}3571delete vim.insertModeRepeat;3572cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1, true);3573vim.insertMode = false;3574cm.setOption('keyMap', 'vim');3575cm.setOption('disableInput', true);3576cm.toggleOverwrite(false); // exit replace mode if we were in it.3577CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});3578}35793580CodeMirror.keyMap['vim-insert'] = {3581// TODO: override navigation keys so that Esc will cancel automatic3582// indentation from o, O, i_<CR>3583'Esc': exitInsertMode,3584'Ctrl-[': exitInsertMode,3585'Ctrl-C': exitInsertMode,3586'Ctrl-N': 'autocomplete',3587'Ctrl-P': 'autocomplete',3588'Enter': function(cm) {3589var fn = CodeMirror.commands.newlineAndIndentContinueComment ||3590CodeMirror.commands.newlineAndIndent;3591fn(cm);3592},3593fallthrough: ['default']3594};35953596CodeMirror.keyMap['vim-replace'] = {3597'Backspace': 'goCharLeft',3598fallthrough: ['vim-insert']3599};36003601function parseRegisterToKeyBuffer(macroModeState, registerName) {3602var match, key;3603var register = vimGlobalState.registerController.getRegister(registerName);3604var text = register.toString();3605var macroKeyBuffer = macroModeState.macroKeyBuffer;3606emptyMacroKeyBuffer(macroModeState);3607do {3608match = (/<\w+-.+?>|<\w+>|./).exec(text);3609if(match === null)break;3610key = match[0];3611text = text.substring(match.index + key.length);3612macroKeyBuffer.push(key);3613} while (text);3614return macroKeyBuffer;3615}36163617function parseKeyBufferToRegister(registerName, keyBuffer) {3618var text = keyBuffer.join('');3619vimGlobalState.registerController.setRegisterText(registerName, text);3620}36213622function emptyMacroKeyBuffer(macroModeState) {3623if(macroModeState.isMacroPlaying)return;3624var macroKeyBuffer = macroModeState.macroKeyBuffer;3625macroKeyBuffer.length = 0;3626}36273628function executeMacroKeyBuffer(cm, macroModeState, keyBuffer) {3629macroModeState.isMacroPlaying = true;3630for (var i = 0, len = keyBuffer.length; i < len; i++) {3631CodeMirror.Vim.handleKey(cm, keyBuffer[i]);3632};3633macroModeState.isMacroPlaying = false;3634}36353636function logKey(macroModeState, key) {3637if(macroModeState.isMacroPlaying)return;3638var macroKeyBuffer = macroModeState.macroKeyBuffer;3639macroKeyBuffer.push(key);3640}36413642/**3643* Listens for changes made in insert mode.3644* Should only be active in insert mode.3645*/3646function onChange(_cm, changeObj) {3647var macroModeState = vimGlobalState.macroModeState;3648var lastChange = macroModeState.lastInsertModeChanges;3649while (changeObj) {3650lastChange.expectCursorActivityForChange = true;3651if (changeObj.origin == '+input' || changeObj.origin == 'paste'3652|| changeObj.origin === undefined /* only in testing */) {3653var text = changeObj.text.join('\n');3654lastChange.changes.push(text);3655}3656// Change objects may be chained with next.3657changeObj = changeObj.next;3658}3659}36603661/**3662* Listens for any kind of cursor activity on CodeMirror.3663* - For tracking cursor activity in insert mode.3664* - Should only be active in insert mode.3665*/3666function onCursorActivity() {3667var macroModeState = vimGlobalState.macroModeState;3668var lastChange = macroModeState.lastInsertModeChanges;3669if (lastChange.expectCursorActivityForChange) {3670lastChange.expectCursorActivityForChange = false;3671} else {3672// Cursor moved outside the context of an edit. Reset the change.3673lastChange.changes = [];3674}3675}36763677/** Wrapper for special keys pressed in insert mode */3678function InsertModeKey(keyName) {3679this.keyName = keyName;3680}36813682/**3683* Handles raw key down events from the text area.3684* - Should only be active in insert mode.3685* - For recording deletes in insert mode.3686*/3687function onKeyEventTargetKeyDown(e) {3688var macroModeState = vimGlobalState.macroModeState;3689var lastChange = macroModeState.lastInsertModeChanges;3690var keyName = CodeMirror.keyName(e);3691function onKeyFound() {3692lastChange.changes.push(new InsertModeKey(keyName));3693return true;3694}3695if (keyName.indexOf('Delete') != -1 || keyName.indexOf('Backspace') != -1) {3696CodeMirror.lookupKey(keyName, ['vim-insert'], onKeyFound);3697}3698}36993700/**3701* Repeats the last edit, which includes exactly 1 command and at most 13702* insert. Operator and motion commands are read from lastEditInputState,3703* while action commands are read from lastEditActionCommand.3704*3705* If repeatForInsert is true, then the function was called by3706* exitInsertMode to repeat the insert mode changes the user just made. The3707* corresponding enterInsertMode call was made with a count.3708*/3709function repeatLastEdit(cm, vim, repeat, repeatForInsert) {3710var macroModeState = vimGlobalState.macroModeState;3711macroModeState.inReplay = true;3712var isAction = !!vim.lastEditActionCommand;3713var cachedInputState = vim.inputState;3714function repeatCommand() {3715if (isAction) {3716commandDispatcher.processAction(cm, vim, vim.lastEditActionCommand);3717} else {3718commandDispatcher.evalInput(cm, vim);3719}3720}3721function repeatInsert(repeat) {3722if (macroModeState.lastInsertModeChanges.changes.length > 0) {3723// For some reason, repeat cw in desktop VIM will does not repeat3724// insert mode changes. Will conform to that behavior.3725repeat = !vim.lastEditActionCommand ? 1 : repeat;3726repeatLastInsertModeChanges(cm, repeat, macroModeState);3727}3728}3729vim.inputState = vim.lastEditInputState;3730if (isAction && vim.lastEditActionCommand.interlaceInsertRepeat) {3731// o and O repeat have to be interlaced with insert repeats so that the3732// insertions appear on separate lines instead of the last line.3733for (var i = 0; i < repeat; i++) {3734repeatCommand();3735repeatInsert(1);3736}3737} else {3738if (!repeatForInsert) {3739// Hack to get the cursor to end up at the right place. If I is3740// repeated in insert mode repeat, cursor will be 1 insert3741// change set left of where it should be.3742repeatCommand();3743}3744repeatInsert(repeat);3745}3746vim.inputState = cachedInputState;3747if (vim.insertMode && !repeatForInsert) {3748// Don't exit insert mode twice. If repeatForInsert is set, then we3749// were called by an exitInsertMode call lower on the stack.3750exitInsertMode(cm);3751}3752macroModeState.inReplay = false;3753};37543755function repeatLastInsertModeChanges(cm, repeat, macroModeState) {3756var lastChange = macroModeState.lastInsertModeChanges;3757function keyHandler(binding) {3758if (typeof binding == 'string') {3759CodeMirror.commands[binding](cm);3760} else {3761binding(cm);3762}3763return true;3764}3765for (var i = 0; i < repeat; i++) {3766for (var j = 0; j < lastChange.changes.length; j++) {3767var change = lastChange.changes[j];3768if (change instanceof InsertModeKey) {3769CodeMirror.lookupKey(change.keyName, ['vim-insert'], keyHandler);3770} else {3771var cur = cm.getCursor();3772cm.replaceRange(change, cur, cur);3773}3774}3775}3776}37773778resetVimGlobalState();3779return vimApi;3780};3781// Initialize Vim and make it available as an API.3782CodeMirror.Vim = Vim();3783}3784)();378537863787