src/cursor.js

Summary

Maintainability
F
5 days
Test Coverage
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;
  }
};