[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 // Distributed under an MIT license: https://codemirror.net/5/LICENSE 3 4 /** 5 * Supported keybindings: 6 * Too many to list. Refer to defaultKeymap below. 7 * 8 * Supported Ex commands: 9 * Refer to defaultExCommandMap below. 10 * 11 * Registers: unnamed, -, ., :, /, _, a-z, A-Z, 0-9 12 * (Does not respect the special case for number registers when delete 13 * operator is made with these commands: %, (, ), , /, ?, n, N, {, } ) 14 * TODO: Implement the remaining registers. 15 * 16 * Marks: a-z, A-Z, and 0-9 17 * TODO: Implement the remaining special marks. They have more complex 18 * behavior. 19 * 20 * Events: 21 * 'vim-mode-change' - raised on the editor anytime the current mode changes, 22 * Event object: {mode: "visual", subMode: "linewise"} 23 * 24 * Code structure: 25 * 1. Default keymap 26 * 2. Variable declarations and short basic helpers 27 * 3. Instance (External API) implementation 28 * 4. Internal state tracking objects (input state, counter) implementation 29 * and instantiation 30 * 5. Key handler (the main command dispatcher) implementation 31 * 6. Motion, operator, and action implementations 32 * 7. Helper functions for the key handler, motions, operators, and actions 33 * 8. Set up Vim to work as a keymap for CodeMirror. 34 * 9. Ex command implementations. 35 */ 36 37 (function(mod) { 38 if (typeof exports == "object" && typeof module == "object") // CommonJS 39 mod(require("../lib/codemirror"), require("../addon/search/searchcursor"), require("../addon/dialog/dialog"), require("../addon/edit/matchbrackets.js")); 40 else if (typeof define == "function" && define.amd) // AMD 41 define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/dialog/dialog", "../addon/edit/matchbrackets"], mod); 42 else // Plain browser env 43 mod(CodeMirror); 44 })(function(CodeMirror) { 45 'use strict'; 46 47 var Pos = CodeMirror.Pos; 48 49 function transformCursor(cm, range) { 50 var vim = cm.state.vim; 51 if (!vim || vim.insertMode) return range.head; 52 var head = vim.sel.head; 53 if (!head) return range.head; 54 55 if (vim.visualBlock) { 56 if (range.head.line != head.line) { 57 return; 58 } 59 } 60 if (range.from() == range.anchor && !range.empty()) { 61 if (range.head.line == head.line && range.head.ch != head.ch) 62 return new Pos(range.head.line, range.head.ch - 1); 63 } 64 65 return range.head; 66 } 67 68 var defaultKeymap = [ 69 // Key to key mapping. This goes first to make it possible to override 70 // existing mappings. 71 { keys: '<Left>', type: 'keyToKey', toKeys: 'h' }, 72 { keys: '<Right>', type: 'keyToKey', toKeys: 'l' }, 73 { keys: '<Up>', type: 'keyToKey', toKeys: 'k' }, 74 { keys: '<Down>', type: 'keyToKey', toKeys: 'j' }, 75 { keys: 'g<Up>', type: 'keyToKey', toKeys: 'gk' }, 76 { keys: 'g<Down>', type: 'keyToKey', toKeys: 'gj' }, 77 { keys: '<Space>', type: 'keyToKey', toKeys: 'l' }, 78 { keys: '<BS>', type: 'keyToKey', toKeys: 'h', context: 'normal'}, 79 { keys: '<Del>', type: 'keyToKey', toKeys: 'x', context: 'normal'}, 80 { keys: '<C-Space>', type: 'keyToKey', toKeys: 'W' }, 81 { keys: '<C-BS>', type: 'keyToKey', toKeys: 'B', context: 'normal' }, 82 { keys: '<S-Space>', type: 'keyToKey', toKeys: 'w' }, 83 { keys: '<S-BS>', type: 'keyToKey', toKeys: 'b', context: 'normal' }, 84 { keys: '<C-n>', type: 'keyToKey', toKeys: 'j' }, 85 { keys: '<C-p>', type: 'keyToKey', toKeys: 'k' }, 86 { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>' }, 87 { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>' }, 88 { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' }, 89 { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' }, 90 { keys: 's', type: 'keyToKey', toKeys: 'cl', context: 'normal' }, 91 { keys: 's', type: 'keyToKey', toKeys: 'c', context: 'visual'}, 92 { keys: 'S', type: 'keyToKey', toKeys: 'cc', context: 'normal' }, 93 { keys: 'S', type: 'keyToKey', toKeys: 'VdO', context: 'visual' }, 94 { keys: '<Home>', type: 'keyToKey', toKeys: '0' }, 95 { keys: '<End>', type: 'keyToKey', toKeys: '$' }, 96 { keys: '<PageUp>', type: 'keyToKey', toKeys: '<C-b>' }, 97 { keys: '<PageDown>', type: 'keyToKey', toKeys: '<C-f>' }, 98 { keys: '<CR>', type: 'keyToKey', toKeys: 'j^', context: 'normal' }, 99 { keys: '<Ins>', type: 'keyToKey', toKeys: 'i', context: 'normal'}, 100 { keys: '<Ins>', type: 'action', action: 'toggleOverwrite', context: 'insert' }, 101 // Motions 102 { keys: 'H', type: 'motion', motion: 'moveToTopLine', motionArgs: { linewise: true, toJumplist: true }}, 103 { keys: 'M', type: 'motion', motion: 'moveToMiddleLine', motionArgs: { linewise: true, toJumplist: true }}, 104 { keys: 'L', type: 'motion', motion: 'moveToBottomLine', motionArgs: { linewise: true, toJumplist: true }}, 105 { keys: 'h', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: false }}, 106 { keys: 'l', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: true }}, 107 { keys: 'j', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, linewise: true }}, 108 { keys: 'k', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, linewise: true }}, 109 { keys: 'gj', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: true }}, 110 { keys: 'gk', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: false }}, 111 { keys: 'w', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false }}, 112 { keys: 'W', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false, bigWord: true }}, 113 { keys: 'e', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, inclusive: true }}, 114 { keys: 'E', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, bigWord: true, inclusive: true }}, 115 { keys: 'b', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }}, 116 { keys: 'B', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false, bigWord: true }}, 117 { keys: 'ge', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, inclusive: true }}, 118 { keys: 'gE', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, bigWord: true, inclusive: true }}, 119 { keys: '{', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: false, toJumplist: true }}, 120 { keys: '}', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: true, toJumplist: true }}, 121 { keys: '(', type: 'motion', motion: 'moveBySentence', motionArgs: { forward: false }}, 122 { keys: ')', type: 'motion', motion: 'moveBySentence', motionArgs: { forward: true }}, 123 { keys: '<C-f>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: true }}, 124 { keys: '<C-b>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: false }}, 125 { keys: '<C-d>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: true, explicitRepeat: true }}, 126 { keys: '<C-u>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: false, explicitRepeat: true }}, 127 { keys: 'gg', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }}, 128 { keys: 'G', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }}, 129 {keys: "g$", type: "motion", motion: "moveToEndOfDisplayLine"}, 130 {keys: "g^", type: "motion", motion: "moveToStartOfDisplayLine"}, 131 {keys: "g0", type: "motion", motion: "moveToStartOfDisplayLine"}, 132 { keys: '0', type: 'motion', motion: 'moveToStartOfLine' }, 133 { keys: '^', type: 'motion', motion: 'moveToFirstNonWhiteSpaceCharacter' }, 134 { keys: '+', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true }}, 135 { keys: '-', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, toFirstChar:true }}, 136 { keys: '_', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }}, 137 { keys: '$', type: 'motion', motion: 'moveToEol', motionArgs: { inclusive: true }}, 138 { keys: '%', type: 'motion', motion: 'moveToMatchedSymbol', motionArgs: { inclusive: true, toJumplist: true }}, 139 { keys: 'f<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: true , inclusive: true }}, 140 { keys: 'F<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: false }}, 141 { keys: 't<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: true, inclusive: true }}, 142 { keys: 'T<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: false }}, 143 { keys: ';', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: true }}, 144 { keys: ',', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: false }}, 145 { keys: '\'<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true, linewise: true}}, 146 { keys: '`<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true}}, 147 { keys: ']`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } }, 148 { keys: '[`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } }, 149 { keys: ']\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } }, 150 { keys: '[\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } }, 151 // the next two aren't motions but must come before more general motion declarations 152 { keys: ']p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true, matchIndent: true}}, 153 { keys: '[p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true, matchIndent: true}}, 154 { keys: ']<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: true, toJumplist: true}}, 155 { keys: '[<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: false, toJumplist: true}}, 156 { keys: '|', type: 'motion', motion: 'moveToColumn'}, 157 { keys: 'o', type: 'motion', motion: 'moveToOtherHighlightedEnd', context:'visual'}, 158 { keys: 'O', type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: {sameLine: true}, context:'visual'}, 159 // Operators 160 { keys: 'd', type: 'operator', operator: 'delete' }, 161 { keys: 'y', type: 'operator', operator: 'yank' }, 162 { keys: 'c', type: 'operator', operator: 'change' }, 163 { keys: '=', type: 'operator', operator: 'indentAuto' }, 164 { keys: '>', type: 'operator', operator: 'indent', operatorArgs: { indentRight: true }}, 165 { keys: '<', type: 'operator', operator: 'indent', operatorArgs: { indentRight: false }}, 166 { keys: 'g~', type: 'operator', operator: 'changeCase' }, 167 { keys: 'gu', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, isEdit: true }, 168 { keys: 'gU', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, isEdit: true }, 169 { keys: 'n', type: 'motion', motion: 'findNext', motionArgs: { forward: true, toJumplist: true }}, 170 { keys: 'N', type: 'motion', motion: 'findNext', motionArgs: { forward: false, toJumplist: true }}, 171 { keys: 'gn', type: 'motion', motion: 'findAndSelectNextInclusive', motionArgs: { forward: true }}, 172 { keys: 'gN', type: 'motion', motion: 'findAndSelectNextInclusive', motionArgs: { forward: false }}, 173 // Operator-Motion dual commands 174 { keys: 'x', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorMotionArgs: { visualLine: false }}, 175 { keys: 'X', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }}, 176 { keys: 'D', type: 'operatorMotion', operator: 'delete', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'}, 177 { keys: 'D', type: 'operator', operator: 'delete', operatorArgs: { linewise: true }, context: 'visual'}, 178 { keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'expandToLine', motionArgs: { linewise: true }, context: 'normal'}, 179 { keys: 'Y', type: 'operator', operator: 'yank', operatorArgs: { linewise: true }, context: 'visual'}, 180 { keys: 'C', type: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'}, 181 { keys: 'C', type: 'operator', operator: 'change', operatorArgs: { linewise: true }, context: 'visual'}, 182 { keys: '~', type: 'operatorMotion', operator: 'changeCase', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorArgs: { shouldMoveCursor: true }, context: 'normal'}, 183 { keys: '~', type: 'operator', operator: 'changeCase', context: 'visual'}, 184 { keys: '<C-u>', type: 'operatorMotion', operator: 'delete', motion: 'moveToStartOfLine', context: 'insert' }, 185 { keys: '<C-w>', type: 'operatorMotion', operator: 'delete', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }, context: 'insert' }, 186 //ignore C-w in normal mode 187 { keys: '<C-w>', type: 'idle', context: 'normal' }, 188 // Actions 189 { keys: '<C-i>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: true }}, 190 { keys: '<C-o>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: false }}, 191 { keys: '<C-e>', type: 'action', action: 'scroll', actionArgs: { forward: true, linewise: true }}, 192 { keys: '<C-y>', type: 'action', action: 'scroll', actionArgs: { forward: false, linewise: true }}, 193 { keys: 'a', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'charAfter' }, context: 'normal' }, 194 { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'eol' }, context: 'normal' }, 195 { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'endOfSelectedArea' }, context: 'visual' }, 196 { keys: 'i', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'inplace' }, context: 'normal' }, 197 { keys: 'gi', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'lastEdit' }, context: 'normal' }, 198 { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'firstNonBlank'}, context: 'normal' }, 199 { keys: 'gI', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'bol'}, context: 'normal' }, 200 { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'startOfSelectedArea' }, context: 'visual' }, 201 { keys: 'o', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: true }, context: 'normal' }, 202 { keys: 'O', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: false }, context: 'normal' }, 203 { keys: 'v', type: 'action', action: 'toggleVisualMode' }, 204 { keys: 'V', type: 'action', action: 'toggleVisualMode', actionArgs: { linewise: true }}, 205 { keys: '<C-v>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }}, 206 { keys: '<C-q>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }}, 207 { keys: 'gv', type: 'action', action: 'reselectLastSelection' }, 208 { keys: 'J', type: 'action', action: 'joinLines', isEdit: true }, 209 { keys: 'gJ', type: 'action', action: 'joinLines', actionArgs: { keepSpaces: true }, isEdit: true }, 210 { keys: 'p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true }}, 211 { keys: 'P', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true }}, 212 { keys: 'r<character>', type: 'action', action: 'replace', isEdit: true }, 213 { keys: '@<character>', type: 'action', action: 'replayMacro' }, 214 { keys: 'q<character>', type: 'action', action: 'enterMacroRecordMode' }, 215 // Handle Replace-mode as a special case of insert mode. 216 { keys: 'R', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { replace: true }, context: 'normal'}, 217 { keys: 'R', type: 'operator', operator: 'change', operatorArgs: { linewise: true, fullLine: true }, context: 'visual', exitVisualBlock: true}, 218 { keys: 'u', type: 'action', action: 'undo', context: 'normal' }, 219 { keys: 'u', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, context: 'visual', isEdit: true }, 220 { keys: 'U', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, context: 'visual', isEdit: true }, 221 { keys: '<C-r>', type: 'action', action: 'redo' }, 222 { keys: 'm<character>', type: 'action', action: 'setMark' }, 223 { keys: '"<character>', type: 'action', action: 'setRegister' }, 224 { keys: 'zz', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }}, 225 { keys: 'z.', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }, motion: 'moveToFirstNonWhiteSpaceCharacter' }, 226 { keys: 'zt', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }}, 227 { keys: 'z<CR>', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }, motion: 'moveToFirstNonWhiteSpaceCharacter' }, 228 { keys: 'z-', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }}, 229 { keys: 'zb', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }, motion: 'moveToFirstNonWhiteSpaceCharacter' }, 230 { keys: '.', type: 'action', action: 'repeatLastEdit' }, 231 { keys: '<C-a>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: true, backtrack: false}}, 232 { keys: '<C-x>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: false, backtrack: false}}, 233 { keys: '<C-t>', type: 'action', action: 'indent', actionArgs: { indentRight: true }, context: 'insert' }, 234 { keys: '<C-d>', type: 'action', action: 'indent', actionArgs: { indentRight: false }, context: 'insert' }, 235 // Text object motions 236 { keys: 'a<character>', type: 'motion', motion: 'textObjectManipulation' }, 237 { keys: 'i<character>', type: 'motion', motion: 'textObjectManipulation', motionArgs: { textObjectInner: true }}, 238 // Search 239 { keys: '/', type: 'search', searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }}, 240 { keys: '?', type: 'search', searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }}, 241 { keys: '*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }}, 242 { keys: '#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }}, 243 { keys: 'g*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }}, 244 { keys: 'g#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }}, 245 // Ex command 246 { keys: ':', type: 'ex' } 247 ]; 248 var defaultKeymapLength = defaultKeymap.length; 249 250 /** 251 * Ex commands 252 * Care must be taken when adding to the default Ex command map. For any 253 * pair of commands that have a shared prefix, at least one of their 254 * shortNames must not match the prefix of the other command. 255 */ 256 var defaultExCommandMap = [ 257 { name: 'colorscheme', shortName: 'colo' }, 258 { name: 'map' }, 259 { name: 'imap', shortName: 'im' }, 260 { name: 'nmap', shortName: 'nm' }, 261 { name: 'vmap', shortName: 'vm' }, 262 { name: 'unmap' }, 263 { name: 'write', shortName: 'w' }, 264 { name: 'undo', shortName: 'u' }, 265 { name: 'redo', shortName: 'red' }, 266 { name: 'set', shortName: 'se' }, 267 { name: 'setlocal', shortName: 'setl' }, 268 { name: 'setglobal', shortName: 'setg' }, 269 { name: 'sort', shortName: 'sor' }, 270 { name: 'substitute', shortName: 's', possiblyAsync: true }, 271 { name: 'nohlsearch', shortName: 'noh' }, 272 { name: 'yank', shortName: 'y' }, 273 { name: 'delmarks', shortName: 'delm' }, 274 { name: 'registers', shortName: 'reg', excludeFromCommandHistory: true }, 275 { name: 'vglobal', shortName: 'v' }, 276 { name: 'global', shortName: 'g' } 277 ]; 278 279 var Vim = function() { 280 function enterVimMode(cm) { 281 cm.setOption('disableInput', true); 282 cm.setOption('showCursorWhenSelecting', false); 283 CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); 284 cm.on('cursorActivity', onCursorActivity); 285 maybeInitVimState(cm); 286 CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm)); 287 } 288 289 function leaveVimMode(cm) { 290 cm.setOption('disableInput', false); 291 cm.off('cursorActivity', onCursorActivity); 292 CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm)); 293 cm.state.vim = null; 294 if (highlightTimeout) clearTimeout(highlightTimeout); 295 } 296 297 function detachVimMap(cm, next) { 298 if (this == CodeMirror.keyMap.vim) { 299 cm.options.$customCursor = null; 300 CodeMirror.rmClass(cm.getWrapperElement(), "cm-fat-cursor"); 301 } 302 303 if (!next || next.attach != attachVimMap) 304 leaveVimMode(cm); 305 } 306 function attachVimMap(cm, prev) { 307 if (this == CodeMirror.keyMap.vim) { 308 if (cm.curOp) cm.curOp.selectionChanged = true; 309 cm.options.$customCursor = transformCursor; 310 CodeMirror.addClass(cm.getWrapperElement(), "cm-fat-cursor"); 311 } 312 313 if (!prev || prev.attach != attachVimMap) 314 enterVimMode(cm); 315 } 316 317 // Deprecated, simply setting the keymap works again. 318 CodeMirror.defineOption('vimMode', false, function(cm, val, prev) { 319 if (val && cm.getOption("keyMap") != "vim") 320 cm.setOption("keyMap", "vim"); 321 else if (!val && prev != CodeMirror.Init && /^vim/.test(cm.getOption("keyMap"))) 322 cm.setOption("keyMap", "default"); 323 }); 324 325 function cmKey(key, cm) { 326 if (!cm) { return undefined; } 327 if (this[key]) { return this[key]; } 328 var vimKey = cmKeyToVimKey(key); 329 if (!vimKey) { 330 return false; 331 } 332 var cmd = vimApi.findKey(cm, vimKey); 333 if (typeof cmd == 'function') { 334 CodeMirror.signal(cm, 'vim-keypress', vimKey); 335 } 336 return cmd; 337 } 338 339 var modifiers = {Shift:'S',Ctrl:'C',Alt:'A',Cmd:'D',Mod:'A',CapsLock:''}; 340 var specialKeys = {Enter:'CR',Backspace:'BS',Delete:'Del',Insert:'Ins'}; 341 function cmKeyToVimKey(key) { 342 if (key.charAt(0) == '\'') { 343 // Keypress character binding of format "'a'" 344 return key.charAt(1); 345 } 346 var pieces = key.split(/-(?!$)/); 347 var lastPiece = pieces[pieces.length - 1]; 348 if (pieces.length == 1 && pieces[0].length == 1) { 349 // No-modifier bindings use literal character bindings above. Skip. 350 return false; 351 } else if (pieces.length == 2 && pieces[0] == 'Shift' && lastPiece.length == 1) { 352 // Ignore Shift+char bindings as they should be handled by literal character. 353 return false; 354 } 355 var hasCharacter = false; 356 for (var i = 0; i < pieces.length; i++) { 357 var piece = pieces[i]; 358 if (piece in modifiers) { pieces[i] = modifiers[piece]; } 359 else { hasCharacter = true; } 360 if (piece in specialKeys) { pieces[i] = specialKeys[piece]; } 361 } 362 if (!hasCharacter) { 363 // Vim does not support modifier only keys. 364 return false; 365 } 366 // TODO: Current bindings expect the character to be lower case, but 367 // it looks like vim key notation uses upper case. 368 if (isUpperCase(lastPiece)) { 369 pieces[pieces.length - 1] = lastPiece.toLowerCase(); 370 } 371 return '<' + pieces.join('-') + '>'; 372 } 373 374 function getOnPasteFn(cm) { 375 var vim = cm.state.vim; 376 if (!vim.onPasteFn) { 377 vim.onPasteFn = function() { 378 if (!vim.insertMode) { 379 cm.setCursor(offsetCursor(cm.getCursor(), 0, 1)); 380 actions.enterInsertMode(cm, {}, vim); 381 } 382 }; 383 } 384 return vim.onPasteFn; 385 } 386 387 var numberRegex = /[\d]/; 388 var wordCharTest = [CodeMirror.isWordChar, function(ch) { 389 return ch && !CodeMirror.isWordChar(ch) && !/\s/.test(ch); 390 }], bigWordCharTest = [function(ch) { 391 return /\S/.test(ch); 392 }]; 393 function makeKeyRange(start, size) { 394 var keys = []; 395 for (var i = start; i < start + size; i++) { 396 keys.push(String.fromCharCode(i)); 397 } 398 return keys; 399 } 400 var upperCaseAlphabet = makeKeyRange(65, 26); 401 var lowerCaseAlphabet = makeKeyRange(97, 26); 402 var numbers = makeKeyRange(48, 10); 403 var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']); 404 var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"', '.', ':', '_', '/']); 405 var upperCaseChars; 406 try { upperCaseChars = new RegExp("^[\\p{Lu}]$", "u"); } 407 catch (_) { upperCaseChars = /^[A-Z]$/; } 408 409 function isLine(cm, line) { 410 return line >= cm.firstLine() && line <= cm.lastLine(); 411 } 412 function isLowerCase(k) { 413 return (/^[a-z]$/).test(k); 414 } 415 function isMatchableSymbol(k) { 416 return '()[]{}'.indexOf(k) != -1; 417 } 418 function isNumber(k) { 419 return numberRegex.test(k); 420 } 421 function isUpperCase(k) { 422 return upperCaseChars.test(k); 423 } 424 function isWhiteSpaceString(k) { 425 return (/^\s*$/).test(k); 426 } 427 function isEndOfSentenceSymbol(k) { 428 return '.?!'.indexOf(k) != -1; 429 } 430 function inArray(val, arr) { 431 for (var i = 0; i < arr.length; i++) { 432 if (arr[i] == val) { 433 return true; 434 } 435 } 436 return false; 437 } 438 439 var options = {}; 440 function defineOption(name, defaultValue, type, aliases, callback) { 441 if (defaultValue === undefined && !callback) { 442 throw Error('defaultValue is required unless callback is provided'); 443 } 444 if (!type) { type = 'string'; } 445 options[name] = { 446 type: type, 447 defaultValue: defaultValue, 448 callback: callback 449 }; 450 if (aliases) { 451 for (var i = 0; i < aliases.length; i++) { 452 options[aliases[i]] = options[name]; 453 } 454 } 455 if (defaultValue) { 456 setOption(name, defaultValue); 457 } 458 } 459 460 function setOption(name, value, cm, cfg) { 461 var option = options[name]; 462 cfg = cfg || {}; 463 var scope = cfg.scope; 464 if (!option) { 465 return new Error('Unknown option: ' + name); 466 } 467 if (option.type == 'boolean') { 468 if (value && value !== true) { 469 return new Error('Invalid argument: ' + name + '=' + value); 470 } else if (value !== false) { 471 // Boolean options are set to true if value is not defined. 472 value = true; 473 } 474 } 475 if (option.callback) { 476 if (scope !== 'local') { 477 option.callback(value, undefined); 478 } 479 if (scope !== 'global' && cm) { 480 option.callback(value, cm); 481 } 482 } else { 483 if (scope !== 'local') { 484 option.value = option.type == 'boolean' ? !!value : value; 485 } 486 if (scope !== 'global' && cm) { 487 cm.state.vim.options[name] = {value: value}; 488 } 489 } 490 } 491 492 function getOption(name, cm, cfg) { 493 var option = options[name]; 494 cfg = cfg || {}; 495 var scope = cfg.scope; 496 if (!option) { 497 return new Error('Unknown option: ' + name); 498 } 499 if (option.callback) { 500 var local = cm && option.callback(undefined, cm); 501 if (scope !== 'global' && local !== undefined) { 502 return local; 503 } 504 if (scope !== 'local') { 505 return option.callback(); 506 } 507 return; 508 } else { 509 var local = (scope !== 'global') && (cm && cm.state.vim.options[name]); 510 return (local || (scope !== 'local') && option || {}).value; 511 } 512 } 513 514 defineOption('filetype', undefined, 'string', ['ft'], function(name, cm) { 515 // Option is local. Do nothing for global. 516 if (cm === undefined) { 517 return; 518 } 519 // The 'filetype' option proxies to the CodeMirror 'mode' option. 520 if (name === undefined) { 521 var mode = cm.getOption('mode'); 522 return mode == 'null' ? '' : mode; 523 } else { 524 var mode = name == '' ? 'null' : name; 525 cm.setOption('mode', mode); 526 } 527 }); 528 529 var createCircularJumpList = function() { 530 var size = 100; 531 var pointer = -1; 532 var head = 0; 533 var tail = 0; 534 var buffer = new Array(size); 535 function add(cm, oldCur, newCur) { 536 var current = pointer % size; 537 var curMark = buffer[current]; 538 function useNextSlot(cursor) { 539 var next = ++pointer % size; 540 var trashMark = buffer[next]; 541 if (trashMark) { 542 trashMark.clear(); 543 } 544 buffer[next] = cm.setBookmark(cursor); 545 } 546 if (curMark) { 547 var markPos = curMark.find(); 548 // avoid recording redundant cursor position 549 if (markPos && !cursorEqual(markPos, oldCur)) { 550 useNextSlot(oldCur); 551 } 552 } else { 553 useNextSlot(oldCur); 554 } 555 useNextSlot(newCur); 556 head = pointer; 557 tail = pointer - size + 1; 558 if (tail < 0) { 559 tail = 0; 560 } 561 } 562 function move(cm, offset) { 563 pointer += offset; 564 if (pointer > head) { 565 pointer = head; 566 } else if (pointer < tail) { 567 pointer = tail; 568 } 569 var mark = buffer[(size + pointer) % size]; 570 // skip marks that are temporarily removed from text buffer 571 if (mark && !mark.find()) { 572 var inc = offset > 0 ? 1 : -1; 573 var newCur; 574 var oldCur = cm.getCursor(); 575 do { 576 pointer += inc; 577 mark = buffer[(size + pointer) % size]; 578 // skip marks that are the same as current position 579 if (mark && 580 (newCur = mark.find()) && 581 !cursorEqual(oldCur, newCur)) { 582 break; 583 } 584 } while (pointer < head && pointer > tail); 585 } 586 return mark; 587 } 588 function find(cm, offset) { 589 var oldPointer = pointer; 590 var mark = move(cm, offset); 591 pointer = oldPointer; 592 return mark && mark.find(); 593 } 594 return { 595 cachedCursor: undefined, //used for # and * jumps 596 add: add, 597 find: find, 598 move: move 599 }; 600 }; 601 602 // Returns an object to track the changes associated insert mode. It 603 // clones the object that is passed in, or creates an empty object one if 604 // none is provided. 605 var createInsertModeChanges = function(c) { 606 if (c) { 607 // Copy construction 608 return { 609 changes: c.changes, 610 expectCursorActivityForChange: c.expectCursorActivityForChange 611 }; 612 } 613 return { 614 // Change list 615 changes: [], 616 // Set to true on change, false on cursorActivity. 617 expectCursorActivityForChange: false 618 }; 619 }; 620 621 function MacroModeState() { 622 this.latestRegister = undefined; 623 this.isPlaying = false; 624 this.isRecording = false; 625 this.replaySearchQueries = []; 626 this.onRecordingDone = undefined; 627 this.lastInsertModeChanges = createInsertModeChanges(); 628 } 629 MacroModeState.prototype = { 630 exitMacroRecordMode: function() { 631 var macroModeState = vimGlobalState.macroModeState; 632 if (macroModeState.onRecordingDone) { 633 macroModeState.onRecordingDone(); // close dialog 634 } 635 macroModeState.onRecordingDone = undefined; 636 macroModeState.isRecording = false; 637 }, 638 enterMacroRecordMode: function(cm, registerName) { 639 var register = 640 vimGlobalState.registerController.getRegister(registerName); 641 if (register) { 642 register.clear(); 643 this.latestRegister = registerName; 644 if (cm.openDialog) { 645 this.onRecordingDone = cm.openDialog( 646 document.createTextNode('(recording)['+registerName+']'), null, {bottom:true}); 647 } 648 this.isRecording = true; 649 } 650 } 651 }; 652 653 function maybeInitVimState(cm) { 654 if (!cm.state.vim) { 655 // Store instance state in the CodeMirror object. 656 cm.state.vim = { 657 inputState: new InputState(), 658 // Vim's input state that triggered the last edit, used to repeat 659 // motions and operators with '.'. 660 lastEditInputState: undefined, 661 // Vim's action command before the last edit, used to repeat actions 662 // with '.' and insert mode repeat. 663 lastEditActionCommand: undefined, 664 // When using jk for navigation, if you move from a longer line to a 665 // shorter line, the cursor may clip to the end of the shorter line. 666 // If j is pressed again and cursor goes to the next line, the 667 // cursor should go back to its horizontal position on the longer 668 // line if it can. This is to keep track of the horizontal position. 669 lastHPos: -1, 670 // Doing the same with screen-position for gj/gk 671 lastHSPos: -1, 672 // The last motion command run. Cleared if a non-motion command gets 673 // executed in between. 674 lastMotion: null, 675 marks: {}, 676 insertMode: false, 677 // Repeat count for changes made in insert mode, triggered by key 678 // sequences like 3,i. Only exists when insertMode is true. 679 insertModeRepeat: undefined, 680 visualMode: false, 681 // If we are in visual line mode. No effect if visualMode is false. 682 visualLine: false, 683 visualBlock: false, 684 lastSelection: null, 685 lastPastedText: null, 686 sel: {}, 687 // Buffer-local/window-local values of vim options. 688 options: {} 689 }; 690 } 691 return cm.state.vim; 692 } 693 var vimGlobalState; 694 function resetVimGlobalState() { 695 vimGlobalState = { 696 // The current search query. 697 searchQuery: null, 698 // Whether we are searching backwards. 699 searchIsReversed: false, 700 // Replace part of the last substituted pattern 701 lastSubstituteReplacePart: undefined, 702 jumpList: createCircularJumpList(), 703 macroModeState: new MacroModeState, 704 // Recording latest f, t, F or T motion command. 705 lastCharacterSearch: {increment:0, forward:true, selectedCharacter:''}, 706 registerController: new RegisterController({}), 707 // search history buffer 708 searchHistoryController: new HistoryController(), 709 // ex Command history buffer 710 exCommandHistoryController : new HistoryController() 711 }; 712 for (var optionName in options) { 713 var option = options[optionName]; 714 option.value = option.defaultValue; 715 } 716 } 717 718 var lastInsertModeKeyTimer; 719 var vimApi= { 720 buildKeyMap: function() { 721 // TODO: Convert keymap into dictionary format for fast lookup. 722 }, 723 // Testing hook, though it might be useful to expose the register 724 // controller anyway. 725 getRegisterController: function() { 726 return vimGlobalState.registerController; 727 }, 728 // Testing hook. 729 resetVimGlobalState_: resetVimGlobalState, 730 731 // Testing hook. 732 getVimGlobalState_: function() { 733 return vimGlobalState; 734 }, 735 736 // Testing hook. 737 maybeInitVimState_: maybeInitVimState, 738 739 suppressErrorLogging: false, 740 741 InsertModeKey: InsertModeKey, 742 map: function(lhs, rhs, ctx) { 743 // Add user defined key bindings. 744 exCommandDispatcher.map(lhs, rhs, ctx); 745 }, 746 unmap: function(lhs, ctx) { 747 return exCommandDispatcher.unmap(lhs, ctx); 748 }, 749 // Non-recursive map function. 750 // NOTE: This will not create mappings to key maps that aren't present 751 // in the default key map. See TODO at bottom of function. 752 noremap: function(lhs, rhs, ctx) { 753 function toCtxArray(ctx) { 754 return ctx ? [ctx] : ['normal', 'insert', 'visual']; 755 } 756 var ctxsToMap = toCtxArray(ctx); 757 // Look through all actual defaults to find a map candidate. 758 var actualLength = defaultKeymap.length, origLength = defaultKeymapLength; 759 for (var i = actualLength - origLength; 760 i < actualLength && ctxsToMap.length; 761 i++) { 762 var mapping = defaultKeymap[i]; 763 // Omit mappings that operate in the wrong context(s) and those of invalid type. 764 if (mapping.keys == rhs && 765 (!ctx || !mapping.context || mapping.context === ctx) && 766 mapping.type.substr(0, 2) !== 'ex' && 767 mapping.type.substr(0, 3) !== 'key') { 768 // Make a shallow copy of the original keymap entry. 769 var newMapping = {}; 770 for (var key in mapping) { 771 newMapping[key] = mapping[key]; 772 } 773 // Modify it point to the new mapping with the proper context. 774 newMapping.keys = lhs; 775 if (ctx && !newMapping.context) { 776 newMapping.context = ctx; 777 } 778 // Add it to the keymap with a higher priority than the original. 779 this._mapCommand(newMapping); 780 // Record the mapped contexts as complete. 781 var mappedCtxs = toCtxArray(mapping.context); 782 ctxsToMap = ctxsToMap.filter(function(el) { return mappedCtxs.indexOf(el) === -1; }); 783 } 784 } 785 // TODO: Create non-recursive keyToKey mappings for the unmapped contexts once those exist. 786 }, 787 // Remove all user-defined mappings for the provided context. 788 mapclear: function(ctx) { 789 // Partition the existing keymap into user-defined and true defaults. 790 var actualLength = defaultKeymap.length, 791 origLength = defaultKeymapLength; 792 var userKeymap = defaultKeymap.slice(0, actualLength - origLength); 793 defaultKeymap = defaultKeymap.slice(actualLength - origLength); 794 if (ctx) { 795 // If a specific context is being cleared, we need to keep mappings 796 // from all other contexts. 797 for (var i = userKeymap.length - 1; i >= 0; i--) { 798 var mapping = userKeymap[i]; 799 if (ctx !== mapping.context) { 800 if (mapping.context) { 801 this._mapCommand(mapping); 802 } else { 803 // `mapping` applies to all contexts so create keymap copies 804 // for each context except the one being cleared. 805 var contexts = ['normal', 'insert', 'visual']; 806 for (var j in contexts) { 807 if (contexts[j] !== ctx) { 808 var newMapping = {}; 809 for (var key in mapping) { 810 newMapping[key] = mapping[key]; 811 } 812 newMapping.context = contexts[j]; 813 this._mapCommand(newMapping); 814 } 815 } 816 } 817 } 818 } 819 } 820 }, 821 // TODO: Expose setOption and getOption as instance methods. Need to decide how to namespace 822 // them, or somehow make them work with the existing CodeMirror setOption/getOption API. 823 setOption: setOption, 824 getOption: getOption, 825 defineOption: defineOption, 826 defineEx: function(name, prefix, func){ 827 if (!prefix) { 828 prefix = name; 829 } else if (name.indexOf(prefix) !== 0) { 830 throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered'); 831 } 832 exCommands[name]=func; 833 exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'}; 834 }, 835 handleKey: function (cm, key, origin) { 836 var command = this.findKey(cm, key, origin); 837 if (typeof command === 'function') { 838 return command(); 839 } 840 }, 841 /** 842 * This is the outermost function called by CodeMirror, after keys have 843 * been mapped to their Vim equivalents. 844 * 845 * Finds a command based on the key (and cached keys if there is a 846 * multi-key sequence). Returns `undefined` if no key is matched, a noop 847 * function if a partial match is found (multi-key), and a function to 848 * execute the bound command if a a key is matched. The function always 849 * returns true. 850 */ 851 findKey: function(cm, key, origin) { 852 var vim = maybeInitVimState(cm); 853 function handleMacroRecording() { 854 var macroModeState = vimGlobalState.macroModeState; 855 if (macroModeState.isRecording) { 856 if (key == 'q') { 857 macroModeState.exitMacroRecordMode(); 858 clearInputState(cm); 859 return true; 860 } 861 if (origin != 'mapping') { 862 logKey(macroModeState, key); 863 } 864 } 865 } 866 function handleEsc() { 867 if (key == '<Esc>') { 868 if (vim.visualMode) { 869 // Get back to normal mode. 870 exitVisualMode(cm); 871 } else if (vim.insertMode) { 872 // Get back to normal mode. 873 exitInsertMode(cm); 874 } else { 875 // We're already in normal mode. Let '<Esc>' be handled normally. 876 return; 877 } 878 clearInputState(cm); 879 return true; 880 } 881 } 882 function doKeyToKey(keys) { 883 // TODO: prevent infinite recursion. 884 var match; 885 while (keys) { 886 // Pull off one command key, which is either a single character 887 // or a special sequence wrapped in '<' and '>', e.g. '<Space>'. 888 match = (/<\w+-.+?>|<\w+>|./).exec(keys); 889 key = match[0]; 890 keys = keys.substring(match.index + key.length); 891 vimApi.handleKey(cm, key, 'mapping'); 892 } 893 } 894 895 function handleKeyInsertMode() { 896 if (handleEsc()) { return true; } 897 var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key; 898 var keysAreChars = key.length == 1; 899 var match = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert'); 900 // Need to check all key substrings in insert mode. 901 while (keys.length > 1 && match.type != 'full') { 902 var keys = vim.inputState.keyBuffer = keys.slice(1); 903 var thisMatch = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert'); 904 if (thisMatch.type != 'none') { match = thisMatch; } 905 } 906 if (match.type == 'none') { clearInputState(cm); return false; } 907 else if (match.type == 'partial') { 908 if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); } 909 lastInsertModeKeyTimer = window.setTimeout( 910 function() { if (vim.insertMode && vim.inputState.keyBuffer) { clearInputState(cm); } }, 911 getOption('insertModeEscKeysTimeout')); 912 return !keysAreChars; 913 } 914 915 if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); } 916 if (keysAreChars) { 917 var selections = cm.listSelections(); 918 for (var i = 0; i < selections.length; i++) { 919 var here = selections[i].head; 920 cm.replaceRange('', offsetCursor(here, 0, -(keys.length - 1)), here, '+input'); 921 } 922 vimGlobalState.macroModeState.lastInsertModeChanges.changes.pop(); 923 } 924 clearInputState(cm); 925 return match.command; 926 } 927 928 function handleKeyNonInsertMode() { 929 if (handleMacroRecording() || handleEsc()) { return true; } 930 931 var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key; 932 if (/^[1-9]\d*$/.test(keys)) { return true; } 933 934 var keysMatcher = /^(\d*)(.*)$/.exec(keys); 935 if (!keysMatcher) { clearInputState(cm); return false; } 936 var context = vim.visualMode ? 'visual' : 937 'normal'; 938 var mainKey = keysMatcher[2] || keysMatcher[1]; 939 if (vim.inputState.operatorShortcut && vim.inputState.operatorShortcut.slice(-1) == mainKey) { 940 // multikey operators act linewise by repeating only the last character 941 mainKey = vim.inputState.operatorShortcut; 942 } 943 var match = commandDispatcher.matchCommand(mainKey, defaultKeymap, vim.inputState, context); 944 if (match.type == 'none') { clearInputState(cm); return false; } 945 else if (match.type == 'partial') { return true; } 946 947 vim.inputState.keyBuffer = ''; 948 var keysMatcher = /^(\d*)(.*)$/.exec(keys); 949 if (keysMatcher[1] && keysMatcher[1] != '0') { 950 vim.inputState.pushRepeatDigit(keysMatcher[1]); 951 } 952 return match.command; 953 } 954 955 var command; 956 if (vim.insertMode) { command = handleKeyInsertMode(); } 957 else { command = handleKeyNonInsertMode(); } 958 if (command === false) { 959 return !vim.insertMode && key.length === 1 ? function() { return true; } : undefined; 960 } else if (command === true) { 961 // TODO: Look into using CodeMirror's multi-key handling. 962 // Return no-op since we are caching the key. Counts as handled, but 963 // don't want act on it just yet. 964 return function() { return true; }; 965 } else { 966 return function() { 967 return cm.operation(function() { 968 cm.curOp.isVimOp = true; 969 try { 970 if (command.type == 'keyToKey') { 971 doKeyToKey(command.toKeys); 972 } else { 973 commandDispatcher.processCommand(cm, vim, command); 974 } 975 } catch (e) { 976 // clear VIM state in case it's in a bad state. 977 cm.state.vim = undefined; 978 maybeInitVimState(cm); 979 if (!vimApi.suppressErrorLogging) { 980 console['log'](e); 981 } 982 throw e; 983 } 984 return true; 985 }); 986 }; 987 } 988 }, 989 handleEx: function(cm, input) { 990 exCommandDispatcher.processCommand(cm, input); 991 }, 992 993 defineMotion: defineMotion, 994 defineAction: defineAction, 995 defineOperator: defineOperator, 996 mapCommand: mapCommand, 997 _mapCommand: _mapCommand, 998 999 defineRegister: defineRegister, 1000 1001 exitVisualMode: exitVisualMode, 1002 exitInsertMode: exitInsertMode 1003 }; 1004 1005 // Represents the current input state. 1006 function InputState() { 1007 this.prefixRepeat = []; 1008 this.motionRepeat = []; 1009 1010 this.operator = null; 1011 this.operatorArgs = null; 1012 this.motion = null; 1013 this.motionArgs = null; 1014 this.keyBuffer = []; // For matching multi-key commands. 1015 this.registerName = null; // Defaults to the unnamed register. 1016 } 1017 InputState.prototype.pushRepeatDigit = function(n) { 1018 if (!this.operator) { 1019 this.prefixRepeat = this.prefixRepeat.concat(n); 1020 } else { 1021 this.motionRepeat = this.motionRepeat.concat(n); 1022 } 1023 }; 1024 InputState.prototype.getRepeat = function() { 1025 var repeat = 0; 1026 if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) { 1027 repeat = 1; 1028 if (this.prefixRepeat.length > 0) { 1029 repeat *= parseInt(this.prefixRepeat.join(''), 10); 1030 } 1031 if (this.motionRepeat.length > 0) { 1032 repeat *= parseInt(this.motionRepeat.join(''), 10); 1033 } 1034 } 1035 return repeat; 1036 }; 1037 1038 function clearInputState(cm, reason) { 1039 cm.state.vim.inputState = new InputState(); 1040 CodeMirror.signal(cm, 'vim-command-done', reason); 1041 } 1042 1043 /* 1044 * Register stores information about copy and paste registers. Besides 1045 * text, a register must store whether it is linewise (i.e., when it is 1046 * pasted, should it insert itself into a new line, or should the text be 1047 * inserted at the cursor position.) 1048 */ 1049 function Register(text, linewise, blockwise) { 1050 this.clear(); 1051 this.keyBuffer = [text || '']; 1052 this.insertModeChanges = []; 1053 this.searchQueries = []; 1054 this.linewise = !!linewise; 1055 this.blockwise = !!blockwise; 1056 } 1057 Register.prototype = { 1058 setText: function(text, linewise, blockwise) { 1059 this.keyBuffer = [text || '']; 1060 this.linewise = !!linewise; 1061 this.blockwise = !!blockwise; 1062 }, 1063 pushText: function(text, linewise) { 1064 // if this register has ever been set to linewise, use linewise. 1065 if (linewise) { 1066 if (!this.linewise) { 1067 this.keyBuffer.push('\n'); 1068 } 1069 this.linewise = true; 1070 } 1071 this.keyBuffer.push(text); 1072 }, 1073 pushInsertModeChanges: function(changes) { 1074 this.insertModeChanges.push(createInsertModeChanges(changes)); 1075 }, 1076 pushSearchQuery: function(query) { 1077 this.searchQueries.push(query); 1078 }, 1079 clear: function() { 1080 this.keyBuffer = []; 1081 this.insertModeChanges = []; 1082 this.searchQueries = []; 1083 this.linewise = false; 1084 }, 1085 toString: function() { 1086 return this.keyBuffer.join(''); 1087 } 1088 }; 1089 1090 /** 1091 * Defines an external register. 1092 * 1093 * The name should be a single character that will be used to reference the register. 1094 * The register should support setText, pushText, clear, and toString(). See Register 1095 * for a reference implementation. 1096 */ 1097 function defineRegister(name, register) { 1098 var registers = vimGlobalState.registerController.registers; 1099 if (!name || name.length != 1) { 1100 throw Error('Register name must be 1 character'); 1101 } 1102 if (registers[name]) { 1103 throw Error('Register already defined ' + name); 1104 } 1105 registers[name] = register; 1106 validRegisters.push(name); 1107 } 1108 1109 /* 1110 * vim registers allow you to keep many independent copy and paste buffers. 1111 * See http://usevim.com/2012/04/13/registers/ for an introduction. 1112 * 1113 * RegisterController keeps the state of all the registers. An initial 1114 * state may be passed in. The unnamed register '"' will always be 1115 * overridden. 1116 */ 1117 function RegisterController(registers) { 1118 this.registers = registers; 1119 this.unnamedRegister = registers['"'] = new Register(); 1120 registers['.'] = new Register(); 1121 registers[':'] = new Register(); 1122 registers['/'] = new Register(); 1123 } 1124 RegisterController.prototype = { 1125 pushText: function(registerName, operator, text, linewise, blockwise) { 1126 // The black hole register, "_, means delete/yank to nowhere. 1127 if (registerName === '_') return; 1128 if (linewise && text.charAt(text.length - 1) !== '\n'){ 1129 text += '\n'; 1130 } 1131 // Lowercase and uppercase registers refer to the same register. 1132 // Uppercase just means append. 1133 var register = this.isValidRegister(registerName) ? 1134 this.getRegister(registerName) : null; 1135 // if no register/an invalid register was specified, things go to the 1136 // default registers 1137 if (!register) { 1138 switch (operator) { 1139 case 'yank': 1140 // The 0 register contains the text from the most recent yank. 1141 this.registers['0'] = new Register(text, linewise, blockwise); 1142 break; 1143 case 'delete': 1144 case 'change': 1145 if (text.indexOf('\n') == -1) { 1146 // Delete less than 1 line. Update the small delete register. 1147 this.registers['-'] = new Register(text, linewise); 1148 } else { 1149 // Shift down the contents of the numbered registers and put the 1150 // deleted text into register 1. 1151 this.shiftNumericRegisters_(); 1152 this.registers['1'] = new Register(text, linewise); 1153 } 1154 break; 1155 } 1156 // Make sure the unnamed register is set to what just happened 1157 this.unnamedRegister.setText(text, linewise, blockwise); 1158 return; 1159 } 1160 1161 // If we've gotten to this point, we've actually specified a register 1162 var append = isUpperCase(registerName); 1163 if (append) { 1164 register.pushText(text, linewise); 1165 } else { 1166 register.setText(text, linewise, blockwise); 1167 } 1168 // The unnamed register always has the same value as the last used 1169 // register. 1170 this.unnamedRegister.setText(register.toString(), linewise); 1171 }, 1172 // Gets the register named @name. If one of @name doesn't already exist, 1173 // create it. If @name is invalid, return the unnamedRegister. 1174 getRegister: function(name) { 1175 if (!this.isValidRegister(name)) { 1176 return this.unnamedRegister; 1177 } 1178 name = name.toLowerCase(); 1179 if (!this.registers[name]) { 1180 this.registers[name] = new Register(); 1181 } 1182 return this.registers[name]; 1183 }, 1184 isValidRegister: function(name) { 1185 return name && inArray(name, validRegisters); 1186 }, 1187 shiftNumericRegisters_: function() { 1188 for (var i = 9; i >= 2; i--) { 1189 this.registers[i] = this.getRegister('' + (i - 1)); 1190 } 1191 } 1192 }; 1193 function HistoryController() { 1194 this.historyBuffer = []; 1195 this.iterator = 0; 1196 this.initialPrefix = null; 1197 } 1198 HistoryController.prototype = { 1199 // the input argument here acts a user entered prefix for a small time 1200 // until we start autocompletion in which case it is the autocompleted. 1201 nextMatch: function (input, up) { 1202 var historyBuffer = this.historyBuffer; 1203 var dir = up ? -1 : 1; 1204 if (this.initialPrefix === null) this.initialPrefix = input; 1205 for (var i = this.iterator + dir; up ? i >= 0 : i < historyBuffer.length; i+= dir) { 1206 var element = historyBuffer[i]; 1207 for (var j = 0; j <= element.length; j++) { 1208 if (this.initialPrefix == element.substring(0, j)) { 1209 this.iterator = i; 1210 return element; 1211 } 1212 } 1213 } 1214 // should return the user input in case we reach the end of buffer. 1215 if (i >= historyBuffer.length) { 1216 this.iterator = historyBuffer.length; 1217 return this.initialPrefix; 1218 } 1219 // return the last autocompleted query or exCommand as it is. 1220 if (i < 0 ) return input; 1221 }, 1222 pushInput: function(input) { 1223 var index = this.historyBuffer.indexOf(input); 1224 if (index > -1) this.historyBuffer.splice(index, 1); 1225 if (input.length) this.historyBuffer.push(input); 1226 }, 1227 reset: function() { 1228 this.initialPrefix = null; 1229 this.iterator = this.historyBuffer.length; 1230 } 1231 }; 1232 var commandDispatcher = { 1233 matchCommand: function(keys, keyMap, inputState, context) { 1234 var matches = commandMatches(keys, keyMap, context, inputState); 1235 if (!matches.full && !matches.partial) { 1236 return {type: 'none'}; 1237 } else if (!matches.full && matches.partial) { 1238 return {type: 'partial'}; 1239 } 1240 1241 var bestMatch; 1242 for (var i = 0; i < matches.full.length; i++) { 1243 var match = matches.full[i]; 1244 if (!bestMatch) { 1245 bestMatch = match; 1246 } 1247 } 1248 if (bestMatch.keys.slice(-11) == '<character>') { 1249 var character = lastChar(keys); 1250 if (!character) return {type: 'none'}; 1251 inputState.selectedCharacter = character; 1252 } 1253 return {type: 'full', command: bestMatch}; 1254 }, 1255 processCommand: function(cm, vim, command) { 1256 vim.inputState.repeatOverride = command.repeatOverride; 1257 switch (command.type) { 1258 case 'motion': 1259 this.processMotion(cm, vim, command); 1260 break; 1261 case 'operator': 1262 this.processOperator(cm, vim, command); 1263 break; 1264 case 'operatorMotion': 1265 this.processOperatorMotion(cm, vim, command); 1266 break; 1267 case 'action': 1268 this.processAction(cm, vim, command); 1269 break; 1270 case 'search': 1271 this.processSearch(cm, vim, command); 1272 break; 1273 case 'ex': 1274 case 'keyToEx': 1275 this.processEx(cm, vim, command); 1276 break; 1277 default: 1278 break; 1279 } 1280 }, 1281 processMotion: function(cm, vim, command) { 1282 vim.inputState.motion = command.motion; 1283 vim.inputState.motionArgs = copyArgs(command.motionArgs); 1284 this.evalInput(cm, vim); 1285 }, 1286 processOperator: function(cm, vim, command) { 1287 var inputState = vim.inputState; 1288 if (inputState.operator) { 1289 if (inputState.operator == command.operator) { 1290 // Typing an operator twice like 'dd' makes the operator operate 1291 // linewise 1292 inputState.motion = 'expandToLine'; 1293 inputState.motionArgs = { linewise: true }; 1294 this.evalInput(cm, vim); 1295 return; 1296 } else { 1297 // 2 different operators in a row doesn't make sense. 1298 clearInputState(cm); 1299 } 1300 } 1301 inputState.operator = command.operator; 1302 inputState.operatorArgs = copyArgs(command.operatorArgs); 1303 if (command.keys.length > 1) { 1304 inputState.operatorShortcut = command.keys; 1305 } 1306 if (command.exitVisualBlock) { 1307 vim.visualBlock = false; 1308 updateCmSelection(cm); 1309 } 1310 if (vim.visualMode) { 1311 // Operating on a selection in visual mode. We don't need a motion. 1312 this.evalInput(cm, vim); 1313 } 1314 }, 1315 processOperatorMotion: function(cm, vim, command) { 1316 var visualMode = vim.visualMode; 1317 var operatorMotionArgs = copyArgs(command.operatorMotionArgs); 1318 if (operatorMotionArgs) { 1319 // Operator motions may have special behavior in visual mode. 1320 if (visualMode && operatorMotionArgs.visualLine) { 1321 vim.visualLine = true; 1322 } 1323 } 1324 this.processOperator(cm, vim, command); 1325 if (!visualMode) { 1326 this.processMotion(cm, vim, command); 1327 } 1328 }, 1329 processAction: function(cm, vim, command) { 1330 var inputState = vim.inputState; 1331 var repeat = inputState.getRepeat(); 1332 var repeatIsExplicit = !!repeat; 1333 var actionArgs = copyArgs(command.actionArgs) || {}; 1334 if (inputState.selectedCharacter) { 1335 actionArgs.selectedCharacter = inputState.selectedCharacter; 1336 } 1337 // Actions may or may not have motions and operators. Do these first. 1338 if (command.operator) { 1339 this.processOperator(cm, vim, command); 1340 } 1341 if (command.motion) { 1342 this.processMotion(cm, vim, command); 1343 } 1344 if (command.motion || command.operator) { 1345 this.evalInput(cm, vim); 1346 } 1347 actionArgs.repeat = repeat || 1; 1348 actionArgs.repeatIsExplicit = repeatIsExplicit; 1349 actionArgs.registerName = inputState.registerName; 1350 clearInputState(cm); 1351 vim.lastMotion = null; 1352 if (command.isEdit) { 1353 this.recordLastEdit(vim, inputState, command); 1354 } 1355 actions[command.action](cm, actionArgs, vim); 1356 }, 1357 processSearch: function(cm, vim, command) { 1358 if (!cm.getSearchCursor) { 1359 // Search depends on SearchCursor. 1360 return; 1361 } 1362 var forward = command.searchArgs.forward; 1363 var wholeWordOnly = command.searchArgs.wholeWordOnly; 1364 getSearchState(cm).setReversed(!forward); 1365 var promptPrefix = (forward) ? '/' : '?'; 1366 var originalQuery = getSearchState(cm).getQuery(); 1367 var originalScrollPos = cm.getScrollInfo(); 1368 function handleQuery(query, ignoreCase, smartCase) { 1369 vimGlobalState.searchHistoryController.pushInput(query); 1370 vimGlobalState.searchHistoryController.reset(); 1371 try { 1372 updateSearchQuery(cm, query, ignoreCase, smartCase); 1373 } catch (e) { 1374 showConfirm(cm, 'Invalid regex: ' + query); 1375 clearInputState(cm); 1376 return; 1377 } 1378 commandDispatcher.processMotion(cm, vim, { 1379 type: 'motion', 1380 motion: 'findNext', 1381 motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist } 1382 }); 1383 } 1384 function onPromptClose(query) { 1385 cm.scrollTo(originalScrollPos.left, originalScrollPos.top); 1386 handleQuery(query, true /** ignoreCase */, true /** smartCase */); 1387 var macroModeState = vimGlobalState.macroModeState; 1388 if (macroModeState.isRecording) { 1389 logSearchQuery(macroModeState, query); 1390 } 1391 } 1392 function onPromptKeyUp(e, query, close) { 1393 var keyName = CodeMirror.keyName(e), up, offset; 1394 if (keyName == 'Up' || keyName == 'Down') { 1395 up = keyName == 'Up' ? true : false; 1396 offset = e.target ? e.target.selectionEnd : 0; 1397 query = vimGlobalState.searchHistoryController.nextMatch(query, up) || ''; 1398 close(query); 1399 if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length); 1400 } else { 1401 if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift') 1402 vimGlobalState.searchHistoryController.reset(); 1403 } 1404 var parsedQuery; 1405 try { 1406 parsedQuery = updateSearchQuery(cm, query, 1407 true /** ignoreCase */, true /** smartCase */); 1408 } catch (e) { 1409 // Swallow bad regexes for incremental search. 1410 } 1411 if (parsedQuery) { 1412 cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30); 1413 } else { 1414 clearSearchHighlight(cm); 1415 cm.scrollTo(originalScrollPos.left, originalScrollPos.top); 1416 } 1417 } 1418 function onPromptKeyDown(e, query, close) { 1419 var keyName = CodeMirror.keyName(e); 1420 if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' || 1421 (keyName == 'Backspace' && query == '')) { 1422 vimGlobalState.searchHistoryController.pushInput(query); 1423 vimGlobalState.searchHistoryController.reset(); 1424 updateSearchQuery(cm, originalQuery); 1425 clearSearchHighlight(cm); 1426 cm.scrollTo(originalScrollPos.left, originalScrollPos.top); 1427 CodeMirror.e_stop(e); 1428 clearInputState(cm); 1429 close(); 1430 cm.focus(); 1431 } else if (keyName == 'Up' || keyName == 'Down') { 1432 CodeMirror.e_stop(e); 1433 } else if (keyName == 'Ctrl-U') { 1434 // Ctrl-U clears input. 1435 CodeMirror.e_stop(e); 1436 close(''); 1437 } 1438 } 1439 switch (command.searchArgs.querySrc) { 1440 case 'prompt': 1441 var macroModeState = vimGlobalState.macroModeState; 1442 if (macroModeState.isPlaying) { 1443 var query = macroModeState.replaySearchQueries.shift(); 1444 handleQuery(query, true /** ignoreCase */, false /** smartCase */); 1445 } else { 1446 showPrompt(cm, { 1447 onClose: onPromptClose, 1448 prefix: promptPrefix, 1449 desc: '(JavaScript regexp)', 1450 onKeyUp: onPromptKeyUp, 1451 onKeyDown: onPromptKeyDown 1452 }); 1453 } 1454 break; 1455 case 'wordUnderCursor': 1456 var word = expandWordUnderCursor(cm, false /** inclusive */, 1457 true /** forward */, false /** bigWord */, 1458 true /** noSymbol */); 1459 var isKeyword = true; 1460 if (!word) { 1461 word = expandWordUnderCursor(cm, false /** inclusive */, 1462 true /** forward */, false /** bigWord */, 1463 false /** noSymbol */); 1464 isKeyword = false; 1465 } 1466 if (!word) { 1467 return; 1468 } 1469 var query = cm.getLine(word.start.line).substring(word.start.ch, 1470 word.end.ch); 1471 if (isKeyword && wholeWordOnly) { 1472 query = '\\b' + query + '\\b'; 1473 } else { 1474 query = escapeRegex(query); 1475 } 1476 1477 // cachedCursor is used to save the old position of the cursor 1478 // when * or # causes vim to seek for the nearest word and shift 1479 // the cursor before entering the motion. 1480 vimGlobalState.jumpList.cachedCursor = cm.getCursor(); 1481 cm.setCursor(word.start); 1482 1483 handleQuery(query, true /** ignoreCase */, false /** smartCase */); 1484 break; 1485 } 1486 }, 1487 processEx: function(cm, vim, command) { 1488 function onPromptClose(input) { 1489 // Give the prompt some time to close so that if processCommand shows 1490 // an error, the elements don't overlap. 1491 vimGlobalState.exCommandHistoryController.pushInput(input); 1492 vimGlobalState.exCommandHistoryController.reset(); 1493 exCommandDispatcher.processCommand(cm, input); 1494 } 1495 function onPromptKeyDown(e, input, close) { 1496 var keyName = CodeMirror.keyName(e), up, offset; 1497 if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' || 1498 (keyName == 'Backspace' && input == '')) { 1499 vimGlobalState.exCommandHistoryController.pushInput(input); 1500 vimGlobalState.exCommandHistoryController.reset(); 1501 CodeMirror.e_stop(e); 1502 clearInputState(cm); 1503 close(); 1504 cm.focus(); 1505 } 1506 if (keyName == 'Up' || keyName == 'Down') { 1507 CodeMirror.e_stop(e); 1508 up = keyName == 'Up' ? true : false; 1509 offset = e.target ? e.target.selectionEnd : 0; 1510 input = vimGlobalState.exCommandHistoryController.nextMatch(input, up) || ''; 1511 close(input); 1512 if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length); 1513 } else if (keyName == 'Ctrl-U') { 1514 // Ctrl-U clears input. 1515 CodeMirror.e_stop(e); 1516 close(''); 1517 } else { 1518 if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift') 1519 vimGlobalState.exCommandHistoryController.reset(); 1520 } 1521 } 1522 if (command.type == 'keyToEx') { 1523 // Handle user defined Ex to Ex mappings 1524 exCommandDispatcher.processCommand(cm, command.exArgs.input); 1525 } else { 1526 if (vim.visualMode) { 1527 showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>', 1528 onKeyDown: onPromptKeyDown, selectValueOnOpen: false}); 1529 } else { 1530 showPrompt(cm, { onClose: onPromptClose, prefix: ':', 1531 onKeyDown: onPromptKeyDown}); 1532 } 1533 } 1534 }, 1535 evalInput: function(cm, vim) { 1536 // If the motion command is set, execute both the operator and motion. 1537 // Otherwise return. 1538 var inputState = vim.inputState; 1539 var motion = inputState.motion; 1540 var motionArgs = inputState.motionArgs || {}; 1541 var operator = inputState.operator; 1542 var operatorArgs = inputState.operatorArgs || {}; 1543 var registerName = inputState.registerName; 1544 var sel = vim.sel; 1545 // TODO: Make sure cm and vim selections are identical outside visual mode. 1546 var origHead = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.head): cm.getCursor('head')); 1547 var origAnchor = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.anchor) : cm.getCursor('anchor')); 1548 var oldHead = copyCursor(origHead); 1549 var oldAnchor = copyCursor(origAnchor); 1550 var newHead, newAnchor; 1551 var repeat; 1552 if (operator) { 1553 this.recordLastEdit(vim, inputState); 1554 } 1555 if (inputState.repeatOverride !== undefined) { 1556 // If repeatOverride is specified, that takes precedence over the 1557 // input state's repeat. Used by Ex mode and can be user defined. 1558 repeat = inputState.repeatOverride; 1559 } else { 1560 repeat = inputState.getRepeat(); 1561 } 1562 if (repeat > 0 && motionArgs.explicitRepeat) { 1563 motionArgs.repeatIsExplicit = true; 1564 } else if (motionArgs.noRepeat || 1565 (!motionArgs.explicitRepeat && repeat === 0)) { 1566 repeat = 1; 1567 motionArgs.repeatIsExplicit = false; 1568 } 1569 if (inputState.selectedCharacter) { 1570 // If there is a character input, stick it in all of the arg arrays. 1571 motionArgs.selectedCharacter = operatorArgs.selectedCharacter = 1572 inputState.selectedCharacter; 1573 } 1574 motionArgs.repeat = repeat; 1575 clearInputState(cm); 1576 if (motion) { 1577 var motionResult = motions[motion](cm, origHead, motionArgs, vim, inputState); 1578 vim.lastMotion = motions[motion]; 1579 if (!motionResult) { 1580 return; 1581 } 1582 if (motionArgs.toJumplist) { 1583 var jumpList = vimGlobalState.jumpList; 1584 // if the current motion is # or *, use cachedCursor 1585 var cachedCursor = jumpList.cachedCursor; 1586 if (cachedCursor) { 1587 recordJumpPosition(cm, cachedCursor, motionResult); 1588 delete jumpList.cachedCursor; 1589 } else { 1590 recordJumpPosition(cm, origHead, motionResult); 1591 } 1592 } 1593 if (motionResult instanceof Array) { 1594 newAnchor = motionResult[0]; 1595 newHead = motionResult[1]; 1596 } else { 1597 newHead = motionResult; 1598 } 1599 // TODO: Handle null returns from motion commands better. 1600 if (!newHead) { 1601 newHead = copyCursor(origHead); 1602 } 1603 if (vim.visualMode) { 1604 if (!(vim.visualBlock && newHead.ch === Infinity)) { 1605 newHead = clipCursorToContent(cm, newHead); 1606 } 1607 if (newAnchor) { 1608 newAnchor = clipCursorToContent(cm, newAnchor); 1609 } 1610 newAnchor = newAnchor || oldAnchor; 1611 sel.anchor = newAnchor; 1612 sel.head = newHead; 1613 updateCmSelection(cm); 1614 updateMark(cm, vim, '<', 1615 cursorIsBefore(newAnchor, newHead) ? newAnchor 1616 : newHead); 1617 updateMark(cm, vim, '>', 1618 cursorIsBefore(newAnchor, newHead) ? newHead 1619 : newAnchor); 1620 } else if (!operator) { 1621 newHead = clipCursorToContent(cm, newHead); 1622 cm.setCursor(newHead.line, newHead.ch); 1623 } 1624 } 1625 if (operator) { 1626 if (operatorArgs.lastSel) { 1627 // Replaying a visual mode operation 1628 newAnchor = oldAnchor; 1629 var lastSel = operatorArgs.lastSel; 1630 var lineOffset = Math.abs(lastSel.head.line - lastSel.anchor.line); 1631 var chOffset = Math.abs(lastSel.head.ch - lastSel.anchor.ch); 1632 if (lastSel.visualLine) { 1633 // Linewise Visual mode: The same number of lines. 1634 newHead = new Pos(oldAnchor.line + lineOffset, oldAnchor.ch); 1635 } else if (lastSel.visualBlock) { 1636 // Blockwise Visual mode: The same number of lines and columns. 1637 newHead = new Pos(oldAnchor.line + lineOffset, oldAnchor.ch + chOffset); 1638 } else if (lastSel.head.line == lastSel.anchor.line) { 1639 // Normal Visual mode within one line: The same number of characters. 1640 newHead = new Pos(oldAnchor.line, oldAnchor.ch + chOffset); 1641 } else { 1642 // Normal Visual mode with several lines: The same number of lines, in the 1643 // last line the same number of characters as in the last line the last time. 1644 newHead = new Pos(oldAnchor.line + lineOffset, oldAnchor.ch); 1645 } 1646 vim.visualMode = true; 1647 vim.visualLine = lastSel.visualLine; 1648 vim.visualBlock = lastSel.visualBlock; 1649 sel = vim.sel = { 1650 anchor: newAnchor, 1651 head: newHead 1652 }; 1653 updateCmSelection(cm); 1654 } else if (vim.visualMode) { 1655 operatorArgs.lastSel = { 1656 anchor: copyCursor(sel.anchor), 1657 head: copyCursor(sel.head), 1658 visualBlock: vim.visualBlock, 1659 visualLine: vim.visualLine 1660 }; 1661 } 1662 var curStart, curEnd, linewise, mode; 1663 var cmSel; 1664 if (vim.visualMode) { 1665 // Init visual op 1666 curStart = cursorMin(sel.head, sel.anchor); 1667 curEnd = cursorMax(sel.head, sel.anchor); 1668 linewise = vim.visualLine || operatorArgs.linewise; 1669 mode = vim.visualBlock ? 'block' : 1670 linewise ? 'line' : 1671 'char'; 1672 cmSel = makeCmSelection(cm, { 1673 anchor: curStart, 1674 head: curEnd 1675 }, mode); 1676 if (linewise) { 1677 var ranges = cmSel.ranges; 1678 if (mode == 'block') { 1679 // Linewise operators in visual block mode extend to end of line 1680 for (var i = 0; i < ranges.length; i++) { 1681 ranges[i].head.ch = lineLength(cm, ranges[i].head.line); 1682 } 1683 } else if (mode == 'line') { 1684 ranges[0].head = new Pos(ranges[0].head.line + 1, 0); 1685 } 1686 } 1687 } else { 1688 // Init motion op 1689 curStart = copyCursor(newAnchor || oldAnchor); 1690 curEnd = copyCursor(newHead || oldHead); 1691 if (cursorIsBefore(curEnd, curStart)) { 1692 var tmp = curStart; 1693 curStart = curEnd; 1694 curEnd = tmp; 1695 } 1696 linewise = motionArgs.linewise || operatorArgs.linewise; 1697 if (linewise) { 1698 // Expand selection to entire line. 1699 expandSelectionToLine(cm, curStart, curEnd); 1700 } else if (motionArgs.forward) { 1701 // Clip to trailing newlines only if the motion goes forward. 1702 clipToLine(cm, curStart, curEnd); 1703 } 1704 mode = 'char'; 1705 var exclusive = !motionArgs.inclusive || linewise; 1706 cmSel = makeCmSelection(cm, { 1707 anchor: curStart, 1708 head: curEnd 1709 }, mode, exclusive); 1710 } 1711 cm.setSelections(cmSel.ranges, cmSel.primary); 1712 vim.lastMotion = null; 1713 operatorArgs.repeat = repeat; // For indent in visual mode. 1714 operatorArgs.registerName = registerName; 1715 // Keep track of linewise as it affects how paste and change behave. 1716 operatorArgs.linewise = linewise; 1717 var operatorMoveTo = operators[operator]( 1718 cm, operatorArgs, cmSel.ranges, oldAnchor, newHead); 1719 if (vim.visualMode) { 1720 exitVisualMode(cm, operatorMoveTo != null); 1721 } 1722 if (operatorMoveTo) { 1723 cm.setCursor(operatorMoveTo); 1724 } 1725 } 1726 }, 1727 recordLastEdit: function(vim, inputState, actionCommand) { 1728 var macroModeState = vimGlobalState.macroModeState; 1729 if (macroModeState.isPlaying) { return; } 1730 vim.lastEditInputState = inputState; 1731 vim.lastEditActionCommand = actionCommand; 1732 macroModeState.lastInsertModeChanges.changes = []; 1733 macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false; 1734 macroModeState.lastInsertModeChanges.visualBlock = vim.visualBlock ? vim.sel.head.line - vim.sel.anchor.line : 0; 1735 } 1736 }; 1737 1738 /** 1739 * typedef {Object{line:number,ch:number}} Cursor An object containing the 1740 * position of the cursor. 1741 */ 1742 // All of the functions below return Cursor objects. 1743 var motions = { 1744 moveToTopLine: function(cm, _head, motionArgs) { 1745 var line = getUserVisibleLines(cm).top + motionArgs.repeat -1; 1746 return new Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); 1747 }, 1748 moveToMiddleLine: function(cm) { 1749 var range = getUserVisibleLines(cm); 1750 var line = Math.floor((range.top + range.bottom) * 0.5); 1751 return new Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); 1752 }, 1753 moveToBottomLine: function(cm, _head, motionArgs) { 1754 var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1; 1755 return new Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); 1756 }, 1757 expandToLine: function(_cm, head, motionArgs) { 1758 // Expands forward to end of line, and then to next line if repeat is 1759 // >1. Does not handle backward motion! 1760 var cur = head; 1761 return new Pos(cur.line + motionArgs.repeat - 1, Infinity); 1762 }, 1763 findNext: function(cm, _head, motionArgs) { 1764 var state = getSearchState(cm); 1765 var query = state.getQuery(); 1766 if (!query) { 1767 return; 1768 } 1769 var prev = !motionArgs.forward; 1770 // If search is initiated with ? instead of /, negate direction. 1771 prev = (state.isReversed()) ? !prev : prev; 1772 highlightSearchMatches(cm, query); 1773 return findNext(cm, prev/** prev */, query, motionArgs.repeat); 1774 }, 1775 /** 1776 * Find and select the next occurrence of the search query. If the cursor is currently 1777 * within a match, then find and select the current match. Otherwise, find the next occurrence in the 1778 * appropriate direction. 1779 * 1780 * This differs from `findNext` in the following ways: 1781 * 1782 * 1. Instead of only returning the "from", this returns a "from", "to" range. 1783 * 2. If the cursor is currently inside a search match, this selects the current match 1784 * instead of the next match. 1785 * 3. If there is no associated operator, this will turn on visual mode. 1786 */ 1787 findAndSelectNextInclusive: function(cm, _head, motionArgs, vim, prevInputState) { 1788 var state = getSearchState(cm); 1789 var query = state.getQuery(); 1790 1791 if (!query) { 1792 return; 1793 } 1794 1795 var prev = !motionArgs.forward; 1796 prev = (state.isReversed()) ? !prev : prev; 1797 1798 // next: [from, to] | null 1799 var next = findNextFromAndToInclusive(cm, prev, query, motionArgs.repeat, vim); 1800 1801 // No matches. 1802 if (!next) { 1803 return; 1804 } 1805 1806 // If there's an operator that will be executed, return the selection. 1807 if (prevInputState.operator) { 1808 return next; 1809 } 1810 1811 // At this point, we know that there is no accompanying operator -- let's 1812 // deal with visual mode in order to select an appropriate match. 1813 1814 var from = next[0]; 1815 // For whatever reason, when we use the "to" as returned by searchcursor.js directly, 1816 // the resulting selection is extended by 1 char. Let's shrink it so that only the 1817 // match is selected. 1818 var to = new Pos(next[1].line, next[1].ch - 1); 1819 1820 if (vim.visualMode) { 1821 // If we were in visualLine or visualBlock mode, get out of it. 1822 if (vim.visualLine || vim.visualBlock) { 1823 vim.visualLine = false; 1824 vim.visualBlock = false; 1825 CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: ""}); 1826 } 1827 1828 // If we're currently in visual mode, we should extend the selection to include 1829 // the search result. 1830 var anchor = vim.sel.anchor; 1831 if (anchor) { 1832 if (state.isReversed()) { 1833 if (motionArgs.forward) { 1834 return [anchor, from]; 1835 } 1836 1837 return [anchor, to]; 1838 } else { 1839 if (motionArgs.forward) { 1840 return [anchor, to]; 1841 } 1842 1843 return [anchor, from]; 1844 } 1845 } 1846 } else { 1847 // Let's turn visual mode on. 1848 vim.visualMode = true; 1849 vim.visualLine = false; 1850 vim.visualBlock = false; 1851 CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: ""}); 1852 } 1853 1854 return prev ? [to, from] : [from, to]; 1855 }, 1856 goToMark: function(cm, _head, motionArgs, vim) { 1857 var pos = getMarkPos(cm, vim, motionArgs.selectedCharacter); 1858 if (pos) { 1859 return motionArgs.linewise ? { line: pos.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(pos.line)) } : pos; 1860 } 1861 return null; 1862 }, 1863 moveToOtherHighlightedEnd: function(cm, _head, motionArgs, vim) { 1864 if (vim.visualBlock && motionArgs.sameLine) { 1865 var sel = vim.sel; 1866 return [ 1867 clipCursorToContent(cm, new Pos(sel.anchor.line, sel.head.ch)), 1868 clipCursorToContent(cm, new Pos(sel.head.line, sel.anchor.ch)) 1869 ]; 1870 } else { 1871 return ([vim.sel.head, vim.sel.anchor]); 1872 } 1873 }, 1874 jumpToMark: function(cm, head, motionArgs, vim) { 1875 var best = head; 1876 for (var i = 0; i < motionArgs.repeat; i++) { 1877 var cursor = best; 1878 for (var key in vim.marks) { 1879 if (!isLowerCase(key)) { 1880 continue; 1881 } 1882 var mark = vim.marks[key].find(); 1883 var isWrongDirection = (motionArgs.forward) ? 1884 cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark); 1885 1886 if (isWrongDirection) { 1887 continue; 1888 } 1889 if (motionArgs.linewise && (mark.line == cursor.line)) { 1890 continue; 1891 } 1892 1893 var equal = cursorEqual(cursor, best); 1894 var between = (motionArgs.forward) ? 1895 cursorIsBetween(cursor, mark, best) : 1896 cursorIsBetween(best, mark, cursor); 1897 1898 if (equal || between) { 1899 best = mark; 1900 } 1901 } 1902 } 1903 1904 if (motionArgs.linewise) { 1905 // Vim places the cursor on the first non-whitespace character of 1906 // the line if there is one, else it places the cursor at the end 1907 // of the line, regardless of whether a mark was found. 1908 best = new Pos(best.line, findFirstNonWhiteSpaceCharacter(cm.getLine(best.line))); 1909 } 1910 return best; 1911 }, 1912 moveByCharacters: function(_cm, head, motionArgs) { 1913 var cur = head; 1914 var repeat = motionArgs.repeat; 1915 var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat; 1916 return new Pos(cur.line, ch); 1917 }, 1918 moveByLines: function(cm, head, motionArgs, vim) { 1919 var cur = head; 1920 var endCh = cur.ch; 1921 // Depending what our last motion was, we may want to do different 1922 // things. If our last motion was moving vertically, we want to 1923 // preserve the HPos from our last horizontal move. If our last motion 1924 // was going to the end of a line, moving vertically we should go to 1925 // the end of the line, etc. 1926 switch (vim.lastMotion) { 1927 case this.moveByLines: 1928 case this.moveByDisplayLines: 1929 case this.moveByScroll: 1930 case this.moveToColumn: 1931 case this.moveToEol: 1932 endCh = vim.lastHPos; 1933 break; 1934 default: 1935 vim.lastHPos = endCh; 1936 } 1937 var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0); 1938 var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat; 1939 var first = cm.firstLine(); 1940 var last = cm.lastLine(); 1941 var posV = cm.findPosV(cur, (motionArgs.forward ? repeat : -repeat), 'line', vim.lastHSPos); 1942 var hasMarkedText = motionArgs.forward ? posV.line > line : posV.line < line; 1943 if (hasMarkedText) { 1944 line = posV.line; 1945 endCh = posV.ch; 1946 } 1947 // Vim go to line begin or line end when cursor at first/last line and 1948 // move to previous/next line is triggered. 1949 if (line < first && cur.line == first){ 1950 return this.moveToStartOfLine(cm, head, motionArgs, vim); 1951 } else if (line > last && cur.line == last){ 1952 return moveToEol(cm, head, motionArgs, vim, true); 1953 } 1954 if (motionArgs.toFirstChar){ 1955 endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line)); 1956 vim.lastHPos = endCh; 1957 } 1958 vim.lastHSPos = cm.charCoords(new Pos(line, endCh),'div').left; 1959 return new Pos(line, endCh); 1960 }, 1961 moveByDisplayLines: function(cm, head, motionArgs, vim) { 1962 var cur = head; 1963 switch (vim.lastMotion) { 1964 case this.moveByDisplayLines: 1965 case this.moveByScroll: 1966 case this.moveByLines: 1967 case this.moveToColumn: 1968 case this.moveToEol: 1969 break; 1970 default: 1971 vim.lastHSPos = cm.charCoords(cur,'div').left; 1972 } 1973 var repeat = motionArgs.repeat; 1974 var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),'line',vim.lastHSPos); 1975 if (res.hitSide) { 1976 if (motionArgs.forward) { 1977 var lastCharCoords = cm.charCoords(res, 'div'); 1978 var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos }; 1979 var res = cm.coordsChar(goalCoords, 'div'); 1980 } else { 1981 var resCoords = cm.charCoords(new Pos(cm.firstLine(), 0), 'div'); 1982 resCoords.left = vim.lastHSPos; 1983 res = cm.coordsChar(resCoords, 'div'); 1984 } 1985 } 1986 vim.lastHPos = res.ch; 1987 return res; 1988 }, 1989 moveByPage: function(cm, head, motionArgs) { 1990 // CodeMirror only exposes functions that move the cursor page down, so 1991 // doing this bad hack to move the cursor and move it back. evalInput 1992 // will move the cursor to where it should be in the end. 1993 var curStart = head; 1994 var repeat = motionArgs.repeat; 1995 return cm.findPosV(curStart, (motionArgs.forward ? repeat : -repeat), 'page'); 1996 }, 1997 moveByParagraph: function(cm, head, motionArgs) { 1998 var dir = motionArgs.forward ? 1 : -1; 1999 return findParagraph(cm, head, motionArgs.repeat, dir); 2000 }, 2001 moveBySentence: function(cm, head, motionArgs) { 2002 var dir = motionArgs.forward ? 1 : -1; 2003 return findSentence(cm, head, motionArgs.repeat, dir); 2004 }, 2005 moveByScroll: function(cm, head, motionArgs, vim) { 2006 var scrollbox = cm.getScrollInfo(); 2007 var curEnd = null; 2008 var repeat = motionArgs.repeat; 2009 if (!repeat) { 2010 repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight()); 2011 } 2012 var orig = cm.charCoords(head, 'local'); 2013 motionArgs.repeat = repeat; 2014 var curEnd = motions.moveByDisplayLines(cm, head, motionArgs, vim); 2015 if (!curEnd) { 2016 return null; 2017 } 2018 var dest = cm.charCoords(curEnd, 'local'); 2019 cm.scrollTo(null, scrollbox.top + dest.top - orig.top); 2020 return curEnd; 2021 }, 2022 moveByWords: function(cm, head, motionArgs) { 2023 return moveToWord(cm, head, motionArgs.repeat, !!motionArgs.forward, 2024 !!motionArgs.wordEnd, !!motionArgs.bigWord); 2025 }, 2026 moveTillCharacter: function(cm, _head, motionArgs) { 2027 var repeat = motionArgs.repeat; 2028 var curEnd = moveToCharacter(cm, repeat, motionArgs.forward, 2029 motionArgs.selectedCharacter); 2030 var increment = motionArgs.forward ? -1 : 1; 2031 recordLastCharacterSearch(increment, motionArgs); 2032 if (!curEnd) return null; 2033 curEnd.ch += increment; 2034 return curEnd; 2035 }, 2036 moveToCharacter: function(cm, head, motionArgs) { 2037 var repeat = motionArgs.repeat; 2038 recordLastCharacterSearch(0, motionArgs); 2039 return moveToCharacter(cm, repeat, motionArgs.forward, 2040 motionArgs.selectedCharacter) || head; 2041 }, 2042 moveToSymbol: function(cm, head, motionArgs) { 2043 var repeat = motionArgs.repeat; 2044 return findSymbol(cm, repeat, motionArgs.forward, 2045 motionArgs.selectedCharacter) || head; 2046 }, 2047 moveToColumn: function(cm, head, motionArgs, vim) { 2048 var repeat = motionArgs.repeat; 2049 // repeat is equivalent to which column we want to move to! 2050 vim.lastHPos = repeat - 1; 2051 vim.lastHSPos = cm.charCoords(head,'div').left; 2052 return moveToColumn(cm, repeat); 2053 }, 2054 moveToEol: function(cm, head, motionArgs, vim) { 2055 return moveToEol(cm, head, motionArgs, vim, false); 2056 }, 2057 moveToFirstNonWhiteSpaceCharacter: function(cm, head) { 2058 // Go to the start of the line where the text begins, or the end for 2059 // whitespace-only lines 2060 var cursor = head; 2061 return new Pos(cursor.line, 2062 findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line))); 2063 }, 2064 moveToMatchedSymbol: function(cm, head) { 2065 var cursor = head; 2066 var line = cursor.line; 2067 var ch = cursor.ch; 2068 var lineText = cm.getLine(line); 2069 var symbol; 2070 for (; ch < lineText.length; ch++) { 2071 symbol = lineText.charAt(ch); 2072 if (symbol && isMatchableSymbol(symbol)) { 2073 var style = cm.getTokenTypeAt(new Pos(line, ch + 1)); 2074 if (style !== "string" && style !== "comment") { 2075 break; 2076 } 2077 } 2078 } 2079 if (ch < lineText.length) { 2080 // Only include angle brackets in analysis if they are being matched. 2081 var re = (ch === '<' || ch === '>') ? /[(){}[\]<>]/ : /[(){}[\]]/; 2082 var matched = cm.findMatchingBracket(new Pos(line, ch), {bracketRegex: re}); 2083 return matched.to; 2084 } else { 2085 return cursor; 2086 } 2087 }, 2088 moveToStartOfLine: function(_cm, head) { 2089 return new Pos(head.line, 0); 2090 }, 2091 moveToLineOrEdgeOfDocument: function(cm, _head, motionArgs) { 2092 var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine(); 2093 if (motionArgs.repeatIsExplicit) { 2094 lineNum = motionArgs.repeat - cm.getOption('firstLineNumber'); 2095 } 2096 return new Pos(lineNum, 2097 findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum))); 2098 }, 2099 moveToStartOfDisplayLine: function(cm) { 2100 cm.execCommand("goLineLeft"); 2101 return cm.getCursor(); 2102 }, 2103 moveToEndOfDisplayLine: function(cm) { 2104 cm.execCommand("goLineRight"); 2105 var head = cm.getCursor(); 2106 if (head.sticky == "before") head.ch--; 2107 return head; 2108 }, 2109 textObjectManipulation: function(cm, head, motionArgs, vim) { 2110 // TODO: lots of possible exceptions that can be thrown here. Try da( 2111 // outside of a () block. 2112 var mirroredPairs = {'(': ')', ')': '(', 2113 '{': '}', '}': '{', 2114 '[': ']', ']': '[', 2115 '<': '>', '>': '<'}; 2116 var selfPaired = {'\'': true, '"': true, '`': true}; 2117 2118 var character = motionArgs.selectedCharacter; 2119 // 'b' refers to '()' block. 2120 // 'B' refers to '{}' block. 2121 if (character == 'b') { 2122 character = '('; 2123 } else if (character == 'B') { 2124 character = '{'; 2125 } 2126 2127 // Inclusive is the difference between a and i 2128 // TODO: Instead of using the additional text object map to perform text 2129 // object operations, merge the map into the defaultKeyMap and use 2130 // motionArgs to define behavior. Define separate entries for 'aw', 2131 // 'iw', 'a[', 'i[', etc. 2132 var inclusive = !motionArgs.textObjectInner; 2133 2134 var tmp; 2135 if (mirroredPairs[character]) { 2136 tmp = selectCompanionObject(cm, head, character, inclusive); 2137 } else if (selfPaired[character]) { 2138 tmp = findBeginningAndEnd(cm, head, character, inclusive); 2139 } else if (character === 'W') { 2140 tmp = expandWordUnderCursor(cm, inclusive, true /** forward */, 2141 true /** bigWord */); 2142 } else if (character === 'w') { 2143 tmp = expandWordUnderCursor(cm, inclusive, true /** forward */, 2144 false /** bigWord */); 2145 } else if (character === 'p') { 2146 tmp = findParagraph(cm, head, motionArgs.repeat, 0, inclusive); 2147 motionArgs.linewise = true; 2148 if (vim.visualMode) { 2149 if (!vim.visualLine) { vim.visualLine = true; } 2150 } else { 2151 var operatorArgs = vim.inputState.operatorArgs; 2152 if (operatorArgs) { operatorArgs.linewise = true; } 2153 tmp.end.line--; 2154 } 2155 } else if (character === 't') { 2156 tmp = expandTagUnderCursor(cm, head, inclusive); 2157 } else { 2158 // No text object defined for this, don't move. 2159 return null; 2160 } 2161 2162 if (!cm.state.vim.visualMode) { 2163 return [tmp.start, tmp.end]; 2164 } else { 2165 return expandSelection(cm, tmp.start, tmp.end); 2166 } 2167 }, 2168 2169 repeatLastCharacterSearch: function(cm, head, motionArgs) { 2170 var lastSearch = vimGlobalState.lastCharacterSearch; 2171 var repeat = motionArgs.repeat; 2172 var forward = motionArgs.forward === lastSearch.forward; 2173 var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1); 2174 cm.moveH(-increment, 'char'); 2175 motionArgs.inclusive = forward ? true : false; 2176 var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter); 2177 if (!curEnd) { 2178 cm.moveH(increment, 'char'); 2179 return head; 2180 } 2181 curEnd.ch += increment; 2182 return curEnd; 2183 } 2184 }; 2185 2186 function defineMotion(name, fn) { 2187 motions[name] = fn; 2188 } 2189 2190 function fillArray(val, times) { 2191 var arr = []; 2192 for (var i = 0; i < times; i++) { 2193 arr.push(val); 2194 } 2195 return arr; 2196 } 2197 /** 2198 * An operator acts on a text selection. It receives the list of selections 2199 * as input. The corresponding CodeMirror selection is guaranteed to 2200 * match the input selection. 2201 */ 2202 var operators = { 2203 change: function(cm, args, ranges) { 2204 var finalHead, text; 2205 var vim = cm.state.vim; 2206 var anchor = ranges[0].anchor, 2207 head = ranges[0].head; 2208 if (!vim.visualMode) { 2209 text = cm.getRange(anchor, head); 2210 var lastState = vim.lastEditInputState || {}; 2211 if (lastState.motion == "moveByWords" && !isWhiteSpaceString(text)) { 2212 // Exclude trailing whitespace if the range is not all whitespace. 2213 var match = (/\s+$/).exec(text); 2214 if (match && lastState.motionArgs && lastState.motionArgs.forward) { 2215 head = offsetCursor(head, 0, - match[0].length); 2216 text = text.slice(0, - match[0].length); 2217 } 2218 } 2219 var prevLineEnd = new Pos(anchor.line - 1, Number.MAX_VALUE); 2220 var wasLastLine = cm.firstLine() == cm.lastLine(); 2221 if (head.line > cm.lastLine() && args.linewise && !wasLastLine) { 2222 cm.replaceRange('', prevLineEnd, head); 2223 } else { 2224 cm.replaceRange('', anchor, head); 2225 } 2226 if (args.linewise) { 2227 // Push the next line back down, if there is a next line. 2228 if (!wasLastLine) { 2229 cm.setCursor(prevLineEnd); 2230 CodeMirror.commands.newlineAndIndent(cm); 2231 } 2232 // make sure cursor ends up at the end of the line. 2233 anchor.ch = Number.MAX_VALUE; 2234 } 2235 finalHead = anchor; 2236 } else if (args.fullLine) { 2237 head.ch = Number.MAX_VALUE; 2238 head.line--; 2239 cm.setSelection(anchor, head) 2240 text = cm.getSelection(); 2241 cm.replaceSelection(""); 2242 finalHead = anchor; 2243 } else { 2244 text = cm.getSelection(); 2245 var replacement = fillArray('', ranges.length); 2246 cm.replaceSelections(replacement); 2247 finalHead = cursorMin(ranges[0].head, ranges[0].anchor); 2248 } 2249 vimGlobalState.registerController.pushText( 2250 args.registerName, 'change', text, 2251 args.linewise, ranges.length > 1); 2252 actions.enterInsertMode(cm, {head: finalHead}, cm.state.vim); 2253 }, 2254 // delete is a javascript keyword. 2255 'delete': function(cm, args, ranges) { 2256 var finalHead, text; 2257 var vim = cm.state.vim; 2258 if (!vim.visualBlock) { 2259 var anchor = ranges[0].anchor, 2260 head = ranges[0].head; 2261 if (args.linewise && 2262 head.line != cm.firstLine() && 2263 anchor.line == cm.lastLine() && 2264 anchor.line == head.line - 1) { 2265 // Special case for dd on last line (and first line). 2266 if (anchor.line == cm.firstLine()) { 2267 anchor.ch = 0; 2268 } else { 2269 anchor = new Pos(anchor.line - 1, lineLength(cm, anchor.line - 1)); 2270 } 2271 } 2272 text = cm.getRange(anchor, head); 2273 cm.replaceRange('', anchor, head); 2274 finalHead = anchor; 2275 if (args.linewise) { 2276 finalHead = motions.moveToFirstNonWhiteSpaceCharacter(cm, anchor); 2277 } 2278 } else { 2279 text = cm.getSelection(); 2280 var replacement = fillArray('', ranges.length); 2281 cm.replaceSelections(replacement); 2282 finalHead = cursorMin(ranges[0].head, ranges[0].anchor); 2283 } 2284 vimGlobalState.registerController.pushText( 2285 args.registerName, 'delete', text, 2286 args.linewise, vim.visualBlock); 2287 return clipCursorToContent(cm, finalHead); 2288 }, 2289 indent: function(cm, args, ranges) { 2290 var vim = cm.state.vim; 2291 var startLine = ranges[0].anchor.line; 2292 var endLine = vim.visualBlock ? 2293 ranges[ranges.length - 1].anchor.line : 2294 ranges[0].head.line; 2295 // In visual mode, n> shifts the selection right n times, instead of 2296 // shifting n lines right once. 2297 var repeat = (vim.visualMode) ? args.repeat : 1; 2298 if (args.linewise) { 2299 // The only way to delete a newline is to delete until the start of 2300 // the next line, so in linewise mode evalInput will include the next 2301 // line. We don't want this in indent, so we go back a line. 2302 endLine--; 2303 } 2304 for (var i = startLine; i <= endLine; i++) { 2305 for (var j = 0; j < repeat; j++) { 2306 cm.indentLine(i, args.indentRight); 2307 } 2308 } 2309 return motions.moveToFirstNonWhiteSpaceCharacter(cm, ranges[0].anchor); 2310 }, 2311 indentAuto: function(cm, _args, ranges) { 2312 cm.execCommand("indentAuto"); 2313 return motions.moveToFirstNonWhiteSpaceCharacter(cm, ranges[0].anchor); 2314 }, 2315 changeCase: function(cm, args, ranges, oldAnchor, newHead) { 2316 var selections = cm.getSelections(); 2317 var swapped = []; 2318 var toLower = args.toLower; 2319 for (var j = 0; j < selections.length; j++) { 2320 var toSwap = selections[j]; 2321 var text = ''; 2322 if (toLower === true) { 2323 text = toSwap.toLowerCase(); 2324 } else if (toLower === false) { 2325 text = toSwap.toUpperCase(); 2326 } else { 2327 for (var i = 0; i < toSwap.length; i++) { 2328 var character = toSwap.charAt(i); 2329 text += isUpperCase(character) ? character.toLowerCase() : 2330 character.toUpperCase(); 2331 } 2332 } 2333 swapped.push(text); 2334 } 2335 cm.replaceSelections(swapped); 2336 if (args.shouldMoveCursor){ 2337 return newHead; 2338 } else if (!cm.state.vim.visualMode && args.linewise && ranges[0].anchor.line + 1 == ranges[0].head.line) { 2339 return motions.moveToFirstNonWhiteSpaceCharacter(cm, oldAnchor); 2340 } else if (args.linewise){ 2341 return oldAnchor; 2342 } else { 2343 return cursorMin(ranges[0].anchor, ranges[0].head); 2344 } 2345 }, 2346 yank: function(cm, args, ranges, oldAnchor) { 2347 var vim = cm.state.vim; 2348 var text = cm.getSelection(); 2349 var endPos = vim.visualMode 2350 ? cursorMin(vim.sel.anchor, vim.sel.head, ranges[0].head, ranges[0].anchor) 2351 : oldAnchor; 2352 vimGlobalState.registerController.pushText( 2353 args.registerName, 'yank', 2354 text, args.linewise, vim.visualBlock); 2355 return endPos; 2356 } 2357 }; 2358 2359 function defineOperator(name, fn) { 2360 operators[name] = fn; 2361 } 2362 2363 var actions = { 2364 jumpListWalk: function(cm, actionArgs, vim) { 2365 if (vim.visualMode) { 2366 return; 2367 } 2368 var repeat = actionArgs.repeat; 2369 var forward = actionArgs.forward; 2370 var jumpList = vimGlobalState.jumpList; 2371 2372 var mark = jumpList.move(cm, forward ? repeat : -repeat); 2373 var markPos = mark ? mark.find() : undefined; 2374 markPos = markPos ? markPos : cm.getCursor(); 2375 cm.setCursor(markPos); 2376 }, 2377 scroll: function(cm, actionArgs, vim) { 2378 if (vim.visualMode) { 2379 return; 2380 } 2381 var repeat = actionArgs.repeat || 1; 2382 var lineHeight = cm.defaultTextHeight(); 2383 var top = cm.getScrollInfo().top; 2384 var delta = lineHeight * repeat; 2385 var newPos = actionArgs.forward ? top + delta : top - delta; 2386 var cursor = copyCursor(cm.getCursor()); 2387 var cursorCoords = cm.charCoords(cursor, 'local'); 2388 if (actionArgs.forward) { 2389 if (newPos > cursorCoords.top) { 2390 cursor.line += (newPos - cursorCoords.top) / lineHeight; 2391 cursor.line = Math.ceil(cursor.line); 2392 cm.setCursor(cursor); 2393 cursorCoords = cm.charCoords(cursor, 'local'); 2394 cm.scrollTo(null, cursorCoords.top); 2395 } else { 2396 // Cursor stays within bounds. Just reposition the scroll window. 2397 cm.scrollTo(null, newPos); 2398 } 2399 } else { 2400 var newBottom = newPos + cm.getScrollInfo().clientHeight; 2401 if (newBottom < cursorCoords.bottom) { 2402 cursor.line -= (cursorCoords.bottom - newBottom) / lineHeight; 2403 cursor.line = Math.floor(cursor.line); 2404 cm.setCursor(cursor); 2405 cursorCoords = cm.charCoords(cursor, 'local'); 2406 cm.scrollTo( 2407 null, cursorCoords.bottom - cm.getScrollInfo().clientHeight); 2408 } else { 2409 // Cursor stays within bounds. Just reposition the scroll window. 2410 cm.scrollTo(null, newPos); 2411 } 2412 } 2413 }, 2414 scrollToCursor: function(cm, actionArgs) { 2415 var lineNum = cm.getCursor().line; 2416 var charCoords = cm.charCoords(new Pos(lineNum, 0), 'local'); 2417 var height = cm.getScrollInfo().clientHeight; 2418 var y = charCoords.top; 2419 var lineHeight = charCoords.bottom - y; 2420 switch (actionArgs.position) { 2421 case 'center': y = y - (height / 2) + lineHeight; 2422 break; 2423 case 'bottom': y = y - height + lineHeight; 2424 break; 2425 } 2426 cm.scrollTo(null, y); 2427 }, 2428 replayMacro: function(cm, actionArgs, vim) { 2429 var registerName = actionArgs.selectedCharacter; 2430 var repeat = actionArgs.repeat; 2431 var macroModeState = vimGlobalState.macroModeState; 2432 if (registerName == '@') { 2433 registerName = macroModeState.latestRegister; 2434 } else { 2435 macroModeState.latestRegister = registerName; 2436 } 2437 while(repeat--){ 2438 executeMacroRegister(cm, vim, macroModeState, registerName); 2439 } 2440 }, 2441 enterMacroRecordMode: function(cm, actionArgs) { 2442 var macroModeState = vimGlobalState.macroModeState; 2443 var registerName = actionArgs.selectedCharacter; 2444 if (vimGlobalState.registerController.isValidRegister(registerName)) { 2445 macroModeState.enterMacroRecordMode(cm, registerName); 2446 } 2447 }, 2448 toggleOverwrite: function(cm) { 2449 if (!cm.state.overwrite) { 2450 cm.toggleOverwrite(true); 2451 cm.setOption('keyMap', 'vim-replace'); 2452 CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"}); 2453 } else { 2454 cm.toggleOverwrite(false); 2455 cm.setOption('keyMap', 'vim-insert'); 2456 CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"}); 2457 } 2458 }, 2459 enterInsertMode: function(cm, actionArgs, vim) { 2460 if (cm.getOption('readOnly')) { return; } 2461 vim.insertMode = true; 2462 vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1; 2463 var insertAt = (actionArgs) ? actionArgs.insertAt : null; 2464 var sel = vim.sel; 2465 var head = actionArgs.head || cm.getCursor('head'); 2466 var height = cm.listSelections().length; 2467 if (insertAt == 'eol') { 2468 head = new Pos(head.line, lineLength(cm, head.line)); 2469 } else if (insertAt == 'bol') { 2470 head = new Pos(head.line, 0); 2471 } else if (insertAt == 'charAfter') { 2472 head = offsetCursor(head, 0, 1); 2473 } else if (insertAt == 'firstNonBlank') { 2474 head = motions.moveToFirstNonWhiteSpaceCharacter(cm, head); 2475 } else if (insertAt == 'startOfSelectedArea') { 2476 if (!vim.visualMode) 2477 return; 2478 if (!vim.visualBlock) { 2479 if (sel.head.line < sel.anchor.line) { 2480 head = sel.head; 2481 } else { 2482 head = new Pos(sel.anchor.line, 0); 2483 } 2484 } else { 2485 head = new Pos( 2486 Math.min(sel.head.line, sel.anchor.line), 2487 Math.min(sel.head.ch, sel.anchor.ch)); 2488 height = Math.abs(sel.head.line - sel.anchor.line) + 1; 2489 } 2490 } else if (insertAt == 'endOfSelectedArea') { 2491 if (!vim.visualMode) 2492 return; 2493 if (!vim.visualBlock) { 2494 if (sel.head.line >= sel.anchor.line) { 2495 head = offsetCursor(sel.head, 0, 1); 2496 } else { 2497 head = new Pos(sel.anchor.line, 0); 2498 } 2499 } else { 2500 head = new Pos( 2501 Math.min(sel.head.line, sel.anchor.line), 2502 Math.max(sel.head.ch, sel.anchor.ch) + 1); 2503 height = Math.abs(sel.head.line - sel.anchor.line) + 1; 2504 } 2505 } else if (insertAt == 'inplace') { 2506 if (vim.visualMode){ 2507 return; 2508 } 2509 } else if (insertAt == 'lastEdit') { 2510 head = getLastEditPos(cm) || head; 2511 } 2512 cm.setOption('disableInput', false); 2513 if (actionArgs && actionArgs.replace) { 2514 // Handle Replace-mode as a special case of insert mode. 2515 cm.toggleOverwrite(true); 2516 cm.setOption('keyMap', 'vim-replace'); 2517 CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"}); 2518 } else { 2519 cm.toggleOverwrite(false); 2520 cm.setOption('keyMap', 'vim-insert'); 2521 CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"}); 2522 } 2523 if (!vimGlobalState.macroModeState.isPlaying) { 2524 // Only record if not replaying. 2525 cm.on('change', onChange); 2526 CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); 2527 } 2528 if (vim.visualMode) { 2529 exitVisualMode(cm); 2530 } 2531 selectForInsert(cm, head, height); 2532 }, 2533 toggleVisualMode: function(cm, actionArgs, vim) { 2534 var repeat = actionArgs.repeat; 2535 var anchor = cm.getCursor(); 2536 var head; 2537 // TODO: The repeat should actually select number of characters/lines 2538 // equal to the repeat times the size of the previous visual 2539 // operation. 2540 if (!vim.visualMode) { 2541 // Entering visual mode 2542 vim.visualMode = true; 2543 vim.visualLine = !!actionArgs.linewise; 2544 vim.visualBlock = !!actionArgs.blockwise; 2545 head = clipCursorToContent( 2546 cm, new Pos(anchor.line, anchor.ch + repeat - 1)); 2547 vim.sel = { 2548 anchor: anchor, 2549 head: head 2550 }; 2551 CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""}); 2552 updateCmSelection(cm); 2553 updateMark(cm, vim, '<', cursorMin(anchor, head)); 2554 updateMark(cm, vim, '>', cursorMax(anchor, head)); 2555 } else if (vim.visualLine ^ actionArgs.linewise || 2556 vim.visualBlock ^ actionArgs.blockwise) { 2557 // Toggling between modes 2558 vim.visualLine = !!actionArgs.linewise; 2559 vim.visualBlock = !!actionArgs.blockwise; 2560 CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""}); 2561 updateCmSelection(cm); 2562 } else { 2563 exitVisualMode(cm); 2564 } 2565 }, 2566 reselectLastSelection: function(cm, _actionArgs, vim) { 2567 var lastSelection = vim.lastSelection; 2568 if (vim.visualMode) { 2569 updateLastSelection(cm, vim); 2570 } 2571 if (lastSelection) { 2572 var anchor = lastSelection.anchorMark.find(); 2573 var head = lastSelection.headMark.find(); 2574 if (!anchor || !head) { 2575 // If the marks have been destroyed due to edits, do nothing. 2576 return; 2577 } 2578 vim.sel = { 2579 anchor: anchor, 2580 head: head 2581 }; 2582 vim.visualMode = true; 2583 vim.visualLine = lastSelection.visualLine; 2584 vim.visualBlock = lastSelection.visualBlock; 2585 updateCmSelection(cm); 2586 updateMark(cm, vim, '<', cursorMin(anchor, head)); 2587 updateMark(cm, vim, '>', cursorMax(anchor, head)); 2588 CodeMirror.signal(cm, 'vim-mode-change', { 2589 mode: 'visual', 2590 subMode: vim.visualLine ? 'linewise' : 2591 vim.visualBlock ? 'blockwise' : ''}); 2592 } 2593 }, 2594 joinLines: function(cm, actionArgs, vim) { 2595 var curStart, curEnd; 2596 if (vim.visualMode) { 2597 curStart = cm.getCursor('anchor'); 2598 curEnd = cm.getCursor('head'); 2599 if (cursorIsBefore(curEnd, curStart)) { 2600 var tmp = curEnd; 2601 curEnd = curStart; 2602 curStart = tmp; 2603 } 2604 curEnd.ch = lineLength(cm, curEnd.line) - 1; 2605 } else { 2606 // Repeat is the number of lines to join. Minimum 2 lines. 2607 var repeat = Math.max(actionArgs.repeat, 2); 2608 curStart = cm.getCursor(); 2609 curEnd = clipCursorToContent(cm, new Pos(curStart.line + repeat - 1, 2610 Infinity)); 2611 } 2612 var finalCh = 0; 2613 for (var i = curStart.line; i < curEnd.line; i++) { 2614 finalCh = lineLength(cm, curStart.line); 2615 var tmp = new Pos(curStart.line + 1, 2616 lineLength(cm, curStart.line + 1)); 2617 var text = cm.getRange(curStart, tmp); 2618 text = actionArgs.keepSpaces 2619 ? text.replace(/\n\r?/g, '') 2620 : text.replace(/\n\s*/g, ' '); 2621 cm.replaceRange(text, curStart, tmp); 2622 } 2623 var curFinalPos = new Pos(curStart.line, finalCh); 2624 if (vim.visualMode) { 2625 exitVisualMode(cm, false); 2626 } 2627 cm.setCursor(curFinalPos); 2628 }, 2629 newLineAndEnterInsertMode: function(cm, actionArgs, vim) { 2630 vim.insertMode = true; 2631 var insertAt = copyCursor(cm.getCursor()); 2632 if (insertAt.line === cm.firstLine() && !actionArgs.after) { 2633 // Special case for inserting newline before start of document. 2634 cm.replaceRange('\n', new Pos(cm.firstLine(), 0)); 2635 cm.setCursor(cm.firstLine(), 0); 2636 } else { 2637 insertAt.line = (actionArgs.after) ? insertAt.line : 2638 insertAt.line - 1; 2639 insertAt.ch = lineLength(cm, insertAt.line); 2640 cm.setCursor(insertAt); 2641 var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment || 2642 CodeMirror.commands.newlineAndIndent; 2643 newlineFn(cm); 2644 } 2645 this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim); 2646 }, 2647 paste: function(cm, actionArgs, vim) { 2648 var cur = copyCursor(cm.getCursor()); 2649 var register = vimGlobalState.registerController.getRegister( 2650 actionArgs.registerName); 2651 var text = register.toString(); 2652 if (!text) { 2653 return; 2654 } 2655 if (actionArgs.matchIndent) { 2656 var tabSize = cm.getOption("tabSize"); 2657 // length that considers tabs and tabSize 2658 var whitespaceLength = function(str) { 2659 var tabs = (str.split("\t").length - 1); 2660 var spaces = (str.split(" ").length - 1); 2661 return tabs * tabSize + spaces * 1; 2662 }; 2663 var currentLine = cm.getLine(cm.getCursor().line); 2664 var indent = whitespaceLength(currentLine.match(/^\s*/)[0]); 2665 // chomp last newline b/c don't want it to match /^\s*/gm 2666 var chompedText = text.replace(/\n$/, ''); 2667 var wasChomped = text !== chompedText; 2668 var firstIndent = whitespaceLength(text.match(/^\s*/)[0]); 2669 var text = chompedText.replace(/^\s*/gm, function(wspace) { 2670 var newIndent = indent + (whitespaceLength(wspace) - firstIndent); 2671 if (newIndent < 0) { 2672 return ""; 2673 } 2674 else if (cm.getOption("indentWithTabs")) { 2675 var quotient = Math.floor(newIndent / tabSize); 2676 return Array(quotient + 1).join('\t'); 2677 } 2678 else { 2679 return Array(newIndent + 1).join(' '); 2680 } 2681 }); 2682 text += wasChomped ? "\n" : ""; 2683 } 2684 if (actionArgs.repeat > 1) { 2685 var text = Array(actionArgs.repeat + 1).join(text); 2686 } 2687 var linewise = register.linewise; 2688 var blockwise = register.blockwise; 2689 if (blockwise) { 2690 text = text.split('\n'); 2691 if (linewise) { 2692 text.pop(); 2693 } 2694 for (var i = 0; i < text.length; i++) { 2695 text[i] = (text[i] == '') ? ' ' : text[i]; 2696 } 2697 cur.ch += actionArgs.after ? 1 : 0; 2698 cur.ch = Math.min(lineLength(cm, cur.line), cur.ch); 2699 } else if (linewise) { 2700 if(vim.visualMode) { 2701 text = vim.visualLine ? text.slice(0, -1) : '\n' + text.slice(0, text.length - 1) + '\n'; 2702 } else if (actionArgs.after) { 2703 // Move the newline at the end to the start instead, and paste just 2704 // before the newline character of the line we are on right now. 2705 text = '\n' + text.slice(0, text.length - 1); 2706 cur.ch = lineLength(cm, cur.line); 2707 } else { 2708 cur.ch = 0; 2709 } 2710 } else { 2711 cur.ch += actionArgs.after ? 1 : 0; 2712 } 2713 var curPosFinal; 2714 var idx; 2715 if (vim.visualMode) { 2716 // save the pasted text for reselection if the need arises 2717 vim.lastPastedText = text; 2718 var lastSelectionCurEnd; 2719 var selectedArea = getSelectedAreaRange(cm, vim); 2720 var selectionStart = selectedArea[0]; 2721 var selectionEnd = selectedArea[1]; 2722 var selectedText = cm.getSelection(); 2723 var selections = cm.listSelections(); 2724 var emptyStrings = new Array(selections.length).join('1').split('1'); 2725 // save the curEnd marker before it get cleared due to cm.replaceRange. 2726 if (vim.lastSelection) { 2727 lastSelectionCurEnd = vim.lastSelection.headMark.find(); 2728 } 2729 // push the previously selected text to unnamed register 2730 vimGlobalState.registerController.unnamedRegister.setText(selectedText); 2731 if (blockwise) { 2732 // first delete the selected text 2733 cm.replaceSelections(emptyStrings); 2734 // Set new selections as per the block length of the yanked text 2735 selectionEnd = new Pos(selectionStart.line + text.length-1, selectionStart.ch); 2736 cm.setCursor(selectionStart); 2737 selectBlock(cm, selectionEnd); 2738 cm.replaceSelections(text); 2739 curPosFinal = selectionStart; 2740 } else if (vim.visualBlock) { 2741 cm.replaceSelections(emptyStrings); 2742 cm.setCursor(selectionStart); 2743 cm.replaceRange(text, selectionStart, selectionStart); 2744 curPosFinal = selectionStart; 2745 } else { 2746 cm.replaceRange(text, selectionStart, selectionEnd); 2747 curPosFinal = cm.posFromIndex(cm.indexFromPos(selectionStart) + text.length - 1); 2748 } 2749 // restore the the curEnd marker 2750 if(lastSelectionCurEnd) { 2751 vim.lastSelection.headMark = cm.setBookmark(lastSelectionCurEnd); 2752 } 2753 if (linewise) { 2754 curPosFinal.ch=0; 2755 } 2756 } else { 2757 if (blockwise) { 2758 cm.setCursor(cur); 2759 for (var i = 0; i < text.length; i++) { 2760 var line = cur.line+i; 2761 if (line > cm.lastLine()) { 2762 cm.replaceRange('\n', new Pos(line, 0)); 2763 } 2764 var lastCh = lineLength(cm, line); 2765 if (lastCh < cur.ch) { 2766 extendLineToColumn(cm, line, cur.ch); 2767 } 2768 } 2769 cm.setCursor(cur); 2770 selectBlock(cm, new Pos(cur.line + text.length-1, cur.ch)); 2771 cm.replaceSelections(text); 2772 curPosFinal = cur; 2773 } else { 2774 cm.replaceRange(text, cur); 2775 // Now fine tune the cursor to where we want it. 2776 if (linewise && actionArgs.after) { 2777 curPosFinal = new Pos( 2778 cur.line + 1, 2779 findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1))); 2780 } else if (linewise && !actionArgs.after) { 2781 curPosFinal = new Pos( 2782 cur.line, 2783 findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line))); 2784 } else if (!linewise && actionArgs.after) { 2785 idx = cm.indexFromPos(cur); 2786 curPosFinal = cm.posFromIndex(idx + text.length - 1); 2787 } else { 2788 idx = cm.indexFromPos(cur); 2789 curPosFinal = cm.posFromIndex(idx + text.length); 2790 } 2791 } 2792 } 2793 if (vim.visualMode) { 2794 exitVisualMode(cm, false); 2795 } 2796 cm.setCursor(curPosFinal); 2797 }, 2798 undo: function(cm, actionArgs) { 2799 cm.operation(function() { 2800 repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)(); 2801 cm.setCursor(cm.getCursor('anchor')); 2802 }); 2803 }, 2804 redo: function(cm, actionArgs) { 2805 repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)(); 2806 }, 2807 setRegister: function(_cm, actionArgs, vim) { 2808 vim.inputState.registerName = actionArgs.selectedCharacter; 2809 }, 2810 setMark: function(cm, actionArgs, vim) { 2811 var markName = actionArgs.selectedCharacter; 2812 updateMark(cm, vim, markName, cm.getCursor()); 2813 }, 2814 replace: function(cm, actionArgs, vim) { 2815 var replaceWith = actionArgs.selectedCharacter; 2816 var curStart = cm.getCursor(); 2817 var replaceTo; 2818 var curEnd; 2819 var selections = cm.listSelections(); 2820 if (vim.visualMode) { 2821 curStart = cm.getCursor('start'); 2822 curEnd = cm.getCursor('end'); 2823 } else { 2824 var line = cm.getLine(curStart.line); 2825 replaceTo = curStart.ch + actionArgs.repeat; 2826 if (replaceTo > line.length) { 2827 replaceTo=line.length; 2828 } 2829 curEnd = new Pos(curStart.line, replaceTo); 2830 } 2831 if (replaceWith=='\n') { 2832 if (!vim.visualMode) cm.replaceRange('', curStart, curEnd); 2833 // special case, where vim help says to replace by just one line-break 2834 (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm); 2835 } else { 2836 var replaceWithStr = cm.getRange(curStart, curEnd); 2837 //replace all characters in range by selected, but keep linebreaks 2838 replaceWithStr = replaceWithStr.replace(/[^\n]/g, replaceWith); 2839 if (vim.visualBlock) { 2840 // Tabs are split in visua block before replacing 2841 var spaces = new Array(cm.getOption("tabSize")+1).join(' '); 2842 replaceWithStr = cm.getSelection(); 2843 replaceWithStr = replaceWithStr.replace(/\t/g, spaces).replace(/[^\n]/g, replaceWith).split('\n'); 2844 cm.replaceSelections(replaceWithStr); 2845 } else { 2846 cm.replaceRange(replaceWithStr, curStart, curEnd); 2847 } 2848 if (vim.visualMode) { 2849 curStart = cursorIsBefore(selections[0].anchor, selections[0].head) ? 2850 selections[0].anchor : selections[0].head; 2851 cm.setCursor(curStart); 2852 exitVisualMode(cm, false); 2853 } else { 2854 cm.setCursor(offsetCursor(curEnd, 0, -1)); 2855 } 2856 } 2857 }, 2858 incrementNumberToken: function(cm, actionArgs) { 2859 var cur = cm.getCursor(); 2860 var lineStr = cm.getLine(cur.line); 2861 var re = /(-?)(?:(0x)([\da-f]+)|(0b|0|)(\d+))/gi; 2862 var match; 2863 var start; 2864 var end; 2865 var numberStr; 2866 while ((match = re.exec(lineStr)) !== null) { 2867 start = match.index; 2868 end = start + match[0].length; 2869 if (cur.ch < end)break; 2870 } 2871 if (!actionArgs.backtrack && (end <= cur.ch))return; 2872 if (match) { 2873 var baseStr = match[2] || match[4] 2874 var digits = match[3] || match[5] 2875 var increment = actionArgs.increase ? 1 : -1; 2876 var base = {'0b': 2, '0': 8, '': 10, '0x': 16}[baseStr.toLowerCase()]; 2877 var number = parseInt(match[1] + digits, base) + (increment * actionArgs.repeat); 2878 numberStr = number.toString(base); 2879 var zeroPadding = baseStr ? new Array(digits.length - numberStr.length + 1 + match[1].length).join('0') : '' 2880 if (numberStr.charAt(0) === '-') { 2881 numberStr = '-' + baseStr + zeroPadding + numberStr.substr(1); 2882 } else { 2883 numberStr = baseStr + zeroPadding + numberStr; 2884 } 2885 var from = new Pos(cur.line, start); 2886 var to = new Pos(cur.line, end); 2887 cm.replaceRange(numberStr, from, to); 2888 } else { 2889 return; 2890 } 2891 cm.setCursor(new Pos(cur.line, start + numberStr.length - 1)); 2892 }, 2893 repeatLastEdit: function(cm, actionArgs, vim) { 2894 var lastEditInputState = vim.lastEditInputState; 2895 if (!lastEditInputState) { return; } 2896 var repeat = actionArgs.repeat; 2897 if (repeat && actionArgs.repeatIsExplicit) { 2898 vim.lastEditInputState.repeatOverride = repeat; 2899 } else { 2900 repeat = vim.lastEditInputState.repeatOverride || repeat; 2901 } 2902 repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */); 2903 }, 2904 indent: function(cm, actionArgs) { 2905 cm.indentLine(cm.getCursor().line, actionArgs.indentRight); 2906 }, 2907 exitInsertMode: exitInsertMode 2908 }; 2909 2910 function defineAction(name, fn) { 2911 actions[name] = fn; 2912 } 2913 2914 /* 2915 * Below are miscellaneous utility functions used by vim.js 2916 */ 2917 2918 /** 2919 * Clips cursor to ensure that line is within the buffer's range 2920 * If includeLineBreak is true, then allow cur.ch == lineLength. 2921 */ 2922 function clipCursorToContent(cm, cur) { 2923 var vim = cm.state.vim; 2924 var includeLineBreak = vim.insertMode || vim.visualMode; 2925 var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() ); 2926 var maxCh = lineLength(cm, line) - 1 + !!includeLineBreak; 2927 var ch = Math.min(Math.max(0, cur.ch), maxCh); 2928 return new Pos(line, ch); 2929 } 2930 function copyArgs(args) { 2931 var ret = {}; 2932 for (var prop in args) { 2933 if (args.hasOwnProperty(prop)) { 2934 ret[prop] = args[prop]; 2935 } 2936 } 2937 return ret; 2938 } 2939 function offsetCursor(cur, offsetLine, offsetCh) { 2940 if (typeof offsetLine === 'object') { 2941 offsetCh = offsetLine.ch; 2942 offsetLine = offsetLine.line; 2943 } 2944 return new Pos(cur.line + offsetLine, cur.ch + offsetCh); 2945 } 2946 function commandMatches(keys, keyMap, context, inputState) { 2947 // Partial matches are not applied. They inform the key handler 2948 // that the current key sequence is a subsequence of a valid key 2949 // sequence, so that the key buffer is not cleared. 2950 var match, partial = [], full = []; 2951 for (var i = 0; i < keyMap.length; i++) { 2952 var command = keyMap[i]; 2953 if (context == 'insert' && command.context != 'insert' || 2954 command.context && command.context != context || 2955 inputState.operator && command.type == 'action' || 2956 !(match = commandMatch(keys, command.keys))) { continue; } 2957 if (match == 'partial') { partial.push(command); } 2958 if (match == 'full') { full.push(command); } 2959 } 2960 return { 2961 partial: partial.length && partial, 2962 full: full.length && full 2963 }; 2964 } 2965 function commandMatch(pressed, mapped) { 2966 if (mapped.slice(-11) == '<character>') { 2967 // Last character matches anything. 2968 var prefixLen = mapped.length - 11; 2969 var pressedPrefix = pressed.slice(0, prefixLen); 2970 var mappedPrefix = mapped.slice(0, prefixLen); 2971 return pressedPrefix == mappedPrefix && pressed.length > prefixLen ? 'full' : 2972 mappedPrefix.indexOf(pressedPrefix) == 0 ? 'partial' : false; 2973 } else { 2974 return pressed == mapped ? 'full' : 2975 mapped.indexOf(pressed) == 0 ? 'partial' : false; 2976 } 2977 } 2978 function lastChar(keys) { 2979 var match = /^.*(<[^>]+>)$/.exec(keys); 2980 var selectedCharacter = match ? match[1] : keys.slice(-1); 2981 if (selectedCharacter.length > 1){ 2982 switch(selectedCharacter){ 2983 case '<CR>': 2984 selectedCharacter='\n'; 2985 break; 2986 case '<Space>': 2987 selectedCharacter=' '; 2988 break; 2989 default: 2990 selectedCharacter=''; 2991 break; 2992 } 2993 } 2994 return selectedCharacter; 2995 } 2996 function repeatFn(cm, fn, repeat) { 2997 return function() { 2998 for (var i = 0; i < repeat; i++) { 2999 fn(cm); 3000 } 3001 }; 3002 } 3003 function copyCursor(cur) { 3004 return new Pos(cur.line, cur.ch); 3005 } 3006 function cursorEqual(cur1, cur2) { 3007 return cur1.ch == cur2.ch && cur1.line == cur2.line; 3008 } 3009 function cursorIsBefore(cur1, cur2) { 3010 if (cur1.line < cur2.line) { 3011 return true; 3012 } 3013 if (cur1.line == cur2.line && cur1.ch < cur2.ch) { 3014 return true; 3015 } 3016 return false; 3017 } 3018 function cursorMin(cur1, cur2) { 3019 if (arguments.length > 2) { 3020 cur2 = cursorMin.apply(undefined, Array.prototype.slice.call(arguments, 1)); 3021 } 3022 return cursorIsBefore(cur1, cur2) ? cur1 : cur2; 3023 } 3024 function cursorMax(cur1, cur2) { 3025 if (arguments.length > 2) { 3026 cur2 = cursorMax.apply(undefined, Array.prototype.slice.call(arguments, 1)); 3027 } 3028 return cursorIsBefore(cur1, cur2) ? cur2 : cur1; 3029 } 3030 function cursorIsBetween(cur1, cur2, cur3) { 3031 // returns true if cur2 is between cur1 and cur3. 3032 var cur1before2 = cursorIsBefore(cur1, cur2); 3033 var cur2before3 = cursorIsBefore(cur2, cur3); 3034 return cur1before2 && cur2before3; 3035 } 3036 function lineLength(cm, lineNum) { 3037 return cm.getLine(lineNum).length; 3038 } 3039 function trim(s) { 3040 if (s.trim) { 3041 return s.trim(); 3042 } 3043 return s.replace(/^\s+|\s+$/g, ''); 3044 } 3045 function escapeRegex(s) { 3046 return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1'); 3047 } 3048 function extendLineToColumn(cm, lineNum, column) { 3049 var endCh = lineLength(cm, lineNum); 3050 var spaces = new Array(column-endCh+1).join(' '); 3051 cm.setCursor(new Pos(lineNum, endCh)); 3052 cm.replaceRange(spaces, cm.getCursor()); 3053 } 3054 // This functions selects a rectangular block 3055 // of text with selectionEnd as any of its corner 3056 // Height of block: 3057 // Difference in selectionEnd.line and first/last selection.line 3058 // Width of the block: 3059 // Distance between selectionEnd.ch and any(first considered here) selection.ch 3060 function selectBlock(cm, selectionEnd) { 3061 var selections = [], ranges = cm.listSelections(); 3062 var head = copyCursor(cm.clipPos(selectionEnd)); 3063 var isClipped = !cursorEqual(selectionEnd, head); 3064 var curHead = cm.getCursor('head'); 3065 var primIndex = getIndex(ranges, curHead); 3066 var wasClipped = cursorEqual(ranges[primIndex].head, ranges[primIndex].anchor); 3067 var max = ranges.length - 1; 3068 var index = max - primIndex > primIndex ? max : 0; 3069 var base = ranges[index].anchor; 3070 3071 var firstLine = Math.min(base.line, head.line); 3072 var lastLine = Math.max(base.line, head.line); 3073 var baseCh = base.ch, headCh = head.ch; 3074 3075 var dir = ranges[index].head.ch - baseCh; 3076 var newDir = headCh - baseCh; 3077 if (dir > 0 && newDir <= 0) { 3078 baseCh++; 3079 if (!isClipped) { headCh--; } 3080 } else if (dir < 0 && newDir >= 0) { 3081 baseCh--; 3082 if (!wasClipped) { headCh++; } 3083 } else if (dir < 0 && newDir == -1) { 3084 baseCh--; 3085 headCh++; 3086 } 3087 for (var line = firstLine; line <= lastLine; line++) { 3088 var range = {anchor: new Pos(line, baseCh), head: new Pos(line, headCh)}; 3089 selections.push(range); 3090 } 3091 cm.setSelections(selections); 3092 selectionEnd.ch = headCh; 3093 base.ch = baseCh; 3094 return base; 3095 } 3096 function selectForInsert(cm, head, height) { 3097 var sel = []; 3098 for (var i = 0; i < height; i++) { 3099 var lineHead = offsetCursor(head, i, 0); 3100 sel.push({anchor: lineHead, head: lineHead}); 3101 } 3102 cm.setSelections(sel, 0); 3103 } 3104 // getIndex returns the index of the cursor in the selections. 3105 function getIndex(ranges, cursor, end) { 3106 for (var i = 0; i < ranges.length; i++) { 3107 var atAnchor = end != 'head' && cursorEqual(ranges[i].anchor, cursor); 3108 var atHead = end != 'anchor' && cursorEqual(ranges[i].head, cursor); 3109 if (atAnchor || atHead) { 3110 return i; 3111 } 3112 } 3113 return -1; 3114 } 3115 function getSelectedAreaRange(cm, vim) { 3116 var lastSelection = vim.lastSelection; 3117 var getCurrentSelectedAreaRange = function() { 3118 var selections = cm.listSelections(); 3119 var start = selections[0]; 3120 var end = selections[selections.length-1]; 3121 var selectionStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head; 3122 var selectionEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor; 3123 return [selectionStart, selectionEnd]; 3124 }; 3125 var getLastSelectedAreaRange = function() { 3126 var selectionStart = cm.getCursor(); 3127 var selectionEnd = cm.getCursor(); 3128 var block = lastSelection.visualBlock; 3129 if (block) { 3130 var width = block.width; 3131 var height = block.height; 3132 selectionEnd = new Pos(selectionStart.line + height, selectionStart.ch + width); 3133 var selections = []; 3134 // selectBlock creates a 'proper' rectangular block. 3135 // We do not want that in all cases, so we manually set selections. 3136 for (var i = selectionStart.line; i < selectionEnd.line; i++) { 3137 var anchor = new Pos(i, selectionStart.ch); 3138 var head = new Pos(i, selectionEnd.ch); 3139 var range = {anchor: anchor, head: head}; 3140 selections.push(range); 3141 } 3142 cm.setSelections(selections); 3143 } else { 3144 var start = lastSelection.anchorMark.find(); 3145 var end = lastSelection.headMark.find(); 3146 var line = end.line - start.line; 3147 var ch = end.ch - start.ch; 3148 selectionEnd = {line: selectionEnd.line + line, ch: line ? selectionEnd.ch : ch + selectionEnd.ch}; 3149 if (lastSelection.visualLine) { 3150 selectionStart = new Pos(selectionStart.line, 0); 3151 selectionEnd = new Pos(selectionEnd.line, lineLength(cm, selectionEnd.line)); 3152 } 3153 cm.setSelection(selectionStart, selectionEnd); 3154 } 3155 return [selectionStart, selectionEnd]; 3156 }; 3157 if (!vim.visualMode) { 3158 // In case of replaying the action. 3159 return getLastSelectedAreaRange(); 3160 } else { 3161 return getCurrentSelectedAreaRange(); 3162 } 3163 } 3164 // Updates the previous selection with the current selection's values. This 3165 // should only be called in visual mode. 3166 function updateLastSelection(cm, vim) { 3167 var anchor = vim.sel.anchor; 3168 var head = vim.sel.head; 3169 // To accommodate the effect of lastPastedText in the last selection 3170 if (vim.lastPastedText) { 3171 head = cm.posFromIndex(cm.indexFromPos(anchor) + vim.lastPastedText.length); 3172 vim.lastPastedText = null; 3173 } 3174 vim.lastSelection = {'anchorMark': cm.setBookmark(anchor), 3175 'headMark': cm.setBookmark(head), 3176 'anchor': copyCursor(anchor), 3177 'head': copyCursor(head), 3178 'visualMode': vim.visualMode, 3179 'visualLine': vim.visualLine, 3180 'visualBlock': vim.visualBlock}; 3181 } 3182 function expandSelection(cm, start, end) { 3183 var sel = cm.state.vim.sel; 3184 var head = sel.head; 3185 var anchor = sel.anchor; 3186 var tmp; 3187 if (cursorIsBefore(end, start)) { 3188 tmp = end; 3189 end = start; 3190 start = tmp; 3191 } 3192 if (cursorIsBefore(head, anchor)) { 3193 head = cursorMin(start, head); 3194 anchor = cursorMax(anchor, end); 3195 } else { 3196 anchor = cursorMin(start, anchor); 3197 head = cursorMax(head, end); 3198 head = offsetCursor(head, 0, -1); 3199 if (head.ch == -1 && head.line != cm.firstLine()) { 3200 head = new Pos(head.line - 1, lineLength(cm, head.line - 1)); 3201 } 3202 } 3203 return [anchor, head]; 3204 } 3205 /** 3206 * Updates the CodeMirror selection to match the provided vim selection. 3207 * If no arguments are given, it uses the current vim selection state. 3208 */ 3209 function updateCmSelection(cm, sel, mode) { 3210 var vim = cm.state.vim; 3211 sel = sel || vim.sel; 3212 var mode = mode || 3213 vim.visualLine ? 'line' : vim.visualBlock ? 'block' : 'char'; 3214 var cmSel = makeCmSelection(cm, sel, mode); 3215 cm.setSelections(cmSel.ranges, cmSel.primary); 3216 } 3217 function makeCmSelection(cm, sel, mode, exclusive) { 3218 var head = copyCursor(sel.head); 3219 var anchor = copyCursor(sel.anchor); 3220 if (mode == 'char') { 3221 var headOffset = !exclusive && !cursorIsBefore(sel.head, sel.anchor) ? 1 : 0; 3222 var anchorOffset = cursorIsBefore(sel.head, sel.anchor) ? 1 : 0; 3223 head = offsetCursor(sel.head, 0, headOffset); 3224 anchor = offsetCursor(sel.anchor, 0, anchorOffset); 3225 return { 3226 ranges: [{anchor: anchor, head: head}], 3227 primary: 0 3228 }; 3229 } else if (mode == 'line') { 3230 if (!cursorIsBefore(sel.head, sel.anchor)) { 3231 anchor.ch = 0; 3232 3233 var lastLine = cm.lastLine(); 3234 if (head.line > lastLine) { 3235 head.line = lastLine; 3236 } 3237 head.ch = lineLength(cm, head.line); 3238 } else { 3239 head.ch = 0; 3240 anchor.ch = lineLength(cm, anchor.line); 3241 } 3242 return { 3243 ranges: [{anchor: anchor, head: head}], 3244 primary: 0 3245 }; 3246 } else if (mode == 'block') { 3247 var top = Math.min(anchor.line, head.line), 3248 fromCh = anchor.ch, 3249 bottom = Math.max(anchor.line, head.line), 3250 toCh = head.ch; 3251 if (fromCh < toCh) { toCh += 1 } 3252 else { fromCh += 1 }; 3253 var height = bottom - top + 1; 3254 var primary = head.line == top ? 0 : height - 1; 3255 var ranges = []; 3256 for (var i = 0; i < height; i++) { 3257 ranges.push({ 3258 anchor: new Pos(top + i, fromCh), 3259 head: new Pos(top + i, toCh) 3260 }); 3261 } 3262 return { 3263 ranges: ranges, 3264 primary: primary 3265 }; 3266 } 3267 } 3268 function getHead(cm) { 3269 var cur = cm.getCursor('head'); 3270 if (cm.getSelection().length == 1) { 3271 // Small corner case when only 1 character is selected. The "real" 3272 // head is the left of head and anchor. 3273 cur = cursorMin(cur, cm.getCursor('anchor')); 3274 } 3275 return cur; 3276 } 3277 3278 /** 3279 * If moveHead is set to false, the CodeMirror selection will not be 3280 * touched. The caller assumes the responsibility of putting the cursor 3281 * in the right place. 3282 */ 3283 function exitVisualMode(cm, moveHead) { 3284 var vim = cm.state.vim; 3285 if (moveHead !== false) { 3286 cm.setCursor(clipCursorToContent(cm, vim.sel.head)); 3287 } 3288 updateLastSelection(cm, vim); 3289 vim.visualMode = false; 3290 vim.visualLine = false; 3291 vim.visualBlock = false; 3292 if (!vim.insertMode) CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); 3293 } 3294 3295 // Remove any trailing newlines from the selection. For 3296 // example, with the caret at the start of the last word on the line, 3297 // 'dw' should word, but not the newline, while 'w' should advance the 3298 // caret to the first character of the next line. 3299 function clipToLine(cm, curStart, curEnd) { 3300 var selection = cm.getRange(curStart, curEnd); 3301 // Only clip if the selection ends with trailing newline + whitespace 3302 if (/\n\s*$/.test(selection)) { 3303 var lines = selection.split('\n'); 3304 // We know this is all whitespace. 3305 lines.pop(); 3306 3307 // Cases: 3308 // 1. Last word is an empty line - do not clip the trailing '\n' 3309 // 2. Last word is not an empty line - clip the trailing '\n' 3310 var line; 3311 // Find the line containing the last word, and clip all whitespace up 3312 // to it. 3313 for (var line = lines.pop(); lines.length > 0 && line && isWhiteSpaceString(line); line = lines.pop()) { 3314 curEnd.line--; 3315 curEnd.ch = 0; 3316 } 3317 // If the last word is not an empty line, clip an additional newline 3318 if (line) { 3319 curEnd.line--; 3320 curEnd.ch = lineLength(cm, curEnd.line); 3321 } else { 3322 curEnd.ch = 0; 3323 } 3324 } 3325 } 3326 3327 // Expand the selection to line ends. 3328 function expandSelectionToLine(_cm, curStart, curEnd) { 3329 curStart.ch = 0; 3330 curEnd.ch = 0; 3331 curEnd.line++; 3332 } 3333 3334 function findFirstNonWhiteSpaceCharacter(text) { 3335 if (!text) { 3336 return 0; 3337 } 3338 var firstNonWS = text.search(/\S/); 3339 return firstNonWS == -1 ? text.length : firstNonWS; 3340 } 3341 3342 function expandWordUnderCursor(cm, inclusive, _forward, bigWord, noSymbol) { 3343 var cur = getHead(cm); 3344 var line = cm.getLine(cur.line); 3345 var idx = cur.ch; 3346 3347 // Seek to first word or non-whitespace character, depending on if 3348 // noSymbol is true. 3349 var test = noSymbol ? wordCharTest[0] : bigWordCharTest [0]; 3350 while (!test(line.charAt(idx))) { 3351 idx++; 3352 if (idx >= line.length) { return null; } 3353 } 3354 3355 if (bigWord) { 3356 test = bigWordCharTest[0]; 3357 } else { 3358 test = wordCharTest[0]; 3359 if (!test(line.charAt(idx))) { 3360 test = wordCharTest[1]; 3361 } 3362 } 3363 3364 var end = idx, start = idx; 3365 while (test(line.charAt(end)) && end < line.length) { end++; } 3366 while (test(line.charAt(start)) && start >= 0) { start--; } 3367 start++; 3368 3369 if (inclusive) { 3370 // If present, include all whitespace after word. 3371 // Otherwise, include all whitespace before word, except indentation. 3372 var wordEnd = end; 3373 while (/\s/.test(line.charAt(end)) && end < line.length) { end++; } 3374 if (wordEnd == end) { 3375 var wordStart = start; 3376 while (/\s/.test(line.charAt(start - 1)) && start > 0) { start--; } 3377 if (!start) { start = wordStart; } 3378 } 3379 } 3380 return { start: new Pos(cur.line, start), end: new Pos(cur.line, end) }; 3381 } 3382 3383 /** 3384 * Depends on the following: 3385 * 3386 * - editor mode should be htmlmixedmode / xml 3387 * - mode/xml/xml.js should be loaded 3388 * - addon/fold/xml-fold.js should be loaded 3389 * 3390 * If any of the above requirements are not true, this function noops. 3391 * 3392 * This is _NOT_ a 100% accurate implementation of vim tag text objects. 3393 * The following caveats apply (based off cursory testing, I'm sure there 3394 * are other discrepancies): 3395 * 3396 * - Does not work inside comments: 3397 * ``` 3398 * <!-- <div>broken</div> --> 3399 * ``` 3400 * - Does not work when tags have different cases: 3401 * ``` 3402 * <div>broken</DIV> 3403 * ``` 3404 * - Does not work when cursor is inside a broken tag: 3405 * ``` 3406 * <div><brok><en></div> 3407 * ``` 3408 */ 3409 function expandTagUnderCursor(cm, head, inclusive) { 3410 var cur = head; 3411 if (!CodeMirror.findMatchingTag || !CodeMirror.findEnclosingTag) { 3412 return { start: cur, end: cur }; 3413 } 3414 3415 var tags = CodeMirror.findMatchingTag(cm, head) || CodeMirror.findEnclosingTag(cm, head); 3416 if (!tags || !tags.open || !tags.close) { 3417 return { start: cur, end: cur }; 3418 } 3419 3420 if (inclusive) { 3421 return { start: tags.open.from, end: tags.close.to }; 3422 } 3423 return { start: tags.open.to, end: tags.close.from }; 3424 } 3425 3426 function recordJumpPosition(cm, oldCur, newCur) { 3427 if (!cursorEqual(oldCur, newCur)) { 3428 vimGlobalState.jumpList.add(cm, oldCur, newCur); 3429 } 3430 } 3431 3432 function recordLastCharacterSearch(increment, args) { 3433 vimGlobalState.lastCharacterSearch.increment = increment; 3434 vimGlobalState.lastCharacterSearch.forward = args.forward; 3435 vimGlobalState.lastCharacterSearch.selectedCharacter = args.selectedCharacter; 3436 } 3437 3438 var symbolToMode = { 3439 '(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket', 3440 '[': 'section', ']': 'section', 3441 '*': 'comment', '/': 'comment', 3442 'm': 'method', 'M': 'method', 3443 '#': 'preprocess' 3444 }; 3445 var findSymbolModes = { 3446 bracket: { 3447 isComplete: function(state) { 3448 if (state.nextCh === state.symb) { 3449 state.depth++; 3450 if (state.depth >= 1)return true; 3451 } else if (state.nextCh === state.reverseSymb) { 3452 state.depth--; 3453 } 3454 return false; 3455 } 3456 }, 3457 section: { 3458 init: function(state) { 3459 state.curMoveThrough = true; 3460 state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}'; 3461 }, 3462 isComplete: function(state) { 3463 return state.index === 0 && state.nextCh === state.symb; 3464 } 3465 }, 3466 comment: { 3467 isComplete: function(state) { 3468 var found = state.lastCh === '*' && state.nextCh === '/'; 3469 state.lastCh = state.nextCh; 3470 return found; 3471 } 3472 }, 3473 // TODO: The original Vim implementation only operates on level 1 and 2. 3474 // The current implementation doesn't check for code block level and 3475 // therefore it operates on any levels. 3476 method: { 3477 init: function(state) { 3478 state.symb = (state.symb === 'm' ? '{' : '}'); 3479 state.reverseSymb = state.symb === '{' ? '}' : '{'; 3480 }, 3481 isComplete: function(state) { 3482 if (state.nextCh === state.symb)return true; 3483 return false; 3484 } 3485 }, 3486 preprocess: { 3487 init: function(state) { 3488 state.index = 0; 3489 }, 3490 isComplete: function(state) { 3491 if (state.nextCh === '#') { 3492 var token = state.lineText.match(/^#(\w+)/)[1]; 3493 if (token === 'endif') { 3494 if (state.forward && state.depth === 0) { 3495 return true; 3496 } 3497 state.depth++; 3498 } else if (token === 'if') { 3499 if (!state.forward && state.depth === 0) { 3500 return true; 3501 } 3502 state.depth--; 3503 } 3504 if (token === 'else' && state.depth === 0)return true; 3505 } 3506 return false; 3507 } 3508 } 3509 }; 3510 function findSymbol(cm, repeat, forward, symb) { 3511 var cur = copyCursor(cm.getCursor()); 3512 var increment = forward ? 1 : -1; 3513 var endLine = forward ? cm.lineCount() : -1; 3514 var curCh = cur.ch; 3515 var line = cur.line; 3516 var lineText = cm.getLine(line); 3517 var state = { 3518 lineText: lineText, 3519 nextCh: lineText.charAt(curCh), 3520 lastCh: null, 3521 index: curCh, 3522 symb: symb, 3523 reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb], 3524 forward: forward, 3525 depth: 0, 3526 curMoveThrough: false 3527 }; 3528 var mode = symbolToMode[symb]; 3529 if (!mode)return cur; 3530 var init = findSymbolModes[mode].init; 3531 var isComplete = findSymbolModes[mode].isComplete; 3532 if (init) { init(state); } 3533 while (line !== endLine && repeat) { 3534 state.index += increment; 3535 state.nextCh = state.lineText.charAt(state.index); 3536 if (!state.nextCh) { 3537 line += increment; 3538 state.lineText = cm.getLine(line) || ''; 3539 if (increment > 0) { 3540 state.index = 0; 3541 } else { 3542 var lineLen = state.lineText.length; 3543 state.index = (lineLen > 0) ? (lineLen-1) : 0; 3544 } 3545 state.nextCh = state.lineText.charAt(state.index); 3546 } 3547 if (isComplete(state)) { 3548 cur.line = line; 3549 cur.ch = state.index; 3550 repeat--; 3551 } 3552 } 3553 if (state.nextCh || state.curMoveThrough) { 3554 return new Pos(line, state.index); 3555 } 3556 return cur; 3557 } 3558 3559 /* 3560 * Returns the boundaries of the next word. If the cursor in the middle of 3561 * the word, then returns the boundaries of the current word, starting at 3562 * the cursor. If the cursor is at the start/end of a word, and we are going 3563 * forward/backward, respectively, find the boundaries of the next word. 3564 * 3565 * @param {CodeMirror} cm CodeMirror object. 3566 * @param {Cursor} cur The cursor position. 3567 * @param {boolean} forward True to search forward. False to search 3568 * backward. 3569 * @param {boolean} bigWord True if punctuation count as part of the word. 3570 * False if only [a-zA-Z0-9] characters count as part of the word. 3571 * @param {boolean} emptyLineIsWord True if empty lines should be treated 3572 * as words. 3573 * @return {Object{from:number, to:number, line: number}} The boundaries of 3574 * the word, or null if there are no more words. 3575 */ 3576 function findWord(cm, cur, forward, bigWord, emptyLineIsWord) { 3577 var lineNum = cur.line; 3578 var pos = cur.ch; 3579 var line = cm.getLine(lineNum); 3580 var dir = forward ? 1 : -1; 3581 var charTests = bigWord ? bigWordCharTest: wordCharTest; 3582 3583 if (emptyLineIsWord && line == '') { 3584 lineNum += dir; 3585 line = cm.getLine(lineNum); 3586 if (!isLine(cm, lineNum)) { 3587 return null; 3588 } 3589 pos = (forward) ? 0 : line.length; 3590 } 3591 3592 while (true) { 3593 if (emptyLineIsWord && line == '') { 3594 return { from: 0, to: 0, line: lineNum }; 3595 } 3596 var stop = (dir > 0) ? line.length : -1; 3597 var wordStart = stop, wordEnd = stop; 3598 // Find bounds of next word. 3599 while (pos != stop) { 3600 var foundWord = false; 3601 for (var i = 0; i < charTests.length && !foundWord; ++i) { 3602 if (charTests[i](line.charAt(pos))) { 3603 wordStart = pos; 3604 // Advance to end of word. 3605 while (pos != stop && charTests[i](line.charAt(pos))) { 3606 pos += dir; 3607 } 3608 wordEnd = pos; 3609 foundWord = wordStart != wordEnd; 3610 if (wordStart == cur.ch && lineNum == cur.line && 3611 wordEnd == wordStart + dir) { 3612 // We started at the end of a word. Find the next one. 3613 continue; 3614 } else { 3615 return { 3616 from: Math.min(wordStart, wordEnd + 1), 3617 to: Math.max(wordStart, wordEnd), 3618 line: lineNum }; 3619 } 3620 } 3621 } 3622 if (!foundWord) { 3623 pos += dir; 3624 } 3625 } 3626 // Advance to next/prev line. 3627 lineNum += dir; 3628 if (!isLine(cm, lineNum)) { 3629 return null; 3630 } 3631 line = cm.getLine(lineNum); 3632 pos = (dir > 0) ? 0 : line.length; 3633 } 3634 } 3635 3636 /** 3637 * @param {CodeMirror} cm CodeMirror object. 3638 * @param {Pos} cur The position to start from. 3639 * @param {int} repeat Number of words to move past. 3640 * @param {boolean} forward True to search forward. False to search 3641 * backward. 3642 * @param {boolean} wordEnd True to move to end of word. False to move to 3643 * beginning of word. 3644 * @param {boolean} bigWord True if punctuation count as part of the word. 3645 * False if only alphabet characters count as part of the word. 3646 * @return {Cursor} The position the cursor should move to. 3647 */ 3648 function moveToWord(cm, cur, repeat, forward, wordEnd, bigWord) { 3649 var curStart = copyCursor(cur); 3650 var words = []; 3651 if (forward && !wordEnd || !forward && wordEnd) { 3652 repeat++; 3653 } 3654 // For 'e', empty lines are not considered words, go figure. 3655 var emptyLineIsWord = !(forward && wordEnd); 3656 for (var i = 0; i < repeat; i++) { 3657 var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord); 3658 if (!word) { 3659 var eodCh = lineLength(cm, cm.lastLine()); 3660 words.push(forward 3661 ? {line: cm.lastLine(), from: eodCh, to: eodCh} 3662 : {line: 0, from: 0, to: 0}); 3663 break; 3664 } 3665 words.push(word); 3666 cur = new Pos(word.line, forward ? (word.to - 1) : word.from); 3667 } 3668 var shortCircuit = words.length != repeat; 3669 var firstWord = words[0]; 3670 var lastWord = words.pop(); 3671 if (forward && !wordEnd) { 3672 // w 3673 if (!shortCircuit && (firstWord.from != curStart.ch || firstWord.line != curStart.line)) { 3674 // We did not start in the middle of a word. Discard the extra word at the end. 3675 lastWord = words.pop(); 3676 } 3677 return new Pos(lastWord.line, lastWord.from); 3678 } else if (forward && wordEnd) { 3679 return new Pos(lastWord.line, lastWord.to - 1); 3680 } else if (!forward && wordEnd) { 3681 // ge 3682 if (!shortCircuit && (firstWord.to != curStart.ch || firstWord.line != curStart.line)) { 3683 // We did not start in the middle of a word. Discard the extra word at the end. 3684 lastWord = words.pop(); 3685 } 3686 return new Pos(lastWord.line, lastWord.to); 3687 } else { 3688 // b 3689 return new Pos(lastWord.line, lastWord.from); 3690 } 3691 } 3692 3693 function moveToEol(cm, head, motionArgs, vim, keepHPos) { 3694 var cur = head; 3695 var retval= new Pos(cur.line + motionArgs.repeat - 1, Infinity); 3696 var end=cm.clipPos(retval); 3697 end.ch--; 3698 if (!keepHPos) { 3699 vim.lastHPos = Infinity; 3700 vim.lastHSPos = cm.charCoords(end,'div').left; 3701 } 3702 return retval; 3703 } 3704 3705 function moveToCharacter(cm, repeat, forward, character) { 3706 var cur = cm.getCursor(); 3707 var start = cur.ch; 3708 var idx; 3709 for (var i = 0; i < repeat; i ++) { 3710 var line = cm.getLine(cur.line); 3711 idx = charIdxInLine(start, line, character, forward, true); 3712 if (idx == -1) { 3713 return null; 3714 } 3715 start = idx; 3716 } 3717 return new Pos(cm.getCursor().line, idx); 3718 } 3719 3720 function moveToColumn(cm, repeat) { 3721 // repeat is always >= 1, so repeat - 1 always corresponds 3722 // to the column we want to go to. 3723 var line = cm.getCursor().line; 3724 return clipCursorToContent(cm, new Pos(line, repeat - 1)); 3725 } 3726 3727 function updateMark(cm, vim, markName, pos) { 3728 if (!inArray(markName, validMarks)) { 3729 return; 3730 } 3731 if (vim.marks[markName]) { 3732 vim.marks[markName].clear(); 3733 } 3734 vim.marks[markName] = cm.setBookmark(pos); 3735 } 3736 3737 function charIdxInLine(start, line, character, forward, includeChar) { 3738 // Search for char in line. 3739 // motion_options: {forward, includeChar} 3740 // If includeChar = true, include it too. 3741 // If forward = true, search forward, else search backwards. 3742 // If char is not found on this line, do nothing 3743 var idx; 3744 if (forward) { 3745 idx = line.indexOf(character, start + 1); 3746 if (idx != -1 && !includeChar) { 3747 idx -= 1; 3748 } 3749 } else { 3750 idx = line.lastIndexOf(character, start - 1); 3751 if (idx != -1 && !includeChar) { 3752 idx += 1; 3753 } 3754 } 3755 return idx; 3756 } 3757 3758 function findParagraph(cm, head, repeat, dir, inclusive) { 3759 var line = head.line; 3760 var min = cm.firstLine(); 3761 var max = cm.lastLine(); 3762 var start, end, i = line; 3763 function isEmpty(i) { return !cm.getLine(i); } 3764 function isBoundary(i, dir, any) { 3765 if (any) { return isEmpty(i) != isEmpty(i + dir); } 3766 return !isEmpty(i) && isEmpty(i + dir); 3767 } 3768 if (dir) { 3769 while (min <= i && i <= max && repeat > 0) { 3770 if (isBoundary(i, dir)) { repeat--; } 3771 i += dir; 3772 } 3773 return new Pos(i, 0); 3774 } 3775 3776 var vim = cm.state.vim; 3777 if (vim.visualLine && isBoundary(line, 1, true)) { 3778 var anchor = vim.sel.anchor; 3779 if (isBoundary(anchor.line, -1, true)) { 3780 if (!inclusive || anchor.line != line) { 3781 line += 1; 3782 } 3783 } 3784 } 3785 var startState = isEmpty(line); 3786 for (i = line; i <= max && repeat; i++) { 3787 if (isBoundary(i, 1, true)) { 3788 if (!inclusive || isEmpty(i) != startState) { 3789 repeat--; 3790 } 3791 } 3792 } 3793 end = new Pos(i, 0); 3794 // select boundary before paragraph for the last one 3795 if (i > max && !startState) { startState = true; } 3796 else { inclusive = false; } 3797 for (i = line; i > min; i--) { 3798 if (!inclusive || isEmpty(i) == startState || i == line) { 3799 if (isBoundary(i, -1, true)) { break; } 3800 } 3801 } 3802 start = new Pos(i, 0); 3803 return { start: start, end: end }; 3804 } 3805 3806 function findSentence(cm, cur, repeat, dir) { 3807 3808 /* 3809 Takes an index object 3810 { 3811 line: the line string, 3812 ln: line number, 3813 pos: index in line, 3814 dir: direction of traversal (-1 or 1) 3815 } 3816 and modifies the line, ln, and pos members to represent the 3817 next valid position or sets them to null if there are 3818 no more valid positions. 3819 */ 3820 function nextChar(cm, idx) { 3821 if (idx.pos + idx.dir < 0 || idx.pos + idx.dir >= idx.line.length) { 3822 idx.ln += idx.dir; 3823 if (!isLine(cm, idx.ln)) { 3824 idx.line = null; 3825 idx.ln = null; 3826 idx.pos = null; 3827 return; 3828 } 3829 idx.line = cm.getLine(idx.ln); 3830 idx.pos = (idx.dir > 0) ? 0 : idx.line.length - 1; 3831 } 3832 else { 3833 idx.pos += idx.dir; 3834 } 3835 } 3836 3837 /* 3838 Performs one iteration of traversal in forward direction 3839 Returns an index object of the new location 3840 */ 3841 function forward(cm, ln, pos, dir) { 3842 var line = cm.getLine(ln); 3843 var stop = (line === ""); 3844 3845 var curr = { 3846 line: line, 3847 ln: ln, 3848 pos: pos, 3849 dir: dir, 3850 } 3851 3852 var last_valid = { 3853 ln: curr.ln, 3854 pos: curr.pos, 3855 } 3856 3857 var skip_empty_lines = (curr.line === ""); 3858 3859 // Move one step to skip character we start on 3860 nextChar(cm, curr); 3861 3862 while (curr.line !== null) { 3863 last_valid.ln = curr.ln; 3864 last_valid.pos = curr.pos; 3865 3866 if (curr.line === "" && !skip_empty_lines) { 3867 return { ln: curr.ln, pos: curr.pos, }; 3868 } 3869 else if (stop && curr.line !== "" && !isWhiteSpaceString(curr.line[curr.pos])) { 3870 return { ln: curr.ln, pos: curr.pos, }; 3871 } 3872 else if (isEndOfSentenceSymbol(curr.line[curr.pos]) 3873 && !stop 3874 && (curr.pos === curr.line.length - 1 3875 || isWhiteSpaceString(curr.line[curr.pos + 1]))) { 3876 stop = true; 3877 } 3878 3879 nextChar(cm, curr); 3880 } 3881 3882 /* 3883 Set the position to the last non whitespace character on the last 3884 valid line in the case that we reach the end of the document. 3885 */ 3886 var line = cm.getLine(last_valid.ln); 3887 last_valid.pos = 0; 3888 for(var i = line.length - 1; i >= 0; --i) { 3889 if (!isWhiteSpaceString(line[i])) { 3890 last_valid.pos = i; 3891 break; 3892 } 3893 } 3894 3895 return last_valid; 3896 3897 } 3898 3899 /* 3900 Performs one iteration of traversal in reverse direction 3901 Returns an index object of the new location 3902 */ 3903 function reverse(cm, ln, pos, dir) { 3904 var line = cm.getLine(ln); 3905 3906 var curr = { 3907 line: line, 3908 ln: ln, 3909 pos: pos, 3910 dir: dir, 3911 } 3912 3913 var last_valid = { 3914 ln: curr.ln, 3915 pos: null, 3916 }; 3917 3918 var skip_empty_lines = (curr.line === ""); 3919 3920 // Move one step to skip character we start on 3921 nextChar(cm, curr); 3922 3923 while (curr.line !== null) { 3924 3925 if (curr.line === "" && !skip_empty_lines) { 3926 if (last_valid.pos !== null) { 3927 return last_valid; 3928 } 3929 else { 3930 return { ln: curr.ln, pos: curr.pos }; 3931 } 3932 } 3933 else if (isEndOfSentenceSymbol(curr.line[curr.pos]) 3934 && last_valid.pos !== null 3935 && !(curr.ln === last_valid.ln && curr.pos + 1 === last_valid.pos)) { 3936 return last_valid; 3937 } 3938 else if (curr.line !== "" && !isWhiteSpaceString(curr.line[curr.pos])) { 3939 skip_empty_lines = false; 3940 last_valid = { ln: curr.ln, pos: curr.pos } 3941 } 3942 3943 nextChar(cm, curr); 3944 } 3945 3946 /* 3947 Set the position to the first non whitespace character on the last 3948 valid line in the case that we reach the beginning of the document. 3949 */ 3950 var line = cm.getLine(last_valid.ln); 3951 last_valid.pos = 0; 3952 for(var i = 0; i < line.length; ++i) { 3953 if (!isWhiteSpaceString(line[i])) { 3954 last_valid.pos = i; 3955 break; 3956 } 3957 } 3958 return last_valid; 3959 } 3960 3961 var curr_index = { 3962 ln: cur.line, 3963 pos: cur.ch, 3964 }; 3965 3966 while (repeat > 0) { 3967 if (dir < 0) { 3968 curr_index = reverse(cm, curr_index.ln, curr_index.pos, dir); 3969 } 3970 else { 3971 curr_index = forward(cm, curr_index.ln, curr_index.pos, dir); 3972 } 3973 repeat--; 3974 } 3975 3976 return new Pos(curr_index.ln, curr_index.pos); 3977 } 3978 3979 // TODO: perhaps this finagling of start and end positions belongs 3980 // in codemirror/replaceRange? 3981 function selectCompanionObject(cm, head, symb, inclusive) { 3982 var cur = head, start, end; 3983 3984 var bracketRegexp = ({ 3985 '(': /[()]/, ')': /[()]/, 3986 '[': /[[\]]/, ']': /[[\]]/, 3987 '{': /[{}]/, '}': /[{}]/, 3988 '<': /[<>]/, '>': /[<>]/})[symb]; 3989 var openSym = ({ 3990 '(': '(', ')': '(', 3991 '[': '[', ']': '[', 3992 '{': '{', '}': '{', 3993 '<': '<', '>': '<'})[symb]; 3994 var curChar = cm.getLine(cur.line).charAt(cur.ch); 3995 // Due to the behavior of scanForBracket, we need to add an offset if the 3996 // cursor is on a matching open bracket. 3997 var offset = curChar === openSym ? 1 : 0; 3998 3999 start = cm.scanForBracket(new Pos(cur.line, cur.ch + offset), -1, undefined, {'bracketRegex': bracketRegexp}); 4000 end = cm.scanForBracket(new Pos(cur.line, cur.ch + offset), 1, undefined, {'bracketRegex': bracketRegexp}); 4001 4002 if (!start || !end) { 4003 return { start: cur, end: cur }; 4004 } 4005 4006 start = start.pos; 4007 end = end.pos; 4008 4009 if ((start.line == end.line && start.ch > end.ch) 4010 || (start.line > end.line)) { 4011 var tmp = start; 4012 start = end; 4013 end = tmp; 4014 } 4015 4016 if (inclusive) { 4017 end.ch += 1; 4018 } else { 4019 start.ch += 1; 4020 } 4021 4022 return { start: start, end: end }; 4023 } 4024 4025 // Takes in a symbol and a cursor and tries to simulate text objects that 4026 // have identical opening and closing symbols 4027 // TODO support across multiple lines 4028 function findBeginningAndEnd(cm, head, symb, inclusive) { 4029 var cur = copyCursor(head); 4030 var line = cm.getLine(cur.line); 4031 var chars = line.split(''); 4032 var start, end, i, len; 4033 var firstIndex = chars.indexOf(symb); 4034 4035 // the decision tree is to always look backwards for the beginning first, 4036 // but if the cursor is in front of the first instance of the symb, 4037 // then move the cursor forward 4038 if (cur.ch < firstIndex) { 4039 cur.ch = firstIndex; 4040 // Why is this line even here??? 4041 // cm.setCursor(cur.line, firstIndex+1); 4042 } 4043 // otherwise if the cursor is currently on the closing symbol 4044 else if (firstIndex < cur.ch && chars[cur.ch] == symb) { 4045 end = cur.ch; // assign end to the current cursor 4046 --cur.ch; // make sure to look backwards 4047 } 4048 4049 // if we're currently on the symbol, we've got a start 4050 if (chars[cur.ch] == symb && !end) { 4051 start = cur.ch + 1; // assign start to ahead of the cursor 4052 } else { 4053 // go backwards to find the start 4054 for (i = cur.ch; i > -1 && !start; i--) { 4055 if (chars[i] == symb) { 4056 start = i + 1; 4057 } 4058 } 4059 } 4060 4061 // look forwards for the end symbol 4062 if (start && !end) { 4063 for (i = start, len = chars.length; i < len && !end; i++) { 4064 if (chars[i] == symb) { 4065 end = i; 4066 } 4067 } 4068 } 4069 4070 // nothing found 4071 if (!start || !end) { 4072 return { start: cur, end: cur }; 4073 } 4074 4075 // include the symbols 4076 if (inclusive) { 4077 --start; ++end; 4078 } 4079 4080 return { 4081 start: new Pos(cur.line, start), 4082 end: new Pos(cur.line, end) 4083 }; 4084 } 4085 4086 // Search functions 4087 defineOption('pcre', true, 'boolean'); 4088 function SearchState() {} 4089 SearchState.prototype = { 4090 getQuery: function() { 4091 return vimGlobalState.query; 4092 }, 4093 setQuery: function(query) { 4094 vimGlobalState.query = query; 4095 }, 4096 getOverlay: function() { 4097 return this.searchOverlay; 4098 }, 4099 setOverlay: function(overlay) { 4100 this.searchOverlay = overlay; 4101 }, 4102 isReversed: function() { 4103 return vimGlobalState.isReversed; 4104 }, 4105 setReversed: function(reversed) { 4106 vimGlobalState.isReversed = reversed; 4107 }, 4108 getScrollbarAnnotate: function() { 4109 return this.annotate; 4110 }, 4111 setScrollbarAnnotate: function(annotate) { 4112 this.annotate = annotate; 4113 } 4114 }; 4115 function getSearchState(cm) { 4116 var vim = cm.state.vim; 4117 return vim.searchState_ || (vim.searchState_ = new SearchState()); 4118 } 4119 function splitBySlash(argString) { 4120 return splitBySeparator(argString, '/'); 4121 } 4122 4123 function findUnescapedSlashes(argString) { 4124 return findUnescapedSeparators(argString, '/'); 4125 } 4126 4127 function splitBySeparator(argString, separator) { 4128 var slashes = findUnescapedSeparators(argString, separator) || []; 4129 if (!slashes.length) return []; 4130 var tokens = []; 4131 // in case of strings like foo/bar 4132 if (slashes[0] !== 0) return; 4133 for (var i = 0; i < slashes.length; i++) { 4134 if (typeof slashes[i] == 'number') 4135 tokens.push(argString.substring(slashes[i] + 1, slashes[i+1])); 4136 } 4137 return tokens; 4138 } 4139 4140 function findUnescapedSeparators(str, separator) { 4141 if (!separator) 4142 separator = '/'; 4143 4144 var escapeNextChar = false; 4145 var slashes = []; 4146 for (var i = 0; i < str.length; i++) { 4147 var c = str.charAt(i); 4148 if (!escapeNextChar && c == separator) { 4149 slashes.push(i); 4150 } 4151 escapeNextChar = !escapeNextChar && (c == '\\'); 4152 } 4153 return slashes; 4154 } 4155 4156 // Translates a search string from ex (vim) syntax into javascript form. 4157 function translateRegex(str) { 4158 // When these match, add a '\' if unescaped or remove one if escaped. 4159 var specials = '|(){'; 4160 // Remove, but never add, a '\' for these. 4161 var unescape = '}'; 4162 var escapeNextChar = false; 4163 var out = []; 4164 for (var i = -1; i < str.length; i++) { 4165 var c = str.charAt(i) || ''; 4166 var n = str.charAt(i+1) || ''; 4167 var specialComesNext = (n && specials.indexOf(n) != -1); 4168 if (escapeNextChar) { 4169 if (c !== '\\' || !specialComesNext) { 4170 out.push(c); 4171 } 4172 escapeNextChar = false; 4173 } else { 4174 if (c === '\\') { 4175 escapeNextChar = true; 4176 // Treat the unescape list as special for removing, but not adding '\'. 4177 if (n && unescape.indexOf(n) != -1) { 4178 specialComesNext = true; 4179 } 4180 // Not passing this test means removing a '\'. 4181 if (!specialComesNext || n === '\\') { 4182 out.push(c); 4183 } 4184 } else { 4185 out.push(c); 4186 if (specialComesNext && n !== '\\') { 4187 out.push('\\'); 4188 } 4189 } 4190 } 4191 } 4192 return out.join(''); 4193 } 4194 4195 // Translates the replace part of a search and replace from ex (vim) syntax into 4196 // javascript form. Similar to translateRegex, but additionally fixes back references 4197 // (translates '\[0..9]' to '$[0..9]') and follows different rules for escaping '$'. 4198 var charUnescapes = {'\\n': '\n', '\\r': '\r', '\\t': '\t'}; 4199 function translateRegexReplace(str) { 4200 var escapeNextChar = false; 4201 var out = []; 4202 for (var i = -1; i < str.length; i++) { 4203 var c = str.charAt(i) || ''; 4204 var n = str.charAt(i+1) || ''; 4205 if (charUnescapes[c + n]) { 4206 out.push(charUnescapes[c+n]); 4207 i++; 4208 } else if (escapeNextChar) { 4209 // At any point in the loop, escapeNextChar is true if the previous 4210 // character was a '\' and was not escaped. 4211 out.push(c); 4212 escapeNextChar = false; 4213 } else { 4214 if (c === '\\') { 4215 escapeNextChar = true; 4216 if ((isNumber(n) || n === '$')) { 4217 out.push('$'); 4218 } else if (n !== '/' && n !== '\\') { 4219 out.push('\\'); 4220 } 4221 } else { 4222 if (c === '$') { 4223 out.push('$'); 4224 } 4225 out.push(c); 4226 if (n === '/') { 4227 out.push('\\'); 4228 } 4229 } 4230 } 4231 } 4232 return out.join(''); 4233 } 4234 4235 // Unescape \ and / in the replace part, for PCRE mode. 4236 var unescapes = {'\\/': '/', '\\\\': '\\', '\\n': '\n', '\\r': '\r', '\\t': '\t', '\\&':'&'}; 4237 function unescapeRegexReplace(str) { 4238 var stream = new CodeMirror.StringStream(str); 4239 var output = []; 4240 while (!stream.eol()) { 4241 // Search for \. 4242 while (stream.peek() && stream.peek() != '\\') { 4243 output.push(stream.next()); 4244 } 4245 var matched = false; 4246 for (var matcher in unescapes) { 4247 if (stream.match(matcher, true)) { 4248 matched = true; 4249 output.push(unescapes[matcher]); 4250 break; 4251 } 4252 } 4253 if (!matched) { 4254 // Don't change anything 4255 output.push(stream.next()); 4256 } 4257 } 4258 return output.join(''); 4259 } 4260 4261 /** 4262 * Extract the regular expression from the query and return a Regexp object. 4263 * Returns null if the query is blank. 4264 * If ignoreCase is passed in, the Regexp object will have the 'i' flag set. 4265 * If smartCase is passed in, and the query contains upper case letters, 4266 * then ignoreCase is overridden, and the 'i' flag will not be set. 4267 * If the query contains the /i in the flag part of the regular expression, 4268 * then both ignoreCase and smartCase are ignored, and 'i' will be passed 4269 * through to the Regex object. 4270 */ 4271 function parseQuery(query, ignoreCase, smartCase) { 4272 // First update the last search register 4273 var lastSearchRegister = vimGlobalState.registerController.getRegister('/'); 4274 lastSearchRegister.setText(query); 4275 // Check if the query is already a regex. 4276 if (query instanceof RegExp) { return query; } 4277 // First try to extract regex + flags from the input. If no flags found, 4278 // extract just the regex. IE does not accept flags directly defined in 4279 // the regex string in the form /regex/flags 4280 var slashes = findUnescapedSlashes(query); 4281 var regexPart; 4282 var forceIgnoreCase; 4283 if (!slashes.length) { 4284 // Query looks like 'regexp' 4285 regexPart = query; 4286 } else { 4287 // Query looks like 'regexp/...' 4288 regexPart = query.substring(0, slashes[0]); 4289 var flagsPart = query.substring(slashes[0]); 4290 forceIgnoreCase = (flagsPart.indexOf('i') != -1); 4291 } 4292 if (!regexPart) { 4293 return null; 4294 } 4295 if (!getOption('pcre')) { 4296 regexPart = translateRegex(regexPart); 4297 } 4298 if (smartCase) { 4299 ignoreCase = (/^[^A-Z]*$/).test(regexPart); 4300 } 4301 var regexp = new RegExp(regexPart, 4302 (ignoreCase || forceIgnoreCase) ? 'im' : 'm'); 4303 return regexp; 4304 } 4305 4306 /** 4307 * dom - Document Object Manipulator 4308 * Usage: 4309 * dom('<tag>'|<node>[, ...{<attributes>|<$styles>}|<child-node>|'<text>']) 4310 * Examples: 4311 * dom('div', {id:'xyz'}, dom('p', 'CM rocks!', {$color:'red'})) 4312 * dom(document.head, dom('script', 'alert("hello!")')) 4313 * Not supported: 4314 * dom('p', ['arrays are objects'], Error('objects specify attributes')) 4315 */ 4316 function dom(n) { 4317 if (typeof n === 'string') n = document.createElement(n); 4318 for (var a, i = 1; i < arguments.length; i++) { 4319 if (!(a = arguments[i])) continue; 4320 if (typeof a !== 'object') a = document.createTextNode(a); 4321 if (a.nodeType) n.appendChild(a); 4322 else for (var key in a) { 4323 if (!Object.prototype.hasOwnProperty.call(a, key)) continue; 4324 if (key[0] === '$') n.style[key.slice(1)] = a[key]; 4325 else n.setAttribute(key, a[key]); 4326 } 4327 } 4328 return n; 4329 } 4330 4331 function showConfirm(cm, template) { 4332 var pre = dom('pre', {$color: 'red', class: 'cm-vim-message'}, template); 4333 if (cm.openNotification) { 4334 cm.openNotification(pre, {bottom: true, duration: 5000}); 4335 } else { 4336 alert(pre.innerText); 4337 } 4338 } 4339 4340 function makePrompt(prefix, desc) { 4341 return dom(document.createDocumentFragment(), 4342 dom('span', {$fontFamily: 'monospace', $whiteSpace: 'pre'}, 4343 prefix, 4344 dom('input', {type: 'text', autocorrect: 'off', 4345 autocapitalize: 'off', spellcheck: 'false'})), 4346 desc && dom('span', {$color: '#888'}, desc)); 4347 } 4348 4349 function showPrompt(cm, options) { 4350 var template = makePrompt(options.prefix, options.desc); 4351 if (cm.openDialog) { 4352 cm.openDialog(template, options.onClose, { 4353 onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp, 4354 bottom: true, selectValueOnOpen: false, value: options.value 4355 }); 4356 } 4357 else { 4358 var shortText = ''; 4359 if (typeof options.prefix != "string" && options.prefix) shortText += options.prefix.textContent; 4360 if (options.desc) shortText += " " + options.desc; 4361 options.onClose(prompt(shortText, '')); 4362 } 4363 } 4364 4365 function regexEqual(r1, r2) { 4366 if (r1 instanceof RegExp && r2 instanceof RegExp) { 4367 var props = ['global', 'multiline', 'ignoreCase', 'source']; 4368 for (var i = 0; i < props.length; i++) { 4369 var prop = props[i]; 4370 if (r1[prop] !== r2[prop]) { 4371 return false; 4372 } 4373 } 4374 return true; 4375 } 4376 return false; 4377 } 4378 // Returns true if the query is valid. 4379 function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) { 4380 if (!rawQuery) { 4381 return; 4382 } 4383 var state = getSearchState(cm); 4384 var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase); 4385 if (!query) { 4386 return; 4387 } 4388 highlightSearchMatches(cm, query); 4389 if (regexEqual(query, state.getQuery())) { 4390 return query; 4391 } 4392 state.setQuery(query); 4393 return query; 4394 } 4395 function searchOverlay(query) { 4396 if (query.source.charAt(0) == '^') { 4397 var matchSol = true; 4398 } 4399 return { 4400 token: function(stream) { 4401 if (matchSol && !stream.sol()) { 4402 stream.skipToEnd(); 4403 return; 4404 } 4405 var match = stream.match(query, false); 4406 if (match) { 4407 if (match[0].length == 0) { 4408 // Matched empty string, skip to next. 4409 stream.next(); 4410 return 'searching'; 4411 } 4412 if (!stream.sol()) { 4413 // Backtrack 1 to match \b 4414 stream.backUp(1); 4415 if (!query.exec(stream.next() + match[0])) { 4416 stream.next(); 4417 return null; 4418 } 4419 } 4420 stream.match(query); 4421 return 'searching'; 4422 } 4423 while (!stream.eol()) { 4424 stream.next(); 4425 if (stream.match(query, false)) break; 4426 } 4427 }, 4428 query: query 4429 }; 4430 } 4431 var highlightTimeout = 0; 4432 function highlightSearchMatches(cm, query) { 4433 clearTimeout(highlightTimeout); 4434 highlightTimeout = setTimeout(function() { 4435 if (!cm.state.vim) return; 4436 var searchState = getSearchState(cm); 4437 var overlay = searchState.getOverlay(); 4438 if (!overlay || query != overlay.query) { 4439 if (overlay) { 4440 cm.removeOverlay(overlay); 4441 } 4442 overlay = searchOverlay(query); 4443 cm.addOverlay(overlay); 4444 if (cm.showMatchesOnScrollbar) { 4445 if (searchState.getScrollbarAnnotate()) { 4446 searchState.getScrollbarAnnotate().clear(); 4447 } 4448 searchState.setScrollbarAnnotate(cm.showMatchesOnScrollbar(query)); 4449 } 4450 searchState.setOverlay(overlay); 4451 } 4452 }, 50); 4453 } 4454 function findNext(cm, prev, query, repeat) { 4455 if (repeat === undefined) { repeat = 1; } 4456 return cm.operation(function() { 4457 var pos = cm.getCursor(); 4458 var cursor = cm.getSearchCursor(query, pos); 4459 for (var i = 0; i < repeat; i++) { 4460 var found = cursor.find(prev); 4461 if (i == 0 && found && cursorEqual(cursor.from(), pos)) { 4462 var lastEndPos = prev ? cursor.from() : cursor.to(); 4463 found = cursor.find(prev); 4464 if (found && !found[0] && cursorEqual(cursor.from(), lastEndPos)) { 4465 if (cm.getLine(lastEndPos.line).length == lastEndPos.ch) 4466 found = cursor.find(prev); 4467 } 4468 } 4469 if (!found) { 4470 // SearchCursor may have returned null because it hit EOF, wrap 4471 // around and try again. 4472 cursor = cm.getSearchCursor(query, 4473 (prev) ? new Pos(cm.lastLine()) : new Pos(cm.firstLine(), 0) ); 4474 if (!cursor.find(prev)) { 4475 return; 4476 } 4477 } 4478 } 4479 return cursor.from(); 4480 }); 4481 } 4482 /** 4483 * Pretty much the same as `findNext`, except for the following differences: 4484 * 4485 * 1. Before starting the search, move to the previous search. This way if our cursor is 4486 * already inside a match, we should return the current match. 4487 * 2. Rather than only returning the cursor's from, we return the cursor's from and to as a tuple. 4488 */ 4489 function findNextFromAndToInclusive(cm, prev, query, repeat, vim) { 4490 if (repeat === undefined) { repeat = 1; } 4491 return cm.operation(function() { 4492 var pos = cm.getCursor(); 4493 var cursor = cm.getSearchCursor(query, pos); 4494 4495 // Go back one result to ensure that if the cursor is currently a match, we keep it. 4496 var found = cursor.find(!prev); 4497 4498 // If we haven't moved, go back one more (similar to if i==0 logic in findNext). 4499 if (!vim.visualMode && found && cursorEqual(cursor.from(), pos)) { 4500 cursor.find(!prev); 4501 } 4502 4503 for (var i = 0; i < repeat; i++) { 4504 found = cursor.find(prev); 4505 if (!found) { 4506 // SearchCursor may have returned null because it hit EOF, wrap 4507 // around and try again. 4508 cursor = cm.getSearchCursor(query, 4509 (prev) ? new Pos(cm.lastLine()) : new Pos(cm.firstLine(), 0) ); 4510 if (!cursor.find(prev)) { 4511 return; 4512 } 4513 } 4514 } 4515 return [cursor.from(), cursor.to()]; 4516 }); 4517 } 4518 function clearSearchHighlight(cm) { 4519 var state = getSearchState(cm); 4520 cm.removeOverlay(getSearchState(cm).getOverlay()); 4521 state.setOverlay(null); 4522 if (state.getScrollbarAnnotate()) { 4523 state.getScrollbarAnnotate().clear(); 4524 state.setScrollbarAnnotate(null); 4525 } 4526 } 4527 /** 4528 * Check if pos is in the specified range, INCLUSIVE. 4529 * Range can be specified with 1 or 2 arguments. 4530 * If the first range argument is an array, treat it as an array of line 4531 * numbers. Match pos against any of the lines. 4532 * If the first range argument is a number, 4533 * if there is only 1 range argument, check if pos has the same line 4534 * number 4535 * if there are 2 range arguments, then check if pos is in between the two 4536 * range arguments. 4537 */ 4538 function isInRange(pos, start, end) { 4539 if (typeof pos != 'number') { 4540 // Assume it is a cursor position. Get the line number. 4541 pos = pos.line; 4542 } 4543 if (start instanceof Array) { 4544 return inArray(pos, start); 4545 } else { 4546 if (typeof end == 'number') { 4547 return (pos >= start && pos <= end); 4548 } else { 4549 return pos == start; 4550 } 4551 } 4552 } 4553 function getUserVisibleLines(cm) { 4554 var scrollInfo = cm.getScrollInfo(); 4555 var occludeToleranceTop = 6; 4556 var occludeToleranceBottom = 10; 4557 var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local'); 4558 var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top; 4559 var to = cm.coordsChar({left:0, top: bottomY}, 'local'); 4560 return {top: from.line, bottom: to.line}; 4561 } 4562 4563 function getMarkPos(cm, vim, markName) { 4564 if (markName == '\'' || markName == '`') { 4565 return vimGlobalState.jumpList.find(cm, -1) || new Pos(0, 0); 4566 } else if (markName == '.') { 4567 return getLastEditPos(cm); 4568 } 4569 4570 var mark = vim.marks[markName]; 4571 return mark && mark.find(); 4572 } 4573 4574 function getLastEditPos(cm) { 4575 var done = cm.doc.history.done; 4576 for (var i = done.length; i--;) { 4577 if (done[i].changes) { 4578 return copyCursor(done[i].changes[0].to); 4579 } 4580 } 4581 } 4582 4583 var ExCommandDispatcher = function() { 4584 this.buildCommandMap_(); 4585 }; 4586 ExCommandDispatcher.prototype = { 4587 processCommand: function(cm, input, opt_params) { 4588 var that = this; 4589 cm.operation(function () { 4590 cm.curOp.isVimOp = true; 4591 that._processCommand(cm, input, opt_params); 4592 }); 4593 }, 4594 _processCommand: function(cm, input, opt_params) { 4595 var vim = cm.state.vim; 4596 var commandHistoryRegister = vimGlobalState.registerController.getRegister(':'); 4597 var previousCommand = commandHistoryRegister.toString(); 4598 if (vim.visualMode) { 4599 exitVisualMode(cm); 4600 } 4601 var inputStream = new CodeMirror.StringStream(input); 4602 // update ": with the latest command whether valid or invalid 4603 commandHistoryRegister.setText(input); 4604 var params = opt_params || {}; 4605 params.input = input; 4606 try { 4607 this.parseInput_(cm, inputStream, params); 4608 } catch(e) { 4609 showConfirm(cm, e.toString()); 4610 throw e; 4611 } 4612 var command; 4613 var commandName; 4614 if (!params.commandName) { 4615 // If only a line range is defined, move to the line. 4616 if (params.line !== undefined) { 4617 commandName = 'move'; 4618 } 4619 } else { 4620 command = this.matchCommand_(params.commandName); 4621 if (command) { 4622 commandName = command.name; 4623 if (command.excludeFromCommandHistory) { 4624 commandHistoryRegister.setText(previousCommand); 4625 } 4626 this.parseCommandArgs_(inputStream, params, command); 4627 if (command.type == 'exToKey') { 4628 // Handle Ex to Key mapping. 4629 for (var i = 0; i < command.toKeys.length; i++) { 4630 vimApi.handleKey(cm, command.toKeys[i], 'mapping'); 4631 } 4632 return; 4633 } else if (command.type == 'exToEx') { 4634 // Handle Ex to Ex mapping. 4635 this.processCommand(cm, command.toInput); 4636 return; 4637 } 4638 } 4639 } 4640 if (!commandName) { 4641 showConfirm(cm, 'Not an editor command ":' + input + '"'); 4642 return; 4643 } 4644 try { 4645 exCommands[commandName](cm, params); 4646 // Possibly asynchronous commands (e.g. substitute, which might have a 4647 // user confirmation), are responsible for calling the callback when 4648 // done. All others have it taken care of for them here. 4649 if ((!command || !command.possiblyAsync) && params.callback) { 4650 params.callback(); 4651 } 4652 } catch(e) { 4653 showConfirm(cm, e.toString()); 4654 throw e; 4655 } 4656 }, 4657 parseInput_: function(cm, inputStream, result) { 4658 inputStream.eatWhile(':'); 4659 // Parse range. 4660 if (inputStream.eat('%')) { 4661 result.line = cm.firstLine(); 4662 result.lineEnd = cm.lastLine(); 4663 } else { 4664 result.line = this.parseLineSpec_(cm, inputStream); 4665 if (result.line !== undefined && inputStream.eat(',')) { 4666 result.lineEnd = this.parseLineSpec_(cm, inputStream); 4667 } 4668 } 4669 4670 // Parse command name. 4671 var commandMatch = inputStream.match(/^(\w+|!!|@@|[!#&*<=>@~])/); 4672 if (commandMatch) { 4673 result.commandName = commandMatch[1]; 4674 } else { 4675 result.commandName = inputStream.match(/.*/)[0]; 4676 } 4677 4678 return result; 4679 }, 4680 parseLineSpec_: function(cm, inputStream) { 4681 var numberMatch = inputStream.match(/^(\d+)/); 4682 if (numberMatch) { 4683 // Absolute line number plus offset (N+M or N-M) is probably a typo, 4684 // not something the user actually wanted. (NB: vim does allow this.) 4685 return parseInt(numberMatch[1], 10) - 1; 4686 } 4687 switch (inputStream.next()) { 4688 case '.': 4689 return this.parseLineSpecOffset_(inputStream, cm.getCursor().line); 4690 case '$': 4691 return this.parseLineSpecOffset_(inputStream, cm.lastLine()); 4692 case '\'': 4693 var markName = inputStream.next(); 4694 var markPos = getMarkPos(cm, cm.state.vim, markName); 4695 if (!markPos) throw new Error('Mark not set'); 4696 return this.parseLineSpecOffset_(inputStream, markPos.line); 4697 case '-': 4698 case '+': 4699 inputStream.backUp(1); 4700 // Offset is relative to current line if not otherwise specified. 4701 return this.parseLineSpecOffset_(inputStream, cm.getCursor().line); 4702 default: 4703 inputStream.backUp(1); 4704 return undefined; 4705 } 4706 }, 4707 parseLineSpecOffset_: function(inputStream, line) { 4708 var offsetMatch = inputStream.match(/^([+-])?(\d+)/); 4709 if (offsetMatch) { 4710 var offset = parseInt(offsetMatch[2], 10); 4711 if (offsetMatch[1] == "-") { 4712 line -= offset; 4713 } else { 4714 line += offset; 4715 } 4716 } 4717 return line; 4718 }, 4719 parseCommandArgs_: function(inputStream, params, command) { 4720 if (inputStream.eol()) { 4721 return; 4722 } 4723 params.argString = inputStream.match(/.*/)[0]; 4724 // Parse command-line arguments 4725 var delim = command.argDelimiter || /\s+/; 4726 var args = trim(params.argString).split(delim); 4727 if (args.length && args[0]) { 4728 params.args = args; 4729 } 4730 }, 4731 matchCommand_: function(commandName) { 4732 // Return the command in the command map that matches the shortest 4733 // prefix of the passed in command name. The match is guaranteed to be 4734 // unambiguous if the defaultExCommandMap's shortNames are set up 4735 // correctly. (see @code{defaultExCommandMap}). 4736 for (var i = commandName.length; i > 0; i--) { 4737 var prefix = commandName.substring(0, i); 4738 if (this.commandMap_[prefix]) { 4739 var command = this.commandMap_[prefix]; 4740 if (command.name.indexOf(commandName) === 0) { 4741 return command; 4742 } 4743 } 4744 } 4745 return null; 4746 }, 4747 buildCommandMap_: function() { 4748 this.commandMap_ = {}; 4749 for (var i = 0; i < defaultExCommandMap.length; i++) { 4750 var command = defaultExCommandMap[i]; 4751 var key = command.shortName || command.name; 4752 this.commandMap_[key] = command; 4753 } 4754 }, 4755 map: function(lhs, rhs, ctx) { 4756 if (lhs != ':' && lhs.charAt(0) == ':') { 4757 if (ctx) { throw Error('Mode not supported for ex mappings'); } 4758 var commandName = lhs.substring(1); 4759 if (rhs != ':' && rhs.charAt(0) == ':') { 4760 // Ex to Ex mapping 4761 this.commandMap_[commandName] = { 4762 name: commandName, 4763 type: 'exToEx', 4764 toInput: rhs.substring(1), 4765 user: true 4766 }; 4767 } else { 4768 // Ex to key mapping 4769 this.commandMap_[commandName] = { 4770 name: commandName, 4771 type: 'exToKey', 4772 toKeys: rhs, 4773 user: true 4774 }; 4775 } 4776 } else { 4777 if (rhs != ':' && rhs.charAt(0) == ':') { 4778 // Key to Ex mapping. 4779 var mapping = { 4780 keys: lhs, 4781 type: 'keyToEx', 4782 exArgs: { input: rhs.substring(1) } 4783 }; 4784 if (ctx) { mapping.context = ctx; } 4785 defaultKeymap.unshift(mapping); 4786 } else { 4787 // Key to key mapping 4788 var mapping = { 4789 keys: lhs, 4790 type: 'keyToKey', 4791 toKeys: rhs 4792 }; 4793 if (ctx) { mapping.context = ctx; } 4794 defaultKeymap.unshift(mapping); 4795 } 4796 } 4797 }, 4798 unmap: function(lhs, ctx) { 4799 if (lhs != ':' && lhs.charAt(0) == ':') { 4800 // Ex to Ex or Ex to key mapping 4801 if (ctx) { throw Error('Mode not supported for ex mappings'); } 4802 var commandName = lhs.substring(1); 4803 if (this.commandMap_[commandName] && this.commandMap_[commandName].user) { 4804 delete this.commandMap_[commandName]; 4805 return true; 4806 } 4807 } else { 4808 // Key to Ex or key to key mapping 4809 var keys = lhs; 4810 for (var i = 0; i < defaultKeymap.length; i++) { 4811 if (keys == defaultKeymap[i].keys 4812 && defaultKeymap[i].context === ctx) { 4813 defaultKeymap.splice(i, 1); 4814 return true; 4815 } 4816 } 4817 } 4818 } 4819 }; 4820 4821 var exCommands = { 4822 colorscheme: function(cm, params) { 4823 if (!params.args || params.args.length < 1) { 4824 showConfirm(cm, cm.getOption('theme')); 4825 return; 4826 } 4827 cm.setOption('theme', params.args[0]); 4828 }, 4829 map: function(cm, params, ctx) { 4830 var mapArgs = params.args; 4831 if (!mapArgs || mapArgs.length < 2) { 4832 if (cm) { 4833 showConfirm(cm, 'Invalid mapping: ' + params.input); 4834 } 4835 return; 4836 } 4837 exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx); 4838 }, 4839 imap: function(cm, params) { this.map(cm, params, 'insert'); }, 4840 nmap: function(cm, params) { this.map(cm, params, 'normal'); }, 4841 vmap: function(cm, params) { this.map(cm, params, 'visual'); }, 4842 unmap: function(cm, params, ctx) { 4843 var mapArgs = params.args; 4844 if (!mapArgs || mapArgs.length < 1 || !exCommandDispatcher.unmap(mapArgs[0], ctx)) { 4845 if (cm) { 4846 showConfirm(cm, 'No such mapping: ' + params.input); 4847 } 4848 } 4849 }, 4850 move: function(cm, params) { 4851 commandDispatcher.processCommand(cm, cm.state.vim, { 4852 type: 'motion', 4853 motion: 'moveToLineOrEdgeOfDocument', 4854 motionArgs: { forward: false, explicitRepeat: true, 4855 linewise: true }, 4856 repeatOverride: params.line+1}); 4857 }, 4858 set: function(cm, params) { 4859 var setArgs = params.args; 4860 // Options passed through to the setOption/getOption calls. May be passed in by the 4861 // local/global versions of the set command 4862 var setCfg = params.setCfg || {}; 4863 if (!setArgs || setArgs.length < 1) { 4864 if (cm) { 4865 showConfirm(cm, 'Invalid mapping: ' + params.input); 4866 } 4867 return; 4868 } 4869 var expr = setArgs[0].split('='); 4870 var optionName = expr[0]; 4871 var value = expr[1]; 4872 var forceGet = false; 4873 4874 if (optionName.charAt(optionName.length - 1) == '?') { 4875 // If post-fixed with ?, then the set is actually a get. 4876 if (value) { throw Error('Trailing characters: ' + params.argString); } 4877 optionName = optionName.substring(0, optionName.length - 1); 4878 forceGet = true; 4879 } 4880 if (value === undefined && optionName.substring(0, 2) == 'no') { 4881 // To set boolean options to false, the option name is prefixed with 4882 // 'no'. 4883 optionName = optionName.substring(2); 4884 value = false; 4885 } 4886 4887 var optionIsBoolean = options[optionName] && options[optionName].type == 'boolean'; 4888 if (optionIsBoolean && value == undefined) { 4889 // Calling set with a boolean option sets it to true. 4890 value = true; 4891 } 4892 // If no value is provided, then we assume this is a get. 4893 if (!optionIsBoolean && value === undefined || forceGet) { 4894 var oldValue = getOption(optionName, cm, setCfg); 4895 if (oldValue instanceof Error) { 4896 showConfirm(cm, oldValue.message); 4897 } else if (oldValue === true || oldValue === false) { 4898 showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName); 4899 } else { 4900 showConfirm(cm, ' ' + optionName + '=' + oldValue); 4901 } 4902 } else { 4903 var setOptionReturn = setOption(optionName, value, cm, setCfg); 4904 if (setOptionReturn instanceof Error) { 4905 showConfirm(cm, setOptionReturn.message); 4906 } 4907 } 4908 }, 4909 setlocal: function (cm, params) { 4910 // setCfg is passed through to setOption 4911 params.setCfg = {scope: 'local'}; 4912 this.set(cm, params); 4913 }, 4914 setglobal: function (cm, params) { 4915 // setCfg is passed through to setOption 4916 params.setCfg = {scope: 'global'}; 4917 this.set(cm, params); 4918 }, 4919 registers: function(cm, params) { 4920 var regArgs = params.args; 4921 var registers = vimGlobalState.registerController.registers; 4922 var regInfo = '----------Registers----------\n\n'; 4923 if (!regArgs) { 4924 for (var registerName in registers) { 4925 var text = registers[registerName].toString(); 4926 if (text.length) { 4927 regInfo += '"' + registerName + ' ' + text + '\n' 4928 } 4929 } 4930 } else { 4931 var registerName; 4932 regArgs = regArgs.join(''); 4933 for (var i = 0; i < regArgs.length; i++) { 4934 registerName = regArgs.charAt(i); 4935 if (!vimGlobalState.registerController.isValidRegister(registerName)) { 4936 continue; 4937 } 4938 var register = registers[registerName] || new Register(); 4939 regInfo += '"' + registerName + ' ' + register.toString() + '\n' 4940 } 4941 } 4942 showConfirm(cm, regInfo); 4943 }, 4944 sort: function(cm, params) { 4945 var reverse, ignoreCase, unique, number, pattern; 4946 function parseArgs() { 4947 if (params.argString) { 4948 var args = new CodeMirror.StringStream(params.argString); 4949 if (args.eat('!')) { reverse = true; } 4950 if (args.eol()) { return; } 4951 if (!args.eatSpace()) { return 'Invalid arguments'; } 4952 var opts = args.match(/([dinuox]+)?\s*(\/.+\/)?\s*/); 4953 if (!opts && !args.eol()) { return 'Invalid arguments'; } 4954 if (opts[1]) { 4955 ignoreCase = opts[1].indexOf('i') != -1; 4956 unique = opts[1].indexOf('u') != -1; 4957 var decimal = opts[1].indexOf('d') != -1 || opts[1].indexOf('n') != -1 && 1; 4958 var hex = opts[1].indexOf('x') != -1 && 1; 4959 var octal = opts[1].indexOf('o') != -1 && 1; 4960 if (decimal + hex + octal > 1) { return 'Invalid arguments'; } 4961 number = decimal && 'decimal' || hex && 'hex' || octal && 'octal'; 4962 } 4963 if (opts[2]) { 4964 pattern = new RegExp(opts[2].substr(1, opts[2].length - 2), ignoreCase ? 'i' : ''); 4965 } 4966 } 4967 } 4968 var err = parseArgs(); 4969 if (err) { 4970 showConfirm(cm, err + ': ' + params.argString); 4971 return; 4972 } 4973 var lineStart = params.line || cm.firstLine(); 4974 var lineEnd = params.lineEnd || params.line || cm.lastLine(); 4975 if (lineStart == lineEnd) { return; } 4976 var curStart = new Pos(lineStart, 0); 4977 var curEnd = new Pos(lineEnd, lineLength(cm, lineEnd)); 4978 var text = cm.getRange(curStart, curEnd).split('\n'); 4979 var numberRegex = pattern ? pattern : 4980 (number == 'decimal') ? /(-?)([\d]+)/ : 4981 (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i : 4982 (number == 'octal') ? /([0-7]+)/ : null; 4983 var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null; 4984 var numPart = [], textPart = []; 4985 if (number || pattern) { 4986 for (var i = 0; i < text.length; i++) { 4987 var matchPart = pattern ? text[i].match(pattern) : null; 4988 if (matchPart && matchPart[0] != '') { 4989 numPart.push(matchPart); 4990 } else if (!pattern && numberRegex.exec(text[i])) { 4991 numPart.push(text[i]); 4992 } else { 4993 textPart.push(text[i]); 4994 } 4995 } 4996 } else { 4997 textPart = text; 4998 } 4999 function compareFn(a, b) { 5000 if (reverse) { var tmp; tmp = a; a = b; b = tmp; } 5001 if (ignoreCase) { a = a.toLowerCase(); b = b.toLowerCase(); } 5002 var anum = number && numberRegex.exec(a); 5003 var bnum = number && numberRegex.exec(b); 5004 if (!anum) { return a < b ? -1 : 1; } 5005 anum = parseInt((anum[1] + anum[2]).toLowerCase(), radix); 5006 bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix); 5007 return anum - bnum; 5008 } 5009 function comparePatternFn(a, b) { 5010 if (reverse) { var tmp; tmp = a; a = b; b = tmp; } 5011 if (ignoreCase) { a[0] = a[0].toLowerCase(); b[0] = b[0].toLowerCase(); } 5012 return (a[0] < b[0]) ? -1 : 1; 5013 } 5014 numPart.sort(pattern ? comparePatternFn : compareFn); 5015 if (pattern) { 5016 for (var i = 0; i < numPart.length; i++) { 5017 numPart[i] = numPart[i].input; 5018 } 5019 } else if (!number) { textPart.sort(compareFn); } 5020 text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart); 5021 if (unique) { // Remove duplicate lines 5022 var textOld = text; 5023 var lastLine; 5024 text = []; 5025 for (var i = 0; i < textOld.length; i++) { 5026 if (textOld[i] != lastLine) { 5027 text.push(textOld[i]); 5028 } 5029 lastLine = textOld[i]; 5030 } 5031 } 5032 cm.replaceRange(text.join('\n'), curStart, curEnd); 5033 }, 5034 vglobal: function(cm, params) { 5035 // global inspects params.commandName 5036 this.global(cm, params); 5037 }, 5038 global: function(cm, params) { 5039 // a global command is of the form 5040 // :[range]g/pattern/[cmd] 5041 // argString holds the string /pattern/[cmd] 5042 var argString = params.argString; 5043 if (!argString) { 5044 showConfirm(cm, 'Regular Expression missing from global'); 5045 return; 5046 } 5047 var inverted = params.commandName[0] === 'v'; 5048 // range is specified here 5049 var lineStart = (params.line !== undefined) ? params.line : cm.firstLine(); 5050 var lineEnd = params.lineEnd || params.line || cm.lastLine(); 5051 // get the tokens from argString 5052 var tokens = splitBySlash(argString); 5053 var regexPart = argString, cmd; 5054 if (tokens.length) { 5055 regexPart = tokens[0]; 5056 cmd = tokens.slice(1, tokens.length).join('/'); 5057 } 5058 if (regexPart) { 5059 // If regex part is empty, then use the previous query. Otherwise 5060 // use the regex part as the new query. 5061 try { 5062 updateSearchQuery(cm, regexPart, true /** ignoreCase */, 5063 true /** smartCase */); 5064 } catch (e) { 5065 showConfirm(cm, 'Invalid regex: ' + regexPart); 5066 return; 5067 } 5068 } 5069 // now that we have the regexPart, search for regex matches in the 5070 // specified range of lines 5071 var query = getSearchState(cm).getQuery(); 5072 var matchedLines = []; 5073 for (var i = lineStart; i <= lineEnd; i++) { 5074 var line = cm.getLineHandle(i); 5075 var matched = query.test(line.text); 5076 if (matched !== inverted) { 5077 matchedLines.push(cmd ? line : line.text); 5078 } 5079 } 5080 // if there is no [cmd], just display the list of matched lines 5081 if (!cmd) { 5082 showConfirm(cm, matchedLines.join('\n')); 5083 return; 5084 } 5085 var index = 0; 5086 var nextCommand = function() { 5087 if (index < matchedLines.length) { 5088 var line = matchedLines[index++]; 5089 var lineNum = cm.getLineNumber(line); 5090 if (lineNum == null) { 5091 nextCommand(); 5092 return; 5093 } 5094 var command = (lineNum + 1) + cmd; 5095 exCommandDispatcher.processCommand(cm, command, { 5096 callback: nextCommand 5097 }); 5098 } 5099 }; 5100 nextCommand(); 5101 }, 5102 substitute: function(cm, params) { 5103 if (!cm.getSearchCursor) { 5104 throw new Error('Search feature not available. Requires searchcursor.js or ' + 5105 'any other getSearchCursor implementation.'); 5106 } 5107 var argString = params.argString; 5108 var tokens = argString ? splitBySeparator(argString, argString[0]) : []; 5109 var regexPart, replacePart = '', trailing, flagsPart, count; 5110 var confirm = false; // Whether to confirm each replace. 5111 var global = false; // True to replace all instances on a line, false to replace only 1. 5112 if (tokens.length) { 5113 regexPart = tokens[0]; 5114 if (getOption('pcre') && regexPart !== '') { 5115 regexPart = new RegExp(regexPart).source; //normalize not escaped characters 5116 } 5117 replacePart = tokens[1]; 5118 if (replacePart !== undefined) { 5119 if (getOption('pcre')) { 5120 replacePart = unescapeRegexReplace(replacePart.replace(/([^\\])&/g,"$1$$&")); 5121 } else { 5122 replacePart = translateRegexReplace(replacePart); 5123 } 5124 vimGlobalState.lastSubstituteReplacePart = replacePart; 5125 } 5126 trailing = tokens[2] ? tokens[2].split(' ') : []; 5127 } else { 5128 // either the argString is empty or its of the form ' hello/world' 5129 // actually splitBySlash returns a list of tokens 5130 // only if the string starts with a '/' 5131 if (argString && argString.length) { 5132 showConfirm(cm, 'Substitutions should be of the form ' + 5133 ':s/pattern/replace/'); 5134 return; 5135 } 5136 } 5137 // After the 3rd slash, we can have flags followed by a space followed 5138 // by count. 5139 if (trailing) { 5140 flagsPart = trailing[0]; 5141 count = parseInt(trailing[1]); 5142 if (flagsPart) { 5143 if (flagsPart.indexOf('c') != -1) { 5144 confirm = true; 5145 } 5146 if (flagsPart.indexOf('g') != -1) { 5147 global = true; 5148 } 5149 if (getOption('pcre')) { 5150 regexPart = regexPart + '/' + flagsPart; 5151 } else { 5152 regexPart = regexPart.replace(/\//g, "\\/") + '/' + flagsPart; 5153 } 5154 } 5155 } 5156 if (regexPart) { 5157 // If regex part is empty, then use the previous query. Otherwise use 5158 // the regex part as the new query. 5159 try { 5160 updateSearchQuery(cm, regexPart, true /** ignoreCase */, 5161 true /** smartCase */); 5162 } catch (e) { 5163 showConfirm(cm, 'Invalid regex: ' + regexPart); 5164 return; 5165 } 5166 } 5167 replacePart = replacePart || vimGlobalState.lastSubstituteReplacePart; 5168 if (replacePart === undefined) { 5169 showConfirm(cm, 'No previous substitute regular expression'); 5170 return; 5171 } 5172 var state = getSearchState(cm); 5173 var query = state.getQuery(); 5174 var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line; 5175 var lineEnd = params.lineEnd || lineStart; 5176 if (lineStart == cm.firstLine() && lineEnd == cm.lastLine()) { 5177 lineEnd = Infinity; 5178 } 5179 if (count) { 5180 lineStart = lineEnd; 5181 lineEnd = lineStart + count - 1; 5182 } 5183 var startPos = clipCursorToContent(cm, new Pos(lineStart, 0)); 5184 var cursor = cm.getSearchCursor(query, startPos); 5185 doReplace(cm, confirm, global, lineStart, lineEnd, cursor, query, replacePart, params.callback); 5186 }, 5187 redo: CodeMirror.commands.redo, 5188 undo: CodeMirror.commands.undo, 5189 write: function(cm) { 5190 if (CodeMirror.commands.save) { 5191 // If a save command is defined, call it. 5192 CodeMirror.commands.save(cm); 5193 } else if (cm.save) { 5194 // Saves to text area if no save command is defined and cm.save() is available. 5195 cm.save(); 5196 } 5197 }, 5198 nohlsearch: function(cm) { 5199 clearSearchHighlight(cm); 5200 }, 5201 yank: function (cm) { 5202 var cur = copyCursor(cm.getCursor()); 5203 var line = cur.line; 5204 var lineText = cm.getLine(line); 5205 vimGlobalState.registerController.pushText( 5206 '0', 'yank', lineText, true, true); 5207 }, 5208 delmarks: function(cm, params) { 5209 if (!params.argString || !trim(params.argString)) { 5210 showConfirm(cm, 'Argument required'); 5211 return; 5212 } 5213 5214 var state = cm.state.vim; 5215 var stream = new CodeMirror.StringStream(trim(params.argString)); 5216 while (!stream.eol()) { 5217 stream.eatSpace(); 5218 5219 // Record the streams position at the beginning of the loop for use 5220 // in error messages. 5221 var count = stream.pos; 5222 5223 if (!stream.match(/[a-zA-Z]/, false)) { 5224 showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); 5225 return; 5226 } 5227 5228 var sym = stream.next(); 5229 // Check if this symbol is part of a range 5230 if (stream.match('-', true)) { 5231 // This symbol is part of a range. 5232 5233 // The range must terminate at an alphabetic character. 5234 if (!stream.match(/[a-zA-Z]/, false)) { 5235 showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); 5236 return; 5237 } 5238 5239 var startMark = sym; 5240 var finishMark = stream.next(); 5241 // The range must terminate at an alphabetic character which 5242 // shares the same case as the start of the range. 5243 if (isLowerCase(startMark) && isLowerCase(finishMark) || 5244 isUpperCase(startMark) && isUpperCase(finishMark)) { 5245 var start = startMark.charCodeAt(0); 5246 var finish = finishMark.charCodeAt(0); 5247 if (start >= finish) { 5248 showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); 5249 return; 5250 } 5251 5252 // Because marks are always ASCII values, and we have 5253 // determined that they are the same case, we can use 5254 // their char codes to iterate through the defined range. 5255 for (var j = 0; j <= finish - start; j++) { 5256 var mark = String.fromCharCode(start + j); 5257 delete state.marks[mark]; 5258 } 5259 } else { 5260 showConfirm(cm, 'Invalid argument: ' + startMark + '-'); 5261 return; 5262 } 5263 } else { 5264 // This symbol is a valid mark, and is not part of a range. 5265 delete state.marks[sym]; 5266 } 5267 } 5268 } 5269 }; 5270 5271 var exCommandDispatcher = new ExCommandDispatcher(); 5272 5273 /** 5274 * @param {CodeMirror} cm CodeMirror instance we are in. 5275 * @param {boolean} confirm Whether to confirm each replace. 5276 * @param {Cursor} lineStart Line to start replacing from. 5277 * @param {Cursor} lineEnd Line to stop replacing at. 5278 * @param {RegExp} query Query for performing matches with. 5279 * @param {string} replaceWith Text to replace matches with. May contain $1, 5280 * $2, etc for replacing captured groups using JavaScript replace. 5281 * @param {function()} callback A callback for when the replace is done. 5282 */ 5283 function doReplace(cm, confirm, global, lineStart, lineEnd, searchCursor, query, 5284 replaceWith, callback) { 5285 // Set up all the functions. 5286 cm.state.vim.exMode = true; 5287 var done = false; 5288 var lastPos, modifiedLineNumber, joined; 5289 function replaceAll() { 5290 cm.operation(function() { 5291 while (!done) { 5292 replace(); 5293 next(); 5294 } 5295 stop(); 5296 }); 5297 } 5298 function replace() { 5299 var text = cm.getRange(searchCursor.from(), searchCursor.to()); 5300 var newText = text.replace(query, replaceWith); 5301 var unmodifiedLineNumber = searchCursor.to().line; 5302 searchCursor.replace(newText); 5303 modifiedLineNumber = searchCursor.to().line; 5304 lineEnd += modifiedLineNumber - unmodifiedLineNumber; 5305 joined = modifiedLineNumber < unmodifiedLineNumber; 5306 } 5307 function findNextValidMatch() { 5308 var lastMatchTo = lastPos && copyCursor(searchCursor.to()); 5309 var match = searchCursor.findNext(); 5310 if (match && !match[0] && lastMatchTo && cursorEqual(searchCursor.from(), lastMatchTo)) { 5311 match = searchCursor.findNext(); 5312 } 5313 return match; 5314 } 5315 function next() { 5316 // The below only loops to skip over multiple occurrences on the same 5317 // line when 'global' is not true. 5318 while(findNextValidMatch() && 5319 isInRange(searchCursor.from(), lineStart, lineEnd)) { 5320 if (!global && searchCursor.from().line == modifiedLineNumber && !joined) { 5321 continue; 5322 } 5323 cm.scrollIntoView(searchCursor.from(), 30); 5324 cm.setSelection(searchCursor.from(), searchCursor.to()); 5325 lastPos = searchCursor.from(); 5326 done = false; 5327 return; 5328 } 5329 done = true; 5330 } 5331 function stop(close) { 5332 if (close) { close(); } 5333 cm.focus(); 5334 if (lastPos) { 5335 cm.setCursor(lastPos); 5336 var vim = cm.state.vim; 5337 vim.exMode = false; 5338 vim.lastHPos = vim.lastHSPos = lastPos.ch; 5339 } 5340 if (callback) { callback(); } 5341 } 5342 function onPromptKeyDown(e, _value, close) { 5343 // Swallow all keys. 5344 CodeMirror.e_stop(e); 5345 var keyName = CodeMirror.keyName(e); 5346 switch (keyName) { 5347 case 'Y': 5348 replace(); next(); break; 5349 case 'N': 5350 next(); break; 5351 case 'A': 5352 // replaceAll contains a call to close of its own. We don't want it 5353 // to fire too early or multiple times. 5354 var savedCallback = callback; 5355 callback = undefined; 5356 cm.operation(replaceAll); 5357 callback = savedCallback; 5358 break; 5359 case 'L': 5360 replace(); 5361 // fall through and exit. 5362 case 'Q': 5363 case 'Esc': 5364 case 'Ctrl-C': 5365 case 'Ctrl-[': 5366 stop(close); 5367 break; 5368 } 5369 if (done) { stop(close); } 5370 return true; 5371 } 5372 5373 // Actually do replace. 5374 next(); 5375 if (done) { 5376 showConfirm(cm, 'No matches for ' + query.source); 5377 return; 5378 } 5379 if (!confirm) { 5380 replaceAll(); 5381 if (callback) { callback(); } 5382 return; 5383 } 5384 showPrompt(cm, { 5385 prefix: dom('span', 'replace with ', dom('strong', replaceWith), ' (y/n/a/q/l)'), 5386 onKeyDown: onPromptKeyDown 5387 }); 5388 } 5389 5390 CodeMirror.keyMap.vim = { 5391 attach: attachVimMap, 5392 detach: detachVimMap, 5393 call: cmKey 5394 }; 5395 5396 function exitInsertMode(cm) { 5397 var vim = cm.state.vim; 5398 var macroModeState = vimGlobalState.macroModeState; 5399 var insertModeChangeRegister = vimGlobalState.registerController.getRegister('.'); 5400 var isPlaying = macroModeState.isPlaying; 5401 var lastChange = macroModeState.lastInsertModeChanges; 5402 if (!isPlaying) { 5403 cm.off('change', onChange); 5404 CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); 5405 } 5406 if (!isPlaying && vim.insertModeRepeat > 1) { 5407 // Perform insert mode repeat for commands like 3,a and 3,o. 5408 repeatLastEdit(cm, vim, vim.insertModeRepeat - 1, 5409 true /** repeatForInsert */); 5410 vim.lastEditInputState.repeatOverride = vim.insertModeRepeat; 5411 } 5412 delete vim.insertModeRepeat; 5413 vim.insertMode = false; 5414 cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1); 5415 cm.setOption('keyMap', 'vim'); 5416 cm.setOption('disableInput', true); 5417 cm.toggleOverwrite(false); // exit replace mode if we were in it. 5418 // update the ". register before exiting insert mode 5419 insertModeChangeRegister.setText(lastChange.changes.join('')); 5420 CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); 5421 if (macroModeState.isRecording) { 5422 logInsertModeChange(macroModeState); 5423 } 5424 } 5425 5426 function _mapCommand(command) { 5427 defaultKeymap.unshift(command); 5428 } 5429 5430 function mapCommand(keys, type, name, args, extra) { 5431 var command = {keys: keys, type: type}; 5432 command[type] = name; 5433 command[type + "Args"] = args; 5434 for (var key in extra) 5435 command[key] = extra[key]; 5436 _mapCommand(command); 5437 } 5438 5439 // The timeout in milliseconds for the two-character ESC keymap should be 5440 // adjusted according to your typing speed to prevent false positives. 5441 defineOption('insertModeEscKeysTimeout', 200, 'number'); 5442 5443 CodeMirror.keyMap['vim-insert'] = { 5444 // TODO: override navigation keys so that Esc will cancel automatic 5445 // indentation from o, O, i_<CR> 5446 fallthrough: ['default'], 5447 attach: attachVimMap, 5448 detach: detachVimMap, 5449 call: cmKey 5450 }; 5451 5452 CodeMirror.keyMap['vim-replace'] = { 5453 'Backspace': 'goCharLeft', 5454 fallthrough: ['vim-insert'], 5455 attach: attachVimMap, 5456 detach: detachVimMap, 5457 call: cmKey 5458 }; 5459 5460 function executeMacroRegister(cm, vim, macroModeState, registerName) { 5461 var register = vimGlobalState.registerController.getRegister(registerName); 5462 if (registerName == ':') { 5463 // Read-only register containing last Ex command. 5464 if (register.keyBuffer[0]) { 5465 exCommandDispatcher.processCommand(cm, register.keyBuffer[0]); 5466 } 5467 macroModeState.isPlaying = false; 5468 return; 5469 } 5470 var keyBuffer = register.keyBuffer; 5471 var imc = 0; 5472 macroModeState.isPlaying = true; 5473 macroModeState.replaySearchQueries = register.searchQueries.slice(0); 5474 for (var i = 0; i < keyBuffer.length; i++) { 5475 var text = keyBuffer[i]; 5476 var match, key; 5477 while (text) { 5478 // Pull off one command key, which is either a single character 5479 // or a special sequence wrapped in '<' and '>', e.g. '<Space>'. 5480 match = (/<\w+-.+?>|<\w+>|./).exec(text); 5481 key = match[0]; 5482 text = text.substring(match.index + key.length); 5483 vimApi.handleKey(cm, key, 'macro'); 5484 if (vim.insertMode) { 5485 var changes = register.insertModeChanges[imc++].changes; 5486 vimGlobalState.macroModeState.lastInsertModeChanges.changes = 5487 changes; 5488 repeatInsertModeChanges(cm, changes, 1); 5489 exitInsertMode(cm); 5490 } 5491 } 5492 } 5493 macroModeState.isPlaying = false; 5494 } 5495 5496 function logKey(macroModeState, key) { 5497 if (macroModeState.isPlaying) { return; } 5498 var registerName = macroModeState.latestRegister; 5499 var register = vimGlobalState.registerController.getRegister(registerName); 5500 if (register) { 5501 register.pushText(key); 5502 } 5503 } 5504 5505 function logInsertModeChange(macroModeState) { 5506 if (macroModeState.isPlaying) { return; } 5507 var registerName = macroModeState.latestRegister; 5508 var register = vimGlobalState.registerController.getRegister(registerName); 5509 if (register && register.pushInsertModeChanges) { 5510 register.pushInsertModeChanges(macroModeState.lastInsertModeChanges); 5511 } 5512 } 5513 5514 function logSearchQuery(macroModeState, query) { 5515 if (macroModeState.isPlaying) { return; } 5516 var registerName = macroModeState.latestRegister; 5517 var register = vimGlobalState.registerController.getRegister(registerName); 5518 if (register && register.pushSearchQuery) { 5519 register.pushSearchQuery(query); 5520 } 5521 } 5522 5523 /** 5524 * Listens for changes made in insert mode. 5525 * Should only be active in insert mode. 5526 */ 5527 function onChange(cm, changeObj) { 5528 var macroModeState = vimGlobalState.macroModeState; 5529 var lastChange = macroModeState.lastInsertModeChanges; 5530 if (!macroModeState.isPlaying) { 5531 while(changeObj) { 5532 lastChange.expectCursorActivityForChange = true; 5533 if (lastChange.ignoreCount > 1) { 5534 lastChange.ignoreCount--; 5535 } else if (changeObj.origin == '+input' || changeObj.origin == 'paste' 5536 || changeObj.origin === undefined /* only in testing */) { 5537 var selectionCount = cm.listSelections().length; 5538 if (selectionCount > 1) 5539 lastChange.ignoreCount = selectionCount; 5540 var text = changeObj.text.join('\n'); 5541 if (lastChange.maybeReset) { 5542 lastChange.changes = []; 5543 lastChange.maybeReset = false; 5544 } 5545 if (text) { 5546 if (cm.state.overwrite && !/\n/.test(text)) { 5547 lastChange.changes.push([text]); 5548 } else { 5549 lastChange.changes.push(text); 5550 } 5551 } 5552 } 5553 // Change objects may be chained with next. 5554 changeObj = changeObj.next; 5555 } 5556 } 5557 } 5558 5559 /** 5560 * Listens for any kind of cursor activity on CodeMirror. 5561 */ 5562 function onCursorActivity(cm) { 5563 var vim = cm.state.vim; 5564 if (vim.insertMode) { 5565 // Tracking cursor activity in insert mode (for macro support). 5566 var macroModeState = vimGlobalState.macroModeState; 5567 if (macroModeState.isPlaying) { return; } 5568 var lastChange = macroModeState.lastInsertModeChanges; 5569 if (lastChange.expectCursorActivityForChange) { 5570 lastChange.expectCursorActivityForChange = false; 5571 } else { 5572 // Cursor moved outside the context of an edit. Reset the change. 5573 lastChange.maybeReset = true; 5574 } 5575 } else if (!cm.curOp.isVimOp) { 5576 handleExternalSelection(cm, vim); 5577 } 5578 } 5579 function handleExternalSelection(cm, vim) { 5580 var anchor = cm.getCursor('anchor'); 5581 var head = cm.getCursor('head'); 5582 // Enter or exit visual mode to match mouse selection. 5583 if (vim.visualMode && !cm.somethingSelected()) { 5584 exitVisualMode(cm, false); 5585 } else if (!vim.visualMode && !vim.insertMode && cm.somethingSelected()) { 5586 vim.visualMode = true; 5587 vim.visualLine = false; 5588 CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"}); 5589 } 5590 if (vim.visualMode) { 5591 // Bind CodeMirror selection model to vim selection model. 5592 // Mouse selections are considered visual characterwise. 5593 var headOffset = !cursorIsBefore(head, anchor) ? -1 : 0; 5594 var anchorOffset = cursorIsBefore(head, anchor) ? -1 : 0; 5595 head = offsetCursor(head, 0, headOffset); 5596 anchor = offsetCursor(anchor, 0, anchorOffset); 5597 vim.sel = { 5598 anchor: anchor, 5599 head: head 5600 }; 5601 updateMark(cm, vim, '<', cursorMin(head, anchor)); 5602 updateMark(cm, vim, '>', cursorMax(head, anchor)); 5603 } else if (!vim.insertMode) { 5604 // Reset lastHPos if selection was modified by something outside of vim mode e.g. by mouse. 5605 vim.lastHPos = cm.getCursor().ch; 5606 } 5607 } 5608 5609 /** Wrapper for special keys pressed in insert mode */ 5610 function InsertModeKey(keyName) { 5611 this.keyName = keyName; 5612 } 5613 5614 /** 5615 * Handles raw key down events from the text area. 5616 * - Should only be active in insert mode. 5617 * - For recording deletes in insert mode. 5618 */ 5619 function onKeyEventTargetKeyDown(e) { 5620 var macroModeState = vimGlobalState.macroModeState; 5621 var lastChange = macroModeState.lastInsertModeChanges; 5622 var keyName = CodeMirror.keyName(e); 5623 if (!keyName) { return; } 5624 function onKeyFound() { 5625 if (lastChange.maybeReset) { 5626 lastChange.changes = []; 5627 lastChange.maybeReset = false; 5628 } 5629 lastChange.changes.push(new InsertModeKey(keyName)); 5630 return true; 5631 } 5632 if (keyName.indexOf('Delete') != -1 || keyName.indexOf('Backspace') != -1) { 5633 CodeMirror.lookupKey(keyName, 'vim-insert', onKeyFound); 5634 } 5635 } 5636 5637 /** 5638 * Repeats the last edit, which includes exactly 1 command and at most 1 5639 * insert. Operator and motion commands are read from lastEditInputState, 5640 * while action commands are read from lastEditActionCommand. 5641 * 5642 * If repeatForInsert is true, then the function was called by 5643 * exitInsertMode to repeat the insert mode changes the user just made. The 5644 * corresponding enterInsertMode call was made with a count. 5645 */ 5646 function repeatLastEdit(cm, vim, repeat, repeatForInsert) { 5647 var macroModeState = vimGlobalState.macroModeState; 5648 macroModeState.isPlaying = true; 5649 var isAction = !!vim.lastEditActionCommand; 5650 var cachedInputState = vim.inputState; 5651 function repeatCommand() { 5652 if (isAction) { 5653 commandDispatcher.processAction(cm, vim, vim.lastEditActionCommand); 5654 } else { 5655 commandDispatcher.evalInput(cm, vim); 5656 } 5657 } 5658 function repeatInsert(repeat) { 5659 if (macroModeState.lastInsertModeChanges.changes.length > 0) { 5660 // For some reason, repeat cw in desktop VIM does not repeat 5661 // insert mode changes. Will conform to that behavior. 5662 repeat = !vim.lastEditActionCommand ? 1 : repeat; 5663 var changeObject = macroModeState.lastInsertModeChanges; 5664 repeatInsertModeChanges(cm, changeObject.changes, repeat); 5665 } 5666 } 5667 vim.inputState = vim.lastEditInputState; 5668 if (isAction && vim.lastEditActionCommand.interlaceInsertRepeat) { 5669 // o and O repeat have to be interlaced with insert repeats so that the 5670 // insertions appear on separate lines instead of the last line. 5671 for (var i = 0; i < repeat; i++) { 5672 repeatCommand(); 5673 repeatInsert(1); 5674 } 5675 } else { 5676 if (!repeatForInsert) { 5677 // Hack to get the cursor to end up at the right place. If I is 5678 // repeated in insert mode repeat, cursor will be 1 insert 5679 // change set left of where it should be. 5680 repeatCommand(); 5681 } 5682 repeatInsert(repeat); 5683 } 5684 vim.inputState = cachedInputState; 5685 if (vim.insertMode && !repeatForInsert) { 5686 // Don't exit insert mode twice. If repeatForInsert is set, then we 5687 // were called by an exitInsertMode call lower on the stack. 5688 exitInsertMode(cm); 5689 } 5690 macroModeState.isPlaying = false; 5691 } 5692 5693 function repeatInsertModeChanges(cm, changes, repeat) { 5694 function keyHandler(binding) { 5695 if (typeof binding == 'string') { 5696 CodeMirror.commands[binding](cm); 5697 } else { 5698 binding(cm); 5699 } 5700 return true; 5701 } 5702 var head = cm.getCursor('head'); 5703 var visualBlock = vimGlobalState.macroModeState.lastInsertModeChanges.visualBlock; 5704 if (visualBlock) { 5705 // Set up block selection again for repeating the changes. 5706 selectForInsert(cm, head, visualBlock + 1); 5707 repeat = cm.listSelections().length; 5708 cm.setCursor(head); 5709 } 5710 for (var i = 0; i < repeat; i++) { 5711 if (visualBlock) { 5712 cm.setCursor(offsetCursor(head, i, 0)); 5713 } 5714 for (var j = 0; j < changes.length; j++) { 5715 var change = changes[j]; 5716 if (change instanceof InsertModeKey) { 5717 CodeMirror.lookupKey(change.keyName, 'vim-insert', keyHandler); 5718 } else if (typeof change == "string") { 5719 cm.replaceSelection(change); 5720 } else { 5721 var start = cm.getCursor(); 5722 var end = offsetCursor(start, 0, change[0].length); 5723 cm.replaceRange(change[0], start, end); 5724 cm.setCursor(end); 5725 } 5726 } 5727 } 5728 if (visualBlock) { 5729 cm.setCursor(offsetCursor(head, 0, 1)); 5730 } 5731 } 5732 5733 resetVimGlobalState(); 5734 return vimApi; 5735 }; 5736 // Initialize Vim and make it available as an API. 5737 CodeMirror.Vim = Vim(); 5738 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |