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