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