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