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