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