mirror of
https://github.com/atom/atom.git
synced 2026-01-12 16:38:20 -05:00
834 lines
26 KiB
JavaScript
834 lines
26 KiB
JavaScript
const { Point, Range } = require('text-buffer');
|
|
const { Emitter } = require('event-kit');
|
|
const _ = require('underscore-plus');
|
|
const Model = require('./model');
|
|
|
|
const EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g;
|
|
|
|
// 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 {DisplayMarker}.
|
|
module.exports = class Cursor extends Model {
|
|
// Instantiated by a {TextEditor}
|
|
constructor(params) {
|
|
super(params);
|
|
this.editor = params.editor;
|
|
this.marker = params.marker;
|
|
this.emitter = new Emitter();
|
|
}
|
|
|
|
destroy() {
|
|
this.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) {
|
|
return this.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) {
|
|
return this.emitter.once('did-destroy', callback);
|
|
}
|
|
|
|
/*
|
|
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 = {}) {
|
|
this.changePosition(options, () => {
|
|
this.marker.setHeadScreenPosition(screenPosition, options);
|
|
});
|
|
}
|
|
|
|
// Public: Returns the screen position of the cursor as a {Point}.
|
|
getScreenPosition() {
|
|
return this.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 = {}) {
|
|
this.changePosition(options, () => {
|
|
this.marker.setHeadBufferPosition(bufferPosition, options);
|
|
});
|
|
}
|
|
|
|
// Public: Returns the current buffer position as an Array.
|
|
getBufferPosition() {
|
|
return this.marker.getHeadBufferPosition();
|
|
}
|
|
|
|
// Public: Returns the cursor's current screen row.
|
|
getScreenRow() {
|
|
return this.getScreenPosition().row;
|
|
}
|
|
|
|
// Public: Returns the cursor's current screen column.
|
|
getScreenColumn() {
|
|
return this.getScreenPosition().column;
|
|
}
|
|
|
|
// Public: Retrieves the cursor's current buffer row.
|
|
getBufferRow() {
|
|
return this.getBufferPosition().row;
|
|
}
|
|
|
|
// Public: Returns the cursor's current buffer column.
|
|
getBufferColumn() {
|
|
return this.getBufferPosition().column;
|
|
}
|
|
|
|
// Public: Returns the cursor's current buffer row of text excluding its line
|
|
// ending.
|
|
getCurrentBufferLine() {
|
|
return this.editor.lineTextForBufferRow(this.getBufferRow());
|
|
}
|
|
|
|
// Public: Returns whether the cursor is at the start of a line.
|
|
isAtBeginningOfLine() {
|
|
return this.getBufferPosition().column === 0;
|
|
}
|
|
|
|
// Public: Returns whether the cursor is on the line return character.
|
|
isAtEndOfLine() {
|
|
return this.getBufferPosition().isEqual(
|
|
this.getCurrentLineBufferRange().end
|
|
);
|
|
}
|
|
|
|
/*
|
|
Section: Cursor Position Details
|
|
*/
|
|
|
|
// Public: Returns the underlying {DisplayMarker} for the cursor.
|
|
// Useful with overlay {Decoration}s.
|
|
getMarker() {
|
|
return this.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() {
|
|
const { row, column } = this.getBufferPosition();
|
|
const range = [[row, column - 1], [row, column + 1]];
|
|
return /^\s+$/.test(this.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() {
|
|
if (this.isAtBeginningOfLine() || this.isAtEndOfLine()) return false;
|
|
|
|
const { row, column } = this.getBufferPosition();
|
|
const range = [[row, column - 1], [row, column + 1]];
|
|
const text = this.editor.getTextInBufferRange(range);
|
|
if (/\s/.test(text[0]) || /\s/.test(text[1])) return false;
|
|
|
|
const nonWordCharacters = this.getNonWordCharacters();
|
|
return (
|
|
nonWordCharacters.includes(text[0]) !==
|
|
nonWordCharacters.includes(text[1])
|
|
);
|
|
}
|
|
|
|
// 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) {
|
|
const { row, column } = this.getBufferPosition();
|
|
const range = [[row, column], [row, Infinity]];
|
|
const text = this.editor.getTextInBufferRange(range);
|
|
return (
|
|
text.search((options && options.wordRegex) || this.wordRegExp()) === 0
|
|
);
|
|
}
|
|
|
|
// Public: Returns the indentation level of the current line.
|
|
getIndentLevel() {
|
|
if (this.editor.getSoftTabs()) {
|
|
return this.getBufferColumn() / this.editor.getTabLength();
|
|
} else {
|
|
return this.getBufferColumn();
|
|
}
|
|
}
|
|
|
|
// Public: Retrieves the scope descriptor for the cursor's current position.
|
|
//
|
|
// Returns a {ScopeDescriptor}
|
|
getScopeDescriptor() {
|
|
return this.editor.scopeDescriptorForBufferPosition(
|
|
this.getBufferPosition()
|
|
);
|
|
}
|
|
|
|
// Public: Retrieves the syntax tree scope descriptor for the cursor's current position.
|
|
//
|
|
// Returns a {ScopeDescriptor}
|
|
getSyntaxTreeScopeDescriptor() {
|
|
return this.editor.syntaxTreeScopeDescriptorForBufferPosition(
|
|
this.getBufferPosition()
|
|
);
|
|
}
|
|
|
|
// Public: Returns true if this cursor has no non-whitespace characters before
|
|
// its current position.
|
|
hasPrecedingCharactersOnLine() {
|
|
const bufferPosition = this.getBufferPosition();
|
|
const line = this.editor.lineTextForBufferRow(bufferPosition.row);
|
|
const firstCharacterColumn = line.search(/\S/);
|
|
|
|
if (firstCharacterColumn === -1) {
|
|
return false;
|
|
} else {
|
|
return 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() {
|
|
return this === 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 } = {}) {
|
|
let row, column;
|
|
const range = this.marker.getScreenRange();
|
|
if (moveToEndOfSelection && !range.isEmpty()) {
|
|
({ row, column } = range.start);
|
|
} else {
|
|
({ row, column } = this.getScreenPosition());
|
|
}
|
|
|
|
if (this.goalColumn != null) column = this.goalColumn;
|
|
this.setScreenPosition(
|
|
{ row: row - rowCount, column },
|
|
{ skipSoftWrapIndentation: true }
|
|
);
|
|
this.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 } = {}) {
|
|
let row, column;
|
|
const range = this.marker.getScreenRange();
|
|
if (moveToEndOfSelection && !range.isEmpty()) {
|
|
({ row, column } = range.end);
|
|
} else {
|
|
({ row, column } = this.getScreenPosition());
|
|
}
|
|
|
|
if (this.goalColumn != null) column = this.goalColumn;
|
|
this.setScreenPosition(
|
|
{ row: row + rowCount, column },
|
|
{ skipSoftWrapIndentation: true }
|
|
);
|
|
this.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 } = {}) {
|
|
const range = this.marker.getScreenRange();
|
|
if (moveToEndOfSelection && !range.isEmpty()) {
|
|
this.setScreenPosition(range.start);
|
|
} else {
|
|
let { row, column } = this.getScreenPosition();
|
|
|
|
while (columnCount > column && row > 0) {
|
|
columnCount -= column;
|
|
column = this.editor.lineLengthForScreenRow(--row);
|
|
columnCount--; // subtract 1 for the row move
|
|
}
|
|
|
|
column = column - columnCount;
|
|
this.setScreenPosition({ row, column }, { clipDirection: '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 } = {}) {
|
|
const range = this.marker.getScreenRange();
|
|
if (moveToEndOfSelection && !range.isEmpty()) {
|
|
this.setScreenPosition(range.end);
|
|
} else {
|
|
let { row, column } = this.getScreenPosition();
|
|
const maxLines = this.editor.getScreenLineCount();
|
|
let rowLength = this.editor.lineLengthForScreenRow(row);
|
|
let columnsRemainingInLine = rowLength - column;
|
|
|
|
while (columnCount > columnsRemainingInLine && row < maxLines - 1) {
|
|
columnCount -= columnsRemainingInLine;
|
|
columnCount--; // subtract 1 for the row move
|
|
|
|
column = 0;
|
|
rowLength = this.editor.lineLengthForScreenRow(++row);
|
|
columnsRemainingInLine = rowLength;
|
|
}
|
|
|
|
column = column + columnCount;
|
|
this.setScreenPosition({ row, column }, { clipDirection: 'forward' });
|
|
}
|
|
}
|
|
|
|
// Public: Moves the cursor to the top of the buffer.
|
|
moveToTop() {
|
|
this.setBufferPosition([0, 0]);
|
|
}
|
|
|
|
// Public: Moves the cursor to the bottom of the buffer.
|
|
moveToBottom() {
|
|
const column = this.goalColumn;
|
|
this.setBufferPosition(this.editor.getEofBufferPosition());
|
|
this.goalColumn = column;
|
|
}
|
|
|
|
// Public: Moves the cursor to the beginning of the line.
|
|
moveToBeginningOfScreenLine() {
|
|
this.setScreenPosition([this.getScreenRow(), 0]);
|
|
}
|
|
|
|
// Public: Moves the cursor to the beginning of the buffer line.
|
|
moveToBeginningOfLine() {
|
|
this.setBufferPosition([this.getBufferRow(), 0]);
|
|
}
|
|
|
|
// Public: Moves the cursor to the beginning of the first character in the
|
|
// line.
|
|
moveToFirstCharacterOfLine() {
|
|
let targetBufferColumn;
|
|
const screenRow = this.getScreenRow();
|
|
const screenLineStart = this.editor.clipScreenPosition([screenRow, 0], {
|
|
skipSoftWrapIndentation: true
|
|
});
|
|
const screenLineEnd = [screenRow, Infinity];
|
|
const screenLineBufferRange = this.editor.bufferRangeForScreenRange([
|
|
screenLineStart,
|
|
screenLineEnd
|
|
]);
|
|
|
|
let firstCharacterColumn = null;
|
|
this.editor.scanInBufferRange(
|
|
/\S/,
|
|
screenLineBufferRange,
|
|
({ range, stop }) => {
|
|
firstCharacterColumn = range.start.column;
|
|
stop();
|
|
}
|
|
);
|
|
|
|
if (
|
|
firstCharacterColumn != null &&
|
|
firstCharacterColumn !== this.getBufferColumn()
|
|
) {
|
|
targetBufferColumn = firstCharacterColumn;
|
|
} else {
|
|
targetBufferColumn = screenLineBufferRange.start.column;
|
|
}
|
|
|
|
this.setBufferPosition([
|
|
screenLineBufferRange.start.row,
|
|
targetBufferColumn
|
|
]);
|
|
}
|
|
|
|
// Public: Moves the cursor to the end of the line.
|
|
moveToEndOfScreenLine() {
|
|
this.setScreenPosition([this.getScreenRow(), Infinity]);
|
|
}
|
|
|
|
// Public: Moves the cursor to the end of the buffer line.
|
|
moveToEndOfLine() {
|
|
this.setBufferPosition([this.getBufferRow(), Infinity]);
|
|
}
|
|
|
|
// Public: Moves the cursor to the beginning of the word.
|
|
moveToBeginningOfWord() {
|
|
this.setBufferPosition(this.getBeginningOfCurrentWordBufferPosition());
|
|
}
|
|
|
|
// Public: Moves the cursor to the end of the word.
|
|
moveToEndOfWord() {
|
|
const position = this.getEndOfCurrentWordBufferPosition();
|
|
if (position) this.setBufferPosition(position);
|
|
}
|
|
|
|
// Public: Moves the cursor to the beginning of the next word.
|
|
moveToBeginningOfNextWord() {
|
|
const position = this.getBeginningOfNextWordBufferPosition();
|
|
if (position) this.setBufferPosition(position);
|
|
}
|
|
|
|
// Public: Moves the cursor to the previous word boundary.
|
|
moveToPreviousWordBoundary() {
|
|
const position = this.getPreviousWordBoundaryBufferPosition();
|
|
if (position) this.setBufferPosition(position);
|
|
}
|
|
|
|
// Public: Moves the cursor to the next word boundary.
|
|
moveToNextWordBoundary() {
|
|
const position = this.getNextWordBoundaryBufferPosition();
|
|
if (position) this.setBufferPosition(position);
|
|
}
|
|
|
|
// Public: Moves the cursor to the previous subword boundary.
|
|
moveToPreviousSubwordBoundary() {
|
|
const options = { wordRegex: this.subwordRegExp({ backwards: true }) };
|
|
const position = this.getPreviousWordBoundaryBufferPosition(options);
|
|
if (position) this.setBufferPosition(position);
|
|
}
|
|
|
|
// Public: Moves the cursor to the next subword boundary.
|
|
moveToNextSubwordBoundary() {
|
|
const options = { wordRegex: this.subwordRegExp() };
|
|
const position = this.getNextWordBoundaryBufferPosition(options);
|
|
if (position) this.setBufferPosition(position);
|
|
}
|
|
|
|
// Public: Moves the cursor to the beginning of the buffer line, skipping all
|
|
// whitespace.
|
|
skipLeadingWhitespace() {
|
|
const position = this.getBufferPosition();
|
|
const scanRange = this.getCurrentLineBufferRange();
|
|
let endOfLeadingWhitespace = null;
|
|
this.editor.scanInBufferRange(/^[ \t]*/, scanRange, ({ range }) => {
|
|
endOfLeadingWhitespace = range.end;
|
|
});
|
|
|
|
if (endOfLeadingWhitespace.isGreaterThan(position))
|
|
this.setBufferPosition(endOfLeadingWhitespace);
|
|
}
|
|
|
|
// Public: Moves the cursor to the beginning of the next paragraph
|
|
moveToBeginningOfNextParagraph() {
|
|
const position = this.getBeginningOfNextParagraphBufferPosition();
|
|
if (position) this.setBufferPosition(position);
|
|
}
|
|
|
|
// Public: Moves the cursor to the beginning of the previous paragraph
|
|
moveToBeginningOfPreviousParagraph() {
|
|
const position = this.getBeginningOfPreviousParagraphBufferPosition();
|
|
if (position) this.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 = {}) {
|
|
const currentBufferPosition = this.getBufferPosition();
|
|
const previousNonBlankRow = this.editor.buffer.previousNonBlankRow(
|
|
currentBufferPosition.row
|
|
);
|
|
const scanRange = Range(
|
|
Point(previousNonBlankRow || 0, 0),
|
|
currentBufferPosition
|
|
);
|
|
|
|
const ranges = this.editor.buffer.findAllInRangeSync(
|
|
options.wordRegex || this.wordRegExp(),
|
|
scanRange
|
|
);
|
|
|
|
const range = ranges[ranges.length - 1];
|
|
if (range) {
|
|
if (
|
|
range.start.row < currentBufferPosition.row &&
|
|
currentBufferPosition.column > 0
|
|
) {
|
|
return Point(currentBufferPosition.row, 0);
|
|
} else if (currentBufferPosition.isGreaterThan(range.end)) {
|
|
return Point.fromObject(range.end);
|
|
} else {
|
|
return Point.fromObject(range.start);
|
|
}
|
|
} else {
|
|
return 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 = {}) {
|
|
const currentBufferPosition = this.getBufferPosition();
|
|
const scanRange = Range(
|
|
currentBufferPosition,
|
|
this.editor.getEofBufferPosition()
|
|
);
|
|
|
|
const range = this.editor.buffer.findInRangeSync(
|
|
options.wordRegex || this.wordRegExp(),
|
|
scanRange
|
|
);
|
|
|
|
if (range) {
|
|
if (range.start.row > currentBufferPosition.row) {
|
|
return Point(range.start.row, 0);
|
|
} else if (currentBufferPosition.isLessThan(range.start)) {
|
|
return Point.fromObject(range.start);
|
|
} else {
|
|
return Point.fromObject(range.end);
|
|
}
|
|
} else {
|
|
return currentBufferPosition;
|
|
}
|
|
}
|
|
|
|
// 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 = {}) {
|
|
const allowPrevious = options.allowPrevious !== false;
|
|
const position = this.getBufferPosition();
|
|
|
|
const scanRange = allowPrevious
|
|
? new Range(new Point(position.row - 1, 0), position)
|
|
: new Range(new Point(position.row, 0), position);
|
|
|
|
const ranges = this.editor.buffer.findAllInRangeSync(
|
|
options.wordRegex || this.wordRegExp(options),
|
|
scanRange
|
|
);
|
|
|
|
let result;
|
|
for (let range of ranges) {
|
|
if (position.isLessThanOrEqual(range.start)) break;
|
|
if (allowPrevious || position.isLessThanOrEqual(range.end))
|
|
result = Point.fromObject(range.start);
|
|
}
|
|
|
|
return result || (allowPrevious ? new Point(0, 0) : position);
|
|
}
|
|
|
|
// 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 = {}) {
|
|
const allowNext = options.allowNext !== false;
|
|
const position = this.getBufferPosition();
|
|
|
|
const scanRange = allowNext
|
|
? new Range(position, new Point(position.row + 2, 0))
|
|
: new Range(position, new Point(position.row, Infinity));
|
|
|
|
const ranges = this.editor.buffer.findAllInRangeSync(
|
|
options.wordRegex || this.wordRegExp(options),
|
|
scanRange
|
|
);
|
|
|
|
for (let range of ranges) {
|
|
if (position.isLessThan(range.start) && !allowNext) break;
|
|
if (position.isLessThan(range.end)) return Point.fromObject(range.end);
|
|
}
|
|
|
|
return allowNext ? this.editor.getEofBufferPosition() : position;
|
|
}
|
|
|
|
// 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 = {}) {
|
|
const currentBufferPosition = this.getBufferPosition();
|
|
const start = this.isInsideWord(options)
|
|
? this.getEndOfCurrentWordBufferPosition(options)
|
|
: currentBufferPosition;
|
|
const scanRange = [start, this.editor.getEofBufferPosition()];
|
|
|
|
let beginningOfNextWordPosition;
|
|
this.editor.scanInBufferRange(
|
|
options.wordRegex || this.wordRegExp(),
|
|
scanRange,
|
|
({ range, stop }) => {
|
|
beginningOfNextWordPosition = range.start;
|
|
stop();
|
|
}
|
|
);
|
|
|
|
return beginningOfNextWordPosition || 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 = {}) {
|
|
const position = this.getBufferPosition();
|
|
const ranges = this.editor.buffer.findAllInRangeSync(
|
|
options.wordRegex || this.wordRegExp(options),
|
|
new Range(new Point(position.row, 0), new Point(position.row, Infinity))
|
|
);
|
|
const range = ranges.find(
|
|
range =>
|
|
range.end.column >= position.column &&
|
|
range.start.column <= position.column
|
|
);
|
|
return range ? Range.fromObject(range) : new Range(position, position);
|
|
}
|
|
|
|
// 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) {
|
|
return this.editor.bufferRangeForBufferRow(this.getBufferRow(), options);
|
|
}
|
|
|
|
// Public: Retrieves the range for the current paragraph.
|
|
//
|
|
// A paragraph is defined as a block of text surrounded by empty lines or comments.
|
|
//
|
|
// Returns a {Range}.
|
|
getCurrentParagraphBufferRange() {
|
|
return this.editor.rowRangeForParagraphAtBufferRow(this.getBufferRow());
|
|
}
|
|
|
|
// Public: Returns the characters preceding the cursor in the current word.
|
|
getCurrentWordPrefix() {
|
|
return this.editor.getTextInBufferRange([
|
|
this.getBeginningOfCurrentWordBufferPosition(),
|
|
this.getBufferPosition()
|
|
]);
|
|
}
|
|
|
|
/*
|
|
Section: Visibility
|
|
*/
|
|
|
|
/*
|
|
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) {
|
|
return this.getBufferPosition().compare(otherCursor.getBufferPosition());
|
|
}
|
|
|
|
/*
|
|
Section: Utilities
|
|
*/
|
|
|
|
// Public: Deselects the current selection.
|
|
clearSelection(options) {
|
|
if (this.selection) this.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(options) {
|
|
const nonWordCharacters = _.escapeRegExp(this.getNonWordCharacters());
|
|
let source = `^[\t ]*$|[^\\s${nonWordCharacters}]+`;
|
|
if (!options || options.includeNonWordCharacters !== false) {
|
|
source += `|${`[${nonWordCharacters}]+`}`;
|
|
}
|
|
return new RegExp(source, 'g');
|
|
}
|
|
|
|
// Public: Get the RegExp used by the cursor to determine what a "subword" is.
|
|
//
|
|
// * `options` (optional) {Object} with the following keys:
|
|
// * `backwards` A {Boolean} indicating whether to look forwards or backwards
|
|
// for the next subword. (default: false)
|
|
//
|
|
// Returns a {RegExp}.
|
|
subwordRegExp(options = {}) {
|
|
const nonWordCharacters = this.getNonWordCharacters();
|
|
const lowercaseLetters = 'a-z\\u00DF-\\u00F6\\u00F8-\\u00FF';
|
|
const uppercaseLetters = 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE';
|
|
const snakeCamelSegment = `[${uppercaseLetters}]?[${lowercaseLetters}]+`;
|
|
const segments = [
|
|
'^[\t ]+',
|
|
'[\t ]+$',
|
|
`[${uppercaseLetters}]+(?![${lowercaseLetters}])`,
|
|
'\\d+'
|
|
];
|
|
if (options.backwards) {
|
|
segments.push(`${snakeCamelSegment}_*`);
|
|
segments.push(`[${_.escapeRegExp(nonWordCharacters)}]+\\s*`);
|
|
} else {
|
|
segments.push(`_*${snakeCamelSegment}`);
|
|
segments.push(`\\s*[${_.escapeRegExp(nonWordCharacters)}]+`);
|
|
}
|
|
segments.push('_+');
|
|
return new RegExp(segments.join('|'), 'g');
|
|
}
|
|
|
|
/*
|
|
Section: Private
|
|
*/
|
|
|
|
getNonWordCharacters() {
|
|
return this.editor.getNonWordCharacters(this.getBufferPosition());
|
|
}
|
|
|
|
changePosition(options, fn) {
|
|
this.clearSelection({ autoscroll: false });
|
|
fn();
|
|
this.goalColumn = null;
|
|
const autoscroll =
|
|
options && options.autoscroll != null
|
|
? options.autoscroll
|
|
: this.isLastCursor();
|
|
if (autoscroll) this.autoscroll();
|
|
}
|
|
|
|
getScreenRange() {
|
|
const { row, column } = this.getScreenPosition();
|
|
return new Range(new Point(row, column), new Point(row, column + 1));
|
|
}
|
|
|
|
autoscroll(options = {}) {
|
|
options.clip = false;
|
|
this.editor.scrollToScreenRange(this.getScreenRange(), options);
|
|
}
|
|
|
|
getBeginningOfNextParagraphBufferPosition() {
|
|
const start = this.getBufferPosition();
|
|
const eof = this.editor.getEofBufferPosition();
|
|
const scanRange = [start, eof];
|
|
|
|
const { row, column } = eof;
|
|
let position = new Point(row, column - 1);
|
|
|
|
this.editor.scanInBufferRange(
|
|
EmptyLineRegExp,
|
|
scanRange,
|
|
({ range, stop }) => {
|
|
position = range.start.traverse(Point(1, 0));
|
|
if (!position.isEqual(start)) stop();
|
|
}
|
|
);
|
|
return position;
|
|
}
|
|
|
|
getBeginningOfPreviousParagraphBufferPosition() {
|
|
const start = this.getBufferPosition();
|
|
|
|
const { row, column } = start;
|
|
const scanRange = [[row - 1, column], [0, 0]];
|
|
let position = new Point(0, 0);
|
|
this.editor.backwardsScanInBufferRange(
|
|
EmptyLineRegExp,
|
|
scanRange,
|
|
({ range, stop }) => {
|
|
position = range.start.traverse(Point(1, 0));
|
|
if (!position.isEqual(start)) stop();
|
|
}
|
|
);
|
|
return position;
|
|
}
|
|
};
|