_ = 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 }