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