laughing-hipster/.atom/packages/vim-mode/lib/motions/general-motions.coffee

555 lines
17 KiB
CoffeeScript

_ = require 'underscore-plus'
{$$, Point, Range} = require 'atom'
class MotionError
constructor: (@message) ->
@name = 'Motion Error'
class Motion
constructor: (@editor, @vimState) ->
@vimState.desiredCursorColumn = null
isComplete: -> true
isRecordable: -> false
inVisualMode: -> @vimState.mode == "visual"
class CurrentSelection extends Motion
execute: (count=1) ->
_.times(count, -> true)
select: (count=1) ->
_.times(count, -> true)
isLinewise: -> @vimState.mode == 'visual' and @vimState.submode == 'linewise'
# Public: Generic class for motions that require extra input
class MotionWithInput extends Motion
constructor: (@editorView, @vimState) ->
super(@editorView.editor, @vimState)
@complete = false
isComplete: -> @complete
canComposeWith: (operation) -> return operation.characters?
compose: (input) ->
if not input.characters
throw new MotionError('Must compose with an Input')
@input = input
@complete = true
class MoveLeft extends Motion
execute: (count=1) ->
_.times count, =>
{row, column} = @editor.getCursorScreenPosition()
@editor.moveCursorLeft() if column > 0
select: (count=1) ->
_.times count, =>
{row, column} = @editor.getCursorScreenPosition()
if column > 0
@editor.selectLeft()
true
else
false
class MoveRight extends Motion
execute: (count=1) ->
_.times count, =>
{row, column} = @editor.getCursorScreenPosition()
lastCharIndex = @editor.getBuffer().lineForRow(row).length - 1
unless column >= lastCharIndex
@editor.moveCursorRight()
select: (count=1) ->
_.times count, =>
{start, end} = @editor.getSelectedBufferRange()
rowLength = @editor.getCursor().getCurrentBufferLine().length
if end.column < rowLength
@editor.selectRight()
true
else
false
class MoveVertically extends Motion
constructor: (@editor, @vimState) ->
# 'desiredCursorColumn' gets overwritten in the Motion constructor,
# so we need to re-set it after calling super.
column = @vimState.desiredCursorColumn
super(@editor, @vimState)
@vimState.desiredCursorColumn = column
execute: (count=1) ->
{row, column} = @editor.getCursorBufferPosition()
nextRow = @nextValidRow(count)
if nextRow != row
nextLineLength = @editor.lineLengthForBufferRow(nextRow)
# The 'nextColumn' the cursor should be in is the
# 'desiredCursorColumn', if it exists. If it does
# not, the current column should be used.
nextColumn = @vimState.desiredCursorColumn || column
# Check to see if the 'nextColumn' position of
# cursor is greater than or equal to the length
# of the next line.
if nextColumn >= nextLineLength
# When the 'nextColumn' is greater than the
# length of the next line, we should move the
# cursor to the end of the next line and save
# 'nextColumn' in 'desiredCursorColumn'.
@editor.setCursorBufferPosition([nextRow, nextLineLength-1])
@vimState.desiredCursorColumn = nextColumn
else
# When the 'nextColumn' is a valid spot to
# move into, in the next line, simply move
# there and unset 'desiredCursorColumn'.
@editor.setCursorBufferPosition([nextRow, nextColumn])
@vimState.desiredCursorColumn = null
# Internal: Finds the next valid row that can be moved
# to. This move takes folded lines into account when
# calculating the next valid row.
#
# count - The number of folded 'buffer' rows away from
# the current row.
#
# Returns an integer row index.
nextValidRow: (count) ->
{row, column} = @editor.getCursorBufferPosition()
maxRow = @editor.getLastBufferRow()
minRow = 0
# For each count, add 1 'directionIncrement' to
# row. Folded rows count as a single row.
_.times count, =>
if @editor.isFoldedAtBufferRow(row)
while @editor.isFoldedAtBufferRow(row)
row += @directionIncrement()
else
row += @directionIncrement()
if row > maxRow
maxRow
else if row < minRow
minRow
else
row
class MoveUp extends MoveVertically
# Internal: The direction to move the cursor. Use -1
# for moving up, 1 for moving down.
#
# Returns -1
directionIncrement: ->
-1
select: (count=1) ->
unless @inVisualMode()
@editor.moveCursorToBeginningOfLine()
@editor.moveCursorDown()
@editor.selectUp()
_.times count, =>
@editor.selectUp()
true
class MoveDown extends MoveVertically
# Internal: The direction to move the cursor. Use -1
# for moving up, 1 for moving down.
#
# Returns 1
directionIncrement: ->
1
select: (count=1) ->
@editor.selectLine() unless @inVisualMode()
_.times count, =>
@editor.selectDown()
true
class MoveToPreviousWord extends Motion
execute: (count=1) ->
_.times count, =>
@editor.moveCursorToBeginningOfWord()
select: (count=1) ->
_.times count, =>
@editor.selectToBeginningOfWord()
true
class MoveToPreviousWholeWord extends Motion
execute: (count=1) ->
_.times count, =>
@editor.moveCursorToBeginningOfWord()
@editor.moveCursorToBeginningOfWord() while not @isWholeWord() and not @isBeginningOfFile()
select: (count=1) ->
_.times count, =>
@editor.selectToBeginningOfWord()
@editor.selectToBeginningOfWord() while not @isWholeWord() and not @isBeginningOfFile()
true
isWholeWord: ->
char = @editor.getCursor().getCurrentWordPrefix().slice(-1)
char is ' ' or char is '\n'
isBeginningOfFile: ->
cur = @editor.getCursorBufferPosition();
not cur.row and not cur.column
class MoveToNextWord extends Motion
execute: (count=1) ->
cursor = @editor.getCursor()
_.times count, =>
current = cursor.getBufferPosition()
next = cursor.getBeginningOfNextWordBufferPosition()
return if @isEndOfFile()
if cursor.isAtEndOfLine()
cursor.moveDown()
cursor.moveToBeginningOfLine()
cursor.skipLeadingWhitespace()
else if current.row is next.row and current.column is next.column
cursor.moveToEndOfWord()
else
cursor.moveToBeginningOfNextWord()
# Options
# excludeWhitespace - if true, whitespace shouldn't be selected
select: (count=1, {excludeWhitespace}={}) ->
cursor = @editor.getCursor()
_.times count, =>
current = cursor.getBufferPosition()
next = cursor.getBeginningOfNextWordBufferPosition()
if current.row != next.row or excludeWhitespace or current == next
@editor.selectToEndOfWord()
else
@editor.selectToBeginningOfNextWord()
true
isEndOfFile: ->
cur = @editor.getCursor().getBufferPosition()
eof = @editor.getEofBufferPosition()
cur.row is eof.row and cur.column is eof.column
class MoveToNextWholeWord extends Motion
execute: (count=1) ->
_.times count, =>
@editor.moveCursorToBeginningOfNextWord()
@editor.moveCursorToBeginningOfNextWord() while not @isWholeWord() and not @isEndOfFile()
select: (count=1, {excludeWhitespace}={}) ->
cursor = @editor.getCursor()
_.times count, =>
current = cursor.getBufferPosition()
next = cursor.getBeginningOfNextWordBufferPosition(/[^\s]/)
if current.row != next.row or excludeWhitespace
@editor.selectToEndOfWord()
else
@editor.selectToBeginningOfNextWord()
@editor.selectToBeginningOfNextWord() while not @isWholeWord() and not @isEndOfFile()
true
isWholeWord: ->
char = @editor.getCursor().getCurrentWordPrefix().slice(-1)
char is ' ' or char is '\n'
isEndOfFile: ->
last = @editor.getEofBufferPosition()
cur = @editor.getCursorBufferPosition()
last.row is cur.row and last.column is cur.column
class MoveToEndOfWord extends Motion
execute: (count=1) ->
cursor = @editor.getCursor()
_.times count, =>
cursor.setBufferPosition(@nextBufferPosition(exclusive: true))
select: (count=1) ->
cursor = @editor.getCursor()
_.times count, =>
bufferPosition = @nextBufferPosition()
screenPosition = @editor.screenPositionForBufferPosition(bufferPosition)
@editor.selectToScreenPosition(screenPosition)
true
# Private: Finds the end of the current word and stops on the last character
#
# exclusive - If true will stop on the last character of the word rather than
# the next character after the word.
#
# The reason this is implemented here is that Atom always stops on the
# character after the word which is only sometimes what vim means.
nextBufferPosition: ({exclusive}={})->
cursor = @editor.getCursor()
current = cursor.getBufferPosition()
next = cursor.getEndOfCurrentWordBufferPosition()
next.column -= 1 if exclusive
if exclusive and current.row == next.row and current.column == next.column
cursor.moveRight()
next = cursor.getEndOfCurrentWordBufferPosition()
next.column -= 1
next
class MoveToEndOfWholeWord extends Motion
execute: (count=1) ->
cursor = @editor.getCursor()
_.times count, =>
cursor.setBufferPosition(@nextBufferPosition(exclusive: true))
select: (count=1) ->
_.times count, =>
bufferPosition = @nextBufferPosition()
screenPosition = @editor.screenPositionForBufferPosition(bufferPosition)
@editor.selectToScreenPosition(screenPosition)
true
# Private: Finds the end of the current whole word and stops on the last character
#
# exclusive - If true will stop on the last character of the whole word rather
# than the next character after the word.
nextBufferPosition: ({exclusive}={})->
# get next position and reset cursor's position
{row, column} = @editor.getCursorBufferPosition()
start = new Point(row, column + 1)
scanRange = [start, @editor.getEofBufferPosition()]
position = @editor.getEofBufferPosition()
@editor.scanInBufferRange /\S+/, scanRange, ({range, stop}) =>
position = range.end
stop()
position.column -= 1 if exclusive
position
class MoveToNextParagraph extends Motion
execute: (count=1) ->
_.times count, =>
@editor.setCursorScreenPosition(@nextPosition())
select: (count=1) ->
_.times count, =>
@editor.selectToScreenPosition(@nextPosition())
true
# Private: Finds the beginning of the next paragraph
#
# If no paragraph is found, the end of the buffer is returned.
nextPosition: ->
start = @editor.getCursorBufferPosition()
scanRange = [start, @editor.getEofBufferPosition()]
{row, column} = @editor.getEofBufferPosition()
position = new Point(row, column - 1)
@editor.scanInBufferRange /^\n*$/g, scanRange, ({range, stop}) =>
if !range.start.isEqual(start)
position = range.start
stop()
@editor.screenPositionForBufferPosition(position)
class MoveToPreviousParagraph extends Motion
execute: (count=1) ->
_.times count, =>
@editor.setCursorScreenPosition(@previousPosition())
select: (count=1) ->
_.times count, =>
@editor.selectToScreenPosition(@previousPosition())
true
# Private: Finds the beginning of the previous paragraph
#
# If no paragraph is found, the beginning of the buffer is returned.
previousPosition: ->
start = @editor.getCursorBufferPosition()
{row, column} = start
scanRange = [[row-1, column], [0,0]]
position = new Point(0, 0)
@editor.backwardsScanInBufferRange /^\n*$/g, scanRange, ({range, stop}) =>
if !range.start.isEqual(new Point(0,0))
position = range.start
stop()
@editor.screenPositionForBufferPosition(position)
class MoveToLine extends Motion
isLinewise: -> true
execute: (count) ->
@setCursorPosition(count)
@editor.getCursor().skipLeadingWhitespace()
# Options
# requireEOL - if true, ensure an end of line character is always selected
select: (count=@editor.getLineCount(), {requireEOL}={}) ->
{row, column} = @editor.getCursorBufferPosition()
@editor.setSelectedBufferRange(@selectRows(row, row + (count - 1), requireEOL: requireEOL))
_.times count, ->
true
# TODO: This is extracted from TextBuffer#deleteRows. Unfortunately
# there isn't a way to call this functionality without actually
# deleting at the same time. This should be extracted out within atom
# and the removed here.
selectRows: (start, end, {requireEOL}={}) =>
startPoint = null
endPoint = null
buffer = @editor.getBuffer()
if end == buffer.getLastRow()
if start > 0 and requireEOL
startPoint = [start - 1, buffer.lineLengthForRow(start - 1)]
else
startPoint = [start, 0]
endPoint = [end, buffer.lineLengthForRow(end)]
else
startPoint = [start, 0]
endPoint = [end + 1, 0]
new Range(startPoint, endPoint)
setCursorPosition: (count) ->
@editor.setCursorBufferPosition([@getDestinationRow(count), 0])
getDestinationRow: (count) ->
if count? then count - 1 else (@editor.getLineCount() - 1)
class MoveToScreenLine extends MoveToLine
constructor: (@editor, @vimState, @editorView, @scrolloff) ->
@scrolloff = 2 # atom default
super(@editor, @vimState)
setCursorPosition: (count) ->
@editor.setCursorScreenPosition([@getDestinationRow(count), 0])
class MoveToBeginningOfLine extends Motion
execute: (count=1) ->
@editor.moveCursorToBeginningOfLine()
select: (count=1) ->
_.times count, =>
@editor.selectToBeginningOfLine()
true
class MoveToFirstCharacterOfLine extends Motion
constructor:(@editor, @vimState) ->
@cursor = @editor.getCursor()
super(@editor, @vimState)
execute: () ->
@editor.setCursorBufferPosition([@cursor.getBufferRow(), @getDestinationColumn()])
select: (count=1) ->
if @getDestinationColumn() isnt @cursor.getBufferColumn()
_.times count, =>
@editor.selectToFirstCharacterOfLine()
true
getDestinationColumn: ->
@editor.lineForBufferRow(@cursor.getBufferRow()).search(/\S/)
class MoveToLastCharacterOfLine extends Motion
execute: (count=1) ->
# After moving to the end of the line, vertical motions
# should stay at the last column.
@vimState.desiredCursorColumn = Infinity
_.times count, =>
@editor.moveCursorToEndOfLine()
@editor.moveCursorLeft() unless @editor.getCursor().getBufferColumn() is 0
select: (count=1) ->
_.times count, =>
@editor.selectToEndOfLine()
true
class MoveToFirstCharacterOfLineUp extends Motion
execute: (count=1) ->
(new MoveUp(@editor, @vimState)).execute(count)
(new MoveToFirstCharacterOfLine(@editor, @vimState)).execute()
select: (count=1) ->
(new MoveUp(@editor, @vimState)).select(count)
class MoveToFirstCharacterOfLineDown extends Motion
execute: (count=1) ->
(new MoveDown(@editor, @vimState)).execute(count)
(new MoveToFirstCharacterOfLine(@editor, @vimState)).execute()
select: (count=1) ->
(new MoveDown(@editor, @vimState)).select(count)
class MoveToStartOfFile extends MoveToLine
isLinewise: -> @vimState.mode == 'visual' and @vimState.submode == 'linewise'
getDestinationRow: (count=1) ->
count - 1
getDestinationColumn: (row) ->
if @isLinewise() then 0 else @editor.lineForBufferRow(row).search(/\S/)
getStartingColumn: (column) ->
if @isLinewise() then column else column + 1
select: (count=1) ->
{row, column} = @editor.getCursorBufferPosition()
startingCol = @getStartingColumn(column)
destinationRow = @getDestinationRow(count)
destinationCol = @getDestinationColumn(destinationRow)
bufferRange = new Range([row, startingCol], [destinationRow, destinationCol])
@editor.setSelectedBufferRange(bufferRange, reversed: true)
class MoveToTopOfScreen extends MoveToScreenLine
getDestinationRow: (count=0) ->
firstScreenRow = @editorView.getFirstVisibleScreenRow()
if firstScreenRow > 0
offset = Math.max(count - 1, @scrolloff)
else
offset = if count > 0 then count - 1 else count
firstScreenRow + offset
class MoveToBottomOfScreen extends MoveToScreenLine
getDestinationRow: (count=0) ->
lastScreenRow = @editorView.getLastVisibleScreenRow()
lastRow = @editor.getBuffer().getLastRow()
if lastScreenRow != lastRow
offset = Math.max(count - 1, @scrolloff)
else
offset = if count > 0 then count - 1 else count
lastScreenRow - offset
class MoveToMiddleOfScreen extends MoveToScreenLine
getDestinationRow: (count) ->
firstScreenRow = @editorView.getFirstVisibleScreenRow()
lastScreenRow = @editorView.getLastVisibleScreenRow()
height = lastScreenRow - firstScreenRow
Math.floor(firstScreenRow + (height / 2))
module.exports = {
Motion, MotionWithInput, CurrentSelection, MoveLeft, MoveRight, MoveUp, MoveDown,
MoveToPreviousWord, MoveToPreviousWholeWord, MoveToNextWord, MoveToNextWholeWord,
MoveToEndOfWord, MoveToNextParagraph, MoveToPreviousParagraph, MoveToLine, MoveToBeginningOfLine,
MoveToFirstCharacterOfLineUp, MoveToFirstCharacterOfLineDown,
MoveToFirstCharacterOfLine, MoveToLastCharacterOfLine, MoveToStartOfFile, MoveToTopOfScreen,
MoveToBottomOfScreen, MoveToMiddleOfScreen, MoveToEndOfWholeWord, MotionError
}