File modules/editor/codemirror/keymap/vim.min.js

Last commit: Tue May 22 22:39:55 2018 +0200	Jan Dankert	Fix für PHP 7.2: 'Object' darf nun nicht mehr als Klassennamen verwendet werden. AUCH NICHT IN EINEM NAMESPACE! WTF, wozu habe ich das in einen verfickten Namespace gepackt? Wozu soll der sonst da sein??? Amateure. Daher nun notgedrungen unbenannt in 'BaseObject'.
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 });
Download modules/editor/codemirror/keymap/vim.min.js
History Tue, 22 May 2018 22:39:55 +0200 Jan Dankert Fix für PHP 7.2: 'Object' darf nun nicht mehr als Klassennamen verwendet werden. AUCH NICHT IN EINEM NAMESPACE! WTF, wozu habe ich das in einen verfickten Namespace gepackt? Wozu soll der sonst da sein??? Amateure. Daher nun notgedrungen unbenannt in 'BaseObject'.