@name = 'Motion Error'
class Motion
- operatesInclusively: true
+ operatesInclusively: false
operatesLinewise: false
constructor: (@editor, @vimState) ->
value = for selection in @editor.getSelections()
if @isLinewise()
@moveSelectionLinewise(selection, count, options)
- else if @isInclusive()
+ else if @vimState.mode is 'visual'
+ @moveSelectionVisual(selection, count, options)
+ else if @operatesInclusively
@moveSelectionInclusively(selection, count, options)
else
@moveSelection(selection, count, options)
selection.setBufferRange([[newStartRow, 0], [newEndRow + 1, 0]])
moveSelectionInclusively: (selection, count, options) ->
+ return @moveSelectionVisual(selection, count, options) unless selection.isEmpty()
+
+ selection.modifySelection =>
+ @moveCursor(selection.cursor, count, options)
+ return if selection.isEmpty()
+
+ if selection.isReversed()
+ # for backward motion, add the original starting character of the motion
+ {start, end} = selection.getBufferRange()
+ selection.setBufferRange([start, [end.row, end.column + 1]])
+ else
+ # for forward motion, add the ending character of the motion
+ selection.cursor.moveRight()
+
+ moveSelectionVisual: (selection, count, options) ->
selection.modifySelection =>
range = selection.getBufferRange()
[oldStart, oldEnd] = [range.start, range.end]
+ # in visual mode, atom cursor is after the last selected character,
+ # so here put cursor in the expected place for the following motion
wasEmpty = selection.isEmpty()
wasReversed = selection.isReversed()
unless wasEmpty or wasReversed
@moveCursor(selection.cursor, count, options)
+ # put cursor back after the last character so it is also selected
isEmpty = selection.isEmpty()
isReversed = selection.isReversed()
unless isEmpty or isReversed
range = selection.getBufferRange()
[newStart, newEnd] = [range.start, range.end]
+ # if we reversed or emptied a normal selection
+ # we need to select again the last character deselected above the motion
if (isReversed or isEmpty) and not (wasReversed or wasEmpty)
selection.setBufferRange([newStart, [newEnd.row, oldStart.column + 1]])
+
+ # if we re-reversed a reversed non-empty selection,
+ # we need to keep the last character of the old selection selected
if wasReversed and not wasEmpty and not isReversed
- selection.setBufferRange([[newStart.row, oldEnd.column - 1], newEnd])
+ selection.setBufferRange([[oldEnd.row, oldEnd.column - 1], newEnd])
+
+ # keep a single-character selection non-reversed
+ range = selection.getBufferRange()
+ [newStart, newEnd] = [range.start, range.end]
+ if selection.isReversed() and newStart.row is newEnd.row and newStart.column + 1 is newEnd.column
+ selection.setBufferRange(range, reversed: false)
moveSelection: (selection, count, options) ->
selection.modifySelection => @moveCursor(selection.cursor, count, options)
- ensureCursorIsWithinLine: (cursor) ->
- return if @vimState.mode is 'visual' or not cursor.selection.isEmpty()
- {goalColumn} = cursor
- {row, column} = cursor.getBufferPosition()
- lastColumn = cursor.getCurrentLineBufferRange().end.column
- if column >= lastColumn - 1
- cursor.setBufferPosition([row, Math.max(lastColumn - 1, 0)])
- cursor.goalColumn ?= goalColumn
-
isComplete: -> true
isRecordable: -> false
else
@operatesLinewise
- isInclusive: ->
- @vimState.mode is 'visual' or @operatesInclusively
-
class CurrentSelection extends Motion
constructor: (@editor, @vimState) ->
super(@editor, @vimState)
- @selection = @editor.getSelectedBufferRanges()
+ @lastSelectionRange = @editor.getSelectedBufferRange()
+ @wasLinewise = @isLinewise()
execute: (count=1) ->
_.times(count, -> true)
select: (count=1) ->
- @editor.setSelectedBufferRanges(@selection)
+ # in visual mode, the current selections are already there
+ # if we're not in visual mode, we are repeating some operation and need to re-do the selections
+ unless @vimState.mode is 'visual'
+ if @wasLinewise
+ @selectLines()
+ else
+ @selectCharacters()
+
_.times(count, -> true)
+ selectLines: ->
+ lastSelectionExtent = @lastSelectionRange.getExtent()
+ for selection in @editor.getSelections()
+ cursor = selection.cursor.getBufferPosition()
+ selection.setBufferRange [[cursor.row, 0], [cursor.row + lastSelectionExtent.row, 0]]
+ return
+
+ selectCharacters: ->
+ lastSelectionExtent = @lastSelectionRange.getExtent()
+ for selection in @editor.getSelections()
+ {start} = selection.getBufferRange()
+ newEnd = start.traverse(lastSelectionExtent)
+ selection.setBufferRange([start, newEnd])
+ return
+
# Public: Generic class for motions that require extra input
class MotionWithInput extends Motion
constructor: (@editor, @vimState) ->
@complete = true
class MoveLeft extends Motion
- operatesInclusively: false
-
moveCursor: (cursor, count=1) ->
- _.times count, =>
+ _.times count, ->
cursor.moveLeft() if not cursor.isAtBeginningOfLine() or settings.wrapLeftRightMotion()
- @ensureCursorIsWithinLine(cursor)
class MoveRight extends Motion
- operatesInclusively: false
-
moveCursor: (cursor, count=1) ->
_.times count, =>
wrapToNextLine = settings.wrapLeftRightMotion()
cursor.moveRight() unless cursor.isAtEndOfLine()
cursor.moveRight() if wrapToNextLine and cursor.isAtEndOfLine()
- @ensureCursorIsWithinLine(cursor)
class MoveUp extends Motion
operatesLinewise: true
moveCursor: (cursor, count=1) ->
- _.times count, =>
+ _.times count, ->
unless cursor.getScreenRow() is 0
cursor.moveUp()
- @ensureCursorIsWithinLine(cursor)
class MoveDown extends Motion
operatesLinewise: true
_.times count, =>
unless cursor.getScreenRow() is @editor.getLastScreenRow()
cursor.moveDown()
- @ensureCursorIsWithinLine(cursor)
class MoveToPreviousWord extends Motion
- operatesInclusively: false
-
moveCursor: (cursor, count=1) ->
_.times count, ->
cursor.moveToBeginningOfWord()
class MoveToPreviousWholeWord extends Motion
- operatesInclusively: false
-
moveCursor: (cursor, count=1) ->
_.times count, =>
cursor.moveToBeginningOfWord()
class MoveToNextWord extends Motion
wordRegex: null
- operatesInclusively: false
moveCursor: (cursor, count=1, options) ->
_.times count, =>
wordRegex: WholeWordOrEmptyLineRegex
class MoveToEndOfWord extends Motion
+ operatesInclusively: true
wordRegex: null
moveCursor: (cursor, count=1) ->
wordRegex: WholeWordRegex
class MoveToNextParagraph extends Motion
- operatesInclusively: false
-
moveCursor: (cursor, count=1) ->
_.times count, ->
cursor.moveToBeginningOfNextParagraph()
cursor.moveToEndOfLine() if cursor.getBufferColumn() is 0
class MoveToRelativeLine extends MoveToLine
- operatesLinewise: true
-
moveCursor: (cursor, count=1) ->
{row, column} = cursor.getBufferPosition()
cursor.setBufferPosition([row + (count - 1), 0])
cursor.setScreenPosition([@getDestinationRow(count), 0])
class MoveToBeginningOfLine extends Motion
- operatesInclusively: false
-
moveCursor: (cursor, count=1) ->
_.times count, ->
cursor.moveToBeginningOfLine()
class MoveToFirstCharacterOfLine extends Motion
- operatesInclusively: false
-
moveCursor: (cursor, count=1) ->
_.times count, ->
cursor.moveToBeginningOfLine()
class MoveToFirstCharacterOfLineAndDown extends Motion
operatesLinewise: true
- operatesInclusively: true
- moveCursor: (cursor, count=0) ->
+ moveCursor: (cursor, count=1) ->
_.times count-1, ->
cursor.moveDown()
cursor.moveToBeginningOfLine()
cursor.moveToFirstCharacterOfLine()
class MoveToLastCharacterOfLine extends Motion
- operatesInclusively: false
-
moveCursor: (cursor, count=1) ->
- _.times count, =>
+ _.times count, ->
cursor.moveToEndOfLine()
cursor.goalColumn = Infinity
- @ensureCursorIsWithinLine(cursor)
class MoveToLastNonblankCharacterOfLineAndDown extends Motion
operatesInclusively: true
class MoveToFirstCharacterOfLineUp extends Motion
operatesLinewise: true
- operatesInclusively: true
moveCursor: (cursor, count=1) ->
_.times count, ->
lastScreenRow - offset
class MoveToMiddleOfScreen extends MoveToScreenLine
- getDestinationRow: (count) ->
+ getDestinationRow: ->
firstScreenRow = @editorElement.getFirstVisibleScreenRow()
lastScreenRow = @editorElement.getLastVisibleScreenRow()
height = lastScreenRow - firstScreenRow
{row, column} = @editor.getCursorScreenPosition()
@currentFirstScreenRow - @previousFirstScreenRow + row
- scrollScreen: (count = 1) ->
+ scrollScreen: (count=1) ->
@previousFirstScreenRow = @editorElement.getFirstVisibleScreenRow()
destination = @scrollDestination(count)
@editor.setScrollTop(destination)