|
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 */ |
|
58 |
|
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'; |
|
68 |
|
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 ]; |
|
338 |
|
339 var Pos = CodeMirror.Pos; |
|
340 |
|
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; |
|
361 |
|
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 } |
|
382 |
|
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, ['-', '"']); |
|
400 |
|
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 } |
|
427 |
|
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 } |
|
438 |
|
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 } |
|
454 |
|
455 function getOption(name) { |
|
456 var option = options[name]; |
|
457 if (!option) { |
|
458 throw Error('Unknown option: ' + name); |
|
459 } |
|
460 return option.value; |
|
461 } |
|
462 |
|
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 }; |
|
528 |
|
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 }; |
|
547 |
|
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 }; |
|
574 |
|
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 } |
|
628 |
|
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, |
|
640 |
|
641 // Testing hook. |
|
642 getVimGlobalState_: function() { |
|
643 return vimGlobalState; |
|
644 }, |
|
645 |
|
646 // Testing hook. |
|
647 maybeInitVimState_: maybeInitVimState, |
|
648 |
|
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 }; |
|
723 |
|
724 // Represents the current input state. |
|
725 function InputState() { |
|
726 this.prefixRepeat = []; |
|
727 this.motionRepeat = []; |
|
728 |
|
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 }; |
|
756 |
|
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 }; |
|
794 |
|
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 } |
|
844 |
|
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 }; |
|
878 |
|
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 } |
|
915 |
|
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 } |
|
932 |
|
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); |
|
1101 |
|
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 } |
|
1138 |
|
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); |
|
1144 |
|
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; |
|
1264 |
|
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 } |
|
1287 |
|
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 }; |
|
1337 |
|
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); |
|
1403 |
|
1404 if (isWrongDirection) { |
|
1405 continue; |
|
1406 } |
|
1407 if (motionArgs.linewise && (mark.line == cursor.line)) { |
|
1408 continue; |
|
1409 } |
|
1410 |
|
1411 var equal = cursorEqual(cursor, best); |
|
1412 var between = (motionArgs.forward) ? |
|
1413 cusrorIsBetween(cursor, mark, best) : |
|
1414 cusrorIsBetween(best, mark, cursor); |
|
1415 |
|
1416 if (equal || between) { |
|
1417 best = mark; |
|
1418 } |
|
1419 } |
|
1420 } |
|
1421 |
|
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. |
|
1631 |
|
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}; |
|
1638 |
|
1639 var character = motionArgs.selectedCharacter; |
|
1640 |
|
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; |
|
1647 |
|
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 } |
|
1663 |
|
1664 return [tmp.start, tmp.end]; |
|
1665 }, |
|
1666 |
|
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 }; |
|
1683 |
|
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 }; |
|
1769 |
|
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; |
|
1778 |
|
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 }; |
|
2141 |
|
2142 /* |
|
2143 * Below are miscellaneous utility functions used by vim.js |
|
2144 */ |
|
2145 |
|
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 } |
|
2221 |
|
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 } |
|
2241 |
|
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(); |
|
2253 |
|
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 } |
|
2273 |
|
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 } |
|
2280 |
|
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 } |
|
2288 |
|
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; |
|
2293 |
|
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); |
|
2309 |
|
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 } |
|
2321 |
|
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 } |
|
2331 |
|
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 } |
|
2346 |
|
2347 return { start: Pos(cur.line, wordStart), |
|
2348 end: Pos(cur.line, wordEnd) }; |
|
2349 } |
|
2350 |
|
2351 function recordJumpPosition(cm, oldCur, newCur) { |
|
2352 if (!cursorEqual(oldCur, newCur)) { |
|
2353 vimGlobalState.jumpList.add(cm, oldCur, newCur); |
|
2354 } |
|
2355 } |
|
2356 |
|
2357 function recordLastCharacterSearch(increment, args) { |
|
2358 vimGlobalState.lastChararacterSearch.increment = increment; |
|
2359 vimGlobalState.lastChararacterSearch.forward = args.forward; |
|
2360 vimGlobalState.lastChararacterSearch.selectedCharacter = args.selectedCharacter; |
|
2361 } |
|
2362 |
|
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 } |
|
2483 |
|
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; |
|
2507 |
|
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 } |
|
2516 |
|
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 } |
|
2562 |
|
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 } |
|
2619 |
|
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 } |
|
2634 |
|
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 } |
|
2641 |
|
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 } |
|
2651 |
|
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 } |
|
2672 |
|
2673 function getContextLevel(ctx) { |
|
2674 return (ctx === 'string' || ctx === 'comment') ? 1 : 0; |
|
2675 } |
|
2676 |
|
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); |
|
2681 |
|
2682 var symbContext = cm.getTokenAt(Pos(line, ch + 1)).type; |
|
2683 var symbCtxLevel = getContextLevel(symbContext); |
|
2684 |
|
2685 var reverseSymb = ({ |
|
2686 '(': ')', ')': '(', |
|
2687 '[': ']', ']': '[', |
|
2688 '{': '}', '}': '{'})[symb]; |
|
2689 |
|
2690 // Couldn't find a matching symbol, abort |
|
2691 if (!reverseSymb) { |
|
2692 return cur; |
|
2693 } |
|
2694 |
|
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 } |
|
2728 |
|
2729 if (nextCh) { |
|
2730 return Pos(line, index); |
|
2731 } |
|
2732 return cur; |
|
2733 } |
|
2734 |
|
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); |
|
2741 |
|
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 } |
|
2748 |
|
2749 if (inclusive) { |
|
2750 end.ch += 1; |
|
2751 } else { |
|
2752 start.ch += 1; |
|
2753 } |
|
2754 |
|
2755 return { start: start, end: end }; |
|
2756 } |
|
2757 |
|
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); |
|
2767 |
|
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 } |
|
2781 |
|
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 } |
|
2793 |
|
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 } |
|
2802 |
|
2803 // nothing found |
|
2804 if (!start || !end) { |
|
2805 return { start: cur, end: cur }; |
|
2806 } |
|
2807 |
|
2808 // include the symbols |
|
2809 if (inclusive) { |
|
2810 --start; ++end; |
|
2811 } |
|
2812 |
|
2813 return { |
|
2814 start: Pos(cur.line, start), |
|
2815 end: Pos(cur.line, end) |
|
2816 }; |
|
2817 } |
|
2818 |
|
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 } |
|
2855 |
|
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 } |
|
2868 |
|
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 } |
|
2907 |
|
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 } |
|
2943 |
|
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 } |
|
2966 |
|
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 } |
|
3173 |
|
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 } |
|
3258 |
|
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 } |
|
3266 |
|
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 }; |
|
3403 |
|
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 } |
|
3418 |
|
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; |
|
3462 |
|
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 } |
|
3673 |
|
3674 var state = cm.state.vim; |
|
3675 var stream = new CodeMirror.StringStream(trim(params.argString)); |
|
3676 while (!stream.eol()) { |
|
3677 stream.eatSpace(); |
|
3678 |
|
3679 // Record the streams position at the beginning of the loop for use |
|
3680 // in error messages. |
|
3681 var count = stream.pos; |
|
3682 |
|
3683 if (!stream.match(/[a-zA-Z]/, false)) { |
|
3684 showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); |
|
3685 return; |
|
3686 } |
|
3687 |
|
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. |
|
3692 |
|
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 } |
|
3698 |
|
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 } |
|
3711 |
|
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 }; |
|
3730 |
|
3731 var exCommandDispatcher = new Vim.ExCommandDispatcher(); |
|
3732 |
|
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 } |
|
3808 |
|
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 } |
|
3824 |
|
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 } |
|
3846 |
|
3847 // Closure to bind CodeMirror, key, modifier. |
|
3848 function keyMapper(vimKey) { |
|
3849 return function(cm) { |
|
3850 CodeMirror.Vim.handleKey(cm, vimKey); |
|
3851 }; |
|
3852 } |
|
3853 |
|
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(); |
|
3883 |
|
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 } |
|
3910 |
|
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 }; |
|
3926 |
|
3927 CodeMirror.keyMap['vim-replace'] = { |
|
3928 'Backspace': 'goCharLeft', |
|
3929 fallthrough: ['vim-insert'] |
|
3930 }; |
|
3931 |
|
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 } |
|
3956 |
|
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 } |
|
3965 |
|
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 } |
|
3974 |
|
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 } |
|
3995 |
|
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 } |
|
4012 |
|
4013 /** Wrapper for special keys pressed in insert mode */ |
|
4014 function InsertModeKey(keyName) { |
|
4015 this.keyName = keyName; |
|
4016 } |
|
4017 |
|
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 } |
|
4035 |
|
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 }; |
|
4095 |
|
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 } |
|
4117 |
|
4118 resetVimGlobalState(); |
|
4119 return vimApi; |
|
4120 }; |
|
4121 // Initialize Vim and make it available as an API. |
|
4122 CodeMirror.Vim = Vim(); |
|
4123 }); |