{Point, Range} = require 'text-buffer' {Model} = require 'theorist' _ = require 'underscore-plus' {deprecate} = require 'grim' # Extended: The `Cursor` class represents the little blinking line identifying # where text can be inserted. # # Cursors belong to {Editor}s and have some metadata attached in the form # of a {Marker}. # # ## Events # # ### moved # # Extended: Emit when a cursor has been moved. If there are multiple cursors, # it will be emit for each cursor. # # * `event` {Object} # * `oldBufferPosition` {Point} # * `oldScreenPosition` {Point} # * `newBufferPosition` {Point} # * `newScreenPosition` {Point} # * `textChanged` {Boolean} # # ### destroyed # # Extended: Emit when the cursor is destroyed # # ### visibility-changed # # Extended: Emit when the Cursor is hidden or shown # # * `visible` {Boolean} true when cursor is visible # module.exports = class Cursor extends Model screenPosition: null bufferPosition: null goalColumn: null visible: true needsAutoscroll: null # Instantiated by an {Editor} constructor: ({@editor, @marker, id}) -> @assignId(id) @updateVisibility() @marker.on 'changed', (e) => @updateVisibility() {oldHeadScreenPosition, newHeadScreenPosition} = e {oldHeadBufferPosition, newHeadBufferPosition} = e {textChanged} = e return if oldHeadScreenPosition.isEqual(newHeadScreenPosition) # Supports old editor view @needsAutoscroll ?= @isLastCursor() and !textChanged @autoscroll() if @editor.manageScrollPosition and @isLastCursor() and textChanged @goalColumn = null movedEvent = oldBufferPosition: oldHeadBufferPosition oldScreenPosition: oldHeadScreenPosition newBufferPosition: newHeadBufferPosition newScreenPosition: newHeadScreenPosition textChanged: textChanged @emit 'moved', movedEvent @editor.cursorMoved(movedEvent) @marker.on 'destroyed', => @destroyed = true @editor.removeCursor(this) @emit 'destroyed' @needsAutoscroll = true destroy: -> @marker.destroy() changePosition: (options, fn) -> @clearSelection() @needsAutoscroll = options.autoscroll ? @isLastCursor() fn() if @needsAutoscroll @emit 'autoscrolled' # Support legacy editor @autoscroll() if @needsAutoscroll and @editor.manageScrollPosition # Support react editor view getPixelRect: -> @editor.pixelRectForScreenRange(@getScreenRange()) # Public: Moves a cursor to a given screen position. # # * `screenPosition` {Array} of two numbers: the screen row, and the screen column. # * `options` (optional) {Object} with the following keys: # * `autoscroll` A Boolean which, if `true`, scrolls the {Editor} to wherever # the cursor moves to. setScreenPosition: (screenPosition, options={}) -> @changePosition options, => @marker.setHeadScreenPosition(screenPosition, options) # Public: Returns the screen position of the cursor as an Array. getScreenPosition: -> @marker.getHeadScreenPosition() getScreenRange: -> {row, column} = @getScreenPosition() new Range(new Point(row, column), new Point(row, column + 1)) # Public: Moves a cursor to a given buffer position. # # * `bufferPosition` {Array} of two numbers: the buffer row, and the buffer column. # * `options` (optional) {Object} with the following keys: # * `autoscroll` A Boolean which, if `true`, scrolls the {Editor} to wherever # the cursor moves to. setBufferPosition: (bufferPosition, options={}) -> @changePosition options, => @marker.setHeadBufferPosition(bufferPosition, options) # Public: Returns the current buffer position as an Array. getBufferPosition: -> @marker.getHeadBufferPosition() autoscroll: (options) -> @editor.scrollToScreenRange(@getScreenRange(), options) # Public: If the marker range is empty, the cursor is marked as being visible. updateVisibility: -> @setVisible(@marker.getBufferRange().isEmpty()) # Public: Sets whether the cursor is visible. setVisible: (visible) -> if @visible != visible @visible = visible @needsAutoscroll ?= true if @visible and @isLastCursor() @emit 'visibility-changed', @visible # Public: Returns the visibility of the cursor. isVisible: -> @visible # Public: Get the RegExp used by the cursor to determine what a "word" is. # # * `options` (optional) {Object} with the following keys: # * `includeNonWordCharacters` A {Boolean} indicating whether to include # non-word characters in the regex. (default: true) # # Returns a {RegExp}. wordRegExp: ({includeNonWordCharacters}={}) -> includeNonWordCharacters ?= true nonWordCharacters = atom.config.get('editor.nonWordCharacters') segments = ["^[\t ]*$"] segments.push("[^\\s#{_.escapeRegExp(nonWordCharacters)}]+") if includeNonWordCharacters segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+") new RegExp(segments.join("|"), "g") # Public: Identifies if this cursor is the last in the {Editor}. # # "Last" is defined as the most recently added cursor. # # Returns a {Boolean}. isLastCursor: -> this == @editor.getLastCursor() # Public: Identifies if the cursor is surrounded by whitespace. # # "Surrounded" here means that the character directly before and after the # cursor are both whitespace. # # Returns a {Boolean}. isSurroundedByWhitespace: -> {row, column} = @getBufferPosition() range = [[row, column - 1], [row, column + 1]] /^\s+$/.test @editor.getTextInBufferRange(range) # Public: Returns whether the cursor is currently between a word and non-word # character. The non-word characters are defined by the # `editor.nonWordCharacters` config value. # # This method returns false if the character before or after the cursor is # whitespace. # # Returns a Boolean. isBetweenWordAndNonWord: -> return false if @isAtBeginningOfLine() or @isAtEndOfLine() {row, column} = @getBufferPosition() range = [[row, column - 1], [row, column + 1]] [before, after] = @editor.getTextInBufferRange(range) return false if /\s/.test(before) or /\s/.test(after) nonWordCharacters = atom.config.get('editor.nonWordCharacters').split('') _.contains(nonWordCharacters, before) isnt _.contains(nonWordCharacters, after) # Public: Returns whether this cursor is between a word's start and end. isInsideWord: -> {row, column} = @getBufferPosition() range = [[row, column], [row, Infinity]] @editor.getTextInBufferRange(range).search(@wordRegExp()) == 0 # Public: Prevents this cursor from causing scrolling. clearAutoscroll: -> @needsAutoscroll = null # Public: Deselects the current selection. clearSelection: -> @selection?.clear() # Public: Returns the cursor's current screen row. getScreenRow: -> @getScreenPosition().row # Public: Returns the cursor's current screen column. getScreenColumn: -> @getScreenPosition().column # Public: Retrieves the cursor's current buffer row. getBufferRow: -> @getBufferPosition().row # Public: Returns the cursor's current buffer column. getBufferColumn: -> @getBufferPosition().column # Public: Returns the cursor's current buffer row of text excluding its line # ending. getCurrentBufferLine: -> @editor.lineForBufferRow(@getBufferRow()) # Public: Moves the cursor up one screen row. moveUp: (rowCount=1, {moveToEndOfSelection}={}) -> range = @marker.getScreenRange() if moveToEndOfSelection and not range.isEmpty() { row, column } = range.start else { row, column } = @getScreenPosition() column = @goalColumn if @goalColumn? @setScreenPosition({row: row - rowCount, column: column}) @goalColumn = column moveCursorUp: (rowCount=1, options) -> deprecate("Use Cursor::moveUp() instead. `Cursor` has been removed from these methods.") @moveUp(rowCount, options) # Public: Moves the cursor down one screen row. moveDown: (rowCount = 1, {moveToEndOfSelection}={}) -> range = @marker.getScreenRange() if moveToEndOfSelection and not range.isEmpty() { row, column } = range.end else { row, column } = @getScreenPosition() column = @goalColumn if @goalColumn? @setScreenPosition({row: row + rowCount, column: column}) @goalColumn = column moveCursorUp: (rowCount=1, options) -> deprecate("Use Cursor::moveUp() instead. `Cursor` has been removed from these methods.") @moveUp(rowCount, options) # Public: Moves the cursor left one screen column. # # * `options` (optional) {Object} with the following keys: # * `moveToEndOfSelection` if true, move to the left of the selection if a # selection exists. moveLeft: ({moveToEndOfSelection}={}) -> range = @marker.getScreenRange() if moveToEndOfSelection and not range.isEmpty() @setScreenPosition(range.start) else {row, column} = @getScreenPosition() [row, column] = if column > 0 then [row, column - 1] else [row - 1, Infinity] @setScreenPosition({row, column}) moveCursorUp: (rowCount=1, options) -> deprecate("Use Cursor::moveUp() instead. `Cursor` has been removed from these methods.") @moveUp(rowCount, options) # Public: Moves the cursor right one screen column. # # * `options` (optional) {Object} with the following keys: # * `moveToEndOfSelection` if true, move to the right of the selection if a # selection exists. moveRight: ({moveToEndOfSelection}={}) -> range = @marker.getScreenRange() if moveToEndOfSelection and not range.isEmpty() @setScreenPosition(range.end) else { row, column } = @getScreenPosition() @setScreenPosition([row, column + 1], skipAtomicTokens: true, wrapBeyondNewlines: true, wrapAtSoftNewlines: true) moveCursorRight: (rowCount=1, options) -> deprecate("Use Cursor::moveRight() instead. `Cursor` has been removed from these methods.") @moveRight(rowCount, options) # Public: Moves the cursor to the top of the buffer. moveToTop: -> @setBufferPosition([0,0]) moveCursorToTop: (rowCount=1, options) -> deprecate("Use Cursor::moveToTop() instead. `Cursor` has been removed from these methods.") @moveToTop(rowCount, options) # Public: Moves the cursor to the bottom of the buffer. moveToBottom: -> @setBufferPosition(@editor.getEofBufferPosition()) moveCursorToBottom: (rowCount=1, options) -> deprecate("Use Cursor::moveToBottom() instead. `Cursor` has been removed from these methods.") @moveToBottom(rowCount, options) # Public: Moves the cursor to the beginning of the line. moveToBeginningOfScreenLine: -> @setScreenPosition([@getScreenRow(), 0]) moveCursorToBeginningOfScreenLine: (rowCount=1, options) -> deprecate("Use Cursor::moveToBeginningOfScreenLine() instead. `Cursor` has been removed from these methods.") @moveToBeginningOfScreenLine(rowCount, options) # Public: Moves the cursor to the beginning of the buffer line. moveToBeginningOfLine: -> @setBufferPosition([@getBufferRow(), 0]) moveCursorToBeginningOfLine: (rowCount=1, options) -> deprecate("Use Cursor::moveToBeginningOfLine() instead. `Cursor` has been removed from these methods.") @moveToBeginningOfLine(rowCount, options) # Public: Moves the cursor to the beginning of the first character in the # line. moveToFirstCharacterOfLine: -> screenRow = @getScreenRow() lineBufferRange = @editor.bufferRangeForScreenRange([[screenRow, 0], [screenRow, Infinity]]) firstCharacterColumn = null @editor.scanInBufferRange /\S/, lineBufferRange, ({range, stop}) -> firstCharacterColumn = range.start.column stop() if firstCharacterColumn? and firstCharacterColumn isnt @getBufferColumn() targetBufferColumn = firstCharacterColumn else targetBufferColumn = lineBufferRange.start.column @setBufferPosition([lineBufferRange.start.row, targetBufferColumn]) moveCursorToFirstCharacterOfLine: (rowCount=1, options) -> deprecate("Use Cursor::moveToFirstCharacterOfLine() instead. `Cursor` has been removed from these methods.") @moveToFirstCharacterOfLine(rowCount, options) # Public: Moves the cursor to the end of the line. moveToEndOfScreenLine: -> @setScreenPosition([@getScreenRow(), Infinity]) moveCursorToEndOfScreenLine: (rowCount=1, options) -> deprecate("Use Cursor::moveToEndOfScreenLine() instead. `Cursor` has been removed from these methods.") @moveToEndOfScreenLine(rowCount, options) # Public: Moves the cursor to the end of the buffer line. moveToEndOfLine: -> @setBufferPosition([@getBufferRow(), Infinity]) moveCursorToEndOfLine: (rowCount=1, options) -> deprecate("Use Cursor::moveToEndOfLine() instead. `Cursor` has been removed from these methods.") @moveToEndOfLine(rowCount, options) # Public: Moves the cursor to the beginning of the word. moveToBeginningOfWord: -> @setBufferPosition(@getBeginningOfCurrentWordBufferPosition()) moveCursorToBeginningOfWord: (rowCount=1, options) -> deprecate("Use Cursor::moveToBeginningOfWord() instead. `Cursor` has been removed from these methods.") @moveToBeginningOfWord(rowCount, options) # Public: Moves the cursor to the end of the word. moveToEndOfWord: -> if position = @getEndOfCurrentWordBufferPosition() @setBufferPosition(position) moveCursorToEndOfWord: (rowCount=1, options) -> deprecate("Use Cursor::moveToEndOfWord() instead. `Cursor` has been removed from these methods.") @moveToEndOfWord(rowCount, options) # Public: Moves the cursor to the beginning of the next word. moveToBeginningOfNextWord: -> if position = @getBeginningOfNextWordBufferPosition() @setBufferPosition(position) moveCursorToBeginningOfNextWord: (rowCount=1, options) -> deprecate("Use Cursor::moveToBeginningOfNextWord() instead. `Cursor` has been removed from these methods.") @moveToBeginningOfNextWord(rowCount, options) # Public: Moves the cursor to the previous word boundary. moveToPreviousWordBoundary: -> if position = @getPreviousWordBoundaryBufferPosition() @setBufferPosition(position) moveCursorToPreviousWordBoundary: (rowCount=1, options) -> deprecate("Use Cursor::moveToPreviousWordBoundary() instead. `Cursor` has been removed from these methods.") @moveToPreviousWordBoundary(rowCount, options) # Public: Moves the cursor to the next word boundary. moveToNextWordBoundary: -> if position = @getMoveNextWordBoundaryBufferPosition() @setBufferPosition(position) moveCursorToNextWordBoundary: (rowCount=1, options) -> deprecate("Use Cursor::moveToNextWordBoundary() instead. `Cursor` has been removed from these methods.") @moveToNextWordBoundary(rowCount, options) # Public: Moves the cursor to the beginning of the buffer line, skipping all # whitespace. skipLeadingWhitespace: -> position = @getBufferPosition() scanRange = @getCurrentLineBufferRange() endOfLeadingWhitespace = null @editor.scanInBufferRange /^[ \t]*/, scanRange, ({range}) -> endOfLeadingWhitespace = range.end @setBufferPosition(endOfLeadingWhitespace) if endOfLeadingWhitespace.isGreaterThan(position) # Public: Retrieves the buffer position of where the current word starts. # # * `options` (optional) An {Object} with the following keys: # * `wordRegex` A {RegExp} indicating what constitutes a "word" # (default: {::wordRegExp}). # * `includeNonWordCharacters` A {Boolean} indicating whether to include # non-word characters in the default word regex. # Has no effect if wordRegex is set. # * `allowPrevious` A {Boolean} indicating whether the beginning of the # previous word can be returned. # # Returns a {Range}. getBeginningOfCurrentWordBufferPosition: (options = {}) -> allowPrevious = options.allowPrevious ? true currentBufferPosition = @getBufferPosition() previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row) ? 0 scanRange = [[previousNonBlankRow, 0], currentBufferPosition] beginningOfWordPosition = null @editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, stop}) -> if range.end.isGreaterThanOrEqual(currentBufferPosition) or allowPrevious beginningOfWordPosition = range.start if not beginningOfWordPosition?.isEqual(currentBufferPosition) stop() if beginningOfWordPosition? beginningOfWordPosition else if allowPrevious new Point(0, 0) else currentBufferPosition # Public: Retrieves buffer position of previous word boundary. It might be on # the current word, or the previous word. getPreviousWordBoundaryBufferPosition: (options = {}) -> currentBufferPosition = @getBufferPosition() previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row) scanRange = [[previousNonBlankRow, 0], currentBufferPosition] beginningOfWordPosition = null @editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) -> if range.start.row < currentBufferPosition.row and currentBufferPosition.column > 0 # force it to stop at the beginning of each line beginningOfWordPosition = new Point(currentBufferPosition.row, 0) else if range.end.isLessThan(currentBufferPosition) beginningOfWordPosition = range.end else beginningOfWordPosition = range.start if not beginningOfWordPosition?.isEqual(currentBufferPosition) stop() beginningOfWordPosition or currentBufferPosition # Public: Retrieves buffer position of the next word boundary. It might be on # the current word, or the previous word. getMoveNextWordBoundaryBufferPosition: (options = {}) -> currentBufferPosition = @getBufferPosition() scanRange = [currentBufferPosition, @editor.getEofBufferPosition()] endOfWordPosition = null @editor.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) -> if range.start.row > currentBufferPosition.row # force it to stop at the beginning of each line endOfWordPosition = new Point(range.start.row, 0) else if range.start.isGreaterThan(currentBufferPosition) endOfWordPosition = range.start else endOfWordPosition = range.end if not endOfWordPosition?.isEqual(currentBufferPosition) stop() endOfWordPosition or currentBufferPosition # Public: Retrieves the buffer position of where the current word ends. # # * `options` (optional) {Object} with the following keys: # * `wordRegex` A {RegExp} indicating what constitutes a "word" # (default: {::wordRegExp}) # * `includeNonWordCharacters` A Boolean indicating whether to include # non-word characters in the default word regex. Has no effect if # wordRegex is set. # # Returns a {Range}. getEndOfCurrentWordBufferPosition: (options = {}) -> allowNext = options.allowNext ? true currentBufferPosition = @getBufferPosition() scanRange = [currentBufferPosition, @editor.getEofBufferPosition()] endOfWordPosition = null @editor.scanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, stop}) -> if range.start.isLessThanOrEqual(currentBufferPosition) or allowNext endOfWordPosition = range.end if not endOfWordPosition?.isEqual(currentBufferPosition) stop() endOfWordPosition ? currentBufferPosition # Public: Retrieves the buffer position of where the next word starts. # # * `options` (optional) {Object} # * `wordRegex` A {RegExp} indicating what constitutes a "word" # (default: {::wordRegExp}). # # Returns a {Range} getBeginningOfNextWordBufferPosition: (options = {}) -> currentBufferPosition = @getBufferPosition() start = if @isInsideWord() then @getEndOfCurrentWordBufferPosition() else currentBufferPosition scanRange = [start, @editor.getEofBufferPosition()] beginningOfNextWordPosition = null @editor.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) -> beginningOfNextWordPosition = range.start stop() beginningOfNextWordPosition or currentBufferPosition # Public: Returns the buffer Range occupied by the word located under the cursor. # # * `options` (optional) {Object} # * `wordRegex` A {RegExp} indicating what constitutes a "word" # (default: {::wordRegExp}). getCurrentWordBufferRange: (options={}) -> startOptions = _.extend(_.clone(options), allowPrevious: false) endOptions = _.extend(_.clone(options), allowNext: false) new Range(@getBeginningOfCurrentWordBufferPosition(startOptions), @getEndOfCurrentWordBufferPosition(endOptions)) # Public: Returns the buffer Range for the current line. # # * `options` (optional) {Object} # * `includeNewline` A {Boolean} which controls whether the Range should # include the newline. getCurrentLineBufferRange: (options) -> @editor.bufferRangeForBufferRow(@getBufferRow(), options) # Public: Moves the cursor to the beginning of the next paragraph moveToBeginningOfNextParagraph: -> if position = @getBeginningOfNextParagraphBufferPosition() @setBufferPosition(position) # Public: Moves the cursor to the beginning of the previous paragraph moveToBeginningOfPreviousParagraph: -> if position = @getBeginningOfPreviousParagraphBufferPosition() @setBufferPosition(position) getBeginningOfNextParagraphBufferPosition: (editor) -> start = @getBufferPosition() eof = @editor.getEofBufferPosition() scanRange = [start, eof] {row, column} = eof 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) getBeginningOfPreviousParagraphBufferPosition: (editor) -> start = @editor.getCursorBufferPosition() {row, column} = start scanRange = [[row-1, column], [0,0]] position = new Point(0, 0) zero = new Point(0,0) @editor.backwardsScanInBufferRange /^\n*$/g, scanRange, ({range, stop}) -> if !range.start.isEqual(zero) position = range.start stop() @editor.screenPositionForBufferPosition(position) # Public: Retrieves the range for the current paragraph. # # A paragraph is defined as a block of text surrounded by empty lines. # # Returns a {Range}. getCurrentParagraphBufferRange: -> @editor.languageMode.rowRangeForParagraphAtBufferRow(@getBufferRow()) # Public: Returns the characters preceding the cursor in the current word. getCurrentWordPrefix: -> @editor.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()]) # Public: Returns whether the cursor is at the start of a line. isAtBeginningOfLine: -> @getBufferPosition().column == 0 # Public: Returns the indentation level of the current line. getIndentLevel: -> if @editor.getSoftTabs() @getBufferColumn() / @editor.getTabLength() else @getBufferColumn() # Public: Returns whether the cursor is on the line return character. isAtEndOfLine: -> @getBufferPosition().isEqual(@getCurrentLineBufferRange().end) # Public: Retrieves the grammar's token scopes for the line. # # Returns an {Array} of {String}s. getScopes: -> @editor.scopesForBufferPosition(@getBufferPosition()) # Public: Returns true if this cursor has no non-whitespace characters before # its current position. hasPrecedingCharactersOnLine: -> bufferPosition = @getBufferPosition() line = @editor.lineForBufferRow(bufferPosition.row) firstCharacterColumn = line.search(/\S/) if firstCharacterColumn is -1 false else bufferPosition.column > firstCharacterColumn # Public: Compare this cursor's buffer position to another cursor's buffer position. # # See {Point::compare} for more details. # # * `otherCursor`{Cursor} to compare against compare: (otherCursor) -> @getBufferPosition().compare(otherCursor.getBufferPosition())