/ src / cursor.js
cursor.js
  1  const { Point, Range } = require('text-buffer');
  2  const { Emitter } = require('event-kit');
  3  const _ = require('underscore-plus');
  4  const Model = require('./model');
  5  
  6  const EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g;
  7  
  8  // Extended: The `Cursor` class represents the little blinking line identifying
  9  // where text can be inserted.
 10  //
 11  // Cursors belong to {TextEditor}s and have some metadata attached in the form
 12  // of a {DisplayMarker}.
 13  module.exports = class Cursor extends Model {
 14    // Instantiated by a {TextEditor}
 15    constructor(params) {
 16      super(params);
 17      this.editor = params.editor;
 18      this.marker = params.marker;
 19      this.emitter = new Emitter();
 20    }
 21  
 22    destroy() {
 23      this.marker.destroy();
 24    }
 25  
 26    /*
 27    Section: Event Subscription
 28    */
 29  
 30    // Public: Calls your `callback` when the cursor has been moved.
 31    //
 32    // * `callback` {Function}
 33    //   * `event` {Object}
 34    //     * `oldBufferPosition` {Point}
 35    //     * `oldScreenPosition` {Point}
 36    //     * `newBufferPosition` {Point}
 37    //     * `newScreenPosition` {Point}
 38    //     * `textChanged` {Boolean}
 39    //     * `cursor` {Cursor} that triggered the event
 40    //
 41    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 42    onDidChangePosition(callback) {
 43      return this.emitter.on('did-change-position', callback);
 44    }
 45  
 46    // Public: Calls your `callback` when the cursor is destroyed
 47    //
 48    // * `callback` {Function}
 49    //
 50    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 51    onDidDestroy(callback) {
 52      return this.emitter.once('did-destroy', callback);
 53    }
 54  
 55    /*
 56    Section: Managing Cursor Position
 57    */
 58  
 59    // Public: Moves a cursor to a given screen position.
 60    //
 61    // * `screenPosition` {Array} of two numbers: the screen row, and the screen column.
 62    // * `options` (optional) {Object} with the following keys:
 63    //   * `autoscroll` A Boolean which, if `true`, scrolls the {TextEditor} to wherever
 64    //     the cursor moves to.
 65    setScreenPosition(screenPosition, options = {}) {
 66      this.changePosition(options, () => {
 67        this.marker.setHeadScreenPosition(screenPosition, options);
 68      });
 69    }
 70  
 71    // Public: Returns the screen position of the cursor as a {Point}.
 72    getScreenPosition() {
 73      return this.marker.getHeadScreenPosition();
 74    }
 75  
 76    // Public: Moves a cursor to a given buffer position.
 77    //
 78    // * `bufferPosition` {Array} of two numbers: the buffer row, and the buffer column.
 79    // * `options` (optional) {Object} with the following keys:
 80    //   * `autoscroll` {Boolean} indicating whether to autoscroll to the new
 81    //     position. Defaults to `true` if this is the most recently added cursor,
 82    //     `false` otherwise.
 83    setBufferPosition(bufferPosition, options = {}) {
 84      this.changePosition(options, () => {
 85        this.marker.setHeadBufferPosition(bufferPosition, options);
 86      });
 87    }
 88  
 89    // Public: Returns the current buffer position as an Array.
 90    getBufferPosition() {
 91      return this.marker.getHeadBufferPosition();
 92    }
 93  
 94    // Public: Returns the cursor's current screen row.
 95    getScreenRow() {
 96      return this.getScreenPosition().row;
 97    }
 98  
 99    // Public: Returns the cursor's current screen column.
100    getScreenColumn() {
101      return this.getScreenPosition().column;
102    }
103  
104    // Public: Retrieves the cursor's current buffer row.
105    getBufferRow() {
106      return this.getBufferPosition().row;
107    }
108  
109    // Public: Returns the cursor's current buffer column.
110    getBufferColumn() {
111      return this.getBufferPosition().column;
112    }
113  
114    // Public: Returns the cursor's current buffer row of text excluding its line
115    // ending.
116    getCurrentBufferLine() {
117      return this.editor.lineTextForBufferRow(this.getBufferRow());
118    }
119  
120    // Public: Returns whether the cursor is at the start of a line.
121    isAtBeginningOfLine() {
122      return this.getBufferPosition().column === 0;
123    }
124  
125    // Public: Returns whether the cursor is on the line return character.
126    isAtEndOfLine() {
127      return this.getBufferPosition().isEqual(
128        this.getCurrentLineBufferRange().end
129      );
130    }
131  
132    /*
133    Section: Cursor Position Details
134    */
135  
136    // Public: Returns the underlying {DisplayMarker} for the cursor.
137    // Useful with overlay {Decoration}s.
138    getMarker() {
139      return this.marker;
140    }
141  
142    // Public: Identifies if the cursor is surrounded by whitespace.
143    //
144    // "Surrounded" here means that the character directly before and after the
145    // cursor are both whitespace.
146    //
147    // Returns a {Boolean}.
148    isSurroundedByWhitespace() {
149      const { row, column } = this.getBufferPosition();
150      const range = [[row, column - 1], [row, column + 1]];
151      return /^\s+$/.test(this.editor.getTextInBufferRange(range));
152    }
153  
154    // Public: Returns whether the cursor is currently between a word and non-word
155    // character. The non-word characters are defined by the
156    // `editor.nonWordCharacters` config value.
157    //
158    // This method returns false if the character before or after the cursor is
159    // whitespace.
160    //
161    // Returns a Boolean.
162    isBetweenWordAndNonWord() {
163      if (this.isAtBeginningOfLine() || this.isAtEndOfLine()) return false;
164  
165      const { row, column } = this.getBufferPosition();
166      const range = [[row, column - 1], [row, column + 1]];
167      const text = this.editor.getTextInBufferRange(range);
168      if (/\s/.test(text[0]) || /\s/.test(text[1])) return false;
169  
170      const nonWordCharacters = this.getNonWordCharacters();
171      return (
172        nonWordCharacters.includes(text[0]) !==
173        nonWordCharacters.includes(text[1])
174      );
175    }
176  
177    // Public: Returns whether this cursor is between a word's start and end.
178    //
179    // * `options` (optional) {Object}
180    //   * `wordRegex` A {RegExp} indicating what constitutes a "word"
181    //     (default: {::wordRegExp}).
182    //
183    // Returns a {Boolean}
184    isInsideWord(options) {
185      const { row, column } = this.getBufferPosition();
186      const range = [[row, column], [row, Infinity]];
187      const text = this.editor.getTextInBufferRange(range);
188      return (
189        text.search((options && options.wordRegex) || this.wordRegExp()) === 0
190      );
191    }
192  
193    // Public: Returns the indentation level of the current line.
194    getIndentLevel() {
195      if (this.editor.getSoftTabs()) {
196        return this.getBufferColumn() / this.editor.getTabLength();
197      } else {
198        return this.getBufferColumn();
199      }
200    }
201  
202    // Public: Retrieves the scope descriptor for the cursor's current position.
203    //
204    // Returns a {ScopeDescriptor}
205    getScopeDescriptor() {
206      return this.editor.scopeDescriptorForBufferPosition(
207        this.getBufferPosition()
208      );
209    }
210  
211    // Public: Retrieves the syntax tree scope descriptor for the cursor's current position.
212    //
213    // Returns a {ScopeDescriptor}
214    getSyntaxTreeScopeDescriptor() {
215      return this.editor.syntaxTreeScopeDescriptorForBufferPosition(
216        this.getBufferPosition()
217      );
218    }
219  
220    // Public: Returns true if this cursor has no non-whitespace characters before
221    // its current position.
222    hasPrecedingCharactersOnLine() {
223      const bufferPosition = this.getBufferPosition();
224      const line = this.editor.lineTextForBufferRow(bufferPosition.row);
225      const firstCharacterColumn = line.search(/\S/);
226  
227      if (firstCharacterColumn === -1) {
228        return false;
229      } else {
230        return bufferPosition.column > firstCharacterColumn;
231      }
232    }
233  
234    // Public: Identifies if this cursor is the last in the {TextEditor}.
235    //
236    // "Last" is defined as the most recently added cursor.
237    //
238    // Returns a {Boolean}.
239    isLastCursor() {
240      return this === this.editor.getLastCursor();
241    }
242  
243    /*
244    Section: Moving the Cursor
245    */
246  
247    // Public: Moves the cursor up one screen row.
248    //
249    // * `rowCount` (optional) {Number} number of rows to move (default: 1)
250    // * `options` (optional) {Object} with the following keys:
251    //   * `moveToEndOfSelection` if true, move to the left of the selection if a
252    //     selection exists.
253    moveUp(rowCount = 1, { moveToEndOfSelection } = {}) {
254      let row, column;
255      const range = this.marker.getScreenRange();
256      if (moveToEndOfSelection && !range.isEmpty()) {
257        ({ row, column } = range.start);
258      } else {
259        ({ row, column } = this.getScreenPosition());
260      }
261  
262      if (this.goalColumn != null) column = this.goalColumn;
263      this.setScreenPosition(
264        { row: row - rowCount, column },
265        { skipSoftWrapIndentation: true }
266      );
267      this.goalColumn = column;
268    }
269  
270    // Public: Moves the cursor down one screen row.
271    //
272    // * `rowCount` (optional) {Number} number of rows to move (default: 1)
273    // * `options` (optional) {Object} with the following keys:
274    //   * `moveToEndOfSelection` if true, move to the left of the selection if a
275    //     selection exists.
276    moveDown(rowCount = 1, { moveToEndOfSelection } = {}) {
277      let row, column;
278      const range = this.marker.getScreenRange();
279      if (moveToEndOfSelection && !range.isEmpty()) {
280        ({ row, column } = range.end);
281      } else {
282        ({ row, column } = this.getScreenPosition());
283      }
284  
285      if (this.goalColumn != null) column = this.goalColumn;
286      this.setScreenPosition(
287        { row: row + rowCount, column },
288        { skipSoftWrapIndentation: true }
289      );
290      this.goalColumn = column;
291    }
292  
293    // Public: Moves the cursor left one screen column.
294    //
295    // * `columnCount` (optional) {Number} number of columns to move (default: 1)
296    // * `options` (optional) {Object} with the following keys:
297    //   * `moveToEndOfSelection` if true, move to the left of the selection if a
298    //     selection exists.
299    moveLeft(columnCount = 1, { moveToEndOfSelection } = {}) {
300      const range = this.marker.getScreenRange();
301      if (moveToEndOfSelection && !range.isEmpty()) {
302        this.setScreenPosition(range.start);
303      } else {
304        let { row, column } = this.getScreenPosition();
305  
306        while (columnCount > column && row > 0) {
307          columnCount -= column;
308          column = this.editor.lineLengthForScreenRow(--row);
309          columnCount--; // subtract 1 for the row move
310        }
311  
312        column = column - columnCount;
313        this.setScreenPosition({ row, column }, { clipDirection: 'backward' });
314      }
315    }
316  
317    // Public: Moves the cursor right one screen column.
318    //
319    // * `columnCount` (optional) {Number} number of columns to move (default: 1)
320    // * `options` (optional) {Object} with the following keys:
321    //   * `moveToEndOfSelection` if true, move to the right of the selection if a
322    //     selection exists.
323    moveRight(columnCount = 1, { moveToEndOfSelection } = {}) {
324      const range = this.marker.getScreenRange();
325      if (moveToEndOfSelection && !range.isEmpty()) {
326        this.setScreenPosition(range.end);
327      } else {
328        let { row, column } = this.getScreenPosition();
329        const maxLines = this.editor.getScreenLineCount();
330        let rowLength = this.editor.lineLengthForScreenRow(row);
331        let columnsRemainingInLine = rowLength - column;
332  
333        while (columnCount > columnsRemainingInLine && row < maxLines - 1) {
334          columnCount -= columnsRemainingInLine;
335          columnCount--; // subtract 1 for the row move
336  
337          column = 0;
338          rowLength = this.editor.lineLengthForScreenRow(++row);
339          columnsRemainingInLine = rowLength;
340        }
341  
342        column = column + columnCount;
343        this.setScreenPosition({ row, column }, { clipDirection: 'forward' });
344      }
345    }
346  
347    // Public: Moves the cursor to the top of the buffer.
348    moveToTop() {
349      this.setBufferPosition([0, 0]);
350    }
351  
352    // Public: Moves the cursor to the bottom of the buffer.
353    moveToBottom() {
354      const column = this.goalColumn;
355      this.setBufferPosition(this.editor.getEofBufferPosition());
356      this.goalColumn = column;
357    }
358  
359    // Public: Moves the cursor to the beginning of the line.
360    moveToBeginningOfScreenLine() {
361      this.setScreenPosition([this.getScreenRow(), 0]);
362    }
363  
364    // Public: Moves the cursor to the beginning of the buffer line.
365    moveToBeginningOfLine() {
366      this.setBufferPosition([this.getBufferRow(), 0]);
367    }
368  
369    // Public: Moves the cursor to the beginning of the first character in the
370    // line.
371    moveToFirstCharacterOfLine() {
372      let targetBufferColumn;
373      const screenRow = this.getScreenRow();
374      const screenLineStart = this.editor.clipScreenPosition([screenRow, 0], {
375        skipSoftWrapIndentation: true
376      });
377      const screenLineEnd = [screenRow, Infinity];
378      const screenLineBufferRange = this.editor.bufferRangeForScreenRange([
379        screenLineStart,
380        screenLineEnd
381      ]);
382  
383      let firstCharacterColumn = null;
384      this.editor.scanInBufferRange(
385        /\S/,
386        screenLineBufferRange,
387        ({ range, stop }) => {
388          firstCharacterColumn = range.start.column;
389          stop();
390        }
391      );
392  
393      if (
394        firstCharacterColumn != null &&
395        firstCharacterColumn !== this.getBufferColumn()
396      ) {
397        targetBufferColumn = firstCharacterColumn;
398      } else {
399        targetBufferColumn = screenLineBufferRange.start.column;
400      }
401  
402      this.setBufferPosition([
403        screenLineBufferRange.start.row,
404        targetBufferColumn
405      ]);
406    }
407  
408    // Public: Moves the cursor to the end of the line.
409    moveToEndOfScreenLine() {
410      this.setScreenPosition([this.getScreenRow(), Infinity]);
411    }
412  
413    // Public: Moves the cursor to the end of the buffer line.
414    moveToEndOfLine() {
415      this.setBufferPosition([this.getBufferRow(), Infinity]);
416    }
417  
418    // Public: Moves the cursor to the beginning of the word.
419    moveToBeginningOfWord() {
420      this.setBufferPosition(this.getBeginningOfCurrentWordBufferPosition());
421    }
422  
423    // Public: Moves the cursor to the end of the word.
424    moveToEndOfWord() {
425      const position = this.getEndOfCurrentWordBufferPosition();
426      if (position) this.setBufferPosition(position);
427    }
428  
429    // Public: Moves the cursor to the beginning of the next word.
430    moveToBeginningOfNextWord() {
431      const position = this.getBeginningOfNextWordBufferPosition();
432      if (position) this.setBufferPosition(position);
433    }
434  
435    // Public: Moves the cursor to the previous word boundary.
436    moveToPreviousWordBoundary() {
437      const position = this.getPreviousWordBoundaryBufferPosition();
438      if (position) this.setBufferPosition(position);
439    }
440  
441    // Public: Moves the cursor to the next word boundary.
442    moveToNextWordBoundary() {
443      const position = this.getNextWordBoundaryBufferPosition();
444      if (position) this.setBufferPosition(position);
445    }
446  
447    // Public: Moves the cursor to the previous subword boundary.
448    moveToPreviousSubwordBoundary() {
449      const options = { wordRegex: this.subwordRegExp({ backwards: true }) };
450      const position = this.getPreviousWordBoundaryBufferPosition(options);
451      if (position) this.setBufferPosition(position);
452    }
453  
454    // Public: Moves the cursor to the next subword boundary.
455    moveToNextSubwordBoundary() {
456      const options = { wordRegex: this.subwordRegExp() };
457      const position = this.getNextWordBoundaryBufferPosition(options);
458      if (position) this.setBufferPosition(position);
459    }
460  
461    // Public: Moves the cursor to the beginning of the buffer line, skipping all
462    // whitespace.
463    skipLeadingWhitespace() {
464      const position = this.getBufferPosition();
465      const scanRange = this.getCurrentLineBufferRange();
466      let endOfLeadingWhitespace = null;
467      this.editor.scanInBufferRange(/^[ \t]*/, scanRange, ({ range }) => {
468        endOfLeadingWhitespace = range.end;
469      });
470  
471      if (endOfLeadingWhitespace.isGreaterThan(position))
472        this.setBufferPosition(endOfLeadingWhitespace);
473    }
474  
475    // Public: Moves the cursor to the beginning of the next paragraph
476    moveToBeginningOfNextParagraph() {
477      const position = this.getBeginningOfNextParagraphBufferPosition();
478      if (position) this.setBufferPosition(position);
479    }
480  
481    // Public: Moves the cursor to the beginning of the previous paragraph
482    moveToBeginningOfPreviousParagraph() {
483      const position = this.getBeginningOfPreviousParagraphBufferPosition();
484      if (position) this.setBufferPosition(position);
485    }
486  
487    /*
488    Section: Local Positions and Ranges
489    */
490  
491    // Public: Returns buffer position of previous word boundary. It might be on
492    // the current word, or the previous word.
493    //
494    // * `options` (optional) {Object} with the following keys:
495    //   * `wordRegex` A {RegExp} indicating what constitutes a "word"
496    //      (default: {::wordRegExp})
497    getPreviousWordBoundaryBufferPosition(options = {}) {
498      const currentBufferPosition = this.getBufferPosition();
499      const previousNonBlankRow = this.editor.buffer.previousNonBlankRow(
500        currentBufferPosition.row
501      );
502      const scanRange = Range(
503        Point(previousNonBlankRow || 0, 0),
504        currentBufferPosition
505      );
506  
507      const ranges = this.editor.buffer.findAllInRangeSync(
508        options.wordRegex || this.wordRegExp(),
509        scanRange
510      );
511  
512      const range = ranges[ranges.length - 1];
513      if (range) {
514        if (
515          range.start.row < currentBufferPosition.row &&
516          currentBufferPosition.column > 0
517        ) {
518          return Point(currentBufferPosition.row, 0);
519        } else if (currentBufferPosition.isGreaterThan(range.end)) {
520          return Point.fromObject(range.end);
521        } else {
522          return Point.fromObject(range.start);
523        }
524      } else {
525        return currentBufferPosition;
526      }
527    }
528  
529    // Public: Returns buffer position of the next word boundary. It might be on
530    // the current word, or the previous word.
531    //
532    // * `options` (optional) {Object} with the following keys:
533    //   * `wordRegex` A {RegExp} indicating what constitutes a "word"
534    //      (default: {::wordRegExp})
535    getNextWordBoundaryBufferPosition(options = {}) {
536      const currentBufferPosition = this.getBufferPosition();
537      const scanRange = Range(
538        currentBufferPosition,
539        this.editor.getEofBufferPosition()
540      );
541  
542      const range = this.editor.buffer.findInRangeSync(
543        options.wordRegex || this.wordRegExp(),
544        scanRange
545      );
546  
547      if (range) {
548        if (range.start.row > currentBufferPosition.row) {
549          return Point(range.start.row, 0);
550        } else if (currentBufferPosition.isLessThan(range.start)) {
551          return Point.fromObject(range.start);
552        } else {
553          return Point.fromObject(range.end);
554        }
555      } else {
556        return currentBufferPosition;
557      }
558    }
559  
560    // Public: Retrieves the buffer position of where the current word starts.
561    //
562    // * `options` (optional) An {Object} with the following keys:
563    //   * `wordRegex` A {RegExp} indicating what constitutes a "word"
564    //     (default: {::wordRegExp}).
565    //   * `includeNonWordCharacters` A {Boolean} indicating whether to include
566    //     non-word characters in the default word regex.
567    //     Has no effect if wordRegex is set.
568    //   * `allowPrevious` A {Boolean} indicating whether the beginning of the
569    //     previous word can be returned.
570    //
571    // Returns a {Range}.
572    getBeginningOfCurrentWordBufferPosition(options = {}) {
573      const allowPrevious = options.allowPrevious !== false;
574      const position = this.getBufferPosition();
575  
576      const scanRange = allowPrevious
577        ? new Range(new Point(position.row - 1, 0), position)
578        : new Range(new Point(position.row, 0), position);
579  
580      const ranges = this.editor.buffer.findAllInRangeSync(
581        options.wordRegex || this.wordRegExp(options),
582        scanRange
583      );
584  
585      let result;
586      for (let range of ranges) {
587        if (position.isLessThanOrEqual(range.start)) break;
588        if (allowPrevious || position.isLessThanOrEqual(range.end))
589          result = Point.fromObject(range.start);
590      }
591  
592      return result || (allowPrevious ? new Point(0, 0) : position);
593    }
594  
595    // Public: Retrieves the buffer position of where the current word ends.
596    //
597    // * `options` (optional) {Object} with the following keys:
598    //   * `wordRegex` A {RegExp} indicating what constitutes a "word"
599    //      (default: {::wordRegExp})
600    //   * `includeNonWordCharacters` A Boolean indicating whether to include
601    //     non-word characters in the default word regex. Has no effect if
602    //     wordRegex is set.
603    //
604    // Returns a {Range}.
605    getEndOfCurrentWordBufferPosition(options = {}) {
606      const allowNext = options.allowNext !== false;
607      const position = this.getBufferPosition();
608  
609      const scanRange = allowNext
610        ? new Range(position, new Point(position.row + 2, 0))
611        : new Range(position, new Point(position.row, Infinity));
612  
613      const ranges = this.editor.buffer.findAllInRangeSync(
614        options.wordRegex || this.wordRegExp(options),
615        scanRange
616      );
617  
618      for (let range of ranges) {
619        if (position.isLessThan(range.start) && !allowNext) break;
620        if (position.isLessThan(range.end)) return Point.fromObject(range.end);
621      }
622  
623      return allowNext ? this.editor.getEofBufferPosition() : position;
624    }
625  
626    // Public: Retrieves the buffer position of where the next word starts.
627    //
628    // * `options` (optional) {Object}
629    //   * `wordRegex` A {RegExp} indicating what constitutes a "word"
630    //     (default: {::wordRegExp}).
631    //
632    // Returns a {Range}
633    getBeginningOfNextWordBufferPosition(options = {}) {
634      const currentBufferPosition = this.getBufferPosition();
635      const start = this.isInsideWord(options)
636        ? this.getEndOfCurrentWordBufferPosition(options)
637        : currentBufferPosition;
638      const scanRange = [start, this.editor.getEofBufferPosition()];
639  
640      let beginningOfNextWordPosition;
641      this.editor.scanInBufferRange(
642        options.wordRegex || this.wordRegExp(),
643        scanRange,
644        ({ range, stop }) => {
645          beginningOfNextWordPosition = range.start;
646          stop();
647        }
648      );
649  
650      return beginningOfNextWordPosition || currentBufferPosition;
651    }
652  
653    // Public: Returns the buffer Range occupied by the word located under the cursor.
654    //
655    // * `options` (optional) {Object}
656    //   * `wordRegex` A {RegExp} indicating what constitutes a "word"
657    //     (default: {::wordRegExp}).
658    getCurrentWordBufferRange(options = {}) {
659      const position = this.getBufferPosition();
660      const ranges = this.editor.buffer.findAllInRangeSync(
661        options.wordRegex || this.wordRegExp(options),
662        new Range(new Point(position.row, 0), new Point(position.row, Infinity))
663      );
664      const range = ranges.find(
665        range =>
666          range.end.column >= position.column &&
667          range.start.column <= position.column
668      );
669      return range ? Range.fromObject(range) : new Range(position, position);
670    }
671  
672    // Public: Returns the buffer Range for the current line.
673    //
674    // * `options` (optional) {Object}
675    //   * `includeNewline` A {Boolean} which controls whether the Range should
676    //     include the newline.
677    getCurrentLineBufferRange(options) {
678      return this.editor.bufferRangeForBufferRow(this.getBufferRow(), options);
679    }
680  
681    // Public: Retrieves the range for the current paragraph.
682    //
683    // A paragraph is defined as a block of text surrounded by empty lines or comments.
684    //
685    // Returns a {Range}.
686    getCurrentParagraphBufferRange() {
687      return this.editor.rowRangeForParagraphAtBufferRow(this.getBufferRow());
688    }
689  
690    // Public: Returns the characters preceding the cursor in the current word.
691    getCurrentWordPrefix() {
692      return this.editor.getTextInBufferRange([
693        this.getBeginningOfCurrentWordBufferPosition(),
694        this.getBufferPosition()
695      ]);
696    }
697  
698    /*
699    Section: Visibility
700    */
701  
702    /*
703    Section: Comparing to another cursor
704    */
705  
706    // Public: Compare this cursor's buffer position to another cursor's buffer position.
707    //
708    // See {Point::compare} for more details.
709    //
710    // * `otherCursor`{Cursor} to compare against
711    compare(otherCursor) {
712      return this.getBufferPosition().compare(otherCursor.getBufferPosition());
713    }
714  
715    /*
716    Section: Utilities
717    */
718  
719    // Public: Deselects the current selection.
720    clearSelection(options) {
721      if (this.selection) this.selection.clear(options);
722    }
723  
724    // Public: Get the RegExp used by the cursor to determine what a "word" is.
725    //
726    // * `options` (optional) {Object} with the following keys:
727    //   * `includeNonWordCharacters` A {Boolean} indicating whether to include
728    //     non-word characters in the regex. (default: true)
729    //
730    // Returns a {RegExp}.
731    wordRegExp(options) {
732      const nonWordCharacters = _.escapeRegExp(this.getNonWordCharacters());
733      let source = `^[\t ]*$|[^\\s${nonWordCharacters}]+`;
734      if (!options || options.includeNonWordCharacters !== false) {
735        source += `|${`[${nonWordCharacters}]+`}`;
736      }
737      return new RegExp(source, 'g');
738    }
739  
740    // Public: Get the RegExp used by the cursor to determine what a "subword" is.
741    //
742    // * `options` (optional) {Object} with the following keys:
743    //   * `backwards` A {Boolean} indicating whether to look forwards or backwards
744    //     for the next subword. (default: false)
745    //
746    // Returns a {RegExp}.
747    subwordRegExp(options = {}) {
748      const nonWordCharacters = this.getNonWordCharacters();
749      const lowercaseLetters = 'a-z\\u00DF-\\u00F6\\u00F8-\\u00FF';
750      const uppercaseLetters = 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE';
751      const snakeCamelSegment = `[${uppercaseLetters}]?[${lowercaseLetters}]+`;
752      const segments = [
753        '^[\t ]+',
754        '[\t ]+$',
755        `[${uppercaseLetters}]+(?![${lowercaseLetters}])`,
756        '\\d+'
757      ];
758      if (options.backwards) {
759        segments.push(`${snakeCamelSegment}_*`);
760        segments.push(`[${_.escapeRegExp(nonWordCharacters)}]+\\s*`);
761      } else {
762        segments.push(`_*${snakeCamelSegment}`);
763        segments.push(`\\s*[${_.escapeRegExp(nonWordCharacters)}]+`);
764      }
765      segments.push('_+');
766      return new RegExp(segments.join('|'), 'g');
767    }
768  
769    /*
770    Section: Private
771    */
772  
773    getNonWordCharacters() {
774      return this.editor.getNonWordCharacters(this.getBufferPosition());
775    }
776  
777    changePosition(options, fn) {
778      this.clearSelection({ autoscroll: false });
779      fn();
780      this.goalColumn = null;
781      const autoscroll =
782        options && options.autoscroll != null
783          ? options.autoscroll
784          : this.isLastCursor();
785      if (autoscroll) this.autoscroll();
786    }
787  
788    getScreenRange() {
789      const { row, column } = this.getScreenPosition();
790      return new Range(new Point(row, column), new Point(row, column + 1));
791    }
792  
793    autoscroll(options = {}) {
794      options.clip = false;
795      this.editor.scrollToScreenRange(this.getScreenRange(), options);
796    }
797  
798    getBeginningOfNextParagraphBufferPosition() {
799      const start = this.getBufferPosition();
800      const eof = this.editor.getEofBufferPosition();
801      const scanRange = [start, eof];
802  
803      const { row, column } = eof;
804      let position = new Point(row, column - 1);
805  
806      this.editor.scanInBufferRange(
807        EmptyLineRegExp,
808        scanRange,
809        ({ range, stop }) => {
810          position = range.start.traverse(Point(1, 0));
811          if (!position.isEqual(start)) stop();
812        }
813      );
814      return position;
815    }
816  
817    getBeginningOfPreviousParagraphBufferPosition() {
818      const start = this.getBufferPosition();
819  
820      const { row, column } = start;
821      const scanRange = [[row - 1, column], [0, 0]];
822      let position = new Point(0, 0);
823      this.editor.backwardsScanInBufferRange(
824        EmptyLineRegExp,
825        scanRange,
826        ({ range, stop }) => {
827          position = range.start.traverse(Point(1, 0));
828          if (!position.isEqual(start)) stop();
829        }
830      );
831      return position;
832    }
833  };