1 _ = require 'underscore-plus'
2 {Point, Range} = require 'atom'
3 settings = require '../settings'
6 WholeWordOrEmptyLineRegex = /^\s*$|\S+/
10 constructor: (@message) ->
11 @name = 'Motion Error'
14 operatesInclusively: false
15 operatesLinewise: false
17 constructor: (@editor, @vimState) ->
19 select: (count, options) ->
20 value = for selection in @editor.getSelections()
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)
28 @moveSelection(selection, count, options)
29 not selection.isEmpty()
31 @editor.mergeCursors()
32 @editor.mergeIntersectingSelections()
36 for cursor in @editor.getCursors()
37 @moveCursor(cursor, count)
38 @editor.mergeCursors()
40 moveSelectionLinewise: (selection, count, options) ->
41 selection.modifySelection =>
42 [oldStartRow, oldEndRow] = selection.getBufferRowRange()
44 wasEmpty = selection.isEmpty()
45 wasReversed = selection.isReversed()
46 unless wasEmpty or wasReversed
47 selection.cursor.moveLeft()
49 @moveCursor(selection.cursor, count, options)
51 isEmpty = selection.isEmpty()
52 isReversed = selection.isReversed()
53 unless isEmpty or isReversed
54 selection.cursor.moveRight()
56 [newStartRow, newEndRow] = selection.getBufferRowRange()
58 if isReversed and not wasReversed
59 newEndRow = Math.max(newEndRow, oldStartRow)
60 if wasReversed and not isReversed
61 newStartRow = Math.min(newStartRow, oldEndRow)
63 selection.setBufferRange([[newStartRow, 0], [newEndRow + 1, 0]])
65 moveSelectionInclusively: (selection, count, options) ->
66 return @moveSelectionVisual(selection, count, options) unless selection.isEmpty()
68 selection.modifySelection =>
69 @moveCursor(selection.cursor, count, options)
70 return if selection.isEmpty()
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]])
77 # for forward motion, add the ending character of the motion
78 selection.cursor.moveRight()
80 moveSelectionVisual: (selection, count, options) ->
81 selection.modifySelection =>
82 range = selection.getBufferRange()
83 [oldStart, oldEnd] = [range.start, range.end]
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()
92 @moveCursor(selection.cursor, count, options)
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()
100 range = selection.getBufferRange()
101 [newStart, newEnd] = [range.start, range.end]
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]])
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])
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)
119 moveSelection: (selection, count, options) ->
120 selection.modifySelection => @moveCursor(selection.cursor, count, options)
124 isRecordable: -> false
127 if @vimState?.mode is 'visual'
128 @vimState?.submode is 'linewise'
132 class CurrentSelection extends Motion
133 constructor: (@editor, @vimState) ->
134 super(@editor, @vimState)
135 @lastSelectionRange = @editor.getSelectedBufferRange()
136 @wasLinewise = @isLinewise()
138 execute: (count=1) ->
139 _.times(count, -> true)
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'
150 _.times(count, -> true)
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]]
160 lastSelectionExtent = @lastSelectionRange.getExtent()
161 for selection in @editor.getSelections()
162 {start} = selection.getBufferRange()
163 newEnd = start.traverse(lastSelectionExtent)
164 selection.setBufferRange([start, newEnd])
167 # Public: Generic class for motions that require extra input
168 class MotionWithInput extends Motion
169 constructor: (@editor, @vimState) ->
170 super(@editor, @vimState)
173 isComplete: -> @complete
175 canComposeWith: (operation) -> return operation.characters?
178 if not input.characters
179 throw new MotionError('Must compose with an Input')
183 class MoveLeft extends Motion
184 moveCursor: (cursor, count=1) ->
186 cursor.moveLeft() if not cursor.isAtBeginningOfLine() or settings.wrapLeftRightMotion()
188 class MoveRight extends Motion
189 moveCursor: (cursor, count=1) ->
191 wrapToNextLine = settings.wrapLeftRightMotion()
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()
197 cursor.moveRight() unless cursor.isAtEndOfLine()
198 cursor.moveRight() if wrapToNextLine and cursor.isAtEndOfLine()
200 class MoveUp extends Motion
201 operatesLinewise: true
203 moveCursor: (cursor, count=1) ->
205 unless cursor.getScreenRow() is 0
208 class MoveDown extends Motion
209 operatesLinewise: true
211 moveCursor: (cursor, count=1) ->
213 unless cursor.getScreenRow() is @editor.getLastScreenRow()
216 class MoveToPreviousWord extends Motion
217 moveCursor: (cursor, count=1) ->
219 cursor.moveToBeginningOfWord()
221 class MoveToPreviousWholeWord extends Motion
222 moveCursor: (cursor, count=1) ->
224 cursor.moveToBeginningOfWord()
225 while not @isWholeWord(cursor) and not @isBeginningOfFile(cursor)
226 cursor.moveToBeginningOfWord()
228 isWholeWord: (cursor) ->
229 char = cursor.getCurrentWordPrefix().slice(-1)
230 AllWhitespace.test(char)
232 isBeginningOfFile: (cursor) ->
233 cur = cursor.getBufferPosition()
234 not cur.row and not cur.column
236 class MoveToNextWord extends Motion
239 moveCursor: (cursor, count=1, options) ->
241 current = cursor.getBufferPosition()
243 next = if options?.excludeWhitespace
244 cursor.getEndOfCurrentWordBufferPosition(wordRegex: @wordRegex)
246 cursor.getBeginningOfNextWordBufferPosition(wordRegex: @wordRegex)
248 return if @isEndOfFile(cursor)
250 if cursor.isAtEndOfLine()
252 cursor.moveToBeginningOfLine()
253 cursor.skipLeadingWhitespace()
254 else if current.row is next.row and current.column is next.column
255 cursor.moveToEndOfWord()
257 cursor.setBufferPosition(next)
259 isEndOfFile: (cursor) ->
260 cur = cursor.getBufferPosition()
261 eof = @editor.getEofBufferPosition()
262 cur.row is eof.row and cur.column is eof.column
264 class MoveToNextWholeWord extends MoveToNextWord
265 wordRegex: WholeWordOrEmptyLineRegex
267 class MoveToEndOfWord extends Motion
268 operatesInclusively: true
271 moveCursor: (cursor, count=1) ->
273 current = cursor.getBufferPosition()
275 next = cursor.getEndOfCurrentWordBufferPosition(wordRegex: @wordRegex)
276 next.column-- if next.column > 0
278 if next.isEqual(current)
280 if cursor.isAtEndOfLine()
282 cursor.moveToBeginningOfLine()
284 next = cursor.getEndOfCurrentWordBufferPosition(wordRegex: @wordRegex)
285 next.column-- if next.column > 0
287 cursor.setBufferPosition(next)
289 class MoveToEndOfWholeWord extends MoveToEndOfWord
290 wordRegex: WholeWordRegex
292 class MoveToNextParagraph extends Motion
293 moveCursor: (cursor, count=1) ->
295 cursor.moveToBeginningOfNextParagraph()
297 class MoveToPreviousParagraph extends Motion
298 moveCursor: (cursor, count=1) ->
300 cursor.moveToBeginningOfPreviousParagraph()
302 class MoveToLine extends Motion
303 operatesLinewise: true
305 getDestinationRow: (count) ->
306 if count? then count - 1 else (@editor.getLineCount() - 1)
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
314 class MoveToRelativeLine extends MoveToLine
315 moveCursor: (cursor, count=1) ->
316 {row, column} = cursor.getBufferPosition()
317 cursor.setBufferPosition([row + (count - 1), 0])
319 class MoveToScreenLine extends MoveToLine
320 constructor: (@editorElement, @vimState, @scrolloff) ->
321 @scrolloff = 2 # atom default
322 super(@editorElement.getModel(), @vimState)
324 moveCursor: (cursor, count=1) ->
325 {row, column} = cursor.getBufferPosition()
326 cursor.setScreenPosition([@getDestinationRow(count), 0])
328 class MoveToBeginningOfLine extends Motion
329 moveCursor: (cursor, count=1) ->
331 cursor.moveToBeginningOfLine()
333 class MoveToFirstCharacterOfLine extends Motion
334 moveCursor: (cursor, count=1) ->
336 cursor.moveToBeginningOfLine()
337 cursor.moveToFirstCharacterOfLine()
339 class MoveToFirstCharacterOfLineAndDown extends Motion
340 operatesLinewise: true
342 moveCursor: (cursor, count=1) ->
345 cursor.moveToBeginningOfLine()
346 cursor.moveToFirstCharacterOfLine()
348 class MoveToLastCharacterOfLine extends Motion
349 moveCursor: (cursor, count=1) ->
351 cursor.moveToEndOfLine()
352 cursor.goalColumn = Infinity
354 class MoveToLastNonblankCharacterOfLineAndDown extends Motion
355 operatesInclusively: true
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)
368 moveCursor: (cursor, count=1) ->
371 @skipTrailingWhitespace(cursor)
373 class MoveToFirstCharacterOfLineUp extends Motion
374 operatesLinewise: true
376 moveCursor: (cursor, count=1) ->
379 cursor.moveToBeginningOfLine()
380 cursor.moveToFirstCharacterOfLine()
382 class MoveToFirstCharacterOfLineDown extends Motion
383 operatesLinewise: true
385 moveCursor: (cursor, count=1) ->
388 cursor.moveToBeginningOfLine()
389 cursor.moveToFirstCharacterOfLine()
391 class MoveToStartOfFile extends MoveToLine
392 moveCursor: (cursor, count=1) ->
393 {row, column} = @editor.getCursorBufferPosition()
394 cursor.setBufferPosition([@getDestinationRow(count), 0])
396 cursor.moveToFirstCharacterOfLine()
398 class MoveToTopOfScreen extends MoveToScreenLine
399 getDestinationRow: (count=0) ->
400 firstScreenRow = @editorElement.getFirstVisibleScreenRow()
401 if firstScreenRow > 0
402 offset = Math.max(count - 1, @scrolloff)
404 offset = if count > 0 then count - 1 else count
405 firstScreenRow + offset
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)
414 offset = if count > 0 then count - 1 else count
415 lastScreenRow - offset
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))
424 class ScrollKeepingCursor extends MoveToLine
425 previousFirstScreenRow: 0
426 currentFirstScreenRow: 0
428 constructor: (@editorElement, @vimState) ->
429 super(@editorElement.getModel(), @vimState)
431 select: (count, options) ->
432 finalDestination = @scrollScreen(count)
433 super(count, options)
434 @editor.setScrollTop(finalDestination)
437 finalDestination = @scrollScreen(count)
439 @editor.setScrollTop(finalDestination)
441 moveCursor: (cursor, count=1) ->
442 cursor.setScreenPosition([@getDestinationRow(count), 0])
444 getDestinationRow: (count) ->
445 {row, column} = @editor.getCursorScreenPosition()
446 @currentFirstScreenRow - @previousFirstScreenRow + row
448 scrollScreen: (count=1) ->
449 @previousFirstScreenRow = @editorElement.getFirstVisibleScreenRow()
450 destination = @scrollDestination(count)
451 @editor.setScrollTop(destination)
452 @currentFirstScreenRow = @editorElement.getFirstVisibleScreenRow()
455 class ScrollHalfUpKeepCursor extends ScrollKeepingCursor
456 scrollDestination: (count) ->
457 half = (Math.floor(@editor.getRowsPerPage() / 2) * @editor.getLineHeightInPixels())
458 @editor.getScrollTop() - count * half
460 class ScrollFullUpKeepCursor extends ScrollKeepingCursor
461 scrollDestination: (count) ->
462 @editor.getScrollTop() - (count * @editor.getHeight())
464 class ScrollHalfDownKeepCursor extends ScrollKeepingCursor
465 scrollDestination: (count) ->
466 half = (Math.floor(@editor.getRowsPerPage() / 2) * @editor.getLineHeightInPixels())
467 @editor.getScrollTop() + count * half
469 class ScrollFullDownKeepCursor extends ScrollKeepingCursor
470 scrollDestination: (count) ->
471 @editor.getScrollTop() + (count * @editor.getHeight())
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