browser/devtools/sourceeditor/codemirror/keymap/vim.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /**
michael@0 2 * Supported keybindings:
michael@0 3 *
michael@0 4 * Motion:
michael@0 5 * h, j, k, l
michael@0 6 * gj, gk
michael@0 7 * e, E, w, W, b, B, ge, gE
michael@0 8 * f<character>, F<character>, t<character>, T<character>
michael@0 9 * $, ^, 0, -, +, _
michael@0 10 * gg, G
michael@0 11 * %
michael@0 12 * '<character>, `<character>
michael@0 13 *
michael@0 14 * Operator:
michael@0 15 * d, y, c
michael@0 16 * dd, yy, cc
michael@0 17 * g~, g~g~
michael@0 18 * >, <, >>, <<
michael@0 19 *
michael@0 20 * Operator-Motion:
michael@0 21 * x, X, D, Y, C, ~
michael@0 22 *
michael@0 23 * Action:
michael@0 24 * a, i, s, A, I, S, o, O
michael@0 25 * zz, z., z<CR>, zt, zb, z-
michael@0 26 * J
michael@0 27 * u, Ctrl-r
michael@0 28 * m<character>
michael@0 29 * r<character>
michael@0 30 *
michael@0 31 * Modes:
michael@0 32 * ESC - leave insert mode, visual mode, and clear input state.
michael@0 33 * Ctrl-[, Ctrl-c - same as ESC.
michael@0 34 *
michael@0 35 * Registers: unnamed, -, a-z, A-Z, 0-9
michael@0 36 * (Does not respect the special case for number registers when delete
michael@0 37 * operator is made with these commands: %, (, ), , /, ?, n, N, {, } )
michael@0 38 * TODO: Implement the remaining registers.
michael@0 39 * Marks: a-z, A-Z, and 0-9
michael@0 40 * TODO: Implement the remaining special marks. They have more complex
michael@0 41 * behavior.
michael@0 42 *
michael@0 43 * Events:
michael@0 44 * 'vim-mode-change' - raised on the editor anytime the current mode changes,
michael@0 45 * Event object: {mode: "visual", subMode: "linewise"}
michael@0 46 *
michael@0 47 * Code structure:
michael@0 48 * 1. Default keymap
michael@0 49 * 2. Variable declarations and short basic helpers
michael@0 50 * 3. Instance (External API) implementation
michael@0 51 * 4. Internal state tracking objects (input state, counter) implementation
michael@0 52 * and instanstiation
michael@0 53 * 5. Key handler (the main command dispatcher) implementation
michael@0 54 * 6. Motion, operator, and action implementations
michael@0 55 * 7. Helper functions for the key handler, motions, operators, and actions
michael@0 56 * 8. Set up Vim to work as a keymap for CodeMirror.
michael@0 57 */
michael@0 58
michael@0 59 (function(mod) {
michael@0 60 if (typeof exports == "object" && typeof module == "object") // CommonJS
michael@0 61 mod(require("../lib/codemirror"), require("../addon/search/searchcursor"), require("../addon/dialog/dialog"));
michael@0 62 else if (typeof define == "function" && define.amd) // AMD
michael@0 63 define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/dialog/dialog"], mod);
michael@0 64 else // Plain browser env
michael@0 65 mod(CodeMirror);
michael@0 66 })(function(CodeMirror) {
michael@0 67 'use strict';
michael@0 68
michael@0 69 var defaultKeymap = [
michael@0 70 // Key to key mapping. This goes first to make it possible to override
michael@0 71 // existing mappings.
michael@0 72 { keys: ['<Left>'], type: 'keyToKey', toKeys: ['h'] },
michael@0 73 { keys: ['<Right>'], type: 'keyToKey', toKeys: ['l'] },
michael@0 74 { keys: ['<Up>'], type: 'keyToKey', toKeys: ['k'] },
michael@0 75 { keys: ['<Down>'], type: 'keyToKey', toKeys: ['j'] },
michael@0 76 { keys: ['<Space>'], type: 'keyToKey', toKeys: ['l'] },
michael@0 77 { keys: ['<BS>'], type: 'keyToKey', toKeys: ['h'] },
michael@0 78 { keys: ['<C-Space>'], type: 'keyToKey', toKeys: ['W'] },
michael@0 79 { keys: ['<C-BS>'], type: 'keyToKey', toKeys: ['B'] },
michael@0 80 { keys: ['<S-Space>'], type: 'keyToKey', toKeys: ['w'] },
michael@0 81 { keys: ['<S-BS>'], type: 'keyToKey', toKeys: ['b'] },
michael@0 82 { keys: ['<C-n>'], type: 'keyToKey', toKeys: ['j'] },
michael@0 83 { keys: ['<C-p>'], type: 'keyToKey', toKeys: ['k'] },
michael@0 84 { keys: ['<C-[>'], type: 'keyToKey', toKeys: ['<Esc>'] },
michael@0 85 { keys: ['<C-c>'], type: 'keyToKey', toKeys: ['<Esc>'] },
michael@0 86 { keys: ['s'], type: 'keyToKey', toKeys: ['c', 'l'], context: 'normal' },
michael@0 87 { keys: ['s'], type: 'keyToKey', toKeys: ['x', 'i'], context: 'visual'},
michael@0 88 { keys: ['S'], type: 'keyToKey', toKeys: ['c', 'c'], context: 'normal' },
michael@0 89 { keys: ['S'], type: 'keyToKey', toKeys: ['d', 'c', 'c'], context: 'visual' },
michael@0 90 { keys: ['<Home>'], type: 'keyToKey', toKeys: ['0'] },
michael@0 91 { keys: ['<End>'], type: 'keyToKey', toKeys: ['$'] },
michael@0 92 { keys: ['<PageUp>'], type: 'keyToKey', toKeys: ['<C-b>'] },
michael@0 93 { keys: ['<PageDown>'], type: 'keyToKey', toKeys: ['<C-f>'] },
michael@0 94 { keys: ['<CR>'], type: 'keyToKey', toKeys: ['j', '^'], context: 'normal' },
michael@0 95 // Motions
michael@0 96 { keys: ['H'], type: 'motion',
michael@0 97 motion: 'moveToTopLine',
michael@0 98 motionArgs: { linewise: true, toJumplist: true }},
michael@0 99 { keys: ['M'], type: 'motion',
michael@0 100 motion: 'moveToMiddleLine',
michael@0 101 motionArgs: { linewise: true, toJumplist: true }},
michael@0 102 { keys: ['L'], type: 'motion',
michael@0 103 motion: 'moveToBottomLine',
michael@0 104 motionArgs: { linewise: true, toJumplist: true }},
michael@0 105 { keys: ['h'], type: 'motion',
michael@0 106 motion: 'moveByCharacters',
michael@0 107 motionArgs: { forward: false }},
michael@0 108 { keys: ['l'], type: 'motion',
michael@0 109 motion: 'moveByCharacters',
michael@0 110 motionArgs: { forward: true }},
michael@0 111 { keys: ['j'], type: 'motion',
michael@0 112 motion: 'moveByLines',
michael@0 113 motionArgs: { forward: true, linewise: true }},
michael@0 114 { keys: ['k'], type: 'motion',
michael@0 115 motion: 'moveByLines',
michael@0 116 motionArgs: { forward: false, linewise: true }},
michael@0 117 { keys: ['g','j'], type: 'motion',
michael@0 118 motion: 'moveByDisplayLines',
michael@0 119 motionArgs: { forward: true }},
michael@0 120 { keys: ['g','k'], type: 'motion',
michael@0 121 motion: 'moveByDisplayLines',
michael@0 122 motionArgs: { forward: false }},
michael@0 123 { keys: ['w'], type: 'motion',
michael@0 124 motion: 'moveByWords',
michael@0 125 motionArgs: { forward: true, wordEnd: false }},
michael@0 126 { keys: ['W'], type: 'motion',
michael@0 127 motion: 'moveByWords',
michael@0 128 motionArgs: { forward: true, wordEnd: false, bigWord: true }},
michael@0 129 { keys: ['e'], type: 'motion',
michael@0 130 motion: 'moveByWords',
michael@0 131 motionArgs: { forward: true, wordEnd: true, inclusive: true }},
michael@0 132 { keys: ['E'], type: 'motion',
michael@0 133 motion: 'moveByWords',
michael@0 134 motionArgs: { forward: true, wordEnd: true, bigWord: true,
michael@0 135 inclusive: true }},
michael@0 136 { keys: ['b'], type: 'motion',
michael@0 137 motion: 'moveByWords',
michael@0 138 motionArgs: { forward: false, wordEnd: false }},
michael@0 139 { keys: ['B'], type: 'motion',
michael@0 140 motion: 'moveByWords',
michael@0 141 motionArgs: { forward: false, wordEnd: false, bigWord: true }},
michael@0 142 { keys: ['g', 'e'], type: 'motion',
michael@0 143 motion: 'moveByWords',
michael@0 144 motionArgs: { forward: false, wordEnd: true, inclusive: true }},
michael@0 145 { keys: ['g', 'E'], type: 'motion',
michael@0 146 motion: 'moveByWords',
michael@0 147 motionArgs: { forward: false, wordEnd: true, bigWord: true,
michael@0 148 inclusive: true }},
michael@0 149 { keys: ['{'], type: 'motion', motion: 'moveByParagraph',
michael@0 150 motionArgs: { forward: false, toJumplist: true }},
michael@0 151 { keys: ['}'], type: 'motion', motion: 'moveByParagraph',
michael@0 152 motionArgs: { forward: true, toJumplist: true }},
michael@0 153 { keys: ['<C-f>'], type: 'motion',
michael@0 154 motion: 'moveByPage', motionArgs: { forward: true }},
michael@0 155 { keys: ['<C-b>'], type: 'motion',
michael@0 156 motion: 'moveByPage', motionArgs: { forward: false }},
michael@0 157 { keys: ['<C-d>'], type: 'motion',
michael@0 158 motion: 'moveByScroll',
michael@0 159 motionArgs: { forward: true, explicitRepeat: true }},
michael@0 160 { keys: ['<C-u>'], type: 'motion',
michael@0 161 motion: 'moveByScroll',
michael@0 162 motionArgs: { forward: false, explicitRepeat: true }},
michael@0 163 { keys: ['g', 'g'], type: 'motion',
michael@0 164 motion: 'moveToLineOrEdgeOfDocument',
michael@0 165 motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }},
michael@0 166 { keys: ['G'], type: 'motion',
michael@0 167 motion: 'moveToLineOrEdgeOfDocument',
michael@0 168 motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }},
michael@0 169 { keys: ['0'], type: 'motion', motion: 'moveToStartOfLine' },
michael@0 170 { keys: ['^'], type: 'motion',
michael@0 171 motion: 'moveToFirstNonWhiteSpaceCharacter' },
michael@0 172 { keys: ['+'], type: 'motion',
michael@0 173 motion: 'moveByLines',
michael@0 174 motionArgs: { forward: true, toFirstChar:true }},
michael@0 175 { keys: ['-'], type: 'motion',
michael@0 176 motion: 'moveByLines',
michael@0 177 motionArgs: { forward: false, toFirstChar:true }},
michael@0 178 { keys: ['_'], type: 'motion',
michael@0 179 motion: 'moveByLines',
michael@0 180 motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }},
michael@0 181 { keys: ['$'], type: 'motion',
michael@0 182 motion: 'moveToEol',
michael@0 183 motionArgs: { inclusive: true }},
michael@0 184 { keys: ['%'], type: 'motion',
michael@0 185 motion: 'moveToMatchedSymbol',
michael@0 186 motionArgs: { inclusive: true, toJumplist: true }},
michael@0 187 { keys: ['f', 'character'], type: 'motion',
michael@0 188 motion: 'moveToCharacter',
michael@0 189 motionArgs: { forward: true , inclusive: true }},
michael@0 190 { keys: ['F', 'character'], type: 'motion',
michael@0 191 motion: 'moveToCharacter',
michael@0 192 motionArgs: { forward: false }},
michael@0 193 { keys: ['t', 'character'], type: 'motion',
michael@0 194 motion: 'moveTillCharacter',
michael@0 195 motionArgs: { forward: true, inclusive: true }},
michael@0 196 { keys: ['T', 'character'], type: 'motion',
michael@0 197 motion: 'moveTillCharacter',
michael@0 198 motionArgs: { forward: false }},
michael@0 199 { keys: [';'], type: 'motion', motion: 'repeatLastCharacterSearch',
michael@0 200 motionArgs: { forward: true }},
michael@0 201 { keys: [','], type: 'motion', motion: 'repeatLastCharacterSearch',
michael@0 202 motionArgs: { forward: false }},
michael@0 203 { keys: ['\'', 'character'], type: 'motion', motion: 'goToMark',
michael@0 204 motionArgs: {toJumplist: true}},
michael@0 205 { keys: ['`', 'character'], type: 'motion', motion: 'goToMark',
michael@0 206 motionArgs: {toJumplist: true}},
michael@0 207 { keys: [']', '`'], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } },
michael@0 208 { keys: ['[', '`'], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } },
michael@0 209 { keys: [']', '\''], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } },
michael@0 210 { keys: ['[', '\''], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } },
michael@0 211 { keys: [']', 'character'], type: 'motion',
michael@0 212 motion: 'moveToSymbol',
michael@0 213 motionArgs: { forward: true, toJumplist: true}},
michael@0 214 { keys: ['[', 'character'], type: 'motion',
michael@0 215 motion: 'moveToSymbol',
michael@0 216 motionArgs: { forward: false, toJumplist: true}},
michael@0 217 { keys: ['|'], type: 'motion',
michael@0 218 motion: 'moveToColumn',
michael@0 219 motionArgs: { }},
michael@0 220 { keys: ['o'], type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: { },context:'visual'},
michael@0 221 // Operators
michael@0 222 { keys: ['d'], type: 'operator', operator: 'delete' },
michael@0 223 { keys: ['y'], type: 'operator', operator: 'yank' },
michael@0 224 { keys: ['c'], type: 'operator', operator: 'change' },
michael@0 225 { keys: ['>'], type: 'operator', operator: 'indent',
michael@0 226 operatorArgs: { indentRight: true }},
michael@0 227 { keys: ['<'], type: 'operator', operator: 'indent',
michael@0 228 operatorArgs: { indentRight: false }},
michael@0 229 { keys: ['g', '~'], type: 'operator', operator: 'swapcase' },
michael@0 230 { keys: ['n'], type: 'motion', motion: 'findNext',
michael@0 231 motionArgs: { forward: true, toJumplist: true }},
michael@0 232 { keys: ['N'], type: 'motion', motion: 'findNext',
michael@0 233 motionArgs: { forward: false, toJumplist: true }},
michael@0 234 // Operator-Motion dual commands
michael@0 235 { keys: ['x'], type: 'operatorMotion', operator: 'delete',
michael@0 236 motion: 'moveByCharacters', motionArgs: { forward: true },
michael@0 237 operatorMotionArgs: { visualLine: false }},
michael@0 238 { keys: ['X'], type: 'operatorMotion', operator: 'delete',
michael@0 239 motion: 'moveByCharacters', motionArgs: { forward: false },
michael@0 240 operatorMotionArgs: { visualLine: true }},
michael@0 241 { keys: ['D'], type: 'operatorMotion', operator: 'delete',
michael@0 242 motion: 'moveToEol', motionArgs: { inclusive: true },
michael@0 243 operatorMotionArgs: { visualLine: true }},
michael@0 244 { keys: ['Y'], type: 'operatorMotion', operator: 'yank',
michael@0 245 motion: 'moveToEol', motionArgs: { inclusive: true },
michael@0 246 operatorMotionArgs: { visualLine: true }},
michael@0 247 { keys: ['C'], type: 'operatorMotion',
michael@0 248 operator: 'change',
michael@0 249 motion: 'moveToEol', motionArgs: { inclusive: true },
michael@0 250 operatorMotionArgs: { visualLine: true }},
michael@0 251 { keys: ['~'], type: 'operatorMotion',
michael@0 252 operator: 'swapcase', operatorArgs: { shouldMoveCursor: true },
michael@0 253 motion: 'moveByCharacters', motionArgs: { forward: true }},
michael@0 254 // Actions
michael@0 255 { keys: ['<C-i>'], type: 'action', action: 'jumpListWalk',
michael@0 256 actionArgs: { forward: true }},
michael@0 257 { keys: ['<C-o>'], type: 'action', action: 'jumpListWalk',
michael@0 258 actionArgs: { forward: false }},
michael@0 259 { keys: ['<C-e>'], type: 'action',
michael@0 260 action: 'scroll',
michael@0 261 actionArgs: { forward: true, linewise: true }},
michael@0 262 { keys: ['<C-y>'], type: 'action',
michael@0 263 action: 'scroll',
michael@0 264 actionArgs: { forward: false, linewise: true }},
michael@0 265 { keys: ['a'], type: 'action', action: 'enterInsertMode', isEdit: true,
michael@0 266 actionArgs: { insertAt: 'charAfter' }},
michael@0 267 { keys: ['A'], type: 'action', action: 'enterInsertMode', isEdit: true,
michael@0 268 actionArgs: { insertAt: 'eol' }},
michael@0 269 { keys: ['i'], type: 'action', action: 'enterInsertMode', isEdit: true,
michael@0 270 actionArgs: { insertAt: 'inplace' }},
michael@0 271 { keys: ['I'], type: 'action', action: 'enterInsertMode', isEdit: true,
michael@0 272 actionArgs: { insertAt: 'firstNonBlank' }},
michael@0 273 { keys: ['o'], type: 'action', action: 'newLineAndEnterInsertMode',
michael@0 274 isEdit: true, interlaceInsertRepeat: true,
michael@0 275 actionArgs: { after: true }},
michael@0 276 { keys: ['O'], type: 'action', action: 'newLineAndEnterInsertMode',
michael@0 277 isEdit: true, interlaceInsertRepeat: true,
michael@0 278 actionArgs: { after: false }},
michael@0 279 { keys: ['v'], type: 'action', action: 'toggleVisualMode' },
michael@0 280 { keys: ['V'], type: 'action', action: 'toggleVisualMode',
michael@0 281 actionArgs: { linewise: true }},
michael@0 282 { keys: ['g', 'v'], type: 'action', action: 'reselectLastSelection' },
michael@0 283 { keys: ['J'], type: 'action', action: 'joinLines', isEdit: true },
michael@0 284 { keys: ['p'], type: 'action', action: 'paste', isEdit: true,
michael@0 285 actionArgs: { after: true, isEdit: true }},
michael@0 286 { keys: ['P'], type: 'action', action: 'paste', isEdit: true,
michael@0 287 actionArgs: { after: false, isEdit: true }},
michael@0 288 { keys: ['r', 'character'], type: 'action', action: 'replace', isEdit: true },
michael@0 289 { keys: ['@', 'character'], type: 'action', action: 'replayMacro' },
michael@0 290 { keys: ['q', 'character'], type: 'action', action: 'enterMacroRecordMode' },
michael@0 291 // Handle Replace-mode as a special case of insert mode.
michael@0 292 { keys: ['R'], type: 'action', action: 'enterInsertMode', isEdit: true,
michael@0 293 actionArgs: { replace: true }},
michael@0 294 { keys: ['u'], type: 'action', action: 'undo' },
michael@0 295 { keys: ['<C-r>'], type: 'action', action: 'redo' },
michael@0 296 { keys: ['m', 'character'], type: 'action', action: 'setMark' },
michael@0 297 { keys: ['"', 'character'], type: 'action', action: 'setRegister' },
michael@0 298 { keys: ['z', 'z'], type: 'action', action: 'scrollToCursor',
michael@0 299 actionArgs: { position: 'center' }},
michael@0 300 { keys: ['z', '.'], type: 'action', action: 'scrollToCursor',
michael@0 301 actionArgs: { position: 'center' },
michael@0 302 motion: 'moveToFirstNonWhiteSpaceCharacter' },
michael@0 303 { keys: ['z', 't'], type: 'action', action: 'scrollToCursor',
michael@0 304 actionArgs: { position: 'top' }},
michael@0 305 { keys: ['z', '<CR>'], type: 'action', action: 'scrollToCursor',
michael@0 306 actionArgs: { position: 'top' },
michael@0 307 motion: 'moveToFirstNonWhiteSpaceCharacter' },
michael@0 308 { keys: ['z', '-'], type: 'action', action: 'scrollToCursor',
michael@0 309 actionArgs: { position: 'bottom' }},
michael@0 310 { keys: ['z', 'b'], type: 'action', action: 'scrollToCursor',
michael@0 311 actionArgs: { position: 'bottom' },
michael@0 312 motion: 'moveToFirstNonWhiteSpaceCharacter' },
michael@0 313 { keys: ['.'], type: 'action', action: 'repeatLastEdit' },
michael@0 314 { keys: ['<C-a>'], type: 'action', action: 'incrementNumberToken',
michael@0 315 isEdit: true,
michael@0 316 actionArgs: {increase: true, backtrack: false}},
michael@0 317 { keys: ['<C-x>'], type: 'action', action: 'incrementNumberToken',
michael@0 318 isEdit: true,
michael@0 319 actionArgs: {increase: false, backtrack: false}},
michael@0 320 // Text object motions
michael@0 321 { keys: ['a', 'character'], type: 'motion',
michael@0 322 motion: 'textObjectManipulation' },
michael@0 323 { keys: ['i', 'character'], type: 'motion',
michael@0 324 motion: 'textObjectManipulation',
michael@0 325 motionArgs: { textObjectInner: true }},
michael@0 326 // Search
michael@0 327 { keys: ['/'], type: 'search',
michael@0 328 searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }},
michael@0 329 { keys: ['?'], type: 'search',
michael@0 330 searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }},
michael@0 331 { keys: ['*'], type: 'search',
michael@0 332 searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }},
michael@0 333 { keys: ['#'], type: 'search',
michael@0 334 searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }},
michael@0 335 // Ex command
michael@0 336 { keys: [':'], type: 'ex' }
michael@0 337 ];
michael@0 338
michael@0 339 var Pos = CodeMirror.Pos;
michael@0 340
michael@0 341 var Vim = function() {
michael@0 342 CodeMirror.defineOption('vimMode', false, function(cm, val) {
michael@0 343 if (val) {
michael@0 344 cm.setOption('keyMap', 'vim');
michael@0 345 cm.setOption('disableInput', true);
michael@0 346 CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
michael@0 347 cm.on('beforeSelectionChange', beforeSelectionChange);
michael@0 348 maybeInitVimState(cm);
michael@0 349 CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm));
michael@0 350 } else if (cm.state.vim) {
michael@0 351 cm.setOption('keyMap', 'default');
michael@0 352 cm.setOption('disableInput', false);
michael@0 353 cm.off('beforeSelectionChange', beforeSelectionChange);
michael@0 354 CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm));
michael@0 355 cm.state.vim = null;
michael@0 356 }
michael@0 357 });
michael@0 358 function beforeSelectionChange(cm, obj) {
michael@0 359 var vim = cm.state.vim;
michael@0 360 if (vim.insertMode || vim.exMode) return;
michael@0 361
michael@0 362 var head = obj.ranges[0].head;
michael@0 363 var anchor = obj.ranges[0].anchor;
michael@0 364 if (head.ch && head.ch == cm.doc.getLine(head.line).length) {
michael@0 365 var pos = Pos(head.line, head.ch - 1);
michael@0 366 obj.update([{anchor: cursorEqual(head, anchor) ? pos : anchor,
michael@0 367 head: pos}]);
michael@0 368 }
michael@0 369 }
michael@0 370 function getOnPasteFn(cm) {
michael@0 371 var vim = cm.state.vim;
michael@0 372 if (!vim.onPasteFn) {
michael@0 373 vim.onPasteFn = function() {
michael@0 374 if (!vim.insertMode) {
michael@0 375 cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));
michael@0 376 actions.enterInsertMode(cm, {}, vim);
michael@0 377 }
michael@0 378 };
michael@0 379 }
michael@0 380 return vim.onPasteFn;
michael@0 381 }
michael@0 382
michael@0 383 var numberRegex = /[\d]/;
michael@0 384 var wordRegexp = [(/\w/), (/[^\w\s]/)], bigWordRegexp = [(/\S/)];
michael@0 385 function makeKeyRange(start, size) {
michael@0 386 var keys = [];
michael@0 387 for (var i = start; i < start + size; i++) {
michael@0 388 keys.push(String.fromCharCode(i));
michael@0 389 }
michael@0 390 return keys;
michael@0 391 }
michael@0 392 var upperCaseAlphabet = makeKeyRange(65, 26);
michael@0 393 var lowerCaseAlphabet = makeKeyRange(97, 26);
michael@0 394 var numbers = makeKeyRange(48, 10);
michael@0 395 var specialSymbols = '~`!@#$%^&*()_-+=[{}]\\|/?.,<>:;"\''.split('');
michael@0 396 var specialKeys = ['Left', 'Right', 'Up', 'Down', 'Space', 'Backspace',
michael@0 397 'Esc', 'Home', 'End', 'PageUp', 'PageDown', 'Enter'];
michael@0 398 var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']);
michael@0 399 var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"']);
michael@0 400
michael@0 401 function isLine(cm, line) {
michael@0 402 return line >= cm.firstLine() && line <= cm.lastLine();
michael@0 403 }
michael@0 404 function isLowerCase(k) {
michael@0 405 return (/^[a-z]$/).test(k);
michael@0 406 }
michael@0 407 function isMatchableSymbol(k) {
michael@0 408 return '()[]{}'.indexOf(k) != -1;
michael@0 409 }
michael@0 410 function isNumber(k) {
michael@0 411 return numberRegex.test(k);
michael@0 412 }
michael@0 413 function isUpperCase(k) {
michael@0 414 return (/^[A-Z]$/).test(k);
michael@0 415 }
michael@0 416 function isWhiteSpaceString(k) {
michael@0 417 return (/^\s*$/).test(k);
michael@0 418 }
michael@0 419 function inArray(val, arr) {
michael@0 420 for (var i = 0; i < arr.length; i++) {
michael@0 421 if (arr[i] == val) {
michael@0 422 return true;
michael@0 423 }
michael@0 424 }
michael@0 425 return false;
michael@0 426 }
michael@0 427
michael@0 428 var options = {};
michael@0 429 function defineOption(name, defaultValue, type) {
michael@0 430 if (defaultValue === undefined) { throw Error('defaultValue is required'); }
michael@0 431 if (!type) { type = 'string'; }
michael@0 432 options[name] = {
michael@0 433 type: type,
michael@0 434 defaultValue: defaultValue
michael@0 435 };
michael@0 436 setOption(name, defaultValue);
michael@0 437 }
michael@0 438
michael@0 439 function setOption(name, value) {
michael@0 440 var option = options[name];
michael@0 441 if (!option) {
michael@0 442 throw Error('Unknown option: ' + name);
michael@0 443 }
michael@0 444 if (option.type == 'boolean') {
michael@0 445 if (value && value !== true) {
michael@0 446 throw Error('Invalid argument: ' + name + '=' + value);
michael@0 447 } else if (value !== false) {
michael@0 448 // Boolean options are set to true if value is not defined.
michael@0 449 value = true;
michael@0 450 }
michael@0 451 }
michael@0 452 option.value = option.type == 'boolean' ? !!value : value;
michael@0 453 }
michael@0 454
michael@0 455 function getOption(name) {
michael@0 456 var option = options[name];
michael@0 457 if (!option) {
michael@0 458 throw Error('Unknown option: ' + name);
michael@0 459 }
michael@0 460 return option.value;
michael@0 461 }
michael@0 462
michael@0 463 var createCircularJumpList = function() {
michael@0 464 var size = 100;
michael@0 465 var pointer = -1;
michael@0 466 var head = 0;
michael@0 467 var tail = 0;
michael@0 468 var buffer = new Array(size);
michael@0 469 function add(cm, oldCur, newCur) {
michael@0 470 var current = pointer % size;
michael@0 471 var curMark = buffer[current];
michael@0 472 function useNextSlot(cursor) {
michael@0 473 var next = ++pointer % size;
michael@0 474 var trashMark = buffer[next];
michael@0 475 if (trashMark) {
michael@0 476 trashMark.clear();
michael@0 477 }
michael@0 478 buffer[next] = cm.setBookmark(cursor);
michael@0 479 }
michael@0 480 if (curMark) {
michael@0 481 var markPos = curMark.find();
michael@0 482 // avoid recording redundant cursor position
michael@0 483 if (markPos && !cursorEqual(markPos, oldCur)) {
michael@0 484 useNextSlot(oldCur);
michael@0 485 }
michael@0 486 } else {
michael@0 487 useNextSlot(oldCur);
michael@0 488 }
michael@0 489 useNextSlot(newCur);
michael@0 490 head = pointer;
michael@0 491 tail = pointer - size + 1;
michael@0 492 if (tail < 0) {
michael@0 493 tail = 0;
michael@0 494 }
michael@0 495 }
michael@0 496 function move(cm, offset) {
michael@0 497 pointer += offset;
michael@0 498 if (pointer > head) {
michael@0 499 pointer = head;
michael@0 500 } else if (pointer < tail) {
michael@0 501 pointer = tail;
michael@0 502 }
michael@0 503 var mark = buffer[(size + pointer) % size];
michael@0 504 // skip marks that are temporarily removed from text buffer
michael@0 505 if (mark && !mark.find()) {
michael@0 506 var inc = offset > 0 ? 1 : -1;
michael@0 507 var newCur;
michael@0 508 var oldCur = cm.getCursor();
michael@0 509 do {
michael@0 510 pointer += inc;
michael@0 511 mark = buffer[(size + pointer) % size];
michael@0 512 // skip marks that are the same as current position
michael@0 513 if (mark &&
michael@0 514 (newCur = mark.find()) &&
michael@0 515 !cursorEqual(oldCur, newCur)) {
michael@0 516 break;
michael@0 517 }
michael@0 518 } while (pointer < head && pointer > tail);
michael@0 519 }
michael@0 520 return mark;
michael@0 521 }
michael@0 522 return {
michael@0 523 cachedCursor: undefined, //used for # and * jumps
michael@0 524 add: add,
michael@0 525 move: move
michael@0 526 };
michael@0 527 };
michael@0 528
michael@0 529 // Returns an object to track the changes associated insert mode. It
michael@0 530 // clones the object that is passed in, or creates an empty object one if
michael@0 531 // none is provided.
michael@0 532 var createInsertModeChanges = function(c) {
michael@0 533 if (c) {
michael@0 534 // Copy construction
michael@0 535 return {
michael@0 536 changes: c.changes,
michael@0 537 expectCursorActivityForChange: c.expectCursorActivityForChange
michael@0 538 };
michael@0 539 }
michael@0 540 return {
michael@0 541 // Change list
michael@0 542 changes: [],
michael@0 543 // Set to true on change, false on cursorActivity.
michael@0 544 expectCursorActivityForChange: false
michael@0 545 };
michael@0 546 };
michael@0 547
michael@0 548 function MacroModeState() {
michael@0 549 this.latestRegister = undefined;
michael@0 550 this.isPlaying = false;
michael@0 551 this.isRecording = false;
michael@0 552 this.onRecordingDone = undefined;
michael@0 553 this.lastInsertModeChanges = createInsertModeChanges();
michael@0 554 }
michael@0 555 MacroModeState.prototype = {
michael@0 556 exitMacroRecordMode: function() {
michael@0 557 var macroModeState = vimGlobalState.macroModeState;
michael@0 558 macroModeState.onRecordingDone(); // close dialog
michael@0 559 macroModeState.onRecordingDone = undefined;
michael@0 560 macroModeState.isRecording = false;
michael@0 561 },
michael@0 562 enterMacroRecordMode: function(cm, registerName) {
michael@0 563 var register =
michael@0 564 vimGlobalState.registerController.getRegister(registerName);
michael@0 565 if (register) {
michael@0 566 register.clear();
michael@0 567 this.latestRegister = registerName;
michael@0 568 this.onRecordingDone = cm.openDialog(
michael@0 569 '(recording)['+registerName+']', null, {bottom:true});
michael@0 570 this.isRecording = true;
michael@0 571 }
michael@0 572 }
michael@0 573 };
michael@0 574
michael@0 575 function maybeInitVimState(cm) {
michael@0 576 if (!cm.state.vim) {
michael@0 577 // Store instance state in the CodeMirror object.
michael@0 578 cm.state.vim = {
michael@0 579 inputState: new InputState(),
michael@0 580 // Vim's input state that triggered the last edit, used to repeat
michael@0 581 // motions and operators with '.'.
michael@0 582 lastEditInputState: undefined,
michael@0 583 // Vim's action command before the last edit, used to repeat actions
michael@0 584 // with '.' and insert mode repeat.
michael@0 585 lastEditActionCommand: undefined,
michael@0 586 // When using jk for navigation, if you move from a longer line to a
michael@0 587 // shorter line, the cursor may clip to the end of the shorter line.
michael@0 588 // If j is pressed again and cursor goes to the next line, the
michael@0 589 // cursor should go back to its horizontal position on the longer
michael@0 590 // line if it can. This is to keep track of the horizontal position.
michael@0 591 lastHPos: -1,
michael@0 592 // Doing the same with screen-position for gj/gk
michael@0 593 lastHSPos: -1,
michael@0 594 // The last motion command run. Cleared if a non-motion command gets
michael@0 595 // executed in between.
michael@0 596 lastMotion: null,
michael@0 597 marks: {},
michael@0 598 insertMode: false,
michael@0 599 // Repeat count for changes made in insert mode, triggered by key
michael@0 600 // sequences like 3,i. Only exists when insertMode is true.
michael@0 601 insertModeRepeat: undefined,
michael@0 602 visualMode: false,
michael@0 603 // If we are in visual line mode. No effect if visualMode is false.
michael@0 604 visualLine: false,
michael@0 605 lastSelection: null
michael@0 606 };
michael@0 607 }
michael@0 608 return cm.state.vim;
michael@0 609 }
michael@0 610 var vimGlobalState;
michael@0 611 function resetVimGlobalState() {
michael@0 612 vimGlobalState = {
michael@0 613 // The current search query.
michael@0 614 searchQuery: null,
michael@0 615 // Whether we are searching backwards.
michael@0 616 searchIsReversed: false,
michael@0 617 jumpList: createCircularJumpList(),
michael@0 618 macroModeState: new MacroModeState,
michael@0 619 // Recording latest f, t, F or T motion command.
michael@0 620 lastChararacterSearch: {increment:0, forward:true, selectedCharacter:''},
michael@0 621 registerController: new RegisterController({})
michael@0 622 };
michael@0 623 for (var optionName in options) {
michael@0 624 var option = options[optionName];
michael@0 625 option.value = option.defaultValue;
michael@0 626 }
michael@0 627 }
michael@0 628
michael@0 629 var vimApi= {
michael@0 630 buildKeyMap: function() {
michael@0 631 // TODO: Convert keymap into dictionary format for fast lookup.
michael@0 632 },
michael@0 633 // Testing hook, though it might be useful to expose the register
michael@0 634 // controller anyways.
michael@0 635 getRegisterController: function() {
michael@0 636 return vimGlobalState.registerController;
michael@0 637 },
michael@0 638 // Testing hook.
michael@0 639 resetVimGlobalState_: resetVimGlobalState,
michael@0 640
michael@0 641 // Testing hook.
michael@0 642 getVimGlobalState_: function() {
michael@0 643 return vimGlobalState;
michael@0 644 },
michael@0 645
michael@0 646 // Testing hook.
michael@0 647 maybeInitVimState_: maybeInitVimState,
michael@0 648
michael@0 649 InsertModeKey: InsertModeKey,
michael@0 650 map: function(lhs, rhs, ctx) {
michael@0 651 // Add user defined key bindings.
michael@0 652 exCommandDispatcher.map(lhs, rhs, ctx);
michael@0 653 },
michael@0 654 setOption: setOption,
michael@0 655 getOption: getOption,
michael@0 656 defineOption: defineOption,
michael@0 657 defineEx: function(name, prefix, func){
michael@0 658 if (name.indexOf(prefix) !== 0) {
michael@0 659 throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered');
michael@0 660 }
michael@0 661 exCommands[name]=func;
michael@0 662 exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'};
michael@0 663 },
michael@0 664 // This is the outermost function called by CodeMirror, after keys have
michael@0 665 // been mapped to their Vim equivalents.
michael@0 666 handleKey: function(cm, key) {
michael@0 667 var command;
michael@0 668 var vim = maybeInitVimState(cm);
michael@0 669 var macroModeState = vimGlobalState.macroModeState;
michael@0 670 if (macroModeState.isRecording) {
michael@0 671 if (key == 'q') {
michael@0 672 macroModeState.exitMacroRecordMode();
michael@0 673 vim.inputState = new InputState();
michael@0 674 return;
michael@0 675 }
michael@0 676 }
michael@0 677 if (key == '<Esc>') {
michael@0 678 // Clear input state and get back to normal mode.
michael@0 679 vim.inputState = new InputState();
michael@0 680 if (vim.visualMode) {
michael@0 681 exitVisualMode(cm);
michael@0 682 }
michael@0 683 return;
michael@0 684 }
michael@0 685 // Enter visual mode when the mouse selects text.
michael@0 686 if (!vim.visualMode &&
michael@0 687 !cursorEqual(cm.getCursor('head'), cm.getCursor('anchor'))) {
michael@0 688 vim.visualMode = true;
michael@0 689 vim.visualLine = false;
michael@0 690 CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"});
michael@0 691 cm.on('mousedown', exitVisualMode);
michael@0 692 }
michael@0 693 if (key != '0' || (key == '0' && vim.inputState.getRepeat() === 0)) {
michael@0 694 // Have to special case 0 since it's both a motion and a number.
michael@0 695 command = commandDispatcher.matchCommand(key, defaultKeymap, vim);
michael@0 696 }
michael@0 697 if (!command) {
michael@0 698 if (isNumber(key)) {
michael@0 699 // Increment count unless count is 0 and key is 0.
michael@0 700 vim.inputState.pushRepeatDigit(key);
michael@0 701 }
michael@0 702 if (macroModeState.isRecording) {
michael@0 703 logKey(macroModeState, key);
michael@0 704 }
michael@0 705 return;
michael@0 706 }
michael@0 707 if (command.type == 'keyToKey') {
michael@0 708 // TODO: prevent infinite recursion.
michael@0 709 for (var i = 0; i < command.toKeys.length; i++) {
michael@0 710 this.handleKey(cm, command.toKeys[i]);
michael@0 711 }
michael@0 712 } else {
michael@0 713 if (macroModeState.isRecording) {
michael@0 714 logKey(macroModeState, key);
michael@0 715 }
michael@0 716 commandDispatcher.processCommand(cm, vim, command);
michael@0 717 }
michael@0 718 },
michael@0 719 handleEx: function(cm, input) {
michael@0 720 exCommandDispatcher.processCommand(cm, input);
michael@0 721 }
michael@0 722 };
michael@0 723
michael@0 724 // Represents the current input state.
michael@0 725 function InputState() {
michael@0 726 this.prefixRepeat = [];
michael@0 727 this.motionRepeat = [];
michael@0 728
michael@0 729 this.operator = null;
michael@0 730 this.operatorArgs = null;
michael@0 731 this.motion = null;
michael@0 732 this.motionArgs = null;
michael@0 733 this.keyBuffer = []; // For matching multi-key commands.
michael@0 734 this.registerName = null; // Defaults to the unnamed register.
michael@0 735 }
michael@0 736 InputState.prototype.pushRepeatDigit = function(n) {
michael@0 737 if (!this.operator) {
michael@0 738 this.prefixRepeat = this.prefixRepeat.concat(n);
michael@0 739 } else {
michael@0 740 this.motionRepeat = this.motionRepeat.concat(n);
michael@0 741 }
michael@0 742 };
michael@0 743 InputState.prototype.getRepeat = function() {
michael@0 744 var repeat = 0;
michael@0 745 if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) {
michael@0 746 repeat = 1;
michael@0 747 if (this.prefixRepeat.length > 0) {
michael@0 748 repeat *= parseInt(this.prefixRepeat.join(''), 10);
michael@0 749 }
michael@0 750 if (this.motionRepeat.length > 0) {
michael@0 751 repeat *= parseInt(this.motionRepeat.join(''), 10);
michael@0 752 }
michael@0 753 }
michael@0 754 return repeat;
michael@0 755 };
michael@0 756
michael@0 757 /*
michael@0 758 * Register stores information about copy and paste registers. Besides
michael@0 759 * text, a register must store whether it is linewise (i.e., when it is
michael@0 760 * pasted, should it insert itself into a new line, or should the text be
michael@0 761 * inserted at the cursor position.)
michael@0 762 */
michael@0 763 function Register(text, linewise) {
michael@0 764 this.clear();
michael@0 765 this.keyBuffer = [text || ''];
michael@0 766 this.insertModeChanges = [];
michael@0 767 this.linewise = !!linewise;
michael@0 768 }
michael@0 769 Register.prototype = {
michael@0 770 setText: function(text, linewise) {
michael@0 771 this.keyBuffer = [text || ''];
michael@0 772 this.linewise = !!linewise;
michael@0 773 },
michael@0 774 pushText: function(text, linewise) {
michael@0 775 // if this register has ever been set to linewise, use linewise.
michael@0 776 if (linewise || this.linewise) {
michael@0 777 this.keyBuffer.push('\n');
michael@0 778 this.linewise = true;
michael@0 779 }
michael@0 780 this.keyBuffer.push(text);
michael@0 781 },
michael@0 782 pushInsertModeChanges: function(changes) {
michael@0 783 this.insertModeChanges.push(createInsertModeChanges(changes));
michael@0 784 },
michael@0 785 clear: function() {
michael@0 786 this.keyBuffer = [];
michael@0 787 this.insertModeChanges = [];
michael@0 788 this.linewise = false;
michael@0 789 },
michael@0 790 toString: function() {
michael@0 791 return this.keyBuffer.join('');
michael@0 792 }
michael@0 793 };
michael@0 794
michael@0 795 /*
michael@0 796 * vim registers allow you to keep many independent copy and paste buffers.
michael@0 797 * See http://usevim.com/2012/04/13/registers/ for an introduction.
michael@0 798 *
michael@0 799 * RegisterController keeps the state of all the registers. An initial
michael@0 800 * state may be passed in. The unnamed register '"' will always be
michael@0 801 * overridden.
michael@0 802 */
michael@0 803 function RegisterController(registers) {
michael@0 804 this.registers = registers;
michael@0 805 this.unnamedRegister = registers['"'] = new Register();
michael@0 806 }
michael@0 807 RegisterController.prototype = {
michael@0 808 pushText: function(registerName, operator, text, linewise) {
michael@0 809 if (linewise && text.charAt(0) == '\n') {
michael@0 810 text = text.slice(1) + '\n';
michael@0 811 }
michael@0 812 if (linewise && text.charAt(text.length - 1) !== '\n'){
michael@0 813 text += '\n';
michael@0 814 }
michael@0 815 // Lowercase and uppercase registers refer to the same register.
michael@0 816 // Uppercase just means append.
michael@0 817 var register = this.isValidRegister(registerName) ?
michael@0 818 this.getRegister(registerName) : null;
michael@0 819 // if no register/an invalid register was specified, things go to the
michael@0 820 // default registers
michael@0 821 if (!register) {
michael@0 822 switch (operator) {
michael@0 823 case 'yank':
michael@0 824 // The 0 register contains the text from the most recent yank.
michael@0 825 this.registers['0'] = new Register(text, linewise);
michael@0 826 break;
michael@0 827 case 'delete':
michael@0 828 case 'change':
michael@0 829 if (text.indexOf('\n') == -1) {
michael@0 830 // Delete less than 1 line. Update the small delete register.
michael@0 831 this.registers['-'] = new Register(text, linewise);
michael@0 832 } else {
michael@0 833 // Shift down the contents of the numbered registers and put the
michael@0 834 // deleted text into register 1.
michael@0 835 this.shiftNumericRegisters_();
michael@0 836 this.registers['1'] = new Register(text, linewise);
michael@0 837 }
michael@0 838 break;
michael@0 839 }
michael@0 840 // Make sure the unnamed register is set to what just happened
michael@0 841 this.unnamedRegister.setText(text, linewise);
michael@0 842 return;
michael@0 843 }
michael@0 844
michael@0 845 // If we've gotten to this point, we've actually specified a register
michael@0 846 var append = isUpperCase(registerName);
michael@0 847 if (append) {
michael@0 848 register.append(text, linewise);
michael@0 849 // The unnamed register always has the same value as the last used
michael@0 850 // register.
michael@0 851 this.unnamedRegister.append(text, linewise);
michael@0 852 } else {
michael@0 853 register.setText(text, linewise);
michael@0 854 this.unnamedRegister.setText(text, linewise);
michael@0 855 }
michael@0 856 },
michael@0 857 // Gets the register named @name. If one of @name doesn't already exist,
michael@0 858 // create it. If @name is invalid, return the unnamedRegister.
michael@0 859 getRegister: function(name) {
michael@0 860 if (!this.isValidRegister(name)) {
michael@0 861 return this.unnamedRegister;
michael@0 862 }
michael@0 863 name = name.toLowerCase();
michael@0 864 if (!this.registers[name]) {
michael@0 865 this.registers[name] = new Register();
michael@0 866 }
michael@0 867 return this.registers[name];
michael@0 868 },
michael@0 869 isValidRegister: function(name) {
michael@0 870 return name && inArray(name, validRegisters);
michael@0 871 },
michael@0 872 shiftNumericRegisters_: function() {
michael@0 873 for (var i = 9; i >= 2; i--) {
michael@0 874 this.registers[i] = this.getRegister('' + (i - 1));
michael@0 875 }
michael@0 876 }
michael@0 877 };
michael@0 878
michael@0 879 var commandDispatcher = {
michael@0 880 matchCommand: function(key, keyMap, vim) {
michael@0 881 var inputState = vim.inputState;
michael@0 882 var keys = inputState.keyBuffer.concat(key);
michael@0 883 var matchedCommands = [];
michael@0 884 var selectedCharacter;
michael@0 885 for (var i = 0; i < keyMap.length; i++) {
michael@0 886 var command = keyMap[i];
michael@0 887 if (matchKeysPartial(keys, command.keys)) {
michael@0 888 if (inputState.operator && command.type == 'action') {
michael@0 889 // Ignore matched action commands after an operator. Operators
michael@0 890 // only operate on motions. This check is really for text
michael@0 891 // objects since aW, a[ etcs conflicts with a.
michael@0 892 continue;
michael@0 893 }
michael@0 894 // Match commands that take <character> as an argument.
michael@0 895 if (command.keys[keys.length - 1] == 'character') {
michael@0 896 selectedCharacter = keys[keys.length - 1];
michael@0 897 if (selectedCharacter.length>1){
michael@0 898 switch(selectedCharacter){
michael@0 899 case '<CR>':
michael@0 900 selectedCharacter='\n';
michael@0 901 break;
michael@0 902 case '<Space>':
michael@0 903 selectedCharacter=' ';
michael@0 904 break;
michael@0 905 default:
michael@0 906 continue;
michael@0 907 }
michael@0 908 }
michael@0 909 }
michael@0 910 // Add the command to the list of matched commands. Choose the best
michael@0 911 // command later.
michael@0 912 matchedCommands.push(command);
michael@0 913 }
michael@0 914 }
michael@0 915
michael@0 916 // Returns the command if it is a full match, or null if not.
michael@0 917 function getFullyMatchedCommandOrNull(command) {
michael@0 918 if (keys.length < command.keys.length) {
michael@0 919 // Matches part of a multi-key command. Buffer and wait for next
michael@0 920 // stroke.
michael@0 921 inputState.keyBuffer.push(key);
michael@0 922 return null;
michael@0 923 } else {
michael@0 924 if (command.keys[keys.length - 1] == 'character') {
michael@0 925 inputState.selectedCharacter = selectedCharacter;
michael@0 926 }
michael@0 927 // Clear the buffer since a full match was found.
michael@0 928 inputState.keyBuffer = [];
michael@0 929 return command;
michael@0 930 }
michael@0 931 }
michael@0 932
michael@0 933 if (!matchedCommands.length) {
michael@0 934 // Clear the buffer since there were no matches.
michael@0 935 inputState.keyBuffer = [];
michael@0 936 return null;
michael@0 937 } else if (matchedCommands.length == 1) {
michael@0 938 return getFullyMatchedCommandOrNull(matchedCommands[0]);
michael@0 939 } else {
michael@0 940 // Find the best match in the list of matchedCommands.
michael@0 941 var context = vim.visualMode ? 'visual' : 'normal';
michael@0 942 var bestMatch; // Default to first in the list.
michael@0 943 for (var i = 0; i < matchedCommands.length; i++) {
michael@0 944 var current = matchedCommands[i];
michael@0 945 if (current.context == context) {
michael@0 946 bestMatch = current;
michael@0 947 break;
michael@0 948 } else if (!bestMatch && !current.context) {
michael@0 949 // Only set an imperfect match to best match if no best match is
michael@0 950 // set and the imperfect match is not restricted to another
michael@0 951 // context.
michael@0 952 bestMatch = current;
michael@0 953 }
michael@0 954 }
michael@0 955 return getFullyMatchedCommandOrNull(bestMatch);
michael@0 956 }
michael@0 957 },
michael@0 958 processCommand: function(cm, vim, command) {
michael@0 959 vim.inputState.repeatOverride = command.repeatOverride;
michael@0 960 switch (command.type) {
michael@0 961 case 'motion':
michael@0 962 this.processMotion(cm, vim, command);
michael@0 963 break;
michael@0 964 case 'operator':
michael@0 965 this.processOperator(cm, vim, command);
michael@0 966 break;
michael@0 967 case 'operatorMotion':
michael@0 968 this.processOperatorMotion(cm, vim, command);
michael@0 969 break;
michael@0 970 case 'action':
michael@0 971 this.processAction(cm, vim, command);
michael@0 972 break;
michael@0 973 case 'search':
michael@0 974 this.processSearch(cm, vim, command);
michael@0 975 break;
michael@0 976 case 'ex':
michael@0 977 case 'keyToEx':
michael@0 978 this.processEx(cm, vim, command);
michael@0 979 break;
michael@0 980 default:
michael@0 981 break;
michael@0 982 }
michael@0 983 },
michael@0 984 processMotion: function(cm, vim, command) {
michael@0 985 vim.inputState.motion = command.motion;
michael@0 986 vim.inputState.motionArgs = copyArgs(command.motionArgs);
michael@0 987 this.evalInput(cm, vim);
michael@0 988 },
michael@0 989 processOperator: function(cm, vim, command) {
michael@0 990 var inputState = vim.inputState;
michael@0 991 if (inputState.operator) {
michael@0 992 if (inputState.operator == command.operator) {
michael@0 993 // Typing an operator twice like 'dd' makes the operator operate
michael@0 994 // linewise
michael@0 995 inputState.motion = 'expandToLine';
michael@0 996 inputState.motionArgs = { linewise: true };
michael@0 997 this.evalInput(cm, vim);
michael@0 998 return;
michael@0 999 } else {
michael@0 1000 // 2 different operators in a row doesn't make sense.
michael@0 1001 vim.inputState = new InputState();
michael@0 1002 }
michael@0 1003 }
michael@0 1004 inputState.operator = command.operator;
michael@0 1005 inputState.operatorArgs = copyArgs(command.operatorArgs);
michael@0 1006 if (vim.visualMode) {
michael@0 1007 // Operating on a selection in visual mode. We don't need a motion.
michael@0 1008 this.evalInput(cm, vim);
michael@0 1009 }
michael@0 1010 },
michael@0 1011 processOperatorMotion: function(cm, vim, command) {
michael@0 1012 var visualMode = vim.visualMode;
michael@0 1013 var operatorMotionArgs = copyArgs(command.operatorMotionArgs);
michael@0 1014 if (operatorMotionArgs) {
michael@0 1015 // Operator motions may have special behavior in visual mode.
michael@0 1016 if (visualMode && operatorMotionArgs.visualLine) {
michael@0 1017 vim.visualLine = true;
michael@0 1018 }
michael@0 1019 }
michael@0 1020 this.processOperator(cm, vim, command);
michael@0 1021 if (!visualMode) {
michael@0 1022 this.processMotion(cm, vim, command);
michael@0 1023 }
michael@0 1024 },
michael@0 1025 processAction: function(cm, vim, command) {
michael@0 1026 var inputState = vim.inputState;
michael@0 1027 var repeat = inputState.getRepeat();
michael@0 1028 var repeatIsExplicit = !!repeat;
michael@0 1029 var actionArgs = copyArgs(command.actionArgs) || {};
michael@0 1030 if (inputState.selectedCharacter) {
michael@0 1031 actionArgs.selectedCharacter = inputState.selectedCharacter;
michael@0 1032 }
michael@0 1033 // Actions may or may not have motions and operators. Do these first.
michael@0 1034 if (command.operator) {
michael@0 1035 this.processOperator(cm, vim, command);
michael@0 1036 }
michael@0 1037 if (command.motion) {
michael@0 1038 this.processMotion(cm, vim, command);
michael@0 1039 }
michael@0 1040 if (command.motion || command.operator) {
michael@0 1041 this.evalInput(cm, vim);
michael@0 1042 }
michael@0 1043 actionArgs.repeat = repeat || 1;
michael@0 1044 actionArgs.repeatIsExplicit = repeatIsExplicit;
michael@0 1045 actionArgs.registerName = inputState.registerName;
michael@0 1046 vim.inputState = new InputState();
michael@0 1047 vim.lastMotion = null;
michael@0 1048 if (command.isEdit) {
michael@0 1049 this.recordLastEdit(vim, inputState, command);
michael@0 1050 }
michael@0 1051 actions[command.action](cm, actionArgs, vim);
michael@0 1052 },
michael@0 1053 processSearch: function(cm, vim, command) {
michael@0 1054 if (!cm.getSearchCursor) {
michael@0 1055 // Search depends on SearchCursor.
michael@0 1056 return;
michael@0 1057 }
michael@0 1058 var forward = command.searchArgs.forward;
michael@0 1059 getSearchState(cm).setReversed(!forward);
michael@0 1060 var promptPrefix = (forward) ? '/' : '?';
michael@0 1061 var originalQuery = getSearchState(cm).getQuery();
michael@0 1062 var originalScrollPos = cm.getScrollInfo();
michael@0 1063 function handleQuery(query, ignoreCase, smartCase) {
michael@0 1064 try {
michael@0 1065 updateSearchQuery(cm, query, ignoreCase, smartCase);
michael@0 1066 } catch (e) {
michael@0 1067 showConfirm(cm, 'Invalid regex: ' + query);
michael@0 1068 return;
michael@0 1069 }
michael@0 1070 commandDispatcher.processMotion(cm, vim, {
michael@0 1071 type: 'motion',
michael@0 1072 motion: 'findNext',
michael@0 1073 motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist }
michael@0 1074 });
michael@0 1075 }
michael@0 1076 function onPromptClose(query) {
michael@0 1077 cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
michael@0 1078 handleQuery(query, true /** ignoreCase */, true /** smartCase */);
michael@0 1079 }
michael@0 1080 function onPromptKeyUp(_e, query) {
michael@0 1081 var parsedQuery;
michael@0 1082 try {
michael@0 1083 parsedQuery = updateSearchQuery(cm, query,
michael@0 1084 true /** ignoreCase */, true /** smartCase */);
michael@0 1085 } catch (e) {
michael@0 1086 // Swallow bad regexes for incremental search.
michael@0 1087 }
michael@0 1088 if (parsedQuery) {
michael@0 1089 cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30);
michael@0 1090 } else {
michael@0 1091 clearSearchHighlight(cm);
michael@0 1092 cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
michael@0 1093 }
michael@0 1094 }
michael@0 1095 function onPromptKeyDown(e, _query, close) {
michael@0 1096 var keyName = CodeMirror.keyName(e);
michael@0 1097 if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') {
michael@0 1098 updateSearchQuery(cm, originalQuery);
michael@0 1099 clearSearchHighlight(cm);
michael@0 1100 cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
michael@0 1101
michael@0 1102 CodeMirror.e_stop(e);
michael@0 1103 close();
michael@0 1104 cm.focus();
michael@0 1105 }
michael@0 1106 }
michael@0 1107 switch (command.searchArgs.querySrc) {
michael@0 1108 case 'prompt':
michael@0 1109 showPrompt(cm, {
michael@0 1110 onClose: onPromptClose,
michael@0 1111 prefix: promptPrefix,
michael@0 1112 desc: searchPromptDesc,
michael@0 1113 onKeyUp: onPromptKeyUp,
michael@0 1114 onKeyDown: onPromptKeyDown
michael@0 1115 });
michael@0 1116 break;
michael@0 1117 case 'wordUnderCursor':
michael@0 1118 var word = expandWordUnderCursor(cm, false /** inclusive */,
michael@0 1119 true /** forward */, false /** bigWord */,
michael@0 1120 true /** noSymbol */);
michael@0 1121 var isKeyword = true;
michael@0 1122 if (!word) {
michael@0 1123 word = expandWordUnderCursor(cm, false /** inclusive */,
michael@0 1124 true /** forward */, false /** bigWord */,
michael@0 1125 false /** noSymbol */);
michael@0 1126 isKeyword = false;
michael@0 1127 }
michael@0 1128 if (!word) {
michael@0 1129 return;
michael@0 1130 }
michael@0 1131 var query = cm.getLine(word.start.line).substring(word.start.ch,
michael@0 1132 word.end.ch);
michael@0 1133 if (isKeyword) {
michael@0 1134 query = '\\b' + query + '\\b';
michael@0 1135 } else {
michael@0 1136 query = escapeRegex(query);
michael@0 1137 }
michael@0 1138
michael@0 1139 // cachedCursor is used to save the old position of the cursor
michael@0 1140 // when * or # causes vim to seek for the nearest word and shift
michael@0 1141 // the cursor before entering the motion.
michael@0 1142 vimGlobalState.jumpList.cachedCursor = cm.getCursor();
michael@0 1143 cm.setCursor(word.start);
michael@0 1144
michael@0 1145 handleQuery(query, true /** ignoreCase */, false /** smartCase */);
michael@0 1146 break;
michael@0 1147 }
michael@0 1148 },
michael@0 1149 processEx: function(cm, vim, command) {
michael@0 1150 function onPromptClose(input) {
michael@0 1151 // Give the prompt some time to close so that if processCommand shows
michael@0 1152 // an error, the elements don't overlap.
michael@0 1153 exCommandDispatcher.processCommand(cm, input);
michael@0 1154 }
michael@0 1155 function onPromptKeyDown(e, _input, close) {
michael@0 1156 var keyName = CodeMirror.keyName(e);
michael@0 1157 if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') {
michael@0 1158 CodeMirror.e_stop(e);
michael@0 1159 close();
michael@0 1160 cm.focus();
michael@0 1161 }
michael@0 1162 }
michael@0 1163 if (command.type == 'keyToEx') {
michael@0 1164 // Handle user defined Ex to Ex mappings
michael@0 1165 exCommandDispatcher.processCommand(cm, command.exArgs.input);
michael@0 1166 } else {
michael@0 1167 if (vim.visualMode) {
michael@0 1168 showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>',
michael@0 1169 onKeyDown: onPromptKeyDown});
michael@0 1170 } else {
michael@0 1171 showPrompt(cm, { onClose: onPromptClose, prefix: ':',
michael@0 1172 onKeyDown: onPromptKeyDown});
michael@0 1173 }
michael@0 1174 }
michael@0 1175 },
michael@0 1176 evalInput: function(cm, vim) {
michael@0 1177 // If the motion comand is set, execute both the operator and motion.
michael@0 1178 // Otherwise return.
michael@0 1179 var inputState = vim.inputState;
michael@0 1180 var motion = inputState.motion;
michael@0 1181 var motionArgs = inputState.motionArgs || {};
michael@0 1182 var operator = inputState.operator;
michael@0 1183 var operatorArgs = inputState.operatorArgs || {};
michael@0 1184 var registerName = inputState.registerName;
michael@0 1185 var selectionEnd = copyCursor(cm.getCursor('head'));
michael@0 1186 var selectionStart = copyCursor(cm.getCursor('anchor'));
michael@0 1187 // The difference between cur and selection cursors are that cur is
michael@0 1188 // being operated on and ignores that there is a selection.
michael@0 1189 var curStart = copyCursor(selectionEnd);
michael@0 1190 var curOriginal = copyCursor(curStart);
michael@0 1191 var curEnd;
michael@0 1192 var repeat;
michael@0 1193 if (operator) {
michael@0 1194 this.recordLastEdit(vim, inputState);
michael@0 1195 }
michael@0 1196 if (inputState.repeatOverride !== undefined) {
michael@0 1197 // If repeatOverride is specified, that takes precedence over the
michael@0 1198 // input state's repeat. Used by Ex mode and can be user defined.
michael@0 1199 repeat = inputState.repeatOverride;
michael@0 1200 } else {
michael@0 1201 repeat = inputState.getRepeat();
michael@0 1202 }
michael@0 1203 if (repeat > 0 && motionArgs.explicitRepeat) {
michael@0 1204 motionArgs.repeatIsExplicit = true;
michael@0 1205 } else if (motionArgs.noRepeat ||
michael@0 1206 (!motionArgs.explicitRepeat && repeat === 0)) {
michael@0 1207 repeat = 1;
michael@0 1208 motionArgs.repeatIsExplicit = false;
michael@0 1209 }
michael@0 1210 if (inputState.selectedCharacter) {
michael@0 1211 // If there is a character input, stick it in all of the arg arrays.
michael@0 1212 motionArgs.selectedCharacter = operatorArgs.selectedCharacter =
michael@0 1213 inputState.selectedCharacter;
michael@0 1214 }
michael@0 1215 motionArgs.repeat = repeat;
michael@0 1216 vim.inputState = new InputState();
michael@0 1217 if (motion) {
michael@0 1218 var motionResult = motions[motion](cm, motionArgs, vim);
michael@0 1219 vim.lastMotion = motions[motion];
michael@0 1220 if (!motionResult) {
michael@0 1221 return;
michael@0 1222 }
michael@0 1223 if (motionArgs.toJumplist) {
michael@0 1224 var jumpList = vimGlobalState.jumpList;
michael@0 1225 // if the current motion is # or *, use cachedCursor
michael@0 1226 var cachedCursor = jumpList.cachedCursor;
michael@0 1227 if (cachedCursor) {
michael@0 1228 recordJumpPosition(cm, cachedCursor, motionResult);
michael@0 1229 delete jumpList.cachedCursor;
michael@0 1230 } else {
michael@0 1231 recordJumpPosition(cm, curOriginal, motionResult);
michael@0 1232 }
michael@0 1233 }
michael@0 1234 if (motionResult instanceof Array) {
michael@0 1235 curStart = motionResult[0];
michael@0 1236 curEnd = motionResult[1];
michael@0 1237 } else {
michael@0 1238 curEnd = motionResult;
michael@0 1239 }
michael@0 1240 // TODO: Handle null returns from motion commands better.
michael@0 1241 if (!curEnd) {
michael@0 1242 curEnd = Pos(curStart.line, curStart.ch);
michael@0 1243 }
michael@0 1244 if (vim.visualMode) {
michael@0 1245 // Check if the selection crossed over itself. Will need to shift
michael@0 1246 // the start point if that happened.
michael@0 1247 if (cursorIsBefore(selectionStart, selectionEnd) &&
michael@0 1248 (cursorEqual(selectionStart, curEnd) ||
michael@0 1249 cursorIsBefore(curEnd, selectionStart))) {
michael@0 1250 // The end of the selection has moved from after the start to
michael@0 1251 // before the start. We will shift the start right by 1.
michael@0 1252 selectionStart.ch += 1;
michael@0 1253 } else if (cursorIsBefore(selectionEnd, selectionStart) &&
michael@0 1254 (cursorEqual(selectionStart, curEnd) ||
michael@0 1255 cursorIsBefore(selectionStart, curEnd))) {
michael@0 1256 // The opposite happened. We will shift the start left by 1.
michael@0 1257 selectionStart.ch -= 1;
michael@0 1258 }
michael@0 1259 selectionEnd = curEnd;
michael@0 1260 selectionStart = (motionResult instanceof Array) ? curStart : selectionStart;
michael@0 1261 if (vim.visualLine) {
michael@0 1262 if (cursorIsBefore(selectionStart, selectionEnd)) {
michael@0 1263 selectionStart.ch = 0;
michael@0 1264
michael@0 1265 var lastLine = cm.lastLine();
michael@0 1266 if (selectionEnd.line > lastLine) {
michael@0 1267 selectionEnd.line = lastLine;
michael@0 1268 }
michael@0 1269 selectionEnd.ch = lineLength(cm, selectionEnd.line);
michael@0 1270 } else {
michael@0 1271 selectionEnd.ch = 0;
michael@0 1272 selectionStart.ch = lineLength(cm, selectionStart.line);
michael@0 1273 }
michael@0 1274 }
michael@0 1275 cm.setSelection(selectionStart, selectionEnd);
michael@0 1276 updateMark(cm, vim, '<',
michael@0 1277 cursorIsBefore(selectionStart, selectionEnd) ? selectionStart
michael@0 1278 : selectionEnd);
michael@0 1279 updateMark(cm, vim, '>',
michael@0 1280 cursorIsBefore(selectionStart, selectionEnd) ? selectionEnd
michael@0 1281 : selectionStart);
michael@0 1282 } else if (!operator) {
michael@0 1283 curEnd = clipCursorToContent(cm, curEnd);
michael@0 1284 cm.setCursor(curEnd.line, curEnd.ch);
michael@0 1285 }
michael@0 1286 }
michael@0 1287
michael@0 1288 if (operator) {
michael@0 1289 var inverted = false;
michael@0 1290 vim.lastMotion = null;
michael@0 1291 operatorArgs.repeat = repeat; // Indent in visual mode needs this.
michael@0 1292 if (vim.visualMode) {
michael@0 1293 curStart = selectionStart;
michael@0 1294 curEnd = selectionEnd;
michael@0 1295 motionArgs.inclusive = true;
michael@0 1296 }
michael@0 1297 // Swap start and end if motion was backward.
michael@0 1298 if (cursorIsBefore(curEnd, curStart)) {
michael@0 1299 var tmp = curStart;
michael@0 1300 curStart = curEnd;
michael@0 1301 curEnd = tmp;
michael@0 1302 inverted = true;
michael@0 1303 }
michael@0 1304 if (motionArgs.inclusive && !(vim.visualMode && inverted)) {
michael@0 1305 // Move the selection end one to the right to include the last
michael@0 1306 // character.
michael@0 1307 curEnd.ch++;
michael@0 1308 }
michael@0 1309 var linewise = motionArgs.linewise ||
michael@0 1310 (vim.visualMode && vim.visualLine);
michael@0 1311 if (linewise) {
michael@0 1312 // Expand selection to entire line.
michael@0 1313 expandSelectionToLine(cm, curStart, curEnd);
michael@0 1314 } else if (motionArgs.forward) {
michael@0 1315 // Clip to trailing newlines only if the motion goes forward.
michael@0 1316 clipToLine(cm, curStart, curEnd);
michael@0 1317 }
michael@0 1318 operatorArgs.registerName = registerName;
michael@0 1319 // Keep track of linewise as it affects how paste and change behave.
michael@0 1320 operatorArgs.linewise = linewise;
michael@0 1321 operators[operator](cm, operatorArgs, vim, curStart,
michael@0 1322 curEnd, curOriginal);
michael@0 1323 if (vim.visualMode) {
michael@0 1324 exitVisualMode(cm);
michael@0 1325 }
michael@0 1326 }
michael@0 1327 },
michael@0 1328 recordLastEdit: function(vim, inputState, actionCommand) {
michael@0 1329 var macroModeState = vimGlobalState.macroModeState;
michael@0 1330 if (macroModeState.isPlaying) { return; }
michael@0 1331 vim.lastEditInputState = inputState;
michael@0 1332 vim.lastEditActionCommand = actionCommand;
michael@0 1333 macroModeState.lastInsertModeChanges.changes = [];
michael@0 1334 macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false;
michael@0 1335 }
michael@0 1336 };
michael@0 1337
michael@0 1338 /**
michael@0 1339 * typedef {Object{line:number,ch:number}} Cursor An object containing the
michael@0 1340 * position of the cursor.
michael@0 1341 */
michael@0 1342 // All of the functions below return Cursor objects.
michael@0 1343 var motions = {
michael@0 1344 moveToTopLine: function(cm, motionArgs) {
michael@0 1345 var line = getUserVisibleLines(cm).top + motionArgs.repeat -1;
michael@0 1346 return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
michael@0 1347 },
michael@0 1348 moveToMiddleLine: function(cm) {
michael@0 1349 var range = getUserVisibleLines(cm);
michael@0 1350 var line = Math.floor((range.top + range.bottom) * 0.5);
michael@0 1351 return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
michael@0 1352 },
michael@0 1353 moveToBottomLine: function(cm, motionArgs) {
michael@0 1354 var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1;
michael@0 1355 return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
michael@0 1356 },
michael@0 1357 expandToLine: function(cm, motionArgs) {
michael@0 1358 // Expands forward to end of line, and then to next line if repeat is
michael@0 1359 // >1. Does not handle backward motion!
michael@0 1360 var cur = cm.getCursor();
michael@0 1361 return Pos(cur.line + motionArgs.repeat - 1, Infinity);
michael@0 1362 },
michael@0 1363 findNext: function(cm, motionArgs) {
michael@0 1364 var state = getSearchState(cm);
michael@0 1365 var query = state.getQuery();
michael@0 1366 if (!query) {
michael@0 1367 return;
michael@0 1368 }
michael@0 1369 var prev = !motionArgs.forward;
michael@0 1370 // If search is initiated with ? instead of /, negate direction.
michael@0 1371 prev = (state.isReversed()) ? !prev : prev;
michael@0 1372 highlightSearchMatches(cm, query);
michael@0 1373 return findNext(cm, prev/** prev */, query, motionArgs.repeat);
michael@0 1374 },
michael@0 1375 goToMark: function(_cm, motionArgs, vim) {
michael@0 1376 var mark = vim.marks[motionArgs.selectedCharacter];
michael@0 1377 if (mark) {
michael@0 1378 return mark.find();
michael@0 1379 }
michael@0 1380 return null;
michael@0 1381 },
michael@0 1382 moveToOtherHighlightedEnd: function(cm) {
michael@0 1383 var curEnd = copyCursor(cm.getCursor('head'));
michael@0 1384 var curStart = copyCursor(cm.getCursor('anchor'));
michael@0 1385 if (cursorIsBefore(curStart, curEnd)) {
michael@0 1386 curEnd.ch += 1;
michael@0 1387 } else if (cursorIsBefore(curEnd, curStart)) {
michael@0 1388 curStart.ch -= 1;
michael@0 1389 }
michael@0 1390 return ([curEnd,curStart]);
michael@0 1391 },
michael@0 1392 jumpToMark: function(cm, motionArgs, vim) {
michael@0 1393 var best = cm.getCursor();
michael@0 1394 for (var i = 0; i < motionArgs.repeat; i++) {
michael@0 1395 var cursor = best;
michael@0 1396 for (var key in vim.marks) {
michael@0 1397 if (!isLowerCase(key)) {
michael@0 1398 continue;
michael@0 1399 }
michael@0 1400 var mark = vim.marks[key].find();
michael@0 1401 var isWrongDirection = (motionArgs.forward) ?
michael@0 1402 cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark);
michael@0 1403
michael@0 1404 if (isWrongDirection) {
michael@0 1405 continue;
michael@0 1406 }
michael@0 1407 if (motionArgs.linewise && (mark.line == cursor.line)) {
michael@0 1408 continue;
michael@0 1409 }
michael@0 1410
michael@0 1411 var equal = cursorEqual(cursor, best);
michael@0 1412 var between = (motionArgs.forward) ?
michael@0 1413 cusrorIsBetween(cursor, mark, best) :
michael@0 1414 cusrorIsBetween(best, mark, cursor);
michael@0 1415
michael@0 1416 if (equal || between) {
michael@0 1417 best = mark;
michael@0 1418 }
michael@0 1419 }
michael@0 1420 }
michael@0 1421
michael@0 1422 if (motionArgs.linewise) {
michael@0 1423 // Vim places the cursor on the first non-whitespace character of
michael@0 1424 // the line if there is one, else it places the cursor at the end
michael@0 1425 // of the line, regardless of whether a mark was found.
michael@0 1426 best = Pos(best.line, findFirstNonWhiteSpaceCharacter(cm.getLine(best.line)));
michael@0 1427 }
michael@0 1428 return best;
michael@0 1429 },
michael@0 1430 moveByCharacters: function(cm, motionArgs) {
michael@0 1431 var cur = cm.getCursor();
michael@0 1432 var repeat = motionArgs.repeat;
michael@0 1433 var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat;
michael@0 1434 return Pos(cur.line, ch);
michael@0 1435 },
michael@0 1436 moveByLines: function(cm, motionArgs, vim) {
michael@0 1437 var cur = cm.getCursor();
michael@0 1438 var endCh = cur.ch;
michael@0 1439 // Depending what our last motion was, we may want to do different
michael@0 1440 // things. If our last motion was moving vertically, we want to
michael@0 1441 // preserve the HPos from our last horizontal move. If our last motion
michael@0 1442 // was going to the end of a line, moving vertically we should go to
michael@0 1443 // the end of the line, etc.
michael@0 1444 switch (vim.lastMotion) {
michael@0 1445 case this.moveByLines:
michael@0 1446 case this.moveByDisplayLines:
michael@0 1447 case this.moveByScroll:
michael@0 1448 case this.moveToColumn:
michael@0 1449 case this.moveToEol:
michael@0 1450 endCh = vim.lastHPos;
michael@0 1451 break;
michael@0 1452 default:
michael@0 1453 vim.lastHPos = endCh;
michael@0 1454 }
michael@0 1455 var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0);
michael@0 1456 var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat;
michael@0 1457 var first = cm.firstLine();
michael@0 1458 var last = cm.lastLine();
michael@0 1459 // Vim cancels linewise motions that start on an edge and move beyond
michael@0 1460 // that edge. It does not cancel motions that do not start on an edge.
michael@0 1461 if ((line < first && cur.line == first) ||
michael@0 1462 (line > last && cur.line == last)) {
michael@0 1463 return;
michael@0 1464 }
michael@0 1465 if (motionArgs.toFirstChar){
michael@0 1466 endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line));
michael@0 1467 vim.lastHPos = endCh;
michael@0 1468 }
michael@0 1469 vim.lastHSPos = cm.charCoords(Pos(line, endCh),'div').left;
michael@0 1470 return Pos(line, endCh);
michael@0 1471 },
michael@0 1472 moveByDisplayLines: function(cm, motionArgs, vim) {
michael@0 1473 var cur = cm.getCursor();
michael@0 1474 switch (vim.lastMotion) {
michael@0 1475 case this.moveByDisplayLines:
michael@0 1476 case this.moveByScroll:
michael@0 1477 case this.moveByLines:
michael@0 1478 case this.moveToColumn:
michael@0 1479 case this.moveToEol:
michael@0 1480 break;
michael@0 1481 default:
michael@0 1482 vim.lastHSPos = cm.charCoords(cur,'div').left;
michael@0 1483 }
michael@0 1484 var repeat = motionArgs.repeat;
michael@0 1485 var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),'line',vim.lastHSPos);
michael@0 1486 if (res.hitSide) {
michael@0 1487 if (motionArgs.forward) {
michael@0 1488 var lastCharCoords = cm.charCoords(res, 'div');
michael@0 1489 var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos };
michael@0 1490 var res = cm.coordsChar(goalCoords, 'div');
michael@0 1491 } else {
michael@0 1492 var resCoords = cm.charCoords(Pos(cm.firstLine(), 0), 'div');
michael@0 1493 resCoords.left = vim.lastHSPos;
michael@0 1494 res = cm.coordsChar(resCoords, 'div');
michael@0 1495 }
michael@0 1496 }
michael@0 1497 vim.lastHPos = res.ch;
michael@0 1498 return res;
michael@0 1499 },
michael@0 1500 moveByPage: function(cm, motionArgs) {
michael@0 1501 // CodeMirror only exposes functions that move the cursor page down, so
michael@0 1502 // doing this bad hack to move the cursor and move it back. evalInput
michael@0 1503 // will move the cursor to where it should be in the end.
michael@0 1504 var curStart = cm.getCursor();
michael@0 1505 var repeat = motionArgs.repeat;
michael@0 1506 cm.moveV((motionArgs.forward ? repeat : -repeat), 'page');
michael@0 1507 var curEnd = cm.getCursor();
michael@0 1508 cm.setCursor(curStart);
michael@0 1509 return curEnd;
michael@0 1510 },
michael@0 1511 moveByParagraph: function(cm, motionArgs) {
michael@0 1512 var line = cm.getCursor().line;
michael@0 1513 var repeat = motionArgs.repeat;
michael@0 1514 var inc = motionArgs.forward ? 1 : -1;
michael@0 1515 for (var i = 0; i < repeat; i++) {
michael@0 1516 if ((!motionArgs.forward && line === cm.firstLine() ) ||
michael@0 1517 (motionArgs.forward && line == cm.lastLine())) {
michael@0 1518 break;
michael@0 1519 }
michael@0 1520 line += inc;
michael@0 1521 while (line !== cm.firstLine() && line != cm.lastLine() && cm.getLine(line)) {
michael@0 1522 line += inc;
michael@0 1523 }
michael@0 1524 }
michael@0 1525 return Pos(line, 0);
michael@0 1526 },
michael@0 1527 moveByScroll: function(cm, motionArgs, vim) {
michael@0 1528 var scrollbox = cm.getScrollInfo();
michael@0 1529 var curEnd = null;
michael@0 1530 var repeat = motionArgs.repeat;
michael@0 1531 if (!repeat) {
michael@0 1532 repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight());
michael@0 1533 }
michael@0 1534 var orig = cm.charCoords(cm.getCursor(), 'local');
michael@0 1535 motionArgs.repeat = repeat;
michael@0 1536 var curEnd = motions.moveByDisplayLines(cm, motionArgs, vim);
michael@0 1537 if (!curEnd) {
michael@0 1538 return null;
michael@0 1539 }
michael@0 1540 var dest = cm.charCoords(curEnd, 'local');
michael@0 1541 cm.scrollTo(null, scrollbox.top + dest.top - orig.top);
michael@0 1542 return curEnd;
michael@0 1543 },
michael@0 1544 moveByWords: function(cm, motionArgs) {
michael@0 1545 return moveToWord(cm, motionArgs.repeat, !!motionArgs.forward,
michael@0 1546 !!motionArgs.wordEnd, !!motionArgs.bigWord);
michael@0 1547 },
michael@0 1548 moveTillCharacter: function(cm, motionArgs) {
michael@0 1549 var repeat = motionArgs.repeat;
michael@0 1550 var curEnd = moveToCharacter(cm, repeat, motionArgs.forward,
michael@0 1551 motionArgs.selectedCharacter);
michael@0 1552 var increment = motionArgs.forward ? -1 : 1;
michael@0 1553 recordLastCharacterSearch(increment, motionArgs);
michael@0 1554 if (!curEnd) return null;
michael@0 1555 curEnd.ch += increment;
michael@0 1556 return curEnd;
michael@0 1557 },
michael@0 1558 moveToCharacter: function(cm, motionArgs) {
michael@0 1559 var repeat = motionArgs.repeat;
michael@0 1560 recordLastCharacterSearch(0, motionArgs);
michael@0 1561 return moveToCharacter(cm, repeat, motionArgs.forward,
michael@0 1562 motionArgs.selectedCharacter) || cm.getCursor();
michael@0 1563 },
michael@0 1564 moveToSymbol: function(cm, motionArgs) {
michael@0 1565 var repeat = motionArgs.repeat;
michael@0 1566 return findSymbol(cm, repeat, motionArgs.forward,
michael@0 1567 motionArgs.selectedCharacter) || cm.getCursor();
michael@0 1568 },
michael@0 1569 moveToColumn: function(cm, motionArgs, vim) {
michael@0 1570 var repeat = motionArgs.repeat;
michael@0 1571 // repeat is equivalent to which column we want to move to!
michael@0 1572 vim.lastHPos = repeat - 1;
michael@0 1573 vim.lastHSPos = cm.charCoords(cm.getCursor(),'div').left;
michael@0 1574 return moveToColumn(cm, repeat);
michael@0 1575 },
michael@0 1576 moveToEol: function(cm, motionArgs, vim) {
michael@0 1577 var cur = cm.getCursor();
michael@0 1578 vim.lastHPos = Infinity;
michael@0 1579 var retval= Pos(cur.line + motionArgs.repeat - 1, Infinity);
michael@0 1580 var end=cm.clipPos(retval);
michael@0 1581 end.ch--;
michael@0 1582 vim.lastHSPos = cm.charCoords(end,'div').left;
michael@0 1583 return retval;
michael@0 1584 },
michael@0 1585 moveToFirstNonWhiteSpaceCharacter: function(cm) {
michael@0 1586 // Go to the start of the line where the text begins, or the end for
michael@0 1587 // whitespace-only lines
michael@0 1588 var cursor = cm.getCursor();
michael@0 1589 return Pos(cursor.line,
michael@0 1590 findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line)));
michael@0 1591 },
michael@0 1592 moveToMatchedSymbol: function(cm) {
michael@0 1593 var cursor = cm.getCursor();
michael@0 1594 var line = cursor.line;
michael@0 1595 var ch = cursor.ch;
michael@0 1596 var lineText = cm.getLine(line);
michael@0 1597 var symbol;
michael@0 1598 var startContext = cm.getTokenAt(cursor).type;
michael@0 1599 var startCtxLevel = getContextLevel(startContext);
michael@0 1600 do {
michael@0 1601 symbol = lineText.charAt(ch++);
michael@0 1602 if (symbol && isMatchableSymbol(symbol)) {
michael@0 1603 var endContext = cm.getTokenAt(Pos(line, ch)).type;
michael@0 1604 var endCtxLevel = getContextLevel(endContext);
michael@0 1605 if (startCtxLevel >= endCtxLevel) {
michael@0 1606 break;
michael@0 1607 }
michael@0 1608 }
michael@0 1609 } while (symbol);
michael@0 1610 if (symbol) {
michael@0 1611 return findMatchedSymbol(cm, Pos(line, ch-1), symbol);
michael@0 1612 } else {
michael@0 1613 return cursor;
michael@0 1614 }
michael@0 1615 },
michael@0 1616 moveToStartOfLine: function(cm) {
michael@0 1617 var cursor = cm.getCursor();
michael@0 1618 return Pos(cursor.line, 0);
michael@0 1619 },
michael@0 1620 moveToLineOrEdgeOfDocument: function(cm, motionArgs) {
michael@0 1621 var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine();
michael@0 1622 if (motionArgs.repeatIsExplicit) {
michael@0 1623 lineNum = motionArgs.repeat - cm.getOption('firstLineNumber');
michael@0 1624 }
michael@0 1625 return Pos(lineNum,
michael@0 1626 findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum)));
michael@0 1627 },
michael@0 1628 textObjectManipulation: function(cm, motionArgs) {
michael@0 1629 // TODO: lots of possible exceptions that can be thrown here. Try da(
michael@0 1630 // outside of a () block.
michael@0 1631
michael@0 1632 // TODO: adding <> >< to this map doesn't work, presumably because
michael@0 1633 // they're operators
michael@0 1634 var mirroredPairs = {'(': ')', ')': '(',
michael@0 1635 '{': '}', '}': '{',
michael@0 1636 '[': ']', ']': '['};
michael@0 1637 var selfPaired = {'\'': true, '"': true};
michael@0 1638
michael@0 1639 var character = motionArgs.selectedCharacter;
michael@0 1640
michael@0 1641 // Inclusive is the difference between a and i
michael@0 1642 // TODO: Instead of using the additional text object map to perform text
michael@0 1643 // object operations, merge the map into the defaultKeyMap and use
michael@0 1644 // motionArgs to define behavior. Define separate entries for 'aw',
michael@0 1645 // 'iw', 'a[', 'i[', etc.
michael@0 1646 var inclusive = !motionArgs.textObjectInner;
michael@0 1647
michael@0 1648 var tmp;
michael@0 1649 if (mirroredPairs[character]) {
michael@0 1650 tmp = selectCompanionObject(cm, mirroredPairs[character], inclusive);
michael@0 1651 } else if (selfPaired[character]) {
michael@0 1652 tmp = findBeginningAndEnd(cm, character, inclusive);
michael@0 1653 } else if (character === 'W') {
michael@0 1654 tmp = expandWordUnderCursor(cm, inclusive, true /** forward */,
michael@0 1655 true /** bigWord */);
michael@0 1656 } else if (character === 'w') {
michael@0 1657 tmp = expandWordUnderCursor(cm, inclusive, true /** forward */,
michael@0 1658 false /** bigWord */);
michael@0 1659 } else {
michael@0 1660 // No text object defined for this, don't move.
michael@0 1661 return null;
michael@0 1662 }
michael@0 1663
michael@0 1664 return [tmp.start, tmp.end];
michael@0 1665 },
michael@0 1666
michael@0 1667 repeatLastCharacterSearch: function(cm, motionArgs) {
michael@0 1668 var lastSearch = vimGlobalState.lastChararacterSearch;
michael@0 1669 var repeat = motionArgs.repeat;
michael@0 1670 var forward = motionArgs.forward === lastSearch.forward;
michael@0 1671 var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1);
michael@0 1672 cm.moveH(-increment, 'char');
michael@0 1673 motionArgs.inclusive = forward ? true : false;
michael@0 1674 var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter);
michael@0 1675 if (!curEnd) {
michael@0 1676 cm.moveH(increment, 'char');
michael@0 1677 return cm.getCursor();
michael@0 1678 }
michael@0 1679 curEnd.ch += increment;
michael@0 1680 return curEnd;
michael@0 1681 }
michael@0 1682 };
michael@0 1683
michael@0 1684 var operators = {
michael@0 1685 change: function(cm, operatorArgs, _vim, curStart, curEnd) {
michael@0 1686 vimGlobalState.registerController.pushText(
michael@0 1687 operatorArgs.registerName, 'change', cm.getRange(curStart, curEnd),
michael@0 1688 operatorArgs.linewise);
michael@0 1689 if (operatorArgs.linewise) {
michael@0 1690 // Push the next line back down, if there is a next line.
michael@0 1691 var replacement = curEnd.line > cm.lastLine() ? '' : '\n';
michael@0 1692 cm.replaceRange(replacement, curStart, curEnd);
michael@0 1693 cm.indentLine(curStart.line, 'smart');
michael@0 1694 // null ch so setCursor moves to end of line.
michael@0 1695 curStart.ch = null;
michael@0 1696 } else {
michael@0 1697 // Exclude trailing whitespace if the range is not all whitespace.
michael@0 1698 var text = cm.getRange(curStart, curEnd);
michael@0 1699 if (!isWhiteSpaceString(text)) {
michael@0 1700 var match = (/\s+$/).exec(text);
michael@0 1701 if (match) {
michael@0 1702 curEnd = offsetCursor(curEnd, 0, - match[0].length);
michael@0 1703 }
michael@0 1704 }
michael@0 1705 cm.replaceRange('', curStart, curEnd);
michael@0 1706 }
michael@0 1707 actions.enterInsertMode(cm, {}, cm.state.vim);
michael@0 1708 cm.setCursor(curStart);
michael@0 1709 },
michael@0 1710 // delete is a javascript keyword.
michael@0 1711 'delete': function(cm, operatorArgs, _vim, curStart, curEnd) {
michael@0 1712 // If the ending line is past the last line, inclusive, instead of
michael@0 1713 // including the trailing \n, include the \n before the starting line
michael@0 1714 if (operatorArgs.linewise &&
michael@0 1715 curEnd.line > cm.lastLine() && curStart.line > cm.firstLine()) {
michael@0 1716 curStart.line--;
michael@0 1717 curStart.ch = lineLength(cm, curStart.line);
michael@0 1718 }
michael@0 1719 vimGlobalState.registerController.pushText(
michael@0 1720 operatorArgs.registerName, 'delete', cm.getRange(curStart, curEnd),
michael@0 1721 operatorArgs.linewise);
michael@0 1722 cm.replaceRange('', curStart, curEnd);
michael@0 1723 if (operatorArgs.linewise) {
michael@0 1724 cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));
michael@0 1725 } else {
michael@0 1726 cm.setCursor(curStart);
michael@0 1727 }
michael@0 1728 },
michael@0 1729 indent: function(cm, operatorArgs, vim, curStart, curEnd) {
michael@0 1730 var startLine = curStart.line;
michael@0 1731 var endLine = curEnd.line;
michael@0 1732 // In visual mode, n> shifts the selection right n times, instead of
michael@0 1733 // shifting n lines right once.
michael@0 1734 var repeat = (vim.visualMode) ? operatorArgs.repeat : 1;
michael@0 1735 if (operatorArgs.linewise) {
michael@0 1736 // The only way to delete a newline is to delete until the start of
michael@0 1737 // the next line, so in linewise mode evalInput will include the next
michael@0 1738 // line. We don't want this in indent, so we go back a line.
michael@0 1739 endLine--;
michael@0 1740 }
michael@0 1741 for (var i = startLine; i <= endLine; i++) {
michael@0 1742 for (var j = 0; j < repeat; j++) {
michael@0 1743 cm.indentLine(i, operatorArgs.indentRight);
michael@0 1744 }
michael@0 1745 }
michael@0 1746 cm.setCursor(curStart);
michael@0 1747 cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));
michael@0 1748 },
michael@0 1749 swapcase: function(cm, operatorArgs, _vim, curStart, curEnd, curOriginal) {
michael@0 1750 var toSwap = cm.getRange(curStart, curEnd);
michael@0 1751 var swapped = '';
michael@0 1752 for (var i = 0; i < toSwap.length; i++) {
michael@0 1753 var character = toSwap.charAt(i);
michael@0 1754 swapped += isUpperCase(character) ? character.toLowerCase() :
michael@0 1755 character.toUpperCase();
michael@0 1756 }
michael@0 1757 cm.replaceRange(swapped, curStart, curEnd);
michael@0 1758 if (!operatorArgs.shouldMoveCursor) {
michael@0 1759 cm.setCursor(curOriginal);
michael@0 1760 }
michael@0 1761 },
michael@0 1762 yank: function(cm, operatorArgs, _vim, curStart, curEnd, curOriginal) {
michael@0 1763 vimGlobalState.registerController.pushText(
michael@0 1764 operatorArgs.registerName, 'yank',
michael@0 1765 cm.getRange(curStart, curEnd), operatorArgs.linewise);
michael@0 1766 cm.setCursor(curOriginal);
michael@0 1767 }
michael@0 1768 };
michael@0 1769
michael@0 1770 var actions = {
michael@0 1771 jumpListWalk: function(cm, actionArgs, vim) {
michael@0 1772 if (vim.visualMode) {
michael@0 1773 return;
michael@0 1774 }
michael@0 1775 var repeat = actionArgs.repeat;
michael@0 1776 var forward = actionArgs.forward;
michael@0 1777 var jumpList = vimGlobalState.jumpList;
michael@0 1778
michael@0 1779 var mark = jumpList.move(cm, forward ? repeat : -repeat);
michael@0 1780 var markPos = mark ? mark.find() : undefined;
michael@0 1781 markPos = markPos ? markPos : cm.getCursor();
michael@0 1782 cm.setCursor(markPos);
michael@0 1783 },
michael@0 1784 scroll: function(cm, actionArgs, vim) {
michael@0 1785 if (vim.visualMode) {
michael@0 1786 return;
michael@0 1787 }
michael@0 1788 var repeat = actionArgs.repeat || 1;
michael@0 1789 var lineHeight = cm.defaultTextHeight();
michael@0 1790 var top = cm.getScrollInfo().top;
michael@0 1791 var delta = lineHeight * repeat;
michael@0 1792 var newPos = actionArgs.forward ? top + delta : top - delta;
michael@0 1793 var cursor = copyCursor(cm.getCursor());
michael@0 1794 var cursorCoords = cm.charCoords(cursor, 'local');
michael@0 1795 if (actionArgs.forward) {
michael@0 1796 if (newPos > cursorCoords.top) {
michael@0 1797 cursor.line += (newPos - cursorCoords.top) / lineHeight;
michael@0 1798 cursor.line = Math.ceil(cursor.line);
michael@0 1799 cm.setCursor(cursor);
michael@0 1800 cursorCoords = cm.charCoords(cursor, 'local');
michael@0 1801 cm.scrollTo(null, cursorCoords.top);
michael@0 1802 } else {
michael@0 1803 // Cursor stays within bounds. Just reposition the scroll window.
michael@0 1804 cm.scrollTo(null, newPos);
michael@0 1805 }
michael@0 1806 } else {
michael@0 1807 var newBottom = newPos + cm.getScrollInfo().clientHeight;
michael@0 1808 if (newBottom < cursorCoords.bottom) {
michael@0 1809 cursor.line -= (cursorCoords.bottom - newBottom) / lineHeight;
michael@0 1810 cursor.line = Math.floor(cursor.line);
michael@0 1811 cm.setCursor(cursor);
michael@0 1812 cursorCoords = cm.charCoords(cursor, 'local');
michael@0 1813 cm.scrollTo(
michael@0 1814 null, cursorCoords.bottom - cm.getScrollInfo().clientHeight);
michael@0 1815 } else {
michael@0 1816 // Cursor stays within bounds. Just reposition the scroll window.
michael@0 1817 cm.scrollTo(null, newPos);
michael@0 1818 }
michael@0 1819 }
michael@0 1820 },
michael@0 1821 scrollToCursor: function(cm, actionArgs) {
michael@0 1822 var lineNum = cm.getCursor().line;
michael@0 1823 var charCoords = cm.charCoords(Pos(lineNum, 0), 'local');
michael@0 1824 var height = cm.getScrollInfo().clientHeight;
michael@0 1825 var y = charCoords.top;
michael@0 1826 var lineHeight = charCoords.bottom - y;
michael@0 1827 switch (actionArgs.position) {
michael@0 1828 case 'center': y = y - (height / 2) + lineHeight;
michael@0 1829 break;
michael@0 1830 case 'bottom': y = y - height + lineHeight*1.4;
michael@0 1831 break;
michael@0 1832 case 'top': y = y + lineHeight*0.4;
michael@0 1833 break;
michael@0 1834 }
michael@0 1835 cm.scrollTo(null, y);
michael@0 1836 },
michael@0 1837 replayMacro: function(cm, actionArgs, vim) {
michael@0 1838 var registerName = actionArgs.selectedCharacter;
michael@0 1839 var repeat = actionArgs.repeat;
michael@0 1840 var macroModeState = vimGlobalState.macroModeState;
michael@0 1841 if (registerName == '@') {
michael@0 1842 registerName = macroModeState.latestRegister;
michael@0 1843 }
michael@0 1844 while(repeat--){
michael@0 1845 executeMacroRegister(cm, vim, macroModeState, registerName);
michael@0 1846 }
michael@0 1847 },
michael@0 1848 enterMacroRecordMode: function(cm, actionArgs) {
michael@0 1849 var macroModeState = vimGlobalState.macroModeState;
michael@0 1850 var registerName = actionArgs.selectedCharacter;
michael@0 1851 macroModeState.enterMacroRecordMode(cm, registerName);
michael@0 1852 },
michael@0 1853 enterInsertMode: function(cm, actionArgs, vim) {
michael@0 1854 if (cm.getOption('readOnly')) { return; }
michael@0 1855 vim.insertMode = true;
michael@0 1856 vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1;
michael@0 1857 var insertAt = (actionArgs) ? actionArgs.insertAt : null;
michael@0 1858 if (insertAt == 'eol') {
michael@0 1859 var cursor = cm.getCursor();
michael@0 1860 cursor = Pos(cursor.line, lineLength(cm, cursor.line));
michael@0 1861 cm.setCursor(cursor);
michael@0 1862 } else if (insertAt == 'charAfter') {
michael@0 1863 cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));
michael@0 1864 } else if (insertAt == 'firstNonBlank') {
michael@0 1865 cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));
michael@0 1866 }
michael@0 1867 cm.setOption('keyMap', 'vim-insert');
michael@0 1868 cm.setOption('disableInput', false);
michael@0 1869 if (actionArgs && actionArgs.replace) {
michael@0 1870 // Handle Replace-mode as a special case of insert mode.
michael@0 1871 cm.toggleOverwrite(true);
michael@0 1872 cm.setOption('keyMap', 'vim-replace');
michael@0 1873 CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"});
michael@0 1874 } else {
michael@0 1875 cm.setOption('keyMap', 'vim-insert');
michael@0 1876 CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"});
michael@0 1877 }
michael@0 1878 if (!vimGlobalState.macroModeState.isPlaying) {
michael@0 1879 // Only record if not replaying.
michael@0 1880 cm.on('change', onChange);
michael@0 1881 cm.on('cursorActivity', onCursorActivity);
michael@0 1882 CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown);
michael@0 1883 }
michael@0 1884 },
michael@0 1885 toggleVisualMode: function(cm, actionArgs, vim) {
michael@0 1886 var repeat = actionArgs.repeat;
michael@0 1887 var curStart = cm.getCursor();
michael@0 1888 var curEnd;
michael@0 1889 // TODO: The repeat should actually select number of characters/lines
michael@0 1890 // equal to the repeat times the size of the previous visual
michael@0 1891 // operation.
michael@0 1892 if (!vim.visualMode) {
michael@0 1893 cm.on('mousedown', exitVisualMode);
michael@0 1894 vim.visualMode = true;
michael@0 1895 vim.visualLine = !!actionArgs.linewise;
michael@0 1896 if (vim.visualLine) {
michael@0 1897 curStart.ch = 0;
michael@0 1898 curEnd = clipCursorToContent(
michael@0 1899 cm, Pos(curStart.line + repeat - 1, lineLength(cm, curStart.line)),
michael@0 1900 true /** includeLineBreak */);
michael@0 1901 } else {
michael@0 1902 curEnd = clipCursorToContent(
michael@0 1903 cm, Pos(curStart.line, curStart.ch + repeat),
michael@0 1904 true /** includeLineBreak */);
michael@0 1905 }
michael@0 1906 // Make the initial selection.
michael@0 1907 if (!actionArgs.repeatIsExplicit && !vim.visualLine) {
michael@0 1908 // This is a strange case. Here the implicit repeat is 1. The
michael@0 1909 // following commands lets the cursor hover over the 1 character
michael@0 1910 // selection.
michael@0 1911 cm.setCursor(curEnd);
michael@0 1912 cm.setSelection(curEnd, curStart);
michael@0 1913 } else {
michael@0 1914 cm.setSelection(curStart, curEnd);
michael@0 1915 }
michael@0 1916 CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : ""});
michael@0 1917 } else {
michael@0 1918 curStart = cm.getCursor('anchor');
michael@0 1919 curEnd = cm.getCursor('head');
michael@0 1920 if (!vim.visualLine && actionArgs.linewise) {
michael@0 1921 // Shift-V pressed in characterwise visual mode. Switch to linewise
michael@0 1922 // visual mode instead of exiting visual mode.
michael@0 1923 vim.visualLine = true;
michael@0 1924 curStart.ch = cursorIsBefore(curStart, curEnd) ? 0 :
michael@0 1925 lineLength(cm, curStart.line);
michael@0 1926 curEnd.ch = cursorIsBefore(curStart, curEnd) ?
michael@0 1927 lineLength(cm, curEnd.line) : 0;
michael@0 1928 cm.setSelection(curStart, curEnd);
michael@0 1929 CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: "linewise"});
michael@0 1930 } else if (vim.visualLine && !actionArgs.linewise) {
michael@0 1931 // v pressed in linewise visual mode. Switch to characterwise visual
michael@0 1932 // mode instead of exiting visual mode.
michael@0 1933 vim.visualLine = false;
michael@0 1934 CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"});
michael@0 1935 } else {
michael@0 1936 exitVisualMode(cm);
michael@0 1937 }
michael@0 1938 }
michael@0 1939 updateMark(cm, vim, '<', cursorIsBefore(curStart, curEnd) ? curStart
michael@0 1940 : curEnd);
michael@0 1941 updateMark(cm, vim, '>', cursorIsBefore(curStart, curEnd) ? curEnd
michael@0 1942 : curStart);
michael@0 1943 },
michael@0 1944 reselectLastSelection: function(cm, _actionArgs, vim) {
michael@0 1945 if (vim.lastSelection) {
michael@0 1946 var lastSelection = vim.lastSelection;
michael@0 1947 cm.setSelection(lastSelection.curStart, lastSelection.curEnd);
michael@0 1948 if (lastSelection.visualLine) {
michael@0 1949 vim.visualMode = true;
michael@0 1950 vim.visualLine = true;
michael@0 1951 }
michael@0 1952 else {
michael@0 1953 vim.visualMode = true;
michael@0 1954 vim.visualLine = false;
michael@0 1955 }
michael@0 1956 CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : ""});
michael@0 1957 }
michael@0 1958 },
michael@0 1959 joinLines: function(cm, actionArgs, vim) {
michael@0 1960 var curStart, curEnd;
michael@0 1961 if (vim.visualMode) {
michael@0 1962 curStart = cm.getCursor('anchor');
michael@0 1963 curEnd = cm.getCursor('head');
michael@0 1964 curEnd.ch = lineLength(cm, curEnd.line) - 1;
michael@0 1965 } else {
michael@0 1966 // Repeat is the number of lines to join. Minimum 2 lines.
michael@0 1967 var repeat = Math.max(actionArgs.repeat, 2);
michael@0 1968 curStart = cm.getCursor();
michael@0 1969 curEnd = clipCursorToContent(cm, Pos(curStart.line + repeat - 1,
michael@0 1970 Infinity));
michael@0 1971 }
michael@0 1972 var finalCh = 0;
michael@0 1973 cm.operation(function() {
michael@0 1974 for (var i = curStart.line; i < curEnd.line; i++) {
michael@0 1975 finalCh = lineLength(cm, curStart.line);
michael@0 1976 var tmp = Pos(curStart.line + 1,
michael@0 1977 lineLength(cm, curStart.line + 1));
michael@0 1978 var text = cm.getRange(curStart, tmp);
michael@0 1979 text = text.replace(/\n\s*/g, ' ');
michael@0 1980 cm.replaceRange(text, curStart, tmp);
michael@0 1981 }
michael@0 1982 var curFinalPos = Pos(curStart.line, finalCh);
michael@0 1983 cm.setCursor(curFinalPos);
michael@0 1984 });
michael@0 1985 },
michael@0 1986 newLineAndEnterInsertMode: function(cm, actionArgs, vim) {
michael@0 1987 vim.insertMode = true;
michael@0 1988 var insertAt = copyCursor(cm.getCursor());
michael@0 1989 if (insertAt.line === cm.firstLine() && !actionArgs.after) {
michael@0 1990 // Special case for inserting newline before start of document.
michael@0 1991 cm.replaceRange('\n', Pos(cm.firstLine(), 0));
michael@0 1992 cm.setCursor(cm.firstLine(), 0);
michael@0 1993 } else {
michael@0 1994 insertAt.line = (actionArgs.after) ? insertAt.line :
michael@0 1995 insertAt.line - 1;
michael@0 1996 insertAt.ch = lineLength(cm, insertAt.line);
michael@0 1997 cm.setCursor(insertAt);
michael@0 1998 var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment ||
michael@0 1999 CodeMirror.commands.newlineAndIndent;
michael@0 2000 newlineFn(cm);
michael@0 2001 }
michael@0 2002 this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim);
michael@0 2003 },
michael@0 2004 paste: function(cm, actionArgs) {
michael@0 2005 var cur = copyCursor(cm.getCursor());
michael@0 2006 var register = vimGlobalState.registerController.getRegister(
michael@0 2007 actionArgs.registerName);
michael@0 2008 var text = register.toString();
michael@0 2009 if (!text) {
michael@0 2010 return;
michael@0 2011 }
michael@0 2012 if (actionArgs.repeat > 1) {
michael@0 2013 var text = Array(actionArgs.repeat + 1).join(text);
michael@0 2014 }
michael@0 2015 var linewise = register.linewise;
michael@0 2016 if (linewise) {
michael@0 2017 if (actionArgs.after) {
michael@0 2018 // Move the newline at the end to the start instead, and paste just
michael@0 2019 // before the newline character of the line we are on right now.
michael@0 2020 text = '\n' + text.slice(0, text.length - 1);
michael@0 2021 cur.ch = lineLength(cm, cur.line);
michael@0 2022 } else {
michael@0 2023 cur.ch = 0;
michael@0 2024 }
michael@0 2025 } else {
michael@0 2026 cur.ch += actionArgs.after ? 1 : 0;
michael@0 2027 }
michael@0 2028 cm.replaceRange(text, cur);
michael@0 2029 // Now fine tune the cursor to where we want it.
michael@0 2030 var curPosFinal;
michael@0 2031 var idx;
michael@0 2032 if (linewise && actionArgs.after) {
michael@0 2033 curPosFinal = Pos(
michael@0 2034 cur.line + 1,
michael@0 2035 findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1)));
michael@0 2036 } else if (linewise && !actionArgs.after) {
michael@0 2037 curPosFinal = Pos(
michael@0 2038 cur.line,
michael@0 2039 findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line)));
michael@0 2040 } else if (!linewise && actionArgs.after) {
michael@0 2041 idx = cm.indexFromPos(cur);
michael@0 2042 curPosFinal = cm.posFromIndex(idx + text.length - 1);
michael@0 2043 } else {
michael@0 2044 idx = cm.indexFromPos(cur);
michael@0 2045 curPosFinal = cm.posFromIndex(idx + text.length);
michael@0 2046 }
michael@0 2047 cm.setCursor(curPosFinal);
michael@0 2048 },
michael@0 2049 undo: function(cm, actionArgs) {
michael@0 2050 cm.operation(function() {
michael@0 2051 repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)();
michael@0 2052 cm.setCursor(cm.getCursor('anchor'));
michael@0 2053 });
michael@0 2054 },
michael@0 2055 redo: function(cm, actionArgs) {
michael@0 2056 repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)();
michael@0 2057 },
michael@0 2058 setRegister: function(_cm, actionArgs, vim) {
michael@0 2059 vim.inputState.registerName = actionArgs.selectedCharacter;
michael@0 2060 },
michael@0 2061 setMark: function(cm, actionArgs, vim) {
michael@0 2062 var markName = actionArgs.selectedCharacter;
michael@0 2063 updateMark(cm, vim, markName, cm.getCursor());
michael@0 2064 },
michael@0 2065 replace: function(cm, actionArgs, vim) {
michael@0 2066 var replaceWith = actionArgs.selectedCharacter;
michael@0 2067 var curStart = cm.getCursor();
michael@0 2068 var replaceTo;
michael@0 2069 var curEnd;
michael@0 2070 if (vim.visualMode){
michael@0 2071 curStart=cm.getCursor('start');
michael@0 2072 curEnd=cm.getCursor('end');
michael@0 2073 // workaround to catch the character under the cursor
michael@0 2074 // existing workaround doesn't cover actions
michael@0 2075 curEnd=cm.clipPos(Pos(curEnd.line, curEnd.ch+1));
michael@0 2076 }else{
michael@0 2077 var line = cm.getLine(curStart.line);
michael@0 2078 replaceTo = curStart.ch + actionArgs.repeat;
michael@0 2079 if (replaceTo > line.length) {
michael@0 2080 replaceTo=line.length;
michael@0 2081 }
michael@0 2082 curEnd = Pos(curStart.line, replaceTo);
michael@0 2083 }
michael@0 2084 if (replaceWith=='\n'){
michael@0 2085 if (!vim.visualMode) cm.replaceRange('', curStart, curEnd);
michael@0 2086 // special case, where vim help says to replace by just one line-break
michael@0 2087 (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm);
michael@0 2088 }else {
michael@0 2089 var replaceWithStr=cm.getRange(curStart, curEnd);
michael@0 2090 //replace all characters in range by selected, but keep linebreaks
michael@0 2091 replaceWithStr=replaceWithStr.replace(/[^\n]/g,replaceWith);
michael@0 2092 cm.replaceRange(replaceWithStr, curStart, curEnd);
michael@0 2093 if (vim.visualMode){
michael@0 2094 cm.setCursor(curStart);
michael@0 2095 exitVisualMode(cm);
michael@0 2096 }else{
michael@0 2097 cm.setCursor(offsetCursor(curEnd, 0, -1));
michael@0 2098 }
michael@0 2099 }
michael@0 2100 },
michael@0 2101 incrementNumberToken: function(cm, actionArgs) {
michael@0 2102 var cur = cm.getCursor();
michael@0 2103 var lineStr = cm.getLine(cur.line);
michael@0 2104 var re = /-?\d+/g;
michael@0 2105 var match;
michael@0 2106 var start;
michael@0 2107 var end;
michael@0 2108 var numberStr;
michael@0 2109 var token;
michael@0 2110 while ((match = re.exec(lineStr)) !== null) {
michael@0 2111 token = match[0];
michael@0 2112 start = match.index;
michael@0 2113 end = start + token.length;
michael@0 2114 if (cur.ch < end)break;
michael@0 2115 }
michael@0 2116 if (!actionArgs.backtrack && (end <= cur.ch))return;
michael@0 2117 if (token) {
michael@0 2118 var increment = actionArgs.increase ? 1 : -1;
michael@0 2119 var number = parseInt(token) + (increment * actionArgs.repeat);
michael@0 2120 var from = Pos(cur.line, start);
michael@0 2121 var to = Pos(cur.line, end);
michael@0 2122 numberStr = number.toString();
michael@0 2123 cm.replaceRange(numberStr, from, to);
michael@0 2124 } else {
michael@0 2125 return;
michael@0 2126 }
michael@0 2127 cm.setCursor(Pos(cur.line, start + numberStr.length - 1));
michael@0 2128 },
michael@0 2129 repeatLastEdit: function(cm, actionArgs, vim) {
michael@0 2130 var lastEditInputState = vim.lastEditInputState;
michael@0 2131 if (!lastEditInputState) { return; }
michael@0 2132 var repeat = actionArgs.repeat;
michael@0 2133 if (repeat && actionArgs.repeatIsExplicit) {
michael@0 2134 vim.lastEditInputState.repeatOverride = repeat;
michael@0 2135 } else {
michael@0 2136 repeat = vim.lastEditInputState.repeatOverride || repeat;
michael@0 2137 }
michael@0 2138 repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */);
michael@0 2139 }
michael@0 2140 };
michael@0 2141
michael@0 2142 /*
michael@0 2143 * Below are miscellaneous utility functions used by vim.js
michael@0 2144 */
michael@0 2145
michael@0 2146 /**
michael@0 2147 * Clips cursor to ensure that line is within the buffer's range
michael@0 2148 * If includeLineBreak is true, then allow cur.ch == lineLength.
michael@0 2149 */
michael@0 2150 function clipCursorToContent(cm, cur, includeLineBreak) {
michael@0 2151 var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() );
michael@0 2152 var maxCh = lineLength(cm, line) - 1;
michael@0 2153 maxCh = (includeLineBreak) ? maxCh + 1 : maxCh;
michael@0 2154 var ch = Math.min(Math.max(0, cur.ch), maxCh);
michael@0 2155 return Pos(line, ch);
michael@0 2156 }
michael@0 2157 function copyArgs(args) {
michael@0 2158 var ret = {};
michael@0 2159 for (var prop in args) {
michael@0 2160 if (args.hasOwnProperty(prop)) {
michael@0 2161 ret[prop] = args[prop];
michael@0 2162 }
michael@0 2163 }
michael@0 2164 return ret;
michael@0 2165 }
michael@0 2166 function offsetCursor(cur, offsetLine, offsetCh) {
michael@0 2167 return Pos(cur.line + offsetLine, cur.ch + offsetCh);
michael@0 2168 }
michael@0 2169 function matchKeysPartial(pressed, mapped) {
michael@0 2170 for (var i = 0; i < pressed.length; i++) {
michael@0 2171 // 'character' means any character. For mark, register commads, etc.
michael@0 2172 if (pressed[i] != mapped[i] && mapped[i] != 'character') {
michael@0 2173 return false;
michael@0 2174 }
michael@0 2175 }
michael@0 2176 return true;
michael@0 2177 }
michael@0 2178 function repeatFn(cm, fn, repeat) {
michael@0 2179 return function() {
michael@0 2180 for (var i = 0; i < repeat; i++) {
michael@0 2181 fn(cm);
michael@0 2182 }
michael@0 2183 };
michael@0 2184 }
michael@0 2185 function copyCursor(cur) {
michael@0 2186 return Pos(cur.line, cur.ch);
michael@0 2187 }
michael@0 2188 function cursorEqual(cur1, cur2) {
michael@0 2189 return cur1.ch == cur2.ch && cur1.line == cur2.line;
michael@0 2190 }
michael@0 2191 function cursorIsBefore(cur1, cur2) {
michael@0 2192 if (cur1.line < cur2.line) {
michael@0 2193 return true;
michael@0 2194 }
michael@0 2195 if (cur1.line == cur2.line && cur1.ch < cur2.ch) {
michael@0 2196 return true;
michael@0 2197 }
michael@0 2198 return false;
michael@0 2199 }
michael@0 2200 function cusrorIsBetween(cur1, cur2, cur3) {
michael@0 2201 // returns true if cur2 is between cur1 and cur3.
michael@0 2202 var cur1before2 = cursorIsBefore(cur1, cur2);
michael@0 2203 var cur2before3 = cursorIsBefore(cur2, cur3);
michael@0 2204 return cur1before2 && cur2before3;
michael@0 2205 }
michael@0 2206 function lineLength(cm, lineNum) {
michael@0 2207 return cm.getLine(lineNum).length;
michael@0 2208 }
michael@0 2209 function reverse(s){
michael@0 2210 return s.split('').reverse().join('');
michael@0 2211 }
michael@0 2212 function trim(s) {
michael@0 2213 if (s.trim) {
michael@0 2214 return s.trim();
michael@0 2215 }
michael@0 2216 return s.replace(/^\s+|\s+$/g, '');
michael@0 2217 }
michael@0 2218 function escapeRegex(s) {
michael@0 2219 return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1');
michael@0 2220 }
michael@0 2221
michael@0 2222 function exitVisualMode(cm) {
michael@0 2223 cm.off('mousedown', exitVisualMode);
michael@0 2224 var vim = cm.state.vim;
michael@0 2225 // can't use selection state here because yank has already reset its cursor
michael@0 2226 vim.lastSelection = {'curStart': vim.marks['<'].find(),
michael@0 2227 'curEnd': vim.marks['>'].find(), 'visualMode': vim.visualMode,
michael@0 2228 'visualLine': vim.visualLine};
michael@0 2229 vim.visualMode = false;
michael@0 2230 vim.visualLine = false;
michael@0 2231 var selectionStart = cm.getCursor('anchor');
michael@0 2232 var selectionEnd = cm.getCursor('head');
michael@0 2233 if (!cursorEqual(selectionStart, selectionEnd)) {
michael@0 2234 // Clear the selection and set the cursor only if the selection has not
michael@0 2235 // already been cleared. Otherwise we risk moving the cursor somewhere
michael@0 2236 // it's not supposed to be.
michael@0 2237 cm.setCursor(clipCursorToContent(cm, selectionEnd));
michael@0 2238 }
michael@0 2239 CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
michael@0 2240 }
michael@0 2241
michael@0 2242 // Remove any trailing newlines from the selection. For
michael@0 2243 // example, with the caret at the start of the last word on the line,
michael@0 2244 // 'dw' should word, but not the newline, while 'w' should advance the
michael@0 2245 // caret to the first character of the next line.
michael@0 2246 function clipToLine(cm, curStart, curEnd) {
michael@0 2247 var selection = cm.getRange(curStart, curEnd);
michael@0 2248 // Only clip if the selection ends with trailing newline + whitespace
michael@0 2249 if (/\n\s*$/.test(selection)) {
michael@0 2250 var lines = selection.split('\n');
michael@0 2251 // We know this is all whitepsace.
michael@0 2252 lines.pop();
michael@0 2253
michael@0 2254 // Cases:
michael@0 2255 // 1. Last word is an empty line - do not clip the trailing '\n'
michael@0 2256 // 2. Last word is not an empty line - clip the trailing '\n'
michael@0 2257 var line;
michael@0 2258 // Find the line containing the last word, and clip all whitespace up
michael@0 2259 // to it.
michael@0 2260 for (var line = lines.pop(); lines.length > 0 && line && isWhiteSpaceString(line); line = lines.pop()) {
michael@0 2261 curEnd.line--;
michael@0 2262 curEnd.ch = 0;
michael@0 2263 }
michael@0 2264 // If the last word is not an empty line, clip an additional newline
michael@0 2265 if (line) {
michael@0 2266 curEnd.line--;
michael@0 2267 curEnd.ch = lineLength(cm, curEnd.line);
michael@0 2268 } else {
michael@0 2269 curEnd.ch = 0;
michael@0 2270 }
michael@0 2271 }
michael@0 2272 }
michael@0 2273
michael@0 2274 // Expand the selection to line ends.
michael@0 2275 function expandSelectionToLine(_cm, curStart, curEnd) {
michael@0 2276 curStart.ch = 0;
michael@0 2277 curEnd.ch = 0;
michael@0 2278 curEnd.line++;
michael@0 2279 }
michael@0 2280
michael@0 2281 function findFirstNonWhiteSpaceCharacter(text) {
michael@0 2282 if (!text) {
michael@0 2283 return 0;
michael@0 2284 }
michael@0 2285 var firstNonWS = text.search(/\S/);
michael@0 2286 return firstNonWS == -1 ? text.length : firstNonWS;
michael@0 2287 }
michael@0 2288
michael@0 2289 function expandWordUnderCursor(cm, inclusive, _forward, bigWord, noSymbol) {
michael@0 2290 var cur = cm.getCursor();
michael@0 2291 var line = cm.getLine(cur.line);
michael@0 2292 var idx = cur.ch;
michael@0 2293
michael@0 2294 // Seek to first word or non-whitespace character, depending on if
michael@0 2295 // noSymbol is true.
michael@0 2296 var textAfterIdx = line.substring(idx);
michael@0 2297 var firstMatchedChar;
michael@0 2298 if (noSymbol) {
michael@0 2299 firstMatchedChar = textAfterIdx.search(/\w/);
michael@0 2300 } else {
michael@0 2301 firstMatchedChar = textAfterIdx.search(/\S/);
michael@0 2302 }
michael@0 2303 if (firstMatchedChar == -1) {
michael@0 2304 return null;
michael@0 2305 }
michael@0 2306 idx += firstMatchedChar;
michael@0 2307 textAfterIdx = line.substring(idx);
michael@0 2308 var textBeforeIdx = line.substring(0, idx);
michael@0 2309
michael@0 2310 var matchRegex;
michael@0 2311 // Greedy matchers for the "word" we are trying to expand.
michael@0 2312 if (bigWord) {
michael@0 2313 matchRegex = /^\S+/;
michael@0 2314 } else {
michael@0 2315 if ((/\w/).test(line.charAt(idx))) {
michael@0 2316 matchRegex = /^\w+/;
michael@0 2317 } else {
michael@0 2318 matchRegex = /^[^\w\s]+/;
michael@0 2319 }
michael@0 2320 }
michael@0 2321
michael@0 2322 var wordAfterRegex = matchRegex.exec(textAfterIdx);
michael@0 2323 var wordStart = idx;
michael@0 2324 var wordEnd = idx + wordAfterRegex[0].length;
michael@0 2325 // TODO: Find a better way to do this. It will be slow on very long lines.
michael@0 2326 var revTextBeforeIdx = reverse(textBeforeIdx);
michael@0 2327 var wordBeforeRegex = matchRegex.exec(revTextBeforeIdx);
michael@0 2328 if (wordBeforeRegex) {
michael@0 2329 wordStart -= wordBeforeRegex[0].length;
michael@0 2330 }
michael@0 2331
michael@0 2332 if (inclusive) {
michael@0 2333 // If present, trim all whitespace after word.
michael@0 2334 // Otherwise, trim all whitespace before word.
michael@0 2335 var textAfterWordEnd = line.substring(wordEnd);
michael@0 2336 var whitespacesAfterWord = textAfterWordEnd.match(/^\s*/)[0].length;
michael@0 2337 if (whitespacesAfterWord > 0) {
michael@0 2338 wordEnd += whitespacesAfterWord;
michael@0 2339 } else {
michael@0 2340 var revTrim = revTextBeforeIdx.length - wordStart;
michael@0 2341 var textBeforeWordStart = revTextBeforeIdx.substring(revTrim);
michael@0 2342 var whitespacesBeforeWord = textBeforeWordStart.match(/^\s*/)[0].length;
michael@0 2343 wordStart -= whitespacesBeforeWord;
michael@0 2344 }
michael@0 2345 }
michael@0 2346
michael@0 2347 return { start: Pos(cur.line, wordStart),
michael@0 2348 end: Pos(cur.line, wordEnd) };
michael@0 2349 }
michael@0 2350
michael@0 2351 function recordJumpPosition(cm, oldCur, newCur) {
michael@0 2352 if (!cursorEqual(oldCur, newCur)) {
michael@0 2353 vimGlobalState.jumpList.add(cm, oldCur, newCur);
michael@0 2354 }
michael@0 2355 }
michael@0 2356
michael@0 2357 function recordLastCharacterSearch(increment, args) {
michael@0 2358 vimGlobalState.lastChararacterSearch.increment = increment;
michael@0 2359 vimGlobalState.lastChararacterSearch.forward = args.forward;
michael@0 2360 vimGlobalState.lastChararacterSearch.selectedCharacter = args.selectedCharacter;
michael@0 2361 }
michael@0 2362
michael@0 2363 var symbolToMode = {
michael@0 2364 '(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket',
michael@0 2365 '[': 'section', ']': 'section',
michael@0 2366 '*': 'comment', '/': 'comment',
michael@0 2367 'm': 'method', 'M': 'method',
michael@0 2368 '#': 'preprocess'
michael@0 2369 };
michael@0 2370 var findSymbolModes = {
michael@0 2371 bracket: {
michael@0 2372 isComplete: function(state) {
michael@0 2373 if (state.nextCh === state.symb) {
michael@0 2374 state.depth++;
michael@0 2375 if (state.depth >= 1)return true;
michael@0 2376 } else if (state.nextCh === state.reverseSymb) {
michael@0 2377 state.depth--;
michael@0 2378 }
michael@0 2379 return false;
michael@0 2380 }
michael@0 2381 },
michael@0 2382 section: {
michael@0 2383 init: function(state) {
michael@0 2384 state.curMoveThrough = true;
michael@0 2385 state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}';
michael@0 2386 },
michael@0 2387 isComplete: function(state) {
michael@0 2388 return state.index === 0 && state.nextCh === state.symb;
michael@0 2389 }
michael@0 2390 },
michael@0 2391 comment: {
michael@0 2392 isComplete: function(state) {
michael@0 2393 var found = state.lastCh === '*' && state.nextCh === '/';
michael@0 2394 state.lastCh = state.nextCh;
michael@0 2395 return found;
michael@0 2396 }
michael@0 2397 },
michael@0 2398 // TODO: The original Vim implementation only operates on level 1 and 2.
michael@0 2399 // The current implementation doesn't check for code block level and
michael@0 2400 // therefore it operates on any levels.
michael@0 2401 method: {
michael@0 2402 init: function(state) {
michael@0 2403 state.symb = (state.symb === 'm' ? '{' : '}');
michael@0 2404 state.reverseSymb = state.symb === '{' ? '}' : '{';
michael@0 2405 },
michael@0 2406 isComplete: function(state) {
michael@0 2407 if (state.nextCh === state.symb)return true;
michael@0 2408 return false;
michael@0 2409 }
michael@0 2410 },
michael@0 2411 preprocess: {
michael@0 2412 init: function(state) {
michael@0 2413 state.index = 0;
michael@0 2414 },
michael@0 2415 isComplete: function(state) {
michael@0 2416 if (state.nextCh === '#') {
michael@0 2417 var token = state.lineText.match(/#(\w+)/)[1];
michael@0 2418 if (token === 'endif') {
michael@0 2419 if (state.forward && state.depth === 0) {
michael@0 2420 return true;
michael@0 2421 }
michael@0 2422 state.depth++;
michael@0 2423 } else if (token === 'if') {
michael@0 2424 if (!state.forward && state.depth === 0) {
michael@0 2425 return true;
michael@0 2426 }
michael@0 2427 state.depth--;
michael@0 2428 }
michael@0 2429 if (token === 'else' && state.depth === 0)return true;
michael@0 2430 }
michael@0 2431 return false;
michael@0 2432 }
michael@0 2433 }
michael@0 2434 };
michael@0 2435 function findSymbol(cm, repeat, forward, symb) {
michael@0 2436 var cur = copyCursor(cm.getCursor());
michael@0 2437 var increment = forward ? 1 : -1;
michael@0 2438 var endLine = forward ? cm.lineCount() : -1;
michael@0 2439 var curCh = cur.ch;
michael@0 2440 var line = cur.line;
michael@0 2441 var lineText = cm.getLine(line);
michael@0 2442 var state = {
michael@0 2443 lineText: lineText,
michael@0 2444 nextCh: lineText.charAt(curCh),
michael@0 2445 lastCh: null,
michael@0 2446 index: curCh,
michael@0 2447 symb: symb,
michael@0 2448 reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb],
michael@0 2449 forward: forward,
michael@0 2450 depth: 0,
michael@0 2451 curMoveThrough: false
michael@0 2452 };
michael@0 2453 var mode = symbolToMode[symb];
michael@0 2454 if (!mode)return cur;
michael@0 2455 var init = findSymbolModes[mode].init;
michael@0 2456 var isComplete = findSymbolModes[mode].isComplete;
michael@0 2457 if (init) { init(state); }
michael@0 2458 while (line !== endLine && repeat) {
michael@0 2459 state.index += increment;
michael@0 2460 state.nextCh = state.lineText.charAt(state.index);
michael@0 2461 if (!state.nextCh) {
michael@0 2462 line += increment;
michael@0 2463 state.lineText = cm.getLine(line) || '';
michael@0 2464 if (increment > 0) {
michael@0 2465 state.index = 0;
michael@0 2466 } else {
michael@0 2467 var lineLen = state.lineText.length;
michael@0 2468 state.index = (lineLen > 0) ? (lineLen-1) : 0;
michael@0 2469 }
michael@0 2470 state.nextCh = state.lineText.charAt(state.index);
michael@0 2471 }
michael@0 2472 if (isComplete(state)) {
michael@0 2473 cur.line = line;
michael@0 2474 cur.ch = state.index;
michael@0 2475 repeat--;
michael@0 2476 }
michael@0 2477 }
michael@0 2478 if (state.nextCh || state.curMoveThrough) {
michael@0 2479 return Pos(line, state.index);
michael@0 2480 }
michael@0 2481 return cur;
michael@0 2482 }
michael@0 2483
michael@0 2484 /*
michael@0 2485 * Returns the boundaries of the next word. If the cursor in the middle of
michael@0 2486 * the word, then returns the boundaries of the current word, starting at
michael@0 2487 * the cursor. If the cursor is at the start/end of a word, and we are going
michael@0 2488 * forward/backward, respectively, find the boundaries of the next word.
michael@0 2489 *
michael@0 2490 * @param {CodeMirror} cm CodeMirror object.
michael@0 2491 * @param {Cursor} cur The cursor position.
michael@0 2492 * @param {boolean} forward True to search forward. False to search
michael@0 2493 * backward.
michael@0 2494 * @param {boolean} bigWord True if punctuation count as part of the word.
michael@0 2495 * False if only [a-zA-Z0-9] characters count as part of the word.
michael@0 2496 * @param {boolean} emptyLineIsWord True if empty lines should be treated
michael@0 2497 * as words.
michael@0 2498 * @return {Object{from:number, to:number, line: number}} The boundaries of
michael@0 2499 * the word, or null if there are no more words.
michael@0 2500 */
michael@0 2501 function findWord(cm, cur, forward, bigWord, emptyLineIsWord) {
michael@0 2502 var lineNum = cur.line;
michael@0 2503 var pos = cur.ch;
michael@0 2504 var line = cm.getLine(lineNum);
michael@0 2505 var dir = forward ? 1 : -1;
michael@0 2506 var regexps = bigWord ? bigWordRegexp : wordRegexp;
michael@0 2507
michael@0 2508 if (emptyLineIsWord && line == '') {
michael@0 2509 lineNum += dir;
michael@0 2510 line = cm.getLine(lineNum);
michael@0 2511 if (!isLine(cm, lineNum)) {
michael@0 2512 return null;
michael@0 2513 }
michael@0 2514 pos = (forward) ? 0 : line.length;
michael@0 2515 }
michael@0 2516
michael@0 2517 while (true) {
michael@0 2518 if (emptyLineIsWord && line == '') {
michael@0 2519 return { from: 0, to: 0, line: lineNum };
michael@0 2520 }
michael@0 2521 var stop = (dir > 0) ? line.length : -1;
michael@0 2522 var wordStart = stop, wordEnd = stop;
michael@0 2523 // Find bounds of next word.
michael@0 2524 while (pos != stop) {
michael@0 2525 var foundWord = false;
michael@0 2526 for (var i = 0; i < regexps.length && !foundWord; ++i) {
michael@0 2527 if (regexps[i].test(line.charAt(pos))) {
michael@0 2528 wordStart = pos;
michael@0 2529 // Advance to end of word.
michael@0 2530 while (pos != stop && regexps[i].test(line.charAt(pos))) {
michael@0 2531 pos += dir;
michael@0 2532 }
michael@0 2533 wordEnd = pos;
michael@0 2534 foundWord = wordStart != wordEnd;
michael@0 2535 if (wordStart == cur.ch && lineNum == cur.line &&
michael@0 2536 wordEnd == wordStart + dir) {
michael@0 2537 // We started at the end of a word. Find the next one.
michael@0 2538 continue;
michael@0 2539 } else {
michael@0 2540 return {
michael@0 2541 from: Math.min(wordStart, wordEnd + 1),
michael@0 2542 to: Math.max(wordStart, wordEnd),
michael@0 2543 line: lineNum };
michael@0 2544 }
michael@0 2545 }
michael@0 2546 }
michael@0 2547 if (!foundWord) {
michael@0 2548 pos += dir;
michael@0 2549 }
michael@0 2550 }
michael@0 2551 // Advance to next/prev line.
michael@0 2552 lineNum += dir;
michael@0 2553 if (!isLine(cm, lineNum)) {
michael@0 2554 return null;
michael@0 2555 }
michael@0 2556 line = cm.getLine(lineNum);
michael@0 2557 pos = (dir > 0) ? 0 : line.length;
michael@0 2558 }
michael@0 2559 // Should never get here.
michael@0 2560 throw new Error('The impossible happened.');
michael@0 2561 }
michael@0 2562
michael@0 2563 /**
michael@0 2564 * @param {CodeMirror} cm CodeMirror object.
michael@0 2565 * @param {int} repeat Number of words to move past.
michael@0 2566 * @param {boolean} forward True to search forward. False to search
michael@0 2567 * backward.
michael@0 2568 * @param {boolean} wordEnd True to move to end of word. False to move to
michael@0 2569 * beginning of word.
michael@0 2570 * @param {boolean} bigWord True if punctuation count as part of the word.
michael@0 2571 * False if only alphabet characters count as part of the word.
michael@0 2572 * @return {Cursor} The position the cursor should move to.
michael@0 2573 */
michael@0 2574 function moveToWord(cm, repeat, forward, wordEnd, bigWord) {
michael@0 2575 var cur = cm.getCursor();
michael@0 2576 var curStart = copyCursor(cur);
michael@0 2577 var words = [];
michael@0 2578 if (forward && !wordEnd || !forward && wordEnd) {
michael@0 2579 repeat++;
michael@0 2580 }
michael@0 2581 // For 'e', empty lines are not considered words, go figure.
michael@0 2582 var emptyLineIsWord = !(forward && wordEnd);
michael@0 2583 for (var i = 0; i < repeat; i++) {
michael@0 2584 var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord);
michael@0 2585 if (!word) {
michael@0 2586 var eodCh = lineLength(cm, cm.lastLine());
michael@0 2587 words.push(forward
michael@0 2588 ? {line: cm.lastLine(), from: eodCh, to: eodCh}
michael@0 2589 : {line: 0, from: 0, to: 0});
michael@0 2590 break;
michael@0 2591 }
michael@0 2592 words.push(word);
michael@0 2593 cur = Pos(word.line, forward ? (word.to - 1) : word.from);
michael@0 2594 }
michael@0 2595 var shortCircuit = words.length != repeat;
michael@0 2596 var firstWord = words[0];
michael@0 2597 var lastWord = words.pop();
michael@0 2598 if (forward && !wordEnd) {
michael@0 2599 // w
michael@0 2600 if (!shortCircuit && (firstWord.from != curStart.ch || firstWord.line != curStart.line)) {
michael@0 2601 // We did not start in the middle of a word. Discard the extra word at the end.
michael@0 2602 lastWord = words.pop();
michael@0 2603 }
michael@0 2604 return Pos(lastWord.line, lastWord.from);
michael@0 2605 } else if (forward && wordEnd) {
michael@0 2606 return Pos(lastWord.line, lastWord.to - 1);
michael@0 2607 } else if (!forward && wordEnd) {
michael@0 2608 // ge
michael@0 2609 if (!shortCircuit && (firstWord.to != curStart.ch || firstWord.line != curStart.line)) {
michael@0 2610 // We did not start in the middle of a word. Discard the extra word at the end.
michael@0 2611 lastWord = words.pop();
michael@0 2612 }
michael@0 2613 return Pos(lastWord.line, lastWord.to);
michael@0 2614 } else {
michael@0 2615 // b
michael@0 2616 return Pos(lastWord.line, lastWord.from);
michael@0 2617 }
michael@0 2618 }
michael@0 2619
michael@0 2620 function moveToCharacter(cm, repeat, forward, character) {
michael@0 2621 var cur = cm.getCursor();
michael@0 2622 var start = cur.ch;
michael@0 2623 var idx;
michael@0 2624 for (var i = 0; i < repeat; i ++) {
michael@0 2625 var line = cm.getLine(cur.line);
michael@0 2626 idx = charIdxInLine(start, line, character, forward, true);
michael@0 2627 if (idx == -1) {
michael@0 2628 return null;
michael@0 2629 }
michael@0 2630 start = idx;
michael@0 2631 }
michael@0 2632 return Pos(cm.getCursor().line, idx);
michael@0 2633 }
michael@0 2634
michael@0 2635 function moveToColumn(cm, repeat) {
michael@0 2636 // repeat is always >= 1, so repeat - 1 always corresponds
michael@0 2637 // to the column we want to go to.
michael@0 2638 var line = cm.getCursor().line;
michael@0 2639 return clipCursorToContent(cm, Pos(line, repeat - 1));
michael@0 2640 }
michael@0 2641
michael@0 2642 function updateMark(cm, vim, markName, pos) {
michael@0 2643 if (!inArray(markName, validMarks)) {
michael@0 2644 return;
michael@0 2645 }
michael@0 2646 if (vim.marks[markName]) {
michael@0 2647 vim.marks[markName].clear();
michael@0 2648 }
michael@0 2649 vim.marks[markName] = cm.setBookmark(pos);
michael@0 2650 }
michael@0 2651
michael@0 2652 function charIdxInLine(start, line, character, forward, includeChar) {
michael@0 2653 // Search for char in line.
michael@0 2654 // motion_options: {forward, includeChar}
michael@0 2655 // If includeChar = true, include it too.
michael@0 2656 // If forward = true, search forward, else search backwards.
michael@0 2657 // If char is not found on this line, do nothing
michael@0 2658 var idx;
michael@0 2659 if (forward) {
michael@0 2660 idx = line.indexOf(character, start + 1);
michael@0 2661 if (idx != -1 && !includeChar) {
michael@0 2662 idx -= 1;
michael@0 2663 }
michael@0 2664 } else {
michael@0 2665 idx = line.lastIndexOf(character, start - 1);
michael@0 2666 if (idx != -1 && !includeChar) {
michael@0 2667 idx += 1;
michael@0 2668 }
michael@0 2669 }
michael@0 2670 return idx;
michael@0 2671 }
michael@0 2672
michael@0 2673 function getContextLevel(ctx) {
michael@0 2674 return (ctx === 'string' || ctx === 'comment') ? 1 : 0;
michael@0 2675 }
michael@0 2676
michael@0 2677 function findMatchedSymbol(cm, cur, symb) {
michael@0 2678 var line = cur.line;
michael@0 2679 var ch = cur.ch;
michael@0 2680 symb = symb ? symb : cm.getLine(line).charAt(ch);
michael@0 2681
michael@0 2682 var symbContext = cm.getTokenAt(Pos(line, ch + 1)).type;
michael@0 2683 var symbCtxLevel = getContextLevel(symbContext);
michael@0 2684
michael@0 2685 var reverseSymb = ({
michael@0 2686 '(': ')', ')': '(',
michael@0 2687 '[': ']', ']': '[',
michael@0 2688 '{': '}', '}': '{'})[symb];
michael@0 2689
michael@0 2690 // Couldn't find a matching symbol, abort
michael@0 2691 if (!reverseSymb) {
michael@0 2692 return cur;
michael@0 2693 }
michael@0 2694
michael@0 2695 // set our increment to move forward (+1) or backwards (-1)
michael@0 2696 // depending on which bracket we're matching
michael@0 2697 var increment = ({'(': 1, '{': 1, '[': 1})[symb] || -1;
michael@0 2698 var endLine = increment === 1 ? cm.lineCount() : -1;
michael@0 2699 var depth = 1, nextCh = symb, index = ch, lineText = cm.getLine(line);
michael@0 2700 // Simple search for closing paren--just count openings and closings till
michael@0 2701 // we find our match
michael@0 2702 // TODO: use info from CodeMirror to ignore closing brackets in comments
michael@0 2703 // and quotes, etc.
michael@0 2704 while (line !== endLine && depth > 0) {
michael@0 2705 index += increment;
michael@0 2706 nextCh = lineText.charAt(index);
michael@0 2707 if (!nextCh) {
michael@0 2708 line += increment;
michael@0 2709 lineText = cm.getLine(line) || '';
michael@0 2710 if (increment > 0) {
michael@0 2711 index = 0;
michael@0 2712 } else {
michael@0 2713 var lineLen = lineText.length;
michael@0 2714 index = (lineLen > 0) ? (lineLen-1) : 0;
michael@0 2715 }
michael@0 2716 nextCh = lineText.charAt(index);
michael@0 2717 }
michael@0 2718 var revSymbContext = cm.getTokenAt(Pos(line, index + 1)).type;
michael@0 2719 var revSymbCtxLevel = getContextLevel(revSymbContext);
michael@0 2720 if (symbCtxLevel >= revSymbCtxLevel) {
michael@0 2721 if (nextCh === symb) {
michael@0 2722 depth++;
michael@0 2723 } else if (nextCh === reverseSymb) {
michael@0 2724 depth--;
michael@0 2725 }
michael@0 2726 }
michael@0 2727 }
michael@0 2728
michael@0 2729 if (nextCh) {
michael@0 2730 return Pos(line, index);
michael@0 2731 }
michael@0 2732 return cur;
michael@0 2733 }
michael@0 2734
michael@0 2735 // TODO: perhaps this finagling of start and end positions belonds
michael@0 2736 // in codmirror/replaceRange?
michael@0 2737 function selectCompanionObject(cm, revSymb, inclusive) {
michael@0 2738 var cur = copyCursor(cm.getCursor());
michael@0 2739 var end = findMatchedSymbol(cm, cur, revSymb);
michael@0 2740 var start = findMatchedSymbol(cm, end);
michael@0 2741
michael@0 2742 if ((start.line == end.line && start.ch > end.ch)
michael@0 2743 || (start.line > end.line)) {
michael@0 2744 var tmp = start;
michael@0 2745 start = end;
michael@0 2746 end = tmp;
michael@0 2747 }
michael@0 2748
michael@0 2749 if (inclusive) {
michael@0 2750 end.ch += 1;
michael@0 2751 } else {
michael@0 2752 start.ch += 1;
michael@0 2753 }
michael@0 2754
michael@0 2755 return { start: start, end: end };
michael@0 2756 }
michael@0 2757
michael@0 2758 // Takes in a symbol and a cursor and tries to simulate text objects that
michael@0 2759 // have identical opening and closing symbols
michael@0 2760 // TODO support across multiple lines
michael@0 2761 function findBeginningAndEnd(cm, symb, inclusive) {
michael@0 2762 var cur = copyCursor(cm.getCursor());
michael@0 2763 var line = cm.getLine(cur.line);
michael@0 2764 var chars = line.split('');
michael@0 2765 var start, end, i, len;
michael@0 2766 var firstIndex = chars.indexOf(symb);
michael@0 2767
michael@0 2768 // the decision tree is to always look backwards for the beginning first,
michael@0 2769 // but if the cursor is in front of the first instance of the symb,
michael@0 2770 // then move the cursor forward
michael@0 2771 if (cur.ch < firstIndex) {
michael@0 2772 cur.ch = firstIndex;
michael@0 2773 // Why is this line even here???
michael@0 2774 // cm.setCursor(cur.line, firstIndex+1);
michael@0 2775 }
michael@0 2776 // otherwise if the cursor is currently on the closing symbol
michael@0 2777 else if (firstIndex < cur.ch && chars[cur.ch] == symb) {
michael@0 2778 end = cur.ch; // assign end to the current cursor
michael@0 2779 --cur.ch; // make sure to look backwards
michael@0 2780 }
michael@0 2781
michael@0 2782 // if we're currently on the symbol, we've got a start
michael@0 2783 if (chars[cur.ch] == symb && !end) {
michael@0 2784 start = cur.ch + 1; // assign start to ahead of the cursor
michael@0 2785 } else {
michael@0 2786 // go backwards to find the start
michael@0 2787 for (i = cur.ch; i > -1 && !start; i--) {
michael@0 2788 if (chars[i] == symb) {
michael@0 2789 start = i + 1;
michael@0 2790 }
michael@0 2791 }
michael@0 2792 }
michael@0 2793
michael@0 2794 // look forwards for the end symbol
michael@0 2795 if (start && !end) {
michael@0 2796 for (i = start, len = chars.length; i < len && !end; i++) {
michael@0 2797 if (chars[i] == symb) {
michael@0 2798 end = i;
michael@0 2799 }
michael@0 2800 }
michael@0 2801 }
michael@0 2802
michael@0 2803 // nothing found
michael@0 2804 if (!start || !end) {
michael@0 2805 return { start: cur, end: cur };
michael@0 2806 }
michael@0 2807
michael@0 2808 // include the symbols
michael@0 2809 if (inclusive) {
michael@0 2810 --start; ++end;
michael@0 2811 }
michael@0 2812
michael@0 2813 return {
michael@0 2814 start: Pos(cur.line, start),
michael@0 2815 end: Pos(cur.line, end)
michael@0 2816 };
michael@0 2817 }
michael@0 2818
michael@0 2819 // Search functions
michael@0 2820 defineOption('pcre', true, 'boolean');
michael@0 2821 function SearchState() {}
michael@0 2822 SearchState.prototype = {
michael@0 2823 getQuery: function() {
michael@0 2824 return vimGlobalState.query;
michael@0 2825 },
michael@0 2826 setQuery: function(query) {
michael@0 2827 vimGlobalState.query = query;
michael@0 2828 },
michael@0 2829 getOverlay: function() {
michael@0 2830 return this.searchOverlay;
michael@0 2831 },
michael@0 2832 setOverlay: function(overlay) {
michael@0 2833 this.searchOverlay = overlay;
michael@0 2834 },
michael@0 2835 isReversed: function() {
michael@0 2836 return vimGlobalState.isReversed;
michael@0 2837 },
michael@0 2838 setReversed: function(reversed) {
michael@0 2839 vimGlobalState.isReversed = reversed;
michael@0 2840 }
michael@0 2841 };
michael@0 2842 function getSearchState(cm) {
michael@0 2843 var vim = cm.state.vim;
michael@0 2844 return vim.searchState_ || (vim.searchState_ = new SearchState());
michael@0 2845 }
michael@0 2846 function dialog(cm, template, shortText, onClose, options) {
michael@0 2847 if (cm.openDialog) {
michael@0 2848 cm.openDialog(template, onClose, { bottom: true, value: options.value,
michael@0 2849 onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp });
michael@0 2850 }
michael@0 2851 else {
michael@0 2852 onClose(prompt(shortText, ''));
michael@0 2853 }
michael@0 2854 }
michael@0 2855
michael@0 2856 function findUnescapedSlashes(str) {
michael@0 2857 var escapeNextChar = false;
michael@0 2858 var slashes = [];
michael@0 2859 for (var i = 0; i < str.length; i++) {
michael@0 2860 var c = str.charAt(i);
michael@0 2861 if (!escapeNextChar && c == '/') {
michael@0 2862 slashes.push(i);
michael@0 2863 }
michael@0 2864 escapeNextChar = !escapeNextChar && (c == '\\');
michael@0 2865 }
michael@0 2866 return slashes;
michael@0 2867 }
michael@0 2868
michael@0 2869 // Translates a search string from ex (vim) syntax into javascript form.
michael@0 2870 function translateRegex(str) {
michael@0 2871 // When these match, add a '\' if unescaped or remove one if escaped.
michael@0 2872 var specials = ['|', '(', ')', '{'];
michael@0 2873 // Remove, but never add, a '\' for these.
michael@0 2874 var unescape = ['}'];
michael@0 2875 var escapeNextChar = false;
michael@0 2876 var out = [];
michael@0 2877 for (var i = -1; i < str.length; i++) {
michael@0 2878 var c = str.charAt(i) || '';
michael@0 2879 var n = str.charAt(i+1) || '';
michael@0 2880 var specialComesNext = (specials.indexOf(n) != -1);
michael@0 2881 if (escapeNextChar) {
michael@0 2882 if (c !== '\\' || !specialComesNext) {
michael@0 2883 out.push(c);
michael@0 2884 }
michael@0 2885 escapeNextChar = false;
michael@0 2886 } else {
michael@0 2887 if (c === '\\') {
michael@0 2888 escapeNextChar = true;
michael@0 2889 // Treat the unescape list as special for removing, but not adding '\'.
michael@0 2890 if (unescape.indexOf(n) != -1) {
michael@0 2891 specialComesNext = true;
michael@0 2892 }
michael@0 2893 // Not passing this test means removing a '\'.
michael@0 2894 if (!specialComesNext || n === '\\') {
michael@0 2895 out.push(c);
michael@0 2896 }
michael@0 2897 } else {
michael@0 2898 out.push(c);
michael@0 2899 if (specialComesNext && n !== '\\') {
michael@0 2900 out.push('\\');
michael@0 2901 }
michael@0 2902 }
michael@0 2903 }
michael@0 2904 }
michael@0 2905 return out.join('');
michael@0 2906 }
michael@0 2907
michael@0 2908 // Translates the replace part of a search and replace from ex (vim) syntax into
michael@0 2909 // javascript form. Similar to translateRegex, but additionally fixes back references
michael@0 2910 // (translates '\[0..9]' to '$[0..9]') and follows different rules for escaping '$'.
michael@0 2911 function translateRegexReplace(str) {
michael@0 2912 var escapeNextChar = false;
michael@0 2913 var out = [];
michael@0 2914 for (var i = -1; i < str.length; i++) {
michael@0 2915 var c = str.charAt(i) || '';
michael@0 2916 var n = str.charAt(i+1) || '';
michael@0 2917 if (escapeNextChar) {
michael@0 2918 // At any point in the loop, escapeNextChar is true if the previous
michael@0 2919 // character was a '\' and was not escaped.
michael@0 2920 out.push(c);
michael@0 2921 escapeNextChar = false;
michael@0 2922 } else {
michael@0 2923 if (c === '\\') {
michael@0 2924 escapeNextChar = true;
michael@0 2925 if ((isNumber(n) || n === '$')) {
michael@0 2926 out.push('$');
michael@0 2927 } else if (n !== '/' && n !== '\\') {
michael@0 2928 out.push('\\');
michael@0 2929 }
michael@0 2930 } else {
michael@0 2931 if (c === '$') {
michael@0 2932 out.push('$');
michael@0 2933 }
michael@0 2934 out.push(c);
michael@0 2935 if (n === '/') {
michael@0 2936 out.push('\\');
michael@0 2937 }
michael@0 2938 }
michael@0 2939 }
michael@0 2940 }
michael@0 2941 return out.join('');
michael@0 2942 }
michael@0 2943
michael@0 2944 // Unescape \ and / in the replace part, for PCRE mode.
michael@0 2945 function unescapeRegexReplace(str) {
michael@0 2946 var stream = new CodeMirror.StringStream(str);
michael@0 2947 var output = [];
michael@0 2948 while (!stream.eol()) {
michael@0 2949 // Search for \.
michael@0 2950 while (stream.peek() && stream.peek() != '\\') {
michael@0 2951 output.push(stream.next());
michael@0 2952 }
michael@0 2953 if (stream.match('\\/', true)) {
michael@0 2954 // \/ => /
michael@0 2955 output.push('/');
michael@0 2956 } else if (stream.match('\\\\', true)) {
michael@0 2957 // \\ => \
michael@0 2958 output.push('\\');
michael@0 2959 } else {
michael@0 2960 // Don't change anything
michael@0 2961 output.push(stream.next());
michael@0 2962 }
michael@0 2963 }
michael@0 2964 return output.join('');
michael@0 2965 }
michael@0 2966
michael@0 2967 /**
michael@0 2968 * Extract the regular expression from the query and return a Regexp object.
michael@0 2969 * Returns null if the query is blank.
michael@0 2970 * If ignoreCase is passed in, the Regexp object will have the 'i' flag set.
michael@0 2971 * If smartCase is passed in, and the query contains upper case letters,
michael@0 2972 * then ignoreCase is overridden, and the 'i' flag will not be set.
michael@0 2973 * If the query contains the /i in the flag part of the regular expression,
michael@0 2974 * then both ignoreCase and smartCase are ignored, and 'i' will be passed
michael@0 2975 * through to the Regex object.
michael@0 2976 */
michael@0 2977 function parseQuery(query, ignoreCase, smartCase) {
michael@0 2978 // Check if the query is already a regex.
michael@0 2979 if (query instanceof RegExp) { return query; }
michael@0 2980 // First try to extract regex + flags from the input. If no flags found,
michael@0 2981 // extract just the regex. IE does not accept flags directly defined in
michael@0 2982 // the regex string in the form /regex/flags
michael@0 2983 var slashes = findUnescapedSlashes(query);
michael@0 2984 var regexPart;
michael@0 2985 var forceIgnoreCase;
michael@0 2986 if (!slashes.length) {
michael@0 2987 // Query looks like 'regexp'
michael@0 2988 regexPart = query;
michael@0 2989 } else {
michael@0 2990 // Query looks like 'regexp/...'
michael@0 2991 regexPart = query.substring(0, slashes[0]);
michael@0 2992 var flagsPart = query.substring(slashes[0]);
michael@0 2993 forceIgnoreCase = (flagsPart.indexOf('i') != -1);
michael@0 2994 }
michael@0 2995 if (!regexPart) {
michael@0 2996 return null;
michael@0 2997 }
michael@0 2998 if (!getOption('pcre')) {
michael@0 2999 regexPart = translateRegex(regexPart);
michael@0 3000 }
michael@0 3001 if (smartCase) {
michael@0 3002 ignoreCase = (/^[^A-Z]*$/).test(regexPart);
michael@0 3003 }
michael@0 3004 var regexp = new RegExp(regexPart,
michael@0 3005 (ignoreCase || forceIgnoreCase) ? 'i' : undefined);
michael@0 3006 return regexp;
michael@0 3007 }
michael@0 3008 function showConfirm(cm, text) {
michael@0 3009 if (cm.openNotification) {
michael@0 3010 cm.openNotification('<span style="color: red">' + text + '</span>',
michael@0 3011 {bottom: true, duration: 5000});
michael@0 3012 } else {
michael@0 3013 alert(text);
michael@0 3014 }
michael@0 3015 }
michael@0 3016 function makePrompt(prefix, desc) {
michael@0 3017 var raw = '';
michael@0 3018 if (prefix) {
michael@0 3019 raw += '<span style="font-family: monospace">' + prefix + '</span>';
michael@0 3020 }
michael@0 3021 raw += '<input type="text"/> ' +
michael@0 3022 '<span style="color: #888">';
michael@0 3023 if (desc) {
michael@0 3024 raw += '<span style="color: #888">';
michael@0 3025 raw += desc;
michael@0 3026 raw += '</span>';
michael@0 3027 }
michael@0 3028 return raw;
michael@0 3029 }
michael@0 3030 var searchPromptDesc = '(Javascript regexp)';
michael@0 3031 function showPrompt(cm, options) {
michael@0 3032 var shortText = (options.prefix || '') + ' ' + (options.desc || '');
michael@0 3033 var prompt = makePrompt(options.prefix, options.desc);
michael@0 3034 dialog(cm, prompt, shortText, options.onClose, options);
michael@0 3035 }
michael@0 3036 function regexEqual(r1, r2) {
michael@0 3037 if (r1 instanceof RegExp && r2 instanceof RegExp) {
michael@0 3038 var props = ['global', 'multiline', 'ignoreCase', 'source'];
michael@0 3039 for (var i = 0; i < props.length; i++) {
michael@0 3040 var prop = props[i];
michael@0 3041 if (r1[prop] !== r2[prop]) {
michael@0 3042 return false;
michael@0 3043 }
michael@0 3044 }
michael@0 3045 return true;
michael@0 3046 }
michael@0 3047 return false;
michael@0 3048 }
michael@0 3049 // Returns true if the query is valid.
michael@0 3050 function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) {
michael@0 3051 if (!rawQuery) {
michael@0 3052 return;
michael@0 3053 }
michael@0 3054 var state = getSearchState(cm);
michael@0 3055 var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase);
michael@0 3056 if (!query) {
michael@0 3057 return;
michael@0 3058 }
michael@0 3059 highlightSearchMatches(cm, query);
michael@0 3060 if (regexEqual(query, state.getQuery())) {
michael@0 3061 return query;
michael@0 3062 }
michael@0 3063 state.setQuery(query);
michael@0 3064 return query;
michael@0 3065 }
michael@0 3066 function searchOverlay(query) {
michael@0 3067 if (query.source.charAt(0) == '^') {
michael@0 3068 var matchSol = true;
michael@0 3069 }
michael@0 3070 return {
michael@0 3071 token: function(stream) {
michael@0 3072 if (matchSol && !stream.sol()) {
michael@0 3073 stream.skipToEnd();
michael@0 3074 return;
michael@0 3075 }
michael@0 3076 var match = stream.match(query, false);
michael@0 3077 if (match) {
michael@0 3078 if (match[0].length == 0) {
michael@0 3079 // Matched empty string, skip to next.
michael@0 3080 stream.next();
michael@0 3081 return 'searching';
michael@0 3082 }
michael@0 3083 if (!stream.sol()) {
michael@0 3084 // Backtrack 1 to match \b
michael@0 3085 stream.backUp(1);
michael@0 3086 if (!query.exec(stream.next() + match[0])) {
michael@0 3087 stream.next();
michael@0 3088 return null;
michael@0 3089 }
michael@0 3090 }
michael@0 3091 stream.match(query);
michael@0 3092 return 'searching';
michael@0 3093 }
michael@0 3094 while (!stream.eol()) {
michael@0 3095 stream.next();
michael@0 3096 if (stream.match(query, false)) break;
michael@0 3097 }
michael@0 3098 },
michael@0 3099 query: query
michael@0 3100 };
michael@0 3101 }
michael@0 3102 function highlightSearchMatches(cm, query) {
michael@0 3103 var overlay = getSearchState(cm).getOverlay();
michael@0 3104 if (!overlay || query != overlay.query) {
michael@0 3105 if (overlay) {
michael@0 3106 cm.removeOverlay(overlay);
michael@0 3107 }
michael@0 3108 overlay = searchOverlay(query);
michael@0 3109 cm.addOverlay(overlay);
michael@0 3110 getSearchState(cm).setOverlay(overlay);
michael@0 3111 }
michael@0 3112 }
michael@0 3113 function findNext(cm, prev, query, repeat) {
michael@0 3114 if (repeat === undefined) { repeat = 1; }
michael@0 3115 return cm.operation(function() {
michael@0 3116 var pos = cm.getCursor();
michael@0 3117 var cursor = cm.getSearchCursor(query, pos);
michael@0 3118 for (var i = 0; i < repeat; i++) {
michael@0 3119 var found = cursor.find(prev);
michael@0 3120 if (i == 0 && found && cursorEqual(cursor.from(), pos)) { found = cursor.find(prev); }
michael@0 3121 if (!found) {
michael@0 3122 // SearchCursor may have returned null because it hit EOF, wrap
michael@0 3123 // around and try again.
michael@0 3124 cursor = cm.getSearchCursor(query,
michael@0 3125 (prev) ? Pos(cm.lastLine()) : Pos(cm.firstLine(), 0) );
michael@0 3126 if (!cursor.find(prev)) {
michael@0 3127 return;
michael@0 3128 }
michael@0 3129 }
michael@0 3130 }
michael@0 3131 return cursor.from();
michael@0 3132 });
michael@0 3133 }
michael@0 3134 function clearSearchHighlight(cm) {
michael@0 3135 cm.removeOverlay(getSearchState(cm).getOverlay());
michael@0 3136 getSearchState(cm).setOverlay(null);
michael@0 3137 }
michael@0 3138 /**
michael@0 3139 * Check if pos is in the specified range, INCLUSIVE.
michael@0 3140 * Range can be specified with 1 or 2 arguments.
michael@0 3141 * If the first range argument is an array, treat it as an array of line
michael@0 3142 * numbers. Match pos against any of the lines.
michael@0 3143 * If the first range argument is a number,
michael@0 3144 * if there is only 1 range argument, check if pos has the same line
michael@0 3145 * number
michael@0 3146 * if there are 2 range arguments, then check if pos is in between the two
michael@0 3147 * range arguments.
michael@0 3148 */
michael@0 3149 function isInRange(pos, start, end) {
michael@0 3150 if (typeof pos != 'number') {
michael@0 3151 // Assume it is a cursor position. Get the line number.
michael@0 3152 pos = pos.line;
michael@0 3153 }
michael@0 3154 if (start instanceof Array) {
michael@0 3155 return inArray(pos, start);
michael@0 3156 } else {
michael@0 3157 if (end) {
michael@0 3158 return (pos >= start && pos <= end);
michael@0 3159 } else {
michael@0 3160 return pos == start;
michael@0 3161 }
michael@0 3162 }
michael@0 3163 }
michael@0 3164 function getUserVisibleLines(cm) {
michael@0 3165 var scrollInfo = cm.getScrollInfo();
michael@0 3166 var occludeToleranceTop = 6;
michael@0 3167 var occludeToleranceBottom = 10;
michael@0 3168 var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local');
michael@0 3169 var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top;
michael@0 3170 var to = cm.coordsChar({left:0, top: bottomY}, 'local');
michael@0 3171 return {top: from.line, bottom: to.line};
michael@0 3172 }
michael@0 3173
michael@0 3174 // Ex command handling
michael@0 3175 // Care must be taken when adding to the default Ex command map. For any
michael@0 3176 // pair of commands that have a shared prefix, at least one of their
michael@0 3177 // shortNames must not match the prefix of the other command.
michael@0 3178 var defaultExCommandMap = [
michael@0 3179 { name: 'map' },
michael@0 3180 { name: 'nmap', shortName: 'nm' },
michael@0 3181 { name: 'vmap', shortName: 'vm' },
michael@0 3182 { name: 'unmap' },
michael@0 3183 { name: 'write', shortName: 'w' },
michael@0 3184 { name: 'undo', shortName: 'u' },
michael@0 3185 { name: 'redo', shortName: 'red' },
michael@0 3186 { name: 'set', shortName: 'set' },
michael@0 3187 { name: 'sort', shortName: 'sor' },
michael@0 3188 { name: 'substitute', shortName: 's' },
michael@0 3189 { name: 'nohlsearch', shortName: 'noh' },
michael@0 3190 { name: 'delmarks', shortName: 'delm' },
michael@0 3191 { name: 'registers', shortName: 'reg' }
michael@0 3192 ];
michael@0 3193 Vim.ExCommandDispatcher = function() {
michael@0 3194 this.buildCommandMap_();
michael@0 3195 };
michael@0 3196 Vim.ExCommandDispatcher.prototype = {
michael@0 3197 processCommand: function(cm, input) {
michael@0 3198 var vim = cm.state.vim;
michael@0 3199 if (vim.visualMode) {
michael@0 3200 exitVisualMode(cm);
michael@0 3201 }
michael@0 3202 var inputStream = new CodeMirror.StringStream(input);
michael@0 3203 var params = {};
michael@0 3204 params.input = input;
michael@0 3205 try {
michael@0 3206 this.parseInput_(cm, inputStream, params);
michael@0 3207 } catch(e) {
michael@0 3208 showConfirm(cm, e);
michael@0 3209 throw e;
michael@0 3210 }
michael@0 3211 var commandName;
michael@0 3212 if (!params.commandName) {
michael@0 3213 // If only a line range is defined, move to the line.
michael@0 3214 if (params.line !== undefined) {
michael@0 3215 commandName = 'move';
michael@0 3216 }
michael@0 3217 } else {
michael@0 3218 var command = this.matchCommand_(params.commandName);
michael@0 3219 if (command) {
michael@0 3220 commandName = command.name;
michael@0 3221 this.parseCommandArgs_(inputStream, params, command);
michael@0 3222 if (command.type == 'exToKey') {
michael@0 3223 // Handle Ex to Key mapping.
michael@0 3224 for (var i = 0; i < command.toKeys.length; i++) {
michael@0 3225 CodeMirror.Vim.handleKey(cm, command.toKeys[i]);
michael@0 3226 }
michael@0 3227 return;
michael@0 3228 } else if (command.type == 'exToEx') {
michael@0 3229 // Handle Ex to Ex mapping.
michael@0 3230 this.processCommand(cm, command.toInput);
michael@0 3231 return;
michael@0 3232 }
michael@0 3233 }
michael@0 3234 }
michael@0 3235 if (!commandName) {
michael@0 3236 showConfirm(cm, 'Not an editor command ":' + input + '"');
michael@0 3237 return;
michael@0 3238 }
michael@0 3239 try {
michael@0 3240 exCommands[commandName](cm, params);
michael@0 3241 } catch(e) {
michael@0 3242 showConfirm(cm, e);
michael@0 3243 throw e;
michael@0 3244 }
michael@0 3245 },
michael@0 3246 parseInput_: function(cm, inputStream, result) {
michael@0 3247 inputStream.eatWhile(':');
michael@0 3248 // Parse range.
michael@0 3249 if (inputStream.eat('%')) {
michael@0 3250 result.line = cm.firstLine();
michael@0 3251 result.lineEnd = cm.lastLine();
michael@0 3252 } else {
michael@0 3253 result.line = this.parseLineSpec_(cm, inputStream);
michael@0 3254 if (result.line !== undefined && inputStream.eat(',')) {
michael@0 3255 result.lineEnd = this.parseLineSpec_(cm, inputStream);
michael@0 3256 }
michael@0 3257 }
michael@0 3258
michael@0 3259 // Parse command name.
michael@0 3260 var commandMatch = inputStream.match(/^(\w+)/);
michael@0 3261 if (commandMatch) {
michael@0 3262 result.commandName = commandMatch[1];
michael@0 3263 } else {
michael@0 3264 result.commandName = inputStream.match(/.*/)[0];
michael@0 3265 }
michael@0 3266
michael@0 3267 return result;
michael@0 3268 },
michael@0 3269 parseLineSpec_: function(cm, inputStream) {
michael@0 3270 var numberMatch = inputStream.match(/^(\d+)/);
michael@0 3271 if (numberMatch) {
michael@0 3272 return parseInt(numberMatch[1], 10) - 1;
michael@0 3273 }
michael@0 3274 switch (inputStream.next()) {
michael@0 3275 case '.':
michael@0 3276 return cm.getCursor().line;
michael@0 3277 case '$':
michael@0 3278 return cm.lastLine();
michael@0 3279 case '\'':
michael@0 3280 var mark = cm.state.vim.marks[inputStream.next()];
michael@0 3281 if (mark && mark.find()) {
michael@0 3282 return mark.find().line;
michael@0 3283 }
michael@0 3284 throw new Error('Mark not set');
michael@0 3285 default:
michael@0 3286 inputStream.backUp(1);
michael@0 3287 return undefined;
michael@0 3288 }
michael@0 3289 },
michael@0 3290 parseCommandArgs_: function(inputStream, params, command) {
michael@0 3291 if (inputStream.eol()) {
michael@0 3292 return;
michael@0 3293 }
michael@0 3294 params.argString = inputStream.match(/.*/)[0];
michael@0 3295 // Parse command-line arguments
michael@0 3296 var delim = command.argDelimiter || /\s+/;
michael@0 3297 var args = trim(params.argString).split(delim);
michael@0 3298 if (args.length && args[0]) {
michael@0 3299 params.args = args;
michael@0 3300 }
michael@0 3301 },
michael@0 3302 matchCommand_: function(commandName) {
michael@0 3303 // Return the command in the command map that matches the shortest
michael@0 3304 // prefix of the passed in command name. The match is guaranteed to be
michael@0 3305 // unambiguous if the defaultExCommandMap's shortNames are set up
michael@0 3306 // correctly. (see @code{defaultExCommandMap}).
michael@0 3307 for (var i = commandName.length; i > 0; i--) {
michael@0 3308 var prefix = commandName.substring(0, i);
michael@0 3309 if (this.commandMap_[prefix]) {
michael@0 3310 var command = this.commandMap_[prefix];
michael@0 3311 if (command.name.indexOf(commandName) === 0) {
michael@0 3312 return command;
michael@0 3313 }
michael@0 3314 }
michael@0 3315 }
michael@0 3316 return null;
michael@0 3317 },
michael@0 3318 buildCommandMap_: function() {
michael@0 3319 this.commandMap_ = {};
michael@0 3320 for (var i = 0; i < defaultExCommandMap.length; i++) {
michael@0 3321 var command = defaultExCommandMap[i];
michael@0 3322 var key = command.shortName || command.name;
michael@0 3323 this.commandMap_[key] = command;
michael@0 3324 }
michael@0 3325 },
michael@0 3326 map: function(lhs, rhs, ctx) {
michael@0 3327 if (lhs != ':' && lhs.charAt(0) == ':') {
michael@0 3328 if (ctx) { throw Error('Mode not supported for ex mappings'); }
michael@0 3329 var commandName = lhs.substring(1);
michael@0 3330 if (rhs != ':' && rhs.charAt(0) == ':') {
michael@0 3331 // Ex to Ex mapping
michael@0 3332 this.commandMap_[commandName] = {
michael@0 3333 name: commandName,
michael@0 3334 type: 'exToEx',
michael@0 3335 toInput: rhs.substring(1),
michael@0 3336 user: true
michael@0 3337 };
michael@0 3338 } else {
michael@0 3339 // Ex to key mapping
michael@0 3340 this.commandMap_[commandName] = {
michael@0 3341 name: commandName,
michael@0 3342 type: 'exToKey',
michael@0 3343 toKeys: parseKeyString(rhs),
michael@0 3344 user: true
michael@0 3345 };
michael@0 3346 }
michael@0 3347 } else {
michael@0 3348 if (rhs != ':' && rhs.charAt(0) == ':') {
michael@0 3349 // Key to Ex mapping.
michael@0 3350 var mapping = {
michael@0 3351 keys: parseKeyString(lhs),
michael@0 3352 type: 'keyToEx',
michael@0 3353 exArgs: { input: rhs.substring(1) },
michael@0 3354 user: true};
michael@0 3355 if (ctx) { mapping.context = ctx; }
michael@0 3356 defaultKeymap.unshift(mapping);
michael@0 3357 } else {
michael@0 3358 // Key to key mapping
michael@0 3359 var mapping = {
michael@0 3360 keys: parseKeyString(lhs),
michael@0 3361 type: 'keyToKey',
michael@0 3362 toKeys: parseKeyString(rhs),
michael@0 3363 user: true
michael@0 3364 };
michael@0 3365 if (ctx) { mapping.context = ctx; }
michael@0 3366 defaultKeymap.unshift(mapping);
michael@0 3367 }
michael@0 3368 }
michael@0 3369 },
michael@0 3370 unmap: function(lhs, ctx) {
michael@0 3371 var arrayEquals = function(a, b) {
michael@0 3372 if (a === b) return true;
michael@0 3373 if (a == null || b == null) return true;
michael@0 3374 if (a.length != b.length) return false;
michael@0 3375 for (var i = 0; i < a.length; i++) {
michael@0 3376 if (a[i] !== b[i]) return false;
michael@0 3377 }
michael@0 3378 return true;
michael@0 3379 };
michael@0 3380 if (lhs != ':' && lhs.charAt(0) == ':') {
michael@0 3381 // Ex to Ex or Ex to key mapping
michael@0 3382 if (ctx) { throw Error('Mode not supported for ex mappings'); }
michael@0 3383 var commandName = lhs.substring(1);
michael@0 3384 if (this.commandMap_[commandName] && this.commandMap_[commandName].user) {
michael@0 3385 delete this.commandMap_[commandName];
michael@0 3386 return;
michael@0 3387 }
michael@0 3388 } else {
michael@0 3389 // Key to Ex or key to key mapping
michael@0 3390 var keys = parseKeyString(lhs);
michael@0 3391 for (var i = 0; i < defaultKeymap.length; i++) {
michael@0 3392 if (arrayEquals(keys, defaultKeymap[i].keys)
michael@0 3393 && defaultKeymap[i].context === ctx
michael@0 3394 && defaultKeymap[i].user) {
michael@0 3395 defaultKeymap.splice(i, 1);
michael@0 3396 return;
michael@0 3397 }
michael@0 3398 }
michael@0 3399 }
michael@0 3400 throw Error('No such mapping.');
michael@0 3401 }
michael@0 3402 };
michael@0 3403
michael@0 3404 // Converts a key string sequence of the form a<C-w>bd<Left> into Vim's
michael@0 3405 // keymap representation.
michael@0 3406 function parseKeyString(str) {
michael@0 3407 var key, match;
michael@0 3408 var keys = [];
michael@0 3409 while (str) {
michael@0 3410 match = (/<\w+-.+?>|<\w+>|./).exec(str);
michael@0 3411 if (match === null)break;
michael@0 3412 key = match[0];
michael@0 3413 str = str.substring(match.index + key.length);
michael@0 3414 keys.push(key);
michael@0 3415 }
michael@0 3416 return keys;
michael@0 3417 }
michael@0 3418
michael@0 3419 var exCommands = {
michael@0 3420 map: function(cm, params, ctx) {
michael@0 3421 var mapArgs = params.args;
michael@0 3422 if (!mapArgs || mapArgs.length < 2) {
michael@0 3423 if (cm) {
michael@0 3424 showConfirm(cm, 'Invalid mapping: ' + params.input);
michael@0 3425 }
michael@0 3426 return;
michael@0 3427 }
michael@0 3428 exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx);
michael@0 3429 },
michael@0 3430 nmap: function(cm, params) { this.map(cm, params, 'normal'); },
michael@0 3431 vmap: function(cm, params) { this.map(cm, params, 'visual'); },
michael@0 3432 unmap: function(cm, params, ctx) {
michael@0 3433 var mapArgs = params.args;
michael@0 3434 if (!mapArgs || mapArgs.length < 1) {
michael@0 3435 if (cm) {
michael@0 3436 showConfirm(cm, 'No such mapping: ' + params.input);
michael@0 3437 }
michael@0 3438 return;
michael@0 3439 }
michael@0 3440 exCommandDispatcher.unmap(mapArgs[0], ctx);
michael@0 3441 },
michael@0 3442 move: function(cm, params) {
michael@0 3443 commandDispatcher.processCommand(cm, cm.state.vim, {
michael@0 3444 type: 'motion',
michael@0 3445 motion: 'moveToLineOrEdgeOfDocument',
michael@0 3446 motionArgs: { forward: false, explicitRepeat: true,
michael@0 3447 linewise: true },
michael@0 3448 repeatOverride: params.line+1});
michael@0 3449 },
michael@0 3450 set: function(cm, params) {
michael@0 3451 var setArgs = params.args;
michael@0 3452 if (!setArgs || setArgs.length < 1) {
michael@0 3453 if (cm) {
michael@0 3454 showConfirm(cm, 'Invalid mapping: ' + params.input);
michael@0 3455 }
michael@0 3456 return;
michael@0 3457 }
michael@0 3458 var expr = setArgs[0].split('=');
michael@0 3459 var optionName = expr[0];
michael@0 3460 var value = expr[1];
michael@0 3461 var forceGet = false;
michael@0 3462
michael@0 3463 if (optionName.charAt(optionName.length - 1) == '?') {
michael@0 3464 // If post-fixed with ?, then the set is actually a get.
michael@0 3465 if (value) { throw Error('Trailing characters: ' + params.argString); }
michael@0 3466 optionName = optionName.substring(0, optionName.length - 1);
michael@0 3467 forceGet = true;
michael@0 3468 }
michael@0 3469 if (value === undefined && optionName.substring(0, 2) == 'no') {
michael@0 3470 // To set boolean options to false, the option name is prefixed with
michael@0 3471 // 'no'.
michael@0 3472 optionName = optionName.substring(2);
michael@0 3473 value = false;
michael@0 3474 }
michael@0 3475 var optionIsBoolean = options[optionName] && options[optionName].type == 'boolean';
michael@0 3476 if (optionIsBoolean && value == undefined) {
michael@0 3477 // Calling set with a boolean option sets it to true.
michael@0 3478 value = true;
michael@0 3479 }
michael@0 3480 if (!optionIsBoolean && !value || forceGet) {
michael@0 3481 var oldValue = getOption(optionName);
michael@0 3482 // If no value is provided, then we assume this is a get.
michael@0 3483 if (oldValue === true || oldValue === false) {
michael@0 3484 showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName);
michael@0 3485 } else {
michael@0 3486 showConfirm(cm, ' ' + optionName + '=' + oldValue);
michael@0 3487 }
michael@0 3488 } else {
michael@0 3489 setOption(optionName, value);
michael@0 3490 }
michael@0 3491 },
michael@0 3492 registers: function(cm,params) {
michael@0 3493 var regArgs = params.args;
michael@0 3494 var registers = vimGlobalState.registerController.registers;
michael@0 3495 var regInfo = '----------Registers----------<br><br>';
michael@0 3496 if (!regArgs) {
michael@0 3497 for (var registerName in registers) {
michael@0 3498 var text = registers[registerName].toString();
michael@0 3499 if (text.length) {
michael@0 3500 regInfo += '"' + registerName + ' ' + text + '<br>';
michael@0 3501 }
michael@0 3502 }
michael@0 3503 } else {
michael@0 3504 var registerName;
michael@0 3505 regArgs = regArgs.join('');
michael@0 3506 for (var i = 0; i < regArgs.length; i++) {
michael@0 3507 registerName = regArgs.charAt(i);
michael@0 3508 if (!vimGlobalState.registerController.isValidRegister(registerName)) {
michael@0 3509 continue;
michael@0 3510 }
michael@0 3511 var register = registers[registerName] || new Register();
michael@0 3512 regInfo += '"' + registerName + ' ' + register.text + '<br>';
michael@0 3513 }
michael@0 3514 }
michael@0 3515 showConfirm(cm, regInfo);
michael@0 3516 },
michael@0 3517 sort: function(cm, params) {
michael@0 3518 var reverse, ignoreCase, unique, number;
michael@0 3519 function parseArgs() {
michael@0 3520 if (params.argString) {
michael@0 3521 var args = new CodeMirror.StringStream(params.argString);
michael@0 3522 if (args.eat('!')) { reverse = true; }
michael@0 3523 if (args.eol()) { return; }
michael@0 3524 if (!args.eatSpace()) { return 'Invalid arguments'; }
michael@0 3525 var opts = args.match(/[a-z]+/);
michael@0 3526 if (opts) {
michael@0 3527 opts = opts[0];
michael@0 3528 ignoreCase = opts.indexOf('i') != -1;
michael@0 3529 unique = opts.indexOf('u') != -1;
michael@0 3530 var decimal = opts.indexOf('d') != -1 && 1;
michael@0 3531 var hex = opts.indexOf('x') != -1 && 1;
michael@0 3532 var octal = opts.indexOf('o') != -1 && 1;
michael@0 3533 if (decimal + hex + octal > 1) { return 'Invalid arguments'; }
michael@0 3534 number = decimal && 'decimal' || hex && 'hex' || octal && 'octal';
michael@0 3535 }
michael@0 3536 if (args.eatSpace() && args.match(/\/.*\//)) { 'patterns not supported'; }
michael@0 3537 }
michael@0 3538 }
michael@0 3539 var err = parseArgs();
michael@0 3540 if (err) {
michael@0 3541 showConfirm(cm, err + ': ' + params.argString);
michael@0 3542 return;
michael@0 3543 }
michael@0 3544 var lineStart = params.line || cm.firstLine();
michael@0 3545 var lineEnd = params.lineEnd || params.line || cm.lastLine();
michael@0 3546 if (lineStart == lineEnd) { return; }
michael@0 3547 var curStart = Pos(lineStart, 0);
michael@0 3548 var curEnd = Pos(lineEnd, lineLength(cm, lineEnd));
michael@0 3549 var text = cm.getRange(curStart, curEnd).split('\n');
michael@0 3550 var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ :
michael@0 3551 (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i :
michael@0 3552 (number == 'octal') ? /([0-7]+)/ : null;
michael@0 3553 var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null;
michael@0 3554 var numPart = [], textPart = [];
michael@0 3555 if (number) {
michael@0 3556 for (var i = 0; i < text.length; i++) {
michael@0 3557 if (numberRegex.exec(text[i])) {
michael@0 3558 numPart.push(text[i]);
michael@0 3559 } else {
michael@0 3560 textPart.push(text[i]);
michael@0 3561 }
michael@0 3562 }
michael@0 3563 } else {
michael@0 3564 textPart = text;
michael@0 3565 }
michael@0 3566 function compareFn(a, b) {
michael@0 3567 if (reverse) { var tmp; tmp = a; a = b; b = tmp; }
michael@0 3568 if (ignoreCase) { a = a.toLowerCase(); b = b.toLowerCase(); }
michael@0 3569 var anum = number && numberRegex.exec(a);
michael@0 3570 var bnum = number && numberRegex.exec(b);
michael@0 3571 if (!anum) { return a < b ? -1 : 1; }
michael@0 3572 anum = parseInt((anum[1] + anum[2]).toLowerCase(), radix);
michael@0 3573 bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix);
michael@0 3574 return anum - bnum;
michael@0 3575 }
michael@0 3576 numPart.sort(compareFn);
michael@0 3577 textPart.sort(compareFn);
michael@0 3578 text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart);
michael@0 3579 if (unique) { // Remove duplicate lines
michael@0 3580 var textOld = text;
michael@0 3581 var lastLine;
michael@0 3582 text = [];
michael@0 3583 for (var i = 0; i < textOld.length; i++) {
michael@0 3584 if (textOld[i] != lastLine) {
michael@0 3585 text.push(textOld[i]);
michael@0 3586 }
michael@0 3587 lastLine = textOld[i];
michael@0 3588 }
michael@0 3589 }
michael@0 3590 cm.replaceRange(text.join('\n'), curStart, curEnd);
michael@0 3591 },
michael@0 3592 substitute: function(cm, params) {
michael@0 3593 if (!cm.getSearchCursor) {
michael@0 3594 throw new Error('Search feature not available. Requires searchcursor.js or ' +
michael@0 3595 'any other getSearchCursor implementation.');
michael@0 3596 }
michael@0 3597 var argString = params.argString;
michael@0 3598 var slashes = findUnescapedSlashes(argString);
michael@0 3599 if (slashes[0] !== 0) {
michael@0 3600 showConfirm(cm, 'Substitutions should be of the form ' +
michael@0 3601 ':s/pattern/replace/');
michael@0 3602 return;
michael@0 3603 }
michael@0 3604 var regexPart = argString.substring(slashes[0] + 1, slashes[1]);
michael@0 3605 var replacePart = '';
michael@0 3606 var flagsPart;
michael@0 3607 var count;
michael@0 3608 var confirm = false; // Whether to confirm each replace.
michael@0 3609 if (slashes[1]) {
michael@0 3610 replacePart = argString.substring(slashes[1] + 1, slashes[2]);
michael@0 3611 if (getOption('pcre')) {
michael@0 3612 replacePart = unescapeRegexReplace(replacePart);
michael@0 3613 } else {
michael@0 3614 replacePart = translateRegexReplace(replacePart);
michael@0 3615 }
michael@0 3616 }
michael@0 3617 if (slashes[2]) {
michael@0 3618 // After the 3rd slash, we can have flags followed by a space followed
michael@0 3619 // by count.
michael@0 3620 var trailing = argString.substring(slashes[2] + 1).split(' ');
michael@0 3621 flagsPart = trailing[0];
michael@0 3622 count = parseInt(trailing[1]);
michael@0 3623 }
michael@0 3624 if (flagsPart) {
michael@0 3625 if (flagsPart.indexOf('c') != -1) {
michael@0 3626 confirm = true;
michael@0 3627 flagsPart.replace('c', '');
michael@0 3628 }
michael@0 3629 regexPart = regexPart + '/' + flagsPart;
michael@0 3630 }
michael@0 3631 if (regexPart) {
michael@0 3632 // If regex part is empty, then use the previous query. Otherwise use
michael@0 3633 // the regex part as the new query.
michael@0 3634 try {
michael@0 3635 updateSearchQuery(cm, regexPart, true /** ignoreCase */,
michael@0 3636 true /** smartCase */);
michael@0 3637 } catch (e) {
michael@0 3638 showConfirm(cm, 'Invalid regex: ' + regexPart);
michael@0 3639 return;
michael@0 3640 }
michael@0 3641 }
michael@0 3642 var state = getSearchState(cm);
michael@0 3643 var query = state.getQuery();
michael@0 3644 var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line;
michael@0 3645 var lineEnd = params.lineEnd || lineStart;
michael@0 3646 if (count) {
michael@0 3647 lineStart = lineEnd;
michael@0 3648 lineEnd = lineStart + count - 1;
michael@0 3649 }
michael@0 3650 var startPos = clipCursorToContent(cm, Pos(lineStart, 0));
michael@0 3651 var cursor = cm.getSearchCursor(query, startPos);
michael@0 3652 doReplace(cm, confirm, lineStart, lineEnd, cursor, query, replacePart);
michael@0 3653 },
michael@0 3654 redo: CodeMirror.commands.redo,
michael@0 3655 undo: CodeMirror.commands.undo,
michael@0 3656 write: function(cm) {
michael@0 3657 if (CodeMirror.commands.save) {
michael@0 3658 // If a save command is defined, call it.
michael@0 3659 CodeMirror.commands.save(cm);
michael@0 3660 } else {
michael@0 3661 // Saves to text area if no save command is defined.
michael@0 3662 cm.save();
michael@0 3663 }
michael@0 3664 },
michael@0 3665 nohlsearch: function(cm) {
michael@0 3666 clearSearchHighlight(cm);
michael@0 3667 },
michael@0 3668 delmarks: function(cm, params) {
michael@0 3669 if (!params.argString || !trim(params.argString)) {
michael@0 3670 showConfirm(cm, 'Argument required');
michael@0 3671 return;
michael@0 3672 }
michael@0 3673
michael@0 3674 var state = cm.state.vim;
michael@0 3675 var stream = new CodeMirror.StringStream(trim(params.argString));
michael@0 3676 while (!stream.eol()) {
michael@0 3677 stream.eatSpace();
michael@0 3678
michael@0 3679 // Record the streams position at the beginning of the loop for use
michael@0 3680 // in error messages.
michael@0 3681 var count = stream.pos;
michael@0 3682
michael@0 3683 if (!stream.match(/[a-zA-Z]/, false)) {
michael@0 3684 showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
michael@0 3685 return;
michael@0 3686 }
michael@0 3687
michael@0 3688 var sym = stream.next();
michael@0 3689 // Check if this symbol is part of a range
michael@0 3690 if (stream.match('-', true)) {
michael@0 3691 // This symbol is part of a range.
michael@0 3692
michael@0 3693 // The range must terminate at an alphabetic character.
michael@0 3694 if (!stream.match(/[a-zA-Z]/, false)) {
michael@0 3695 showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
michael@0 3696 return;
michael@0 3697 }
michael@0 3698
michael@0 3699 var startMark = sym;
michael@0 3700 var finishMark = stream.next();
michael@0 3701 // The range must terminate at an alphabetic character which
michael@0 3702 // shares the same case as the start of the range.
michael@0 3703 if (isLowerCase(startMark) && isLowerCase(finishMark) ||
michael@0 3704 isUpperCase(startMark) && isUpperCase(finishMark)) {
michael@0 3705 var start = startMark.charCodeAt(0);
michael@0 3706 var finish = finishMark.charCodeAt(0);
michael@0 3707 if (start >= finish) {
michael@0 3708 showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
michael@0 3709 return;
michael@0 3710 }
michael@0 3711
michael@0 3712 // Because marks are always ASCII values, and we have
michael@0 3713 // determined that they are the same case, we can use
michael@0 3714 // their char codes to iterate through the defined range.
michael@0 3715 for (var j = 0; j <= finish - start; j++) {
michael@0 3716 var mark = String.fromCharCode(start + j);
michael@0 3717 delete state.marks[mark];
michael@0 3718 }
michael@0 3719 } else {
michael@0 3720 showConfirm(cm, 'Invalid argument: ' + startMark + '-');
michael@0 3721 return;
michael@0 3722 }
michael@0 3723 } else {
michael@0 3724 // This symbol is a valid mark, and is not part of a range.
michael@0 3725 delete state.marks[sym];
michael@0 3726 }
michael@0 3727 }
michael@0 3728 }
michael@0 3729 };
michael@0 3730
michael@0 3731 var exCommandDispatcher = new Vim.ExCommandDispatcher();
michael@0 3732
michael@0 3733 /**
michael@0 3734 * @param {CodeMirror} cm CodeMirror instance we are in.
michael@0 3735 * @param {boolean} confirm Whether to confirm each replace.
michael@0 3736 * @param {Cursor} lineStart Line to start replacing from.
michael@0 3737 * @param {Cursor} lineEnd Line to stop replacing at.
michael@0 3738 * @param {RegExp} query Query for performing matches with.
michael@0 3739 * @param {string} replaceWith Text to replace matches with. May contain $1,
michael@0 3740 * $2, etc for replacing captured groups using Javascript replace.
michael@0 3741 */
michael@0 3742 function doReplace(cm, confirm, lineStart, lineEnd, searchCursor, query,
michael@0 3743 replaceWith) {
michael@0 3744 // Set up all the functions.
michael@0 3745 cm.state.vim.exMode = true;
michael@0 3746 var done = false;
michael@0 3747 var lastPos = searchCursor.from();
michael@0 3748 function replaceAll() {
michael@0 3749 cm.operation(function() {
michael@0 3750 while (!done) {
michael@0 3751 replace();
michael@0 3752 next();
michael@0 3753 }
michael@0 3754 stop();
michael@0 3755 });
michael@0 3756 }
michael@0 3757 function replace() {
michael@0 3758 var text = cm.getRange(searchCursor.from(), searchCursor.to());
michael@0 3759 var newText = text.replace(query, replaceWith);
michael@0 3760 searchCursor.replace(newText);
michael@0 3761 }
michael@0 3762 function next() {
michael@0 3763 var found = searchCursor.findNext();
michael@0 3764 if (!found) {
michael@0 3765 done = true;
michael@0 3766 } else if (isInRange(searchCursor.from(), lineStart, lineEnd)) {
michael@0 3767 cm.scrollIntoView(searchCursor.from(), 30);
michael@0 3768 cm.setSelection(searchCursor.from(), searchCursor.to());
michael@0 3769 lastPos = searchCursor.from();
michael@0 3770 done = false;
michael@0 3771 } else {
michael@0 3772 done = true;
michael@0 3773 }
michael@0 3774 }
michael@0 3775 function stop(close) {
michael@0 3776 if (close) { close(); }
michael@0 3777 cm.focus();
michael@0 3778 if (lastPos) {
michael@0 3779 cm.setCursor(lastPos);
michael@0 3780 var vim = cm.state.vim;
michael@0 3781 vim.exMode = false;
michael@0 3782 vim.lastHPos = vim.lastHSPos = lastPos.ch;
michael@0 3783 }
michael@0 3784 }
michael@0 3785 function onPromptKeyDown(e, _value, close) {
michael@0 3786 // Swallow all keys.
michael@0 3787 CodeMirror.e_stop(e);
michael@0 3788 var keyName = CodeMirror.keyName(e);
michael@0 3789 switch (keyName) {
michael@0 3790 case 'Y':
michael@0 3791 replace(); next(); break;
michael@0 3792 case 'N':
michael@0 3793 next(); break;
michael@0 3794 case 'A':
michael@0 3795 cm.operation(replaceAll); break;
michael@0 3796 case 'L':
michael@0 3797 replace();
michael@0 3798 // fall through and exit.
michael@0 3799 case 'Q':
michael@0 3800 case 'Esc':
michael@0 3801 case 'Ctrl-C':
michael@0 3802 case 'Ctrl-[':
michael@0 3803 stop(close);
michael@0 3804 break;
michael@0 3805 }
michael@0 3806 if (done) { stop(close); }
michael@0 3807 }
michael@0 3808
michael@0 3809 // Actually do replace.
michael@0 3810 next();
michael@0 3811 if (done) {
michael@0 3812 showConfirm(cm, 'No matches for ' + query.source);
michael@0 3813 return;
michael@0 3814 }
michael@0 3815 if (!confirm) {
michael@0 3816 replaceAll();
michael@0 3817 return;
michael@0 3818 }
michael@0 3819 showPrompt(cm, {
michael@0 3820 prefix: 'replace with <strong>' + replaceWith + '</strong> (y/n/a/q/l)',
michael@0 3821 onKeyDown: onPromptKeyDown
michael@0 3822 });
michael@0 3823 }
michael@0 3824
michael@0 3825 // Register Vim with CodeMirror
michael@0 3826 function buildVimKeyMap() {
michael@0 3827 /**
michael@0 3828 * Handle the raw key event from CodeMirror. Translate the
michael@0 3829 * Shift + key modifier to the resulting letter, while preserving other
michael@0 3830 * modifers.
michael@0 3831 */
michael@0 3832 function cmKeyToVimKey(key, modifier) {
michael@0 3833 var vimKey = key;
michael@0 3834 if (isUpperCase(vimKey) && modifier == 'Ctrl') {
michael@0 3835 vimKey = vimKey.toLowerCase();
michael@0 3836 }
michael@0 3837 if (modifier) {
michael@0 3838 // Vim will parse modifier+key combination as a single key.
michael@0 3839 vimKey = modifier.charAt(0) + '-' + vimKey;
michael@0 3840 }
michael@0 3841 var specialKey = ({Enter:'CR',Backspace:'BS',Delete:'Del'})[vimKey];
michael@0 3842 vimKey = specialKey ? specialKey : vimKey;
michael@0 3843 vimKey = vimKey.length > 1 ? '<'+ vimKey + '>' : vimKey;
michael@0 3844 return vimKey;
michael@0 3845 }
michael@0 3846
michael@0 3847 // Closure to bind CodeMirror, key, modifier.
michael@0 3848 function keyMapper(vimKey) {
michael@0 3849 return function(cm) {
michael@0 3850 CodeMirror.Vim.handleKey(cm, vimKey);
michael@0 3851 };
michael@0 3852 }
michael@0 3853
michael@0 3854 var cmToVimKeymap = {
michael@0 3855 'nofallthrough': true,
michael@0 3856 'style': 'fat-cursor'
michael@0 3857 };
michael@0 3858 function bindKeys(keys, modifier) {
michael@0 3859 for (var i = 0; i < keys.length; i++) {
michael@0 3860 var key = keys[i];
michael@0 3861 if (!modifier && key.length == 1) {
michael@0 3862 // Wrap all keys without modifiers with '' to identify them by their
michael@0 3863 // key characters instead of key identifiers.
michael@0 3864 key = "'" + key + "'";
michael@0 3865 }
michael@0 3866 var vimKey = cmKeyToVimKey(keys[i], modifier);
michael@0 3867 var cmKey = modifier ? modifier + '-' + key : key;
michael@0 3868 cmToVimKeymap[cmKey] = keyMapper(vimKey);
michael@0 3869 }
michael@0 3870 }
michael@0 3871 bindKeys(upperCaseAlphabet);
michael@0 3872 bindKeys(lowerCaseAlphabet);
michael@0 3873 bindKeys(upperCaseAlphabet, 'Ctrl');
michael@0 3874 bindKeys(specialSymbols);
michael@0 3875 bindKeys(specialSymbols, 'Ctrl');
michael@0 3876 bindKeys(numbers);
michael@0 3877 bindKeys(numbers, 'Ctrl');
michael@0 3878 bindKeys(specialKeys);
michael@0 3879 bindKeys(specialKeys, 'Ctrl');
michael@0 3880 return cmToVimKeymap;
michael@0 3881 }
michael@0 3882 CodeMirror.keyMap.vim = buildVimKeyMap();
michael@0 3883
michael@0 3884 function exitInsertMode(cm) {
michael@0 3885 var vim = cm.state.vim;
michael@0 3886 var macroModeState = vimGlobalState.macroModeState;
michael@0 3887 var isPlaying = macroModeState.isPlaying;
michael@0 3888 if (!isPlaying) {
michael@0 3889 cm.off('change', onChange);
michael@0 3890 cm.off('cursorActivity', onCursorActivity);
michael@0 3891 CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown);
michael@0 3892 }
michael@0 3893 if (!isPlaying && vim.insertModeRepeat > 1) {
michael@0 3894 // Perform insert mode repeat for commands like 3,a and 3,o.
michael@0 3895 repeatLastEdit(cm, vim, vim.insertModeRepeat - 1,
michael@0 3896 true /** repeatForInsert */);
michael@0 3897 vim.lastEditInputState.repeatOverride = vim.insertModeRepeat;
michael@0 3898 }
michael@0 3899 delete vim.insertModeRepeat;
michael@0 3900 cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1);
michael@0 3901 vim.insertMode = false;
michael@0 3902 cm.setOption('keyMap', 'vim');
michael@0 3903 cm.setOption('disableInput', true);
michael@0 3904 cm.toggleOverwrite(false); // exit replace mode if we were in it.
michael@0 3905 CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
michael@0 3906 if (macroModeState.isRecording) {
michael@0 3907 logInsertModeChange(macroModeState);
michael@0 3908 }
michael@0 3909 }
michael@0 3910
michael@0 3911 CodeMirror.keyMap['vim-insert'] = {
michael@0 3912 // TODO: override navigation keys so that Esc will cancel automatic
michael@0 3913 // indentation from o, O, i_<CR>
michael@0 3914 'Esc': exitInsertMode,
michael@0 3915 'Ctrl-[': exitInsertMode,
michael@0 3916 'Ctrl-C': exitInsertMode,
michael@0 3917 'Ctrl-N': 'autocomplete',
michael@0 3918 'Ctrl-P': 'autocomplete',
michael@0 3919 'Enter': function(cm) {
michael@0 3920 var fn = CodeMirror.commands.newlineAndIndentContinueComment ||
michael@0 3921 CodeMirror.commands.newlineAndIndent;
michael@0 3922 fn(cm);
michael@0 3923 },
michael@0 3924 fallthrough: ['default']
michael@0 3925 };
michael@0 3926
michael@0 3927 CodeMirror.keyMap['vim-replace'] = {
michael@0 3928 'Backspace': 'goCharLeft',
michael@0 3929 fallthrough: ['vim-insert']
michael@0 3930 };
michael@0 3931
michael@0 3932 function executeMacroRegister(cm, vim, macroModeState, registerName) {
michael@0 3933 var register = vimGlobalState.registerController.getRegister(registerName);
michael@0 3934 var keyBuffer = register.keyBuffer;
michael@0 3935 var imc = 0;
michael@0 3936 macroModeState.isPlaying = true;
michael@0 3937 for (var i = 0; i < keyBuffer.length; i++) {
michael@0 3938 var text = keyBuffer[i];
michael@0 3939 var match, key;
michael@0 3940 while (text) {
michael@0 3941 // Pull off one command key, which is either a single character
michael@0 3942 // or a special sequence wrapped in '<' and '>', e.g. '<Space>'.
michael@0 3943 match = (/<\w+-.+?>|<\w+>|./).exec(text);
michael@0 3944 key = match[0];
michael@0 3945 text = text.substring(match.index + key.length);
michael@0 3946 CodeMirror.Vim.handleKey(cm, key);
michael@0 3947 if (vim.insertMode) {
michael@0 3948 repeatInsertModeChanges(
michael@0 3949 cm, register.insertModeChanges[imc++].changes, 1);
michael@0 3950 exitInsertMode(cm);
michael@0 3951 }
michael@0 3952 }
michael@0 3953 };
michael@0 3954 macroModeState.isPlaying = false;
michael@0 3955 }
michael@0 3956
michael@0 3957 function logKey(macroModeState, key) {
michael@0 3958 if (macroModeState.isPlaying) { return; }
michael@0 3959 var registerName = macroModeState.latestRegister;
michael@0 3960 var register = vimGlobalState.registerController.getRegister(registerName);
michael@0 3961 if (register) {
michael@0 3962 register.pushText(key);
michael@0 3963 }
michael@0 3964 }
michael@0 3965
michael@0 3966 function logInsertModeChange(macroModeState) {
michael@0 3967 if (macroModeState.isPlaying) { return; }
michael@0 3968 var registerName = macroModeState.latestRegister;
michael@0 3969 var register = vimGlobalState.registerController.getRegister(registerName);
michael@0 3970 if (register) {
michael@0 3971 register.pushInsertModeChanges(macroModeState.lastInsertModeChanges);
michael@0 3972 }
michael@0 3973 }
michael@0 3974
michael@0 3975 /**
michael@0 3976 * Listens for changes made in insert mode.
michael@0 3977 * Should only be active in insert mode.
michael@0 3978 */
michael@0 3979 function onChange(_cm, changeObj) {
michael@0 3980 var macroModeState = vimGlobalState.macroModeState;
michael@0 3981 var lastChange = macroModeState.lastInsertModeChanges;
michael@0 3982 if (!macroModeState.isPlaying) {
michael@0 3983 while(changeObj) {
michael@0 3984 lastChange.expectCursorActivityForChange = true;
michael@0 3985 if (changeObj.origin == '+input' || changeObj.origin == 'paste'
michael@0 3986 || changeObj.origin === undefined /* only in testing */) {
michael@0 3987 var text = changeObj.text.join('\n');
michael@0 3988 lastChange.changes.push(text);
michael@0 3989 }
michael@0 3990 // Change objects may be chained with next.
michael@0 3991 changeObj = changeObj.next;
michael@0 3992 }
michael@0 3993 }
michael@0 3994 }
michael@0 3995
michael@0 3996 /**
michael@0 3997 * Listens for any kind of cursor activity on CodeMirror.
michael@0 3998 * - For tracking cursor activity in insert mode.
michael@0 3999 * - Should only be active in insert mode.
michael@0 4000 */
michael@0 4001 function onCursorActivity() {
michael@0 4002 var macroModeState = vimGlobalState.macroModeState;
michael@0 4003 if (macroModeState.isPlaying) { return; }
michael@0 4004 var lastChange = macroModeState.lastInsertModeChanges;
michael@0 4005 if (lastChange.expectCursorActivityForChange) {
michael@0 4006 lastChange.expectCursorActivityForChange = false;
michael@0 4007 } else {
michael@0 4008 // Cursor moved outside the context of an edit. Reset the change.
michael@0 4009 lastChange.changes = [];
michael@0 4010 }
michael@0 4011 }
michael@0 4012
michael@0 4013 /** Wrapper for special keys pressed in insert mode */
michael@0 4014 function InsertModeKey(keyName) {
michael@0 4015 this.keyName = keyName;
michael@0 4016 }
michael@0 4017
michael@0 4018 /**
michael@0 4019 * Handles raw key down events from the text area.
michael@0 4020 * - Should only be active in insert mode.
michael@0 4021 * - For recording deletes in insert mode.
michael@0 4022 */
michael@0 4023 function onKeyEventTargetKeyDown(e) {
michael@0 4024 var macroModeState = vimGlobalState.macroModeState;
michael@0 4025 var lastChange = macroModeState.lastInsertModeChanges;
michael@0 4026 var keyName = CodeMirror.keyName(e);
michael@0 4027 function onKeyFound() {
michael@0 4028 lastChange.changes.push(new InsertModeKey(keyName));
michael@0 4029 return true;
michael@0 4030 }
michael@0 4031 if (keyName.indexOf('Delete') != -1 || keyName.indexOf('Backspace') != -1) {
michael@0 4032 CodeMirror.lookupKey(keyName, ['vim-insert'], onKeyFound);
michael@0 4033 }
michael@0 4034 }
michael@0 4035
michael@0 4036 /**
michael@0 4037 * Repeats the last edit, which includes exactly 1 command and at most 1
michael@0 4038 * insert. Operator and motion commands are read from lastEditInputState,
michael@0 4039 * while action commands are read from lastEditActionCommand.
michael@0 4040 *
michael@0 4041 * If repeatForInsert is true, then the function was called by
michael@0 4042 * exitInsertMode to repeat the insert mode changes the user just made. The
michael@0 4043 * corresponding enterInsertMode call was made with a count.
michael@0 4044 */
michael@0 4045 function repeatLastEdit(cm, vim, repeat, repeatForInsert) {
michael@0 4046 var macroModeState = vimGlobalState.macroModeState;
michael@0 4047 macroModeState.isPlaying = true;
michael@0 4048 var isAction = !!vim.lastEditActionCommand;
michael@0 4049 var cachedInputState = vim.inputState;
michael@0 4050 function repeatCommand() {
michael@0 4051 if (isAction) {
michael@0 4052 commandDispatcher.processAction(cm, vim, vim.lastEditActionCommand);
michael@0 4053 } else {
michael@0 4054 commandDispatcher.evalInput(cm, vim);
michael@0 4055 }
michael@0 4056 }
michael@0 4057 function repeatInsert(repeat) {
michael@0 4058 if (macroModeState.lastInsertModeChanges.changes.length > 0) {
michael@0 4059 // For some reason, repeat cw in desktop VIM does not repeat
michael@0 4060 // insert mode changes. Will conform to that behavior.
michael@0 4061 repeat = !vim.lastEditActionCommand ? 1 : repeat;
michael@0 4062 var changeObject = macroModeState.lastInsertModeChanges;
michael@0 4063 // This isn't strictly necessary, but since lastInsertModeChanges is
michael@0 4064 // supposed to be immutable during replay, this helps catch bugs.
michael@0 4065 macroModeState.lastInsertModeChanges = {};
michael@0 4066 repeatInsertModeChanges(cm, changeObject.changes, repeat);
michael@0 4067 macroModeState.lastInsertModeChanges = changeObject;
michael@0 4068 }
michael@0 4069 }
michael@0 4070 vim.inputState = vim.lastEditInputState;
michael@0 4071 if (isAction && vim.lastEditActionCommand.interlaceInsertRepeat) {
michael@0 4072 // o and O repeat have to be interlaced with insert repeats so that the
michael@0 4073 // insertions appear on separate lines instead of the last line.
michael@0 4074 for (var i = 0; i < repeat; i++) {
michael@0 4075 repeatCommand();
michael@0 4076 repeatInsert(1);
michael@0 4077 }
michael@0 4078 } else {
michael@0 4079 if (!repeatForInsert) {
michael@0 4080 // Hack to get the cursor to end up at the right place. If I is
michael@0 4081 // repeated in insert mode repeat, cursor will be 1 insert
michael@0 4082 // change set left of where it should be.
michael@0 4083 repeatCommand();
michael@0 4084 }
michael@0 4085 repeatInsert(repeat);
michael@0 4086 }
michael@0 4087 vim.inputState = cachedInputState;
michael@0 4088 if (vim.insertMode && !repeatForInsert) {
michael@0 4089 // Don't exit insert mode twice. If repeatForInsert is set, then we
michael@0 4090 // were called by an exitInsertMode call lower on the stack.
michael@0 4091 exitInsertMode(cm);
michael@0 4092 }
michael@0 4093 macroModeState.isPlaying = false;
michael@0 4094 };
michael@0 4095
michael@0 4096 function repeatInsertModeChanges(cm, changes, repeat) {
michael@0 4097 function keyHandler(binding) {
michael@0 4098 if (typeof binding == 'string') {
michael@0 4099 CodeMirror.commands[binding](cm);
michael@0 4100 } else {
michael@0 4101 binding(cm);
michael@0 4102 }
michael@0 4103 return true;
michael@0 4104 }
michael@0 4105 for (var i = 0; i < repeat; i++) {
michael@0 4106 for (var j = 0; j < changes.length; j++) {
michael@0 4107 var change = changes[j];
michael@0 4108 if (change instanceof InsertModeKey) {
michael@0 4109 CodeMirror.lookupKey(change.keyName, ['vim-insert'], keyHandler);
michael@0 4110 } else {
michael@0 4111 var cur = cm.getCursor();
michael@0 4112 cm.replaceRange(change, cur, cur);
michael@0 4113 }
michael@0 4114 }
michael@0 4115 }
michael@0 4116 }
michael@0 4117
michael@0 4118 resetVimGlobalState();
michael@0 4119 return vimApi;
michael@0 4120 };
michael@0 4121 // Initialize Vim and make it available as an API.
michael@0 4122 CodeMirror.Vim = Vim();
michael@0 4123 });

mercurial