mirror of
https://github.com/atom/atom.git
synced 2026-01-24 14:28:14 -05:00
When clipping a screen position, callers used to have to pick between clipping to the left edge or the right edge when the position was in the middle of an atomic token. This change allows them to choose the closest edge, and makes this the default. This makes selecting hard tabs (or any other atomic tokens) work in a similar manner as in other text editors; that is, when clicking near the middle of a tab, the insertion point will move to the closest edge rather than the left edge.
691 lines
24 KiB
CoffeeScript
691 lines
24 KiB
CoffeeScript
{Point, Range} = require 'text-buffer'
|
|
{Model} = require 'theorist'
|
|
{Emitter} = require 'event-kit'
|
|
_ = require 'underscore-plus'
|
|
Grim = require 'grim'
|
|
|
|
# Extended: The `Cursor` class represents the little blinking line identifying
|
|
# where text can be inserted.
|
|
#
|
|
# Cursors belong to {TextEditor}s and have some metadata attached in the form
|
|
# of a {Marker}.
|
|
module.exports =
|
|
class Cursor extends Model
|
|
screenPosition: null
|
|
bufferPosition: null
|
|
goalColumn: null
|
|
visible: true
|
|
|
|
# Instantiated by a {TextEditor}
|
|
constructor: ({@editor, @marker, id}) ->
|
|
@emitter = new Emitter
|
|
|
|
@assignId(id)
|
|
@updateVisibility()
|
|
@marker.onDidChange (e) =>
|
|
@updateVisibility()
|
|
{oldHeadScreenPosition, newHeadScreenPosition} = e
|
|
{oldHeadBufferPosition, newHeadBufferPosition} = e
|
|
{textChanged} = e
|
|
return if oldHeadScreenPosition.isEqual(newHeadScreenPosition)
|
|
|
|
@goalColumn = null
|
|
|
|
movedEvent =
|
|
oldBufferPosition: oldHeadBufferPosition
|
|
oldScreenPosition: oldHeadScreenPosition
|
|
newBufferPosition: newHeadBufferPosition
|
|
newScreenPosition: newHeadScreenPosition
|
|
textChanged: textChanged
|
|
cursor: this
|
|
|
|
@emit 'moved', movedEvent
|
|
@emitter.emit 'did-change-position', movedEvent
|
|
@editor.cursorMoved(movedEvent)
|
|
@marker.onDidDestroy =>
|
|
@destroyed = true
|
|
@editor.removeCursor(this)
|
|
@emit 'destroyed'
|
|
@emitter.emit 'did-destroy'
|
|
@emitter.dispose()
|
|
|
|
destroy: ->
|
|
@marker.destroy()
|
|
|
|
###
|
|
Section: Event Subscription
|
|
###
|
|
|
|
# Public: Calls your `callback` when the cursor has been moved.
|
|
#
|
|
# * `callback` {Function}
|
|
# * `event` {Object}
|
|
# * `oldBufferPosition` {Point}
|
|
# * `oldScreenPosition` {Point}
|
|
# * `newBufferPosition` {Point}
|
|
# * `newScreenPosition` {Point}
|
|
# * `textChanged` {Boolean}
|
|
# * `Cursor` {Cursor} that triggered the event
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangePosition: (callback) ->
|
|
@emitter.on 'did-change-position', callback
|
|
|
|
# Public: Calls your `callback` when the cursor is destroyed
|
|
#
|
|
# * `callback` {Function}
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidDestroy: (callback) ->
|
|
@emitter.on 'did-destroy', callback
|
|
|
|
# Public: Calls your `callback` when the cursor's visibility has changed
|
|
#
|
|
# * `callback` {Function}
|
|
# * `visibility` {Boolean}
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangeVisibility: (callback) ->
|
|
@emitter.on 'did-change-visibility', callback
|
|
|
|
on: (eventName) ->
|
|
switch eventName
|
|
when 'moved'
|
|
Grim.deprecate("Use Cursor::onDidChangePosition instead")
|
|
when 'destroyed'
|
|
Grim.deprecate("Use Cursor::onDidDestroy instead")
|
|
when 'destroyed'
|
|
Grim.deprecate("Use Cursor::onDidDestroy instead")
|
|
else
|
|
Grim.deprecate("::on is no longer supported. Use the event subscription methods instead")
|
|
super
|
|
|
|
###
|
|
Section: Managing Cursor Position
|
|
###
|
|
|
|
# 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 {TextEditor} 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` {Array} of two numbers: the buffer row, and the buffer column.
|
|
# * `options` (optional) {Object} with the following keys:
|
|
# * `autoscroll` {Boolean} indicating whether to autoscroll to the new
|
|
# position. Defaults to `true` if this is the most recently added cursor,
|
|
# `false` otherwise.
|
|
setBufferPosition: (bufferPosition, options={}) ->
|
|
@changePosition options, =>
|
|
@marker.setHeadBufferPosition(bufferPosition, options)
|
|
|
|
# Public: Returns the current buffer position as an Array.
|
|
getBufferPosition: ->
|
|
@marker.getHeadBufferPosition()
|
|
|
|
# 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.lineTextForBufferRow(@getBufferRow())
|
|
|
|
# Public: Returns whether the cursor is at the start of a line.
|
|
isAtBeginningOfLine: ->
|
|
@getBufferPosition().column == 0
|
|
|
|
# Public: Returns whether the cursor is on the line return character.
|
|
isAtEndOfLine: ->
|
|
@getBufferPosition().isEqual(@getCurrentLineBufferRange().end)
|
|
|
|
###
|
|
Section: Cursor Position Details
|
|
###
|
|
|
|
# Public: Returns the underlying {Marker} for the cursor.
|
|
# Useful with overlay {Decoration}s.
|
|
getMarker: -> @marker
|
|
|
|
# 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', scope: @getScopeDescriptor()).split('')
|
|
_.contains(nonWordCharacters, before) isnt _.contains(nonWordCharacters, after)
|
|
|
|
# Public: Returns whether this cursor is between a word's start and end.
|
|
#
|
|
# * `options` (optional) {Object}
|
|
# * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
|
# (default: {::wordRegExp}).
|
|
#
|
|
# Returns a {Boolean}
|
|
isInsideWord: (options) ->
|
|
{row, column} = @getBufferPosition()
|
|
range = [[row, column], [row, Infinity]]
|
|
@editor.getTextInBufferRange(range).search(options?.wordRegex ? @wordRegExp()) == 0
|
|
|
|
# Public: Returns the indentation level of the current line.
|
|
getIndentLevel: ->
|
|
if @editor.getSoftTabs()
|
|
@getBufferColumn() / @editor.getTabLength()
|
|
else
|
|
@getBufferColumn()
|
|
|
|
# Public: Retrieves the scope descriptor for the cursor's current position.
|
|
#
|
|
# Returns a {ScopeDescriptor}
|
|
getScopeDescriptor: ->
|
|
@editor.scopeDescriptorForBufferPosition(@getBufferPosition())
|
|
getScopes: ->
|
|
Grim.deprecate 'Use Cursor::getScopeDescriptor() instead'
|
|
@getScopeDescriptor().getScopesArray()
|
|
|
|
# Public: Returns true if this cursor has no non-whitespace characters before
|
|
# its current position.
|
|
hasPrecedingCharactersOnLine: ->
|
|
bufferPosition = @getBufferPosition()
|
|
line = @editor.lineTextForBufferRow(bufferPosition.row)
|
|
firstCharacterColumn = line.search(/\S/)
|
|
|
|
if firstCharacterColumn is -1
|
|
false
|
|
else
|
|
bufferPosition.column > firstCharacterColumn
|
|
|
|
# Public: Identifies if this cursor is the last in the {TextEditor}.
|
|
#
|
|
# "Last" is defined as the most recently added cursor.
|
|
#
|
|
# Returns a {Boolean}.
|
|
isLastCursor: ->
|
|
this == @editor.getLastCursor()
|
|
|
|
###
|
|
Section: Moving the Cursor
|
|
###
|
|
|
|
# Public: Moves the cursor up one screen row.
|
|
#
|
|
# * `rowCount` (optional) {Number} number of rows to move (default: 1)
|
|
# * `options` (optional) {Object} with the following keys:
|
|
# * `moveToEndOfSelection` if true, move to the left of the selection if a
|
|
# selection exists.
|
|
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}, skipSoftWrapIndentation: true)
|
|
@goalColumn = column
|
|
|
|
# Public: Moves the cursor down one screen row.
|
|
#
|
|
# * `rowCount` (optional) {Number} number of rows to move (default: 1)
|
|
# * `options` (optional) {Object} with the following keys:
|
|
# * `moveToEndOfSelection` if true, move to the left of the selection if a
|
|
# selection exists.
|
|
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}, skipSoftWrapIndentation: true)
|
|
@goalColumn = column
|
|
|
|
# Public: Moves the cursor left one screen column.
|
|
#
|
|
# * `columnCount` (optional) {Number} number of columns to move (default: 1)
|
|
# * `options` (optional) {Object} with the following keys:
|
|
# * `moveToEndOfSelection` if true, move to the left of the selection if a
|
|
# selection exists.
|
|
moveLeft: (columnCount=1, {moveToEndOfSelection}={}) ->
|
|
range = @marker.getScreenRange()
|
|
if moveToEndOfSelection and not range.isEmpty()
|
|
@setScreenPosition(range.start)
|
|
else
|
|
{row, column} = @getScreenPosition()
|
|
|
|
while columnCount > column and row > 0
|
|
columnCount -= column
|
|
column = @editor.lineTextForScreenRow(--row).length
|
|
columnCount-- # subtract 1 for the row move
|
|
|
|
column = column - columnCount
|
|
@setScreenPosition({row, column}, clip: 'backward')
|
|
|
|
# Public: Moves the cursor right one screen column.
|
|
#
|
|
# * `columnCount` (optional) {Number} number of columns to move (default: 1)
|
|
# * `options` (optional) {Object} with the following keys:
|
|
# * `moveToEndOfSelection` if true, move to the right of the selection if a
|
|
# selection exists.
|
|
moveRight: (columnCount=1, {moveToEndOfSelection}={}) ->
|
|
range = @marker.getScreenRange()
|
|
if moveToEndOfSelection and not range.isEmpty()
|
|
@setScreenPosition(range.end)
|
|
else
|
|
{ row, column } = @getScreenPosition()
|
|
maxLines = @editor.getScreenLineCount()
|
|
rowLength = @editor.lineTextForScreenRow(row).length
|
|
columnsRemainingInLine = rowLength - column
|
|
|
|
while columnCount > columnsRemainingInLine and row < maxLines - 1
|
|
columnCount -= columnsRemainingInLine
|
|
columnCount-- # subtract 1 for the row move
|
|
|
|
column = 0
|
|
rowLength = @editor.lineTextForScreenRow(++row).length
|
|
columnsRemainingInLine = rowLength
|
|
|
|
column = column + columnCount
|
|
@setScreenPosition({row, column}, clip: 'forward', 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: ->
|
|
screenRow = @getScreenRow()
|
|
screenLineStart = @editor.clipScreenPosition([screenRow, 0], skipSoftWrapIndentation: true)
|
|
screenLineEnd = [screenRow, Infinity]
|
|
screenLineBufferRange = @editor.bufferRangeForScreenRange([screenLineStart, screenLineEnd])
|
|
|
|
firstCharacterColumn = null
|
|
@editor.scanInBufferRange /\S/, screenLineBufferRange, ({range, stop}) ->
|
|
firstCharacterColumn = range.start.column
|
|
stop()
|
|
|
|
if firstCharacterColumn? and firstCharacterColumn isnt @getBufferColumn()
|
|
targetBufferColumn = firstCharacterColumn
|
|
else
|
|
targetBufferColumn = screenLineBufferRange.start.column
|
|
|
|
@setBufferPosition([screenLineBufferRange.start.row, targetBufferColumn])
|
|
|
|
# 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 = @getNextWordBoundaryBufferPosition()
|
|
@setBufferPosition(position)
|
|
|
|
# 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 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)
|
|
|
|
###
|
|
Section: Local Positions and Ranges
|
|
###
|
|
|
|
# Public: Returns buffer position of previous word boundary. It might be on
|
|
# the current word, or the previous word.
|
|
#
|
|
# * `options` (optional) {Object} with the following keys:
|
|
# * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
|
# (default: {::wordRegExp})
|
|
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: Returns buffer position of the next word boundary. It might be on
|
|
# the current word, or the previous word.
|
|
#
|
|
# * `options` (optional) {Object} with the following keys:
|
|
# * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
|
# (default: {::wordRegExp})
|
|
getNextWordBoundaryBufferPosition: (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
|
|
|
|
getMoveNextWordBoundaryBufferPosition: (options) ->
|
|
Grim.deprecate 'Use `::getNextWordBoundaryBufferPosition(options)` instead'
|
|
@getNextWordBoundaryBufferPosition(options)
|
|
|
|
# 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 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(options) then @getEndOfCurrentWordBufferPosition(options) 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: 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()])
|
|
|
|
###
|
|
Section: Visibility
|
|
###
|
|
|
|
# Public: Sets whether the cursor is visible.
|
|
setVisible: (visible) ->
|
|
if @visible != visible
|
|
@visible = visible
|
|
@emit 'visibility-changed', @visible
|
|
@emitter.emit 'did-change-visibility', @visible
|
|
|
|
# Public: Returns the visibility of the cursor.
|
|
isVisible: -> @visible
|
|
|
|
updateVisibility: ->
|
|
@setVisible(@marker.getBufferRange().isEmpty())
|
|
|
|
###
|
|
Section: Comparing to another cursor
|
|
###
|
|
|
|
# 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())
|
|
|
|
###
|
|
Section: Utilities
|
|
###
|
|
|
|
# Public: Prevents this cursor from causing scrolling.
|
|
clearAutoscroll: ->
|
|
|
|
# Public: Deselects the current selection.
|
|
clearSelection: (options) ->
|
|
@selection?.clear(options)
|
|
|
|
# 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', scope: @getScopeDescriptor())
|
|
segments = ["^[\t ]*$"]
|
|
segments.push("[^\\s#{_.escapeRegExp(nonWordCharacters)}]+")
|
|
if includeNonWordCharacters
|
|
segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+")
|
|
new RegExp(segments.join("|"), "g")
|
|
|
|
###
|
|
Section: Private
|
|
###
|
|
|
|
changePosition: (options, fn) ->
|
|
@clearSelection(autoscroll: false)
|
|
fn()
|
|
@autoscroll() if options.autoscroll ? @isLastCursor()
|
|
|
|
getPixelRect: ->
|
|
@editor.pixelRectForScreenRange(@getScreenRange())
|
|
|
|
getScreenRange: ->
|
|
{row, column} = @getScreenPosition()
|
|
new Range(new Point(row, column), new Point(row, column + 1))
|
|
|
|
autoscroll: (options) ->
|
|
@editor.scrollToScreenRange(@getScreenRange(), options)
|
|
|
|
getBeginningOfNextParagraphBufferPosition: ->
|
|
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: ->
|
|
start = @getBufferPosition()
|
|
|
|
{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)
|