{Point, Range} = require 'text-buffer' {Emitter} = require 'emissary' _ = require 'underscore-plus' # Public: 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}. module.exports = class Cursor Emitter.includeInto(this) screenPosition: null bufferPosition: null goalColumn: null visible: true needsAutoscroll: null # Instantiated by an {Editor} constructor: ({@editor, @marker}) -> @updateVisibility() @marker.on 'changed', (e) => @updateVisibility() {oldHeadScreenPosition, newHeadScreenPosition} = e {oldHeadBufferPosition, newHeadBufferPosition} = e {textChanged} = e return if oldHeadScreenPosition.isEqual(newHeadScreenPosition) @needsAutoscroll ?= @isLastCursor() and !textChanged @goalColumn = null movedEvent = oldBufferPosition: oldHeadBufferPosition oldScreenPosition: oldHeadScreenPosition newBufferPosition: newHeadBufferPosition newScreenPosition: newHeadScreenPosition textChanged: textChanged @emit 'moved', movedEvent @editor.emit 'cursor-moved', movedEvent @marker.on 'destroyed', => @destroyed = true @editor.removeCursor(this) @emit 'destroyed' @needsAutoscroll = true destroy: -> @marker.destroy() changePosition: (options, fn) -> @clearSelection() @needsAutoscroll = options.autoscroll ? @isLastCursor() unless fn() @emit 'autoscrolled' if @needsAutoscroll # Public: Moves a cursor to a given screen position. # # screenPosition - An {Array} of two numbers: the screen row, and the screen # column. # options - An {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() # Public: Moves a cursor to a given buffer position. # # bufferPosition - An {Array} of two numbers: the buffer row, and the buffer # column. # options - An {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() # 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: An {Object} with the following keys: # :includeNonWordCharacters - A {Boolean} indicating whether to include # non-word characters in the regex. # # 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.getCursor() # Public: Identifies if the cursor is surrounded by whitespace. # # "Surrounded" here means that all characters before and after the cursor is # whitespace. # # Returns a {Boolean}. isSurroundedByWhitespace: -> {row, column} = @getBufferPosition() range = [[row, Math.min(0, column - 1)], [row, Math.max(0, 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 # 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 # Public: Moves the cursor left one screen column. # # options - An {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}) # Public: Moves the cursor right one screen column. # # options - An {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) # Public: Moves the cursor to the top of the buffer. moveToTop: -> @setBufferPosition([0,0]) # Public: Moves the cursor to the bottom of the buffer. moveToBottom: -> @setBufferPosition(@editor.getEofBufferPosition()) # Public: Moves the cursor to the beginning of the line. moveToBeginningOfScreenLine: -> @setScreenPosition([@getScreenRow(), 0]) # Public: Moves the cursor to the beginning of the buffer line. moveToBeginningOfLine: -> @setBufferPosition([@getBufferRow(), 0]) # Public: Moves the cursor to the beginning of the first character in the # line. moveToFirstCharacterOfLine: -> {row, column} = @getScreenPosition() screenline = @editor.lineForScreenRow(row) goalColumn = screenline.text.search(/\S/) goalColumn = 0 if goalColumn == column or goalColumn == -1 @setScreenPosition([row, goalColumn]) # 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: Moves the cursor to the end of the line. moveToEndOfScreenLine: -> @setScreenPosition([@getScreenRow(), Infinity]) # Public: Moves the cursor to the end of the buffer line. moveToEndOfLine: -> @setBufferPosition([@getBufferRow(), Infinity]) # Public: Moves the cursor to the beginning of the word. moveToBeginningOfWord: -> @setBufferPosition(@getBeginningOfCurrentWordBufferPosition()) # Public: Moves the cursor to the end of the word. moveToEndOfWord: -> if position = @getEndOfCurrentWordBufferPosition() @setBufferPosition(position) # Public: Moves the cursor to the beginning of the next word. moveToBeginningOfNextWord: -> if position = @getBeginningOfNextWordBufferPosition() @setBufferPosition(position) # Public: Moves the cursor to the previous word boundary. moveToPreviousWordBoundary: -> if position = @getPreviousWordBoundaryBufferPosition() @setBufferPosition(position) # Public: Moves the cursor to the next word boundary. moveToNextWordBoundary: -> if position = @getMoveNextWordBoundaryBufferPosition() @setBufferPosition(position) # Public: Retrieves the buffer position of where the current word starts. # # options - 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 - 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. # # 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 - # :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 - # :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 - # :includeNewline: - A {Boolean} which controls whether the Range should # include the newline. getCurrentLineBufferRange: (options) -> @editor.bufferRangeForBufferRow(@getBufferRow(), options) # 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 preceeding 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