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: true
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 @isInclusive()
24 @moveSelectionInclusively(selection, count, options)
26 @moveSelection(selection, count, options)
27 not selection.isEmpty()
29 @editor.mergeCursors()
30 @editor.mergeIntersectingSelections()
34 for cursor in @editor.getCursors()
35 @moveCursor(cursor, count)
36 @editor.mergeCursors()
38 moveSelectionLinewise: (selection, count, options) ->
39 selection.modifySelection =>
40 [oldStartRow, oldEndRow] = selection.getBufferRowRange()
42 wasEmpty = selection.isEmpty()
43 wasReversed = selection.isReversed()
44 unless wasEmpty or wasReversed
45 selection.cursor.moveLeft()
47 @moveCursor(selection.cursor, count, options)
49 isEmpty = selection.isEmpty()
50 isReversed = selection.isReversed()
51 unless isEmpty or isReversed
52 selection.cursor.moveRight()
54 [newStartRow, newEndRow] = selection.getBufferRowRange()
56 if isReversed and not wasReversed
57 newEndRow = Math.max(newEndRow, oldStartRow)
58 if wasReversed and not isReversed
59 newStartRow = Math.min(newStartRow, oldEndRow)
61 selection.setBufferRange([[newStartRow, 0], [newEndRow + 1, 0]])
63 moveSelectionInclusively: (selection, count, options) ->
64 selection.modifySelection =>
65 range = selection.getBufferRange()
66 [oldStart, oldEnd] = [range.start, range.end]
68 wasEmpty = selection.isEmpty()
69 wasReversed = selection.isReversed()
70 unless wasEmpty or wasReversed
71 selection.cursor.moveLeft()
73 @moveCursor(selection.cursor, count, options)
75 isEmpty = selection.isEmpty()
76 isReversed = selection.isReversed()
77 unless isEmpty or isReversed
78 selection.cursor.moveRight()
80 range = selection.getBufferRange()
81 [newStart, newEnd] = [range.start, range.end]
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])
88 moveSelection: (selection, count, options) ->
89 selection.modifySelection => @moveCursor(selection.cursor, count, options)
91 ensureCursorIsWithinLine: (cursor) ->
92 return if @vimState.mode is 'visual' or not cursor.selection.isEmpty()
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
102 isRecordable: -> false
105 if @vimState?.mode is 'visual'
106 @vimState?.submode is 'linewise'
111 @vimState.mode is 'visual' or @operatesInclusively
113 class CurrentSelection extends Motion
114 constructor: (@editor, @vimState) ->
115 super(@editor, @vimState)
116 @selection = @editor.getSelectedBufferRanges()
118 execute: (count=1) ->
119 _.times(count, -> true)
122 @editor.setSelectedBufferRanges(@selection)
123 _.times(count, -> true)
125 # Public: Generic class for motions that require extra input
126 class MotionWithInput extends Motion
127 constructor: (@editor, @vimState) ->
128 super(@editor, @vimState)
131 isComplete: -> @complete
133 canComposeWith: (operation) -> return operation.characters?
136 if not input.characters
137 throw new MotionError('Must compose with an Input')
141 class MoveLeft extends Motion
142 operatesInclusively: false
144 moveCursor: (cursor, count=1) ->
146 cursor.moveLeft() if not cursor.isAtBeginningOfLine() or settings.wrapLeftRightMotion()
147 @ensureCursorIsWithinLine(cursor)
149 class MoveRight extends Motion
150 operatesInclusively: false
152 moveCursor: (cursor, count=1) ->
154 wrapToNextLine = settings.wrapLeftRightMotion()
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()
160 cursor.moveRight() unless cursor.isAtEndOfLine()
161 cursor.moveRight() if wrapToNextLine and cursor.isAtEndOfLine()
162 @ensureCursorIsWithinLine(cursor)
164 class MoveUp extends Motion
165 operatesLinewise: true
167 moveCursor: (cursor, count=1) ->
169 unless cursor.getScreenRow() is 0
171 @ensureCursorIsWithinLine(cursor)
173 class MoveDown extends Motion
174 operatesLinewise: true
176 moveCursor: (cursor, count=1) ->
178 unless cursor.getScreenRow() is @editor.getLastScreenRow()
180 @ensureCursorIsWithinLine(cursor)
182 class MoveToPreviousWord extends Motion
183 operatesInclusively: false
185 moveCursor: (cursor, count=1) ->
187 cursor.moveToBeginningOfWord()
189 class MoveToPreviousWholeWord extends Motion
190 operatesInclusively: false
192 moveCursor: (cursor, count=1) ->
194 cursor.moveToBeginningOfWord()
195 while not @isWholeWord(cursor) and not @isBeginningOfFile(cursor)
196 cursor.moveToBeginningOfWord()
198 isWholeWord: (cursor) ->
199 char = cursor.getCurrentWordPrefix().slice(-1)
200 AllWhitespace.test(char)
202 isBeginningOfFile: (cursor) ->
203 cur = cursor.getBufferPosition()
204 not cur.row and not cur.column
206 class MoveToNextWord extends Motion
208 operatesInclusively: false
210 moveCursor: (cursor, count=1, options) ->
212 current = cursor.getBufferPosition()
214 next = if options?.excludeWhitespace
215 cursor.getEndOfCurrentWordBufferPosition(wordRegex: @wordRegex)
217 cursor.getBeginningOfNextWordBufferPosition(wordRegex: @wordRegex)
219 return if @isEndOfFile(cursor)
221 if cursor.isAtEndOfLine()
223 cursor.moveToBeginningOfLine()
224 cursor.skipLeadingWhitespace()
225 else if current.row is next.row and current.column is next.column
226 cursor.moveToEndOfWord()
228 cursor.setBufferPosition(next)
230 isEndOfFile: (cursor) ->
231 cur = cursor.getBufferPosition()
232 eof = @editor.getEofBufferPosition()
233 cur.row is eof.row and cur.column is eof.column
235 class MoveToNextWholeWord extends MoveToNextWord
236 wordRegex: WholeWordOrEmptyLineRegex
238 class MoveToEndOfWord extends Motion
241 moveCursor: (cursor, count=1) ->
243 current = cursor.getBufferPosition()
245 next = cursor.getEndOfCurrentWordBufferPosition(wordRegex: @wordRegex)
246 next.column-- if next.column > 0
248 if next.isEqual(current)
250 if cursor.isAtEndOfLine()
252 cursor.moveToBeginningOfLine()
254 next = cursor.getEndOfCurrentWordBufferPosition(wordRegex: @wordRegex)
255 next.column-- if next.column > 0
257 cursor.setBufferPosition(next)
259 class MoveToEndOfWholeWord extends MoveToEndOfWord
260 wordRegex: WholeWordRegex
262 class MoveToNextParagraph extends Motion
263 operatesInclusively: false
265 moveCursor: (cursor, count=1) ->
267 cursor.moveToBeginningOfNextParagraph()
269 class MoveToPreviousParagraph extends Motion
270 moveCursor: (cursor, count=1) ->
272 cursor.moveToBeginningOfPreviousParagraph()
274 class MoveToLine extends Motion
275 operatesLinewise: true
277 getDestinationRow: (count) ->
278 if count? then count - 1 else (@editor.getLineCount() - 1)
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
286 class MoveToRelativeLine extends MoveToLine
287 operatesLinewise: true
289 moveCursor: (cursor, count=1) ->
290 {row, column} = cursor.getBufferPosition()
291 cursor.setBufferPosition([row + (count - 1), 0])
293 class MoveToScreenLine extends MoveToLine
294 constructor: (@editorElement, @vimState, @scrolloff) ->
295 @scrolloff = 2 # atom default
296 super(@editorElement.getModel(), @vimState)
298 moveCursor: (cursor, count=1) ->
299 {row, column} = cursor.getBufferPosition()
300 cursor.setScreenPosition([@getDestinationRow(count), 0])
302 class MoveToBeginningOfLine extends Motion
303 operatesInclusively: false
305 moveCursor: (cursor, count=1) ->
307 cursor.moveToBeginningOfLine()
309 class MoveToFirstCharacterOfLine extends Motion
310 operatesInclusively: false
312 moveCursor: (cursor, count=1) ->
314 cursor.moveToBeginningOfLine()
315 cursor.moveToFirstCharacterOfLine()
317 class MoveToFirstCharacterOfLineAndDown extends Motion
318 operatesLinewise: true
319 operatesInclusively: true
321 moveCursor: (cursor, count=0) ->
324 cursor.moveToBeginningOfLine()
325 cursor.moveToFirstCharacterOfLine()
327 class MoveToLastCharacterOfLine extends Motion
328 operatesInclusively: false
330 moveCursor: (cursor, count=1) ->
332 cursor.moveToEndOfLine()
333 cursor.goalColumn = Infinity
334 @ensureCursorIsWithinLine(cursor)
336 class MoveToLastNonblankCharacterOfLineAndDown extends Motion
337 operatesInclusively: true
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)
350 moveCursor: (cursor, count=1) ->
353 @skipTrailingWhitespace(cursor)
355 class MoveToFirstCharacterOfLineUp extends Motion
356 operatesLinewise: true
357 operatesInclusively: true
359 moveCursor: (cursor, count=1) ->
362 cursor.moveToBeginningOfLine()
363 cursor.moveToFirstCharacterOfLine()
365 class MoveToFirstCharacterOfLineDown extends Motion
366 operatesLinewise: true
368 moveCursor: (cursor, count=1) ->
371 cursor.moveToBeginningOfLine()
372 cursor.moveToFirstCharacterOfLine()
374 class MoveToStartOfFile extends MoveToLine
375 moveCursor: (cursor, count=1) ->
376 {row, column} = @editor.getCursorBufferPosition()
377 cursor.setBufferPosition([@getDestinationRow(count), 0])
379 cursor.moveToFirstCharacterOfLine()
381 class MoveToTopOfScreen extends MoveToScreenLine
382 getDestinationRow: (count=0) ->
383 firstScreenRow = @editorElement.getFirstVisibleScreenRow()
384 if firstScreenRow > 0
385 offset = Math.max(count - 1, @scrolloff)
387 offset = if count > 0 then count - 1 else count
388 firstScreenRow + offset
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)
397 offset = if count > 0 then count - 1 else count
398 lastScreenRow - offset
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))
407 class ScrollKeepingCursor extends MoveToLine
408 previousFirstScreenRow: 0
409 currentFirstScreenRow: 0
411 constructor: (@editorElement, @vimState) ->
412 super(@editorElement.getModel(), @vimState)
414 select: (count, options) ->
415 finalDestination = @scrollScreen(count)
416 super(count, options)
417 @editor.setScrollTop(finalDestination)
420 finalDestination = @scrollScreen(count)
422 @editor.setScrollTop(finalDestination)
424 moveCursor: (cursor, count=1) ->
425 cursor.setScreenPosition([@getDestinationRow(count), 0])
427 getDestinationRow: (count) ->
428 {row, column} = @editor.getCursorScreenPosition()
429 @currentFirstScreenRow - @previousFirstScreenRow + row
431 scrollScreen: (count = 1) ->
432 @previousFirstScreenRow = @editorElement.getFirstVisibleScreenRow()
433 destination = @scrollDestination(count)
434 @editor.setScrollTop(destination)
435 @currentFirstScreenRow = @editorElement.getFirstVisibleScreenRow()
438 class ScrollHalfUpKeepCursor extends ScrollKeepingCursor
439 scrollDestination: (count) ->
440 half = (Math.floor(@editor.getRowsPerPage() / 2) * @editor.getLineHeightInPixels())
441 @editor.getScrollTop() - count * half
443 class ScrollFullUpKeepCursor extends ScrollKeepingCursor
444 scrollDestination: (count) ->
445 @editor.getScrollTop() - (count * @editor.getHeight())
447 class ScrollHalfDownKeepCursor extends ScrollKeepingCursor
448 scrollDestination: (count) ->
449 half = (Math.floor(@editor.getRowsPerPage() / 2) * @editor.getLineHeightInPixels())
450 @editor.getScrollTop() + count * half
452 class ScrollFullDownKeepCursor extends ScrollKeepingCursor
453 scrollDestination: (count) ->
454 @editor.getScrollTop() + (count * @editor.getHeight())
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