]>
Commit | Line | Data |
---|---|---|
1 | _ = require 'underscore-plus' | |
2 | {Point, Range} = require 'atom' | |
3 | settings = require '../settings' | |
4 | ||
5 | WholeWordRegex = /\S+/ | |
6 | WholeWordOrEmptyLineRegex = /^\s*$|\S+/ | |
7 | AllWhitespace = /^\s$/ | |
8 | ||
9 | class MotionError | |
10 | constructor: (@message) -> | |
11 | @name = 'Motion Error' | |
12 | ||
13 | class Motion | |
14 | operatesInclusively: false | |
15 | operatesLinewise: false | |
16 | ||
17 | constructor: (@editor, @vimState) -> | |
18 | ||
19 | select: (count, options) -> | |
20 | value = for selection in @editor.getSelections() | |
21 | if @isLinewise() | |
22 | @moveSelectionLinewise(selection, count, options) | |
23 | else if @vimState.mode is 'visual' | |
24 | @moveSelectionVisual(selection, count, options) | |
25 | else if @operatesInclusively | |
26 | @moveSelectionInclusively(selection, count, options) | |
27 | else | |
28 | @moveSelection(selection, count, options) | |
29 | not selection.isEmpty() | |
30 | ||
31 | @editor.mergeCursors() | |
32 | @editor.mergeIntersectingSelections() | |
33 | value | |
34 | ||
35 | execute: (count) -> | |
36 | for cursor in @editor.getCursors() | |
37 | @moveCursor(cursor, count) | |
38 | @editor.mergeCursors() | |
39 | ||
40 | moveSelectionLinewise: (selection, count, options) -> | |
41 | selection.modifySelection => | |
42 | [oldStartRow, oldEndRow] = selection.getBufferRowRange() | |
43 | ||
44 | wasEmpty = selection.isEmpty() | |
45 | wasReversed = selection.isReversed() | |
46 | unless wasEmpty or wasReversed | |
47 | selection.cursor.moveLeft() | |
48 | ||
49 | @moveCursor(selection.cursor, count, options) | |
50 | ||
51 | isEmpty = selection.isEmpty() | |
52 | isReversed = selection.isReversed() | |
53 | unless isEmpty or isReversed | |
54 | selection.cursor.moveRight() | |
55 | ||
56 | [newStartRow, newEndRow] = selection.getBufferRowRange() | |
57 | ||
58 | if isReversed and not wasReversed | |
59 | newEndRow = Math.max(newEndRow, oldStartRow) | |
60 | if wasReversed and not isReversed | |
61 | newStartRow = Math.min(newStartRow, oldEndRow) | |
62 | ||
63 | selection.setBufferRange([[newStartRow, 0], [newEndRow + 1, 0]]) | |
64 | ||
65 | moveSelectionInclusively: (selection, count, options) -> | |
66 | return @moveSelectionVisual(selection, count, options) unless selection.isEmpty() | |
67 | ||
68 | selection.modifySelection => | |
69 | @moveCursor(selection.cursor, count, options) | |
70 | return if selection.isEmpty() | |
71 | ||
72 | if selection.isReversed() | |
73 | # for backward motion, add the original starting character of the motion | |
74 | {start, end} = selection.getBufferRange() | |
75 | selection.setBufferRange([start, [end.row, end.column + 1]]) | |
76 | else | |
77 | # for forward motion, add the ending character of the motion | |
78 | selection.cursor.moveRight() | |
79 | ||
80 | moveSelectionVisual: (selection, count, options) -> | |
81 | selection.modifySelection => | |
82 | range = selection.getBufferRange() | |
83 | [oldStart, oldEnd] = [range.start, range.end] | |
84 | ||
85 | # in visual mode, atom cursor is after the last selected character, | |
86 | # so here put cursor in the expected place for the following motion | |
87 | wasEmpty = selection.isEmpty() | |
88 | wasReversed = selection.isReversed() | |
89 | unless wasEmpty or wasReversed | |
90 | selection.cursor.moveLeft() | |
91 | ||
92 | @moveCursor(selection.cursor, count, options) | |
93 | ||
94 | # put cursor back after the last character so it is also selected | |
95 | isEmpty = selection.isEmpty() | |
96 | isReversed = selection.isReversed() | |
97 | unless isEmpty or isReversed | |
98 | selection.cursor.moveRight() | |
99 | ||
100 | range = selection.getBufferRange() | |
101 | [newStart, newEnd] = [range.start, range.end] | |
102 | ||
103 | # if we reversed or emptied a normal selection | |
104 | # we need to select again the last character deselected above the motion | |
105 | if (isReversed or isEmpty) and not (wasReversed or wasEmpty) | |
106 | selection.setBufferRange([newStart, [newEnd.row, oldStart.column + 1]]) | |
107 | ||
108 | # if we re-reversed a reversed non-empty selection, | |
109 | # we need to keep the last character of the old selection selected | |
110 | if wasReversed and not wasEmpty and not isReversed | |
111 | selection.setBufferRange([[oldEnd.row, oldEnd.column - 1], newEnd]) | |
112 | ||
113 | # keep a single-character selection non-reversed | |
114 | range = selection.getBufferRange() | |
115 | [newStart, newEnd] = [range.start, range.end] | |
116 | if selection.isReversed() and newStart.row is newEnd.row and newStart.column + 1 is newEnd.column | |
117 | selection.setBufferRange(range, reversed: false) | |
118 | ||
119 | moveSelection: (selection, count, options) -> | |
120 | selection.modifySelection => @moveCursor(selection.cursor, count, options) | |
121 | ||
122 | isComplete: -> true | |
123 | ||
124 | isRecordable: -> false | |
125 | ||
126 | isLinewise: -> | |
127 | if @vimState?.mode is 'visual' | |
128 | @vimState?.submode is 'linewise' | |
129 | else | |
130 | @operatesLinewise | |
131 | ||
132 | class CurrentSelection extends Motion | |
133 | constructor: (@editor, @vimState) -> | |
134 | super(@editor, @vimState) | |
135 | @lastSelectionRange = @editor.getSelectedBufferRange() | |
136 | @wasLinewise = @isLinewise() | |
137 | ||
138 | execute: (count=1) -> | |
139 | _.times(count, -> true) | |
140 | ||
141 | select: (count=1) -> | |
142 | # in visual mode, the current selections are already there | |
143 | # if we're not in visual mode, we are repeating some operation and need to re-do the selections | |
144 | unless @vimState.mode is 'visual' | |
145 | if @wasLinewise | |
146 | @selectLines() | |
147 | else | |
148 | @selectCharacters() | |
149 | ||
150 | _.times(count, -> true) | |
151 | ||
152 | selectLines: -> | |
153 | lastSelectionExtent = @lastSelectionRange.getExtent() | |
154 | for selection in @editor.getSelections() | |
155 | cursor = selection.cursor.getBufferPosition() | |
156 | selection.setBufferRange [[cursor.row, 0], [cursor.row + lastSelectionExtent.row, 0]] | |
157 | return | |
158 | ||
159 | selectCharacters: -> | |
160 | lastSelectionExtent = @lastSelectionRange.getExtent() | |
161 | for selection in @editor.getSelections() | |
162 | {start} = selection.getBufferRange() | |
163 | newEnd = start.traverse(lastSelectionExtent) | |
164 | selection.setBufferRange([start, newEnd]) | |
165 | return | |
166 | ||
167 | # Public: Generic class for motions that require extra input | |
168 | class MotionWithInput extends Motion | |
169 | constructor: (@editor, @vimState) -> | |
170 | super(@editor, @vimState) | |
171 | @complete = false | |
172 | ||
173 | isComplete: -> @complete | |
174 | ||
175 | canComposeWith: (operation) -> return operation.characters? | |
176 | ||
177 | compose: (input) -> | |
178 | if not input.characters | |
179 | throw new MotionError('Must compose with an Input') | |
180 | @input = input | |
181 | @complete = true | |
182 | ||
183 | class MoveLeft extends Motion | |
184 | moveCursor: (cursor, count=1) -> | |
185 | _.times count, -> | |
186 | cursor.moveLeft() if not cursor.isAtBeginningOfLine() or settings.wrapLeftRightMotion() | |
187 | ||
188 | class MoveRight extends Motion | |
189 | moveCursor: (cursor, count=1) -> | |
190 | _.times count, => | |
191 | wrapToNextLine = settings.wrapLeftRightMotion() | |
192 | ||
193 | # when the motion is combined with an operator, we will only wrap to the next line | |
194 | # if we are already at the end of the line (after the last character) | |
195 | wrapToNextLine = false if @vimState.mode is 'operator-pending' and not cursor.isAtEndOfLine() | |
196 | ||
197 | cursor.moveRight() unless cursor.isAtEndOfLine() | |
198 | cursor.moveRight() if wrapToNextLine and cursor.isAtEndOfLine() | |
199 | ||
200 | class MoveUp extends Motion | |
201 | operatesLinewise: true | |
202 | ||
203 | moveCursor: (cursor, count=1) -> | |
204 | _.times count, -> | |
205 | unless cursor.getScreenRow() is 0 | |
206 | cursor.moveUp() | |
207 | ||
208 | class MoveDown extends Motion | |
209 | operatesLinewise: true | |
210 | ||
211 | moveCursor: (cursor, count=1) -> | |
212 | _.times count, => | |
213 | unless cursor.getScreenRow() is @editor.getLastScreenRow() | |
214 | cursor.moveDown() | |
215 | ||
216 | class MoveToPreviousWord extends Motion | |
217 | moveCursor: (cursor, count=1) -> | |
218 | _.times count, -> | |
219 | cursor.moveToBeginningOfWord() | |
220 | ||
221 | class MoveToPreviousWholeWord extends Motion | |
222 | moveCursor: (cursor, count=1) -> | |
223 | _.times count, => | |
224 | cursor.moveToBeginningOfWord() | |
225 | while not @isWholeWord(cursor) and not @isBeginningOfFile(cursor) | |
226 | cursor.moveToBeginningOfWord() | |
227 | ||
228 | isWholeWord: (cursor) -> | |
229 | char = cursor.getCurrentWordPrefix().slice(-1) | |
230 | AllWhitespace.test(char) | |
231 | ||
232 | isBeginningOfFile: (cursor) -> | |
233 | cur = cursor.getBufferPosition() | |
234 | not cur.row and not cur.column | |
235 | ||
236 | class MoveToNextWord extends Motion | |
237 | wordRegex: null | |
238 | ||
239 | moveCursor: (cursor, count=1, options) -> | |
240 | _.times count, => | |
241 | current = cursor.getBufferPosition() | |
242 | ||
243 | next = if options?.excludeWhitespace | |
244 | cursor.getEndOfCurrentWordBufferPosition(wordRegex: @wordRegex) | |
245 | else | |
246 | cursor.getBeginningOfNextWordBufferPosition(wordRegex: @wordRegex) | |
247 | ||
248 | return if @isEndOfFile(cursor) | |
249 | ||
250 | if cursor.isAtEndOfLine() | |
251 | cursor.moveDown() | |
252 | cursor.moveToBeginningOfLine() | |
253 | cursor.skipLeadingWhitespace() | |
254 | else if current.row is next.row and current.column is next.column | |
255 | cursor.moveToEndOfWord() | |
256 | else | |
257 | cursor.setBufferPosition(next) | |
258 | ||
259 | isEndOfFile: (cursor) -> | |
260 | cur = cursor.getBufferPosition() | |
261 | eof = @editor.getEofBufferPosition() | |
262 | cur.row is eof.row and cur.column is eof.column | |
263 | ||
264 | class MoveToNextWholeWord extends MoveToNextWord | |
265 | wordRegex: WholeWordOrEmptyLineRegex | |
266 | ||
267 | class MoveToEndOfWord extends Motion | |
268 | operatesInclusively: true | |
269 | wordRegex: null | |
270 | ||
271 | moveCursor: (cursor, count=1) -> | |
272 | _.times count, => | |
273 | current = cursor.getBufferPosition() | |
274 | ||
275 | next = cursor.getEndOfCurrentWordBufferPosition(wordRegex: @wordRegex) | |
276 | next.column-- if next.column > 0 | |
277 | ||
278 | if next.isEqual(current) | |
279 | cursor.moveRight() | |
280 | if cursor.isAtEndOfLine() | |
281 | cursor.moveDown() | |
282 | cursor.moveToBeginningOfLine() | |
283 | ||
284 | next = cursor.getEndOfCurrentWordBufferPosition(wordRegex: @wordRegex) | |
285 | next.column-- if next.column > 0 | |
286 | ||
287 | cursor.setBufferPosition(next) | |
288 | ||
289 | class MoveToEndOfWholeWord extends MoveToEndOfWord | |
290 | wordRegex: WholeWordRegex | |
291 | ||
292 | class MoveToNextParagraph extends Motion | |
293 | moveCursor: (cursor, count=1) -> | |
294 | _.times count, -> | |
295 | cursor.moveToBeginningOfNextParagraph() | |
296 | ||
297 | class MoveToPreviousParagraph extends Motion | |
298 | moveCursor: (cursor, count=1) -> | |
299 | _.times count, -> | |
300 | cursor.moveToBeginningOfPreviousParagraph() | |
301 | ||
302 | class MoveToLine extends Motion | |
303 | operatesLinewise: true | |
304 | ||
305 | getDestinationRow: (count) -> | |
306 | if count? then count - 1 else (@editor.getLineCount() - 1) | |
307 | ||
308 | class MoveToAbsoluteLine extends MoveToLine | |
309 | moveCursor: (cursor, count) -> | |
310 | cursor.setBufferPosition([@getDestinationRow(count), Infinity]) | |
311 | cursor.moveToFirstCharacterOfLine() | |
312 | cursor.moveToEndOfLine() if cursor.getBufferColumn() is 0 | |
313 | ||
314 | class MoveToRelativeLine extends MoveToLine | |
315 | moveCursor: (cursor, count=1) -> | |
316 | {row, column} = cursor.getBufferPosition() | |
317 | cursor.setBufferPosition([row + (count - 1), 0]) | |
318 | ||
319 | class MoveToScreenLine extends MoveToLine | |
320 | constructor: (@editorElement, @vimState, @scrolloff) -> | |
321 | @scrolloff = 2 # atom default | |
322 | super(@editorElement.getModel(), @vimState) | |
323 | ||
324 | moveCursor: (cursor, count=1) -> | |
325 | {row, column} = cursor.getBufferPosition() | |
326 | cursor.setScreenPosition([@getDestinationRow(count), 0]) | |
327 | ||
328 | class MoveToBeginningOfLine extends Motion | |
329 | moveCursor: (cursor, count=1) -> | |
330 | _.times count, -> | |
331 | cursor.moveToBeginningOfLine() | |
332 | ||
333 | class MoveToFirstCharacterOfLine extends Motion | |
334 | moveCursor: (cursor, count=1) -> | |
335 | _.times count, -> | |
336 | cursor.moveToBeginningOfLine() | |
337 | cursor.moveToFirstCharacterOfLine() | |
338 | ||
339 | class MoveToFirstCharacterOfLineAndDown extends Motion | |
340 | operatesLinewise: true | |
341 | ||
342 | moveCursor: (cursor, count=1) -> | |
343 | _.times count-1, -> | |
344 | cursor.moveDown() | |
345 | cursor.moveToBeginningOfLine() | |
346 | cursor.moveToFirstCharacterOfLine() | |
347 | ||
348 | class MoveToLastCharacterOfLine extends Motion | |
349 | moveCursor: (cursor, count=1) -> | |
350 | _.times count, -> | |
351 | cursor.moveToEndOfLine() | |
352 | cursor.goalColumn = Infinity | |
353 | ||
354 | class MoveToLastNonblankCharacterOfLineAndDown extends Motion | |
355 | operatesInclusively: true | |
356 | ||
357 | # moves cursor to the last non-whitespace character on the line | |
358 | # similar to skipLeadingWhitespace() in atom's cursor.coffee | |
359 | skipTrailingWhitespace: (cursor) -> | |
360 | position = cursor.getBufferPosition() | |
361 | scanRange = cursor.getCurrentLineBufferRange() | |
362 | startOfTrailingWhitespace = [scanRange.end.row, scanRange.end.column - 1] | |
363 | @editor.scanInBufferRange /[ \t]+$/, scanRange, ({range}) -> | |
364 | startOfTrailingWhitespace = range.start | |
365 | startOfTrailingWhitespace.column -= 1 | |
366 | cursor.setBufferPosition(startOfTrailingWhitespace) | |
367 | ||
368 | moveCursor: (cursor, count=1) -> | |
369 | _.times count-1, -> | |
370 | cursor.moveDown() | |
371 | @skipTrailingWhitespace(cursor) | |
372 | ||
373 | class MoveToFirstCharacterOfLineUp extends Motion | |
374 | operatesLinewise: true | |
375 | ||
376 | moveCursor: (cursor, count=1) -> | |
377 | _.times count, -> | |
378 | cursor.moveUp() | |
379 | cursor.moveToBeginningOfLine() | |
380 | cursor.moveToFirstCharacterOfLine() | |
381 | ||
382 | class MoveToFirstCharacterOfLineDown extends Motion | |
383 | operatesLinewise: true | |
384 | ||
385 | moveCursor: (cursor, count=1) -> | |
386 | _.times count, -> | |
387 | cursor.moveDown() | |
388 | cursor.moveToBeginningOfLine() | |
389 | cursor.moveToFirstCharacterOfLine() | |
390 | ||
391 | class MoveToStartOfFile extends MoveToLine | |
392 | moveCursor: (cursor, count=1) -> | |
393 | {row, column} = @editor.getCursorBufferPosition() | |
394 | cursor.setBufferPosition([@getDestinationRow(count), 0]) | |
395 | unless @isLinewise() | |
396 | cursor.moveToFirstCharacterOfLine() | |
397 | ||
398 | class MoveToTopOfScreen extends MoveToScreenLine | |
399 | getDestinationRow: (count=0) -> | |
400 | firstScreenRow = @editorElement.getFirstVisibleScreenRow() | |
401 | if firstScreenRow > 0 | |
402 | offset = Math.max(count - 1, @scrolloff) | |
403 | else | |
404 | offset = if count > 0 then count - 1 else count | |
405 | firstScreenRow + offset | |
406 | ||
407 | class MoveToBottomOfScreen extends MoveToScreenLine | |
408 | getDestinationRow: (count=0) -> | |
409 | lastScreenRow = @editorElement.getLastVisibleScreenRow() | |
410 | lastRow = @editor.getBuffer().getLastRow() | |
411 | if lastScreenRow isnt lastRow | |
412 | offset = Math.max(count - 1, @scrolloff) | |
413 | else | |
414 | offset = if count > 0 then count - 1 else count | |
415 | lastScreenRow - offset | |
416 | ||
417 | class MoveToMiddleOfScreen extends MoveToScreenLine | |
418 | getDestinationRow: -> | |
419 | firstScreenRow = @editorElement.getFirstVisibleScreenRow() | |
420 | lastScreenRow = @editorElement.getLastVisibleScreenRow() | |
421 | height = lastScreenRow - firstScreenRow | |
422 | Math.floor(firstScreenRow + (height / 2)) | |
423 | ||
424 | class ScrollKeepingCursor extends MoveToLine | |
425 | previousFirstScreenRow: 0 | |
426 | currentFirstScreenRow: 0 | |
427 | ||
428 | constructor: (@editorElement, @vimState) -> | |
429 | super(@editorElement.getModel(), @vimState) | |
430 | ||
431 | select: (count, options) -> | |
432 | finalDestination = @scrollScreen(count) | |
433 | super(count, options) | |
434 | @editor.setScrollTop(finalDestination) | |
435 | ||
436 | execute: (count) -> | |
437 | finalDestination = @scrollScreen(count) | |
438 | super(count) | |
439 | @editor.setScrollTop(finalDestination) | |
440 | ||
441 | moveCursor: (cursor, count=1) -> | |
442 | cursor.setScreenPosition([@getDestinationRow(count), 0]) | |
443 | ||
444 | getDestinationRow: (count) -> | |
445 | {row, column} = @editor.getCursorScreenPosition() | |
446 | @currentFirstScreenRow - @previousFirstScreenRow + row | |
447 | ||
448 | scrollScreen: (count=1) -> | |
449 | @previousFirstScreenRow = @editorElement.getFirstVisibleScreenRow() | |
450 | destination = @scrollDestination(count) | |
451 | @editor.setScrollTop(destination) | |
452 | @currentFirstScreenRow = @editorElement.getFirstVisibleScreenRow() | |
453 | destination | |
454 | ||
455 | class ScrollHalfUpKeepCursor extends ScrollKeepingCursor | |
456 | scrollDestination: (count) -> | |
457 | half = (Math.floor(@editor.getRowsPerPage() / 2) * @editor.getLineHeightInPixels()) | |
458 | @editor.getScrollTop() - count * half | |
459 | ||
460 | class ScrollFullUpKeepCursor extends ScrollKeepingCursor | |
461 | scrollDestination: (count) -> | |
462 | @editor.getScrollTop() - (count * @editor.getHeight()) | |
463 | ||
464 | class ScrollHalfDownKeepCursor extends ScrollKeepingCursor | |
465 | scrollDestination: (count) -> | |
466 | half = (Math.floor(@editor.getRowsPerPage() / 2) * @editor.getLineHeightInPixels()) | |
467 | @editor.getScrollTop() + count * half | |
468 | ||
469 | class ScrollFullDownKeepCursor extends ScrollKeepingCursor | |
470 | scrollDestination: (count) -> | |
471 | @editor.getScrollTop() + (count * @editor.getHeight()) | |
472 | ||
473 | module.exports = { | |
474 | Motion, MotionWithInput, CurrentSelection, MoveLeft, MoveRight, MoveUp, MoveDown, | |
475 | MoveToPreviousWord, MoveToPreviousWholeWord, MoveToNextWord, MoveToNextWholeWord, | |
476 | MoveToEndOfWord, MoveToNextParagraph, MoveToPreviousParagraph, MoveToAbsoluteLine, MoveToRelativeLine, MoveToBeginningOfLine, | |
477 | MoveToFirstCharacterOfLineUp, MoveToFirstCharacterOfLineDown, | |
478 | MoveToFirstCharacterOfLine, MoveToFirstCharacterOfLineAndDown, MoveToLastCharacterOfLine, | |
479 | MoveToLastNonblankCharacterOfLineAndDown, MoveToStartOfFile, | |
480 | MoveToTopOfScreen, MoveToBottomOfScreen, MoveToMiddleOfScreen, MoveToEndOfWholeWord, MotionError, | |
481 | ScrollHalfUpKeepCursor, ScrollFullUpKeepCursor, | |
482 | ScrollHalfDownKeepCursor, ScrollFullDownKeepCursor | |
483 | } |