/ src / selection.js
selection.js
   1  const { Point, Range } = require('text-buffer');
   2  const { pick } = require('underscore-plus');
   3  const { Emitter } = require('event-kit');
   4  
   5  const NonWhitespaceRegExp = /\S/;
   6  let nextId = 0;
   7  
   8  // Extended: Represents a selection in the {TextEditor}.
   9  module.exports = class Selection {
  10    constructor({ cursor, marker, editor, id }) {
  11      this.id = id != null ? id : nextId++;
  12      this.cursor = cursor;
  13      this.marker = marker;
  14      this.editor = editor;
  15      this.emitter = new Emitter();
  16      this.initialScreenRange = null;
  17      this.wordwise = false;
  18      this.cursor.selection = this;
  19      this.decoration = this.editor.decorateMarker(this.marker, {
  20        type: 'highlight',
  21        class: 'selection'
  22      });
  23      this.marker.onDidChange(e => this.markerDidChange(e));
  24      this.marker.onDidDestroy(() => this.markerDidDestroy());
  25    }
  26  
  27    destroy() {
  28      this.marker.destroy();
  29    }
  30  
  31    isLastSelection() {
  32      return this === this.editor.getLastSelection();
  33    }
  34  
  35    /*
  36    Section: Event Subscription
  37    */
  38  
  39    // Extended: Calls your `callback` when the selection was moved.
  40    //
  41    // * `callback` {Function}
  42    //   * `event` {Object}
  43    //     * `oldBufferRange` {Range}
  44    //     * `oldScreenRange` {Range}
  45    //     * `newBufferRange` {Range}
  46    //     * `newScreenRange` {Range}
  47    //     * `selection` {Selection} that triggered the event
  48    //
  49    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  50    onDidChangeRange(callback) {
  51      return this.emitter.on('did-change-range', callback);
  52    }
  53  
  54    // Extended: Calls your `callback` when the selection was destroyed
  55    //
  56    // * `callback` {Function}
  57    //
  58    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  59    onDidDestroy(callback) {
  60      return this.emitter.once('did-destroy', callback);
  61    }
  62  
  63    /*
  64    Section: Managing the selection range
  65    */
  66  
  67    // Public: Returns the screen {Range} for the selection.
  68    getScreenRange() {
  69      return this.marker.getScreenRange();
  70    }
  71  
  72    // Public: Modifies the screen range for the selection.
  73    //
  74    // * `screenRange` The new {Range} to use.
  75    // * `options` (optional) {Object} options matching those found in {::setBufferRange}.
  76    setScreenRange(screenRange, options) {
  77      return this.setBufferRange(
  78        this.editor.bufferRangeForScreenRange(screenRange),
  79        options
  80      );
  81    }
  82  
  83    // Public: Returns the buffer {Range} for the selection.
  84    getBufferRange() {
  85      return this.marker.getBufferRange();
  86    }
  87  
  88    // Public: Modifies the buffer {Range} for the selection.
  89    //
  90    // * `bufferRange` The new {Range} to select.
  91    // * `options` (optional) {Object} with the keys:
  92    //   * `reversed` {Boolean} indicating whether to set the selection in a
  93    //     reversed orientation.
  94    //   * `preserveFolds` if `true`, the fold settings are preserved after the
  95    //     selection moves.
  96    //   * `autoscroll` {Boolean} indicating whether to autoscroll to the new
  97    //     range. Defaults to `true` if this is the most recently added selection,
  98    //     `false` otherwise.
  99    setBufferRange(bufferRange, options = {}) {
 100      bufferRange = Range.fromObject(bufferRange);
 101      if (options.reversed == null) options.reversed = this.isReversed();
 102      if (!options.preserveFolds)
 103        this.editor.destroyFoldsContainingBufferPositions(
 104          [bufferRange.start, bufferRange.end],
 105          true
 106        );
 107      this.modifySelection(() => {
 108        const needsFlash = options.flash;
 109        options.flash = null;
 110        this.marker.setBufferRange(bufferRange, options);
 111        const autoscroll =
 112          options.autoscroll != null
 113            ? options.autoscroll
 114            : this.isLastSelection();
 115        if (autoscroll) this.autoscroll();
 116        if (needsFlash)
 117          this.decoration.flash('flash', this.editor.selectionFlashDuration);
 118      });
 119    }
 120  
 121    // Public: Returns the starting and ending buffer rows the selection is
 122    // highlighting.
 123    //
 124    // Returns an {Array} of two {Number}s: the starting row, and the ending row.
 125    getBufferRowRange() {
 126      const range = this.getBufferRange();
 127      const start = range.start.row;
 128      let end = range.end.row;
 129      if (range.end.column === 0) end = Math.max(start, end - 1);
 130      return [start, end];
 131    }
 132  
 133    getTailScreenPosition() {
 134      return this.marker.getTailScreenPosition();
 135    }
 136  
 137    getTailBufferPosition() {
 138      return this.marker.getTailBufferPosition();
 139    }
 140  
 141    getHeadScreenPosition() {
 142      return this.marker.getHeadScreenPosition();
 143    }
 144  
 145    getHeadBufferPosition() {
 146      return this.marker.getHeadBufferPosition();
 147    }
 148  
 149    /*
 150    Section: Info about the selection
 151    */
 152  
 153    // Public: Determines if the selection contains anything.
 154    isEmpty() {
 155      return this.getBufferRange().isEmpty();
 156    }
 157  
 158    // Public: Determines if the ending position of a marker is greater than the
 159    // starting position.
 160    //
 161    // This can happen when, for example, you highlight text "up" in a {TextBuffer}.
 162    isReversed() {
 163      return this.marker.isReversed();
 164    }
 165  
 166    // Public: Returns whether the selection is a single line or not.
 167    isSingleScreenLine() {
 168      return this.getScreenRange().isSingleLine();
 169    }
 170  
 171    // Public: Returns the text in the selection.
 172    getText() {
 173      return this.editor.buffer.getTextInRange(this.getBufferRange());
 174    }
 175  
 176    // Public: Identifies if a selection intersects with a given buffer range.
 177    //
 178    // * `bufferRange` A {Range} to check against.
 179    //
 180    // Returns a {Boolean}
 181    intersectsBufferRange(bufferRange) {
 182      return this.getBufferRange().intersectsWith(bufferRange);
 183    }
 184  
 185    intersectsScreenRowRange(startRow, endRow) {
 186      return this.getScreenRange().intersectsRowRange(startRow, endRow);
 187    }
 188  
 189    intersectsScreenRow(screenRow) {
 190      return this.getScreenRange().intersectsRow(screenRow);
 191    }
 192  
 193    // Public: Identifies if a selection intersects with another selection.
 194    //
 195    // * `otherSelection` A {Selection} to check against.
 196    //
 197    // Returns a {Boolean}
 198    intersectsWith(otherSelection, exclusive) {
 199      return this.getBufferRange().intersectsWith(
 200        otherSelection.getBufferRange(),
 201        exclusive
 202      );
 203    }
 204  
 205    /*
 206    Section: Modifying the selected range
 207    */
 208  
 209    // Public: Clears the selection, moving the marker to the head.
 210    //
 211    // * `options` (optional) {Object} with the following keys:
 212    //   * `autoscroll` {Boolean} indicating whether to autoscroll to the new
 213    //     range. Defaults to `true` if this is the most recently added selection,
 214    //     `false` otherwise.
 215    clear(options) {
 216      this.goalScreenRange = null;
 217      if (!this.retainSelection) this.marker.clearTail();
 218      const autoscroll =
 219        options && options.autoscroll != null
 220          ? options.autoscroll
 221          : this.isLastSelection();
 222      if (autoscroll) this.autoscroll();
 223      this.finalize();
 224    }
 225  
 226    // Public: Selects the text from the current cursor position to a given screen
 227    // position.
 228    //
 229    // * `position` An instance of {Point}, with a given `row` and `column`.
 230    selectToScreenPosition(position, options) {
 231      position = Point.fromObject(position);
 232  
 233      this.modifySelection(() => {
 234        if (this.initialScreenRange) {
 235          if (position.isLessThan(this.initialScreenRange.start)) {
 236            this.marker.setScreenRange([position, this.initialScreenRange.end], {
 237              reversed: true
 238            });
 239          } else {
 240            this.marker.setScreenRange(
 241              [this.initialScreenRange.start, position],
 242              { reversed: false }
 243            );
 244          }
 245        } else {
 246          this.cursor.setScreenPosition(position, options);
 247        }
 248  
 249        if (this.linewise) {
 250          this.expandOverLine(options);
 251        } else if (this.wordwise) {
 252          this.expandOverWord(options);
 253        }
 254      });
 255    }
 256  
 257    // Public: Selects the text from the current cursor position to a given buffer
 258    // position.
 259    //
 260    // * `position` An instance of {Point}, with a given `row` and `column`.
 261    selectToBufferPosition(position) {
 262      this.modifySelection(() => this.cursor.setBufferPosition(position));
 263    }
 264  
 265    // Public: Selects the text one position right of the cursor.
 266    //
 267    // * `columnCount` (optional) {Number} number of columns to select (default: 1)
 268    selectRight(columnCount) {
 269      this.modifySelection(() => this.cursor.moveRight(columnCount));
 270    }
 271  
 272    // Public: Selects the text one position left of the cursor.
 273    //
 274    // * `columnCount` (optional) {Number} number of columns to select (default: 1)
 275    selectLeft(columnCount) {
 276      this.modifySelection(() => this.cursor.moveLeft(columnCount));
 277    }
 278  
 279    // Public: Selects all the text one position above the cursor.
 280    //
 281    // * `rowCount` (optional) {Number} number of rows to select (default: 1)
 282    selectUp(rowCount) {
 283      this.modifySelection(() => this.cursor.moveUp(rowCount));
 284    }
 285  
 286    // Public: Selects all the text one position below the cursor.
 287    //
 288    // * `rowCount` (optional) {Number} number of rows to select (default: 1)
 289    selectDown(rowCount) {
 290      this.modifySelection(() => this.cursor.moveDown(rowCount));
 291    }
 292  
 293    // Public: Selects all the text from the current cursor position to the top of
 294    // the buffer.
 295    selectToTop() {
 296      this.modifySelection(() => this.cursor.moveToTop());
 297    }
 298  
 299    // Public: Selects all the text from the current cursor position to the bottom
 300    // of the buffer.
 301    selectToBottom() {
 302      this.modifySelection(() => this.cursor.moveToBottom());
 303    }
 304  
 305    // Public: Selects all the text in the buffer.
 306    selectAll() {
 307      this.setBufferRange(this.editor.buffer.getRange(), { autoscroll: false });
 308    }
 309  
 310    // Public: Selects all the text from the current cursor position to the
 311    // beginning of the line.
 312    selectToBeginningOfLine() {
 313      this.modifySelection(() => this.cursor.moveToBeginningOfLine());
 314    }
 315  
 316    // Public: Selects all the text from the current cursor position to the first
 317    // character of the line.
 318    selectToFirstCharacterOfLine() {
 319      this.modifySelection(() => this.cursor.moveToFirstCharacterOfLine());
 320    }
 321  
 322    // Public: Selects all the text from the current cursor position to the end of
 323    // the screen line.
 324    selectToEndOfLine() {
 325      this.modifySelection(() => this.cursor.moveToEndOfScreenLine());
 326    }
 327  
 328    // Public: Selects all the text from the current cursor position to the end of
 329    // the buffer line.
 330    selectToEndOfBufferLine() {
 331      this.modifySelection(() => this.cursor.moveToEndOfLine());
 332    }
 333  
 334    // Public: Selects all the text from the current cursor position to the
 335    // beginning of the word.
 336    selectToBeginningOfWord() {
 337      this.modifySelection(() => this.cursor.moveToBeginningOfWord());
 338    }
 339  
 340    // Public: Selects all the text from the current cursor position to the end of
 341    // the word.
 342    selectToEndOfWord() {
 343      this.modifySelection(() => this.cursor.moveToEndOfWord());
 344    }
 345  
 346    // Public: Selects all the text from the current cursor position to the
 347    // beginning of the next word.
 348    selectToBeginningOfNextWord() {
 349      this.modifySelection(() => this.cursor.moveToBeginningOfNextWord());
 350    }
 351  
 352    // Public: Selects text to the previous word boundary.
 353    selectToPreviousWordBoundary() {
 354      this.modifySelection(() => this.cursor.moveToPreviousWordBoundary());
 355    }
 356  
 357    // Public: Selects text to the next word boundary.
 358    selectToNextWordBoundary() {
 359      this.modifySelection(() => this.cursor.moveToNextWordBoundary());
 360    }
 361  
 362    // Public: Selects text to the previous subword boundary.
 363    selectToPreviousSubwordBoundary() {
 364      this.modifySelection(() => this.cursor.moveToPreviousSubwordBoundary());
 365    }
 366  
 367    // Public: Selects text to the next subword boundary.
 368    selectToNextSubwordBoundary() {
 369      this.modifySelection(() => this.cursor.moveToNextSubwordBoundary());
 370    }
 371  
 372    // Public: Selects all the text from the current cursor position to the
 373    // beginning of the next paragraph.
 374    selectToBeginningOfNextParagraph() {
 375      this.modifySelection(() => this.cursor.moveToBeginningOfNextParagraph());
 376    }
 377  
 378    // Public: Selects all the text from the current cursor position to the
 379    // beginning of the previous paragraph.
 380    selectToBeginningOfPreviousParagraph() {
 381      this.modifySelection(() =>
 382        this.cursor.moveToBeginningOfPreviousParagraph()
 383      );
 384    }
 385  
 386    // Public: Modifies the selection to encompass the current word.
 387    //
 388    // Returns a {Range}.
 389    selectWord(options = {}) {
 390      if (this.cursor.isSurroundedByWhitespace()) options.wordRegex = /[\t ]*/;
 391      if (this.cursor.isBetweenWordAndNonWord()) {
 392        options.includeNonWordCharacters = false;
 393      }
 394  
 395      this.setBufferRange(
 396        this.cursor.getCurrentWordBufferRange(options),
 397        options
 398      );
 399      this.wordwise = true;
 400      this.initialScreenRange = this.getScreenRange();
 401    }
 402  
 403    // Public: Expands the newest selection to include the entire word on which
 404    // the cursors rests.
 405    expandOverWord(options) {
 406      this.setBufferRange(
 407        this.getBufferRange().union(this.cursor.getCurrentWordBufferRange()),
 408        { autoscroll: false }
 409      );
 410      const autoscroll =
 411        options && options.autoscroll != null
 412          ? options.autoscroll
 413          : this.isLastSelection();
 414      if (autoscroll) this.cursor.autoscroll();
 415    }
 416  
 417    // Public: Selects an entire line in the buffer.
 418    //
 419    // * `row` The line {Number} to select (default: the row of the cursor).
 420    selectLine(row, options) {
 421      if (row != null) {
 422        this.setBufferRange(
 423          this.editor.bufferRangeForBufferRow(row, { includeNewline: true }),
 424          options
 425        );
 426      } else {
 427        const startRange = this.editor.bufferRangeForBufferRow(
 428          this.marker.getStartBufferPosition().row
 429        );
 430        const endRange = this.editor.bufferRangeForBufferRow(
 431          this.marker.getEndBufferPosition().row,
 432          { includeNewline: true }
 433        );
 434        this.setBufferRange(startRange.union(endRange), options);
 435      }
 436  
 437      this.linewise = true;
 438      this.wordwise = false;
 439      this.initialScreenRange = this.getScreenRange();
 440    }
 441  
 442    // Public: Expands the newest selection to include the entire line on which
 443    // the cursor currently rests.
 444    //
 445    // It also includes the newline character.
 446    expandOverLine(options) {
 447      const range = this.getBufferRange().union(
 448        this.cursor.getCurrentLineBufferRange({ includeNewline: true })
 449      );
 450      this.setBufferRange(range, { autoscroll: false });
 451      const autoscroll =
 452        options && options.autoscroll != null
 453          ? options.autoscroll
 454          : this.isLastSelection();
 455      if (autoscroll) this.cursor.autoscroll();
 456    }
 457  
 458    // Private: Ensure that the {TextEditor} is not marked read-only before allowing a buffer modification to occur. if
 459    // the editor is read-only, require an explicit opt-in option to proceed (`bypassReadOnly`) or throw an Error.
 460    ensureWritable(methodName, opts) {
 461      if (!opts.bypassReadOnly && this.editor.isReadOnly()) {
 462        if (atom.inDevMode() || atom.inSpecMode()) {
 463          const e = new Error(
 464            'Attempt to mutate a read-only TextEditor through a Selection'
 465          );
 466          e.detail =
 467            `Your package is attempting to call ${methodName} on a selection within an editor that has been marked ` +
 468            ' read-only. Pass {bypassReadOnly: true} to modify it anyway, or test editors with .isReadOnly() before ' +
 469            ' attempting modifications.';
 470          throw e;
 471        }
 472  
 473        return false;
 474      }
 475  
 476      return true;
 477    }
 478  
 479    /*
 480    Section: Modifying the selected text
 481    */
 482  
 483    // Public: Replaces text at the current selection.
 484    //
 485    // * `text` A {String} representing the text to add
 486    // * `options` (optional) {Object} with keys:
 487    //   * `select` If `true`, selects the newly added text.
 488    //   * `autoIndent` If `true`, indents all inserted text appropriately.
 489    //   * `autoIndentNewline` If `true`, indent newline appropriately.
 490    //   * `autoDecreaseIndent` If `true`, decreases indent level appropriately
 491    //     (for example, when a closing bracket is inserted).
 492    //   * `preserveTrailingLineIndentation` By default, when pasting multiple
 493    //   lines, Atom attempts to preserve the relative indent level between the
 494    //   first line and trailing lines, even if the indent level of the first
 495    //   line has changed from the copied text. If this option is `true`, this
 496    //   behavior is suppressed.
 497    //     level between the first lines and the trailing lines.
 498    //   * `normalizeLineEndings` (optional) {Boolean} (default: true)
 499    //   * `undo` *Deprecated* If `skip`, skips the undo stack for this operation. This property is deprecated. Call groupLastChanges() on the {TextBuffer} afterward instead.
 500    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
 501    insertText(text, options = {}) {
 502      if (!this.ensureWritable('insertText', options)) return;
 503  
 504      let desiredIndentLevel, indentAdjustment;
 505      const oldBufferRange = this.getBufferRange();
 506      const wasReversed = this.isReversed();
 507      this.clear(options);
 508  
 509      let autoIndentFirstLine = false;
 510      const precedingText = this.editor.getTextInRange([
 511        [oldBufferRange.start.row, 0],
 512        oldBufferRange.start
 513      ]);
 514      const remainingLines = text.split('\n');
 515      const firstInsertedLine = remainingLines.shift();
 516  
 517      if (
 518        options.indentBasis != null &&
 519        !options.preserveTrailingLineIndentation
 520      ) {
 521        indentAdjustment =
 522          this.editor.indentLevelForLine(precedingText) - options.indentBasis;
 523        this.adjustIndent(remainingLines, indentAdjustment);
 524      }
 525  
 526      const textIsAutoIndentable =
 527        text === '\n' || text === '\r\n' || NonWhitespaceRegExp.test(text);
 528      if (
 529        options.autoIndent &&
 530        textIsAutoIndentable &&
 531        !NonWhitespaceRegExp.test(precedingText) &&
 532        remainingLines.length > 0
 533      ) {
 534        autoIndentFirstLine = true;
 535        const firstLine = precedingText + firstInsertedLine;
 536        const languageMode = this.editor.buffer.getLanguageMode();
 537        desiredIndentLevel =
 538          languageMode.suggestedIndentForLineAtBufferRow &&
 539          languageMode.suggestedIndentForLineAtBufferRow(
 540            oldBufferRange.start.row,
 541            firstLine,
 542            this.editor.getTabLength()
 543          );
 544        if (desiredIndentLevel != null) {
 545          indentAdjustment =
 546            desiredIndentLevel - this.editor.indentLevelForLine(firstLine);
 547          this.adjustIndent(remainingLines, indentAdjustment);
 548        }
 549      }
 550  
 551      text = firstInsertedLine;
 552      if (remainingLines.length > 0) text += `\n${remainingLines.join('\n')}`;
 553  
 554      const newBufferRange = this.editor.buffer.setTextInRange(
 555        oldBufferRange,
 556        text,
 557        pick(options, 'undo', 'normalizeLineEndings')
 558      );
 559  
 560      if (options.select) {
 561        this.setBufferRange(newBufferRange, { reversed: wasReversed });
 562      } else {
 563        if (wasReversed) this.cursor.setBufferPosition(newBufferRange.end);
 564      }
 565  
 566      if (autoIndentFirstLine) {
 567        this.editor.setIndentationForBufferRow(
 568          oldBufferRange.start.row,
 569          desiredIndentLevel
 570        );
 571      }
 572  
 573      if (options.autoIndentNewline && text === '\n') {
 574        this.editor.autoIndentBufferRow(newBufferRange.end.row, {
 575          preserveLeadingWhitespace: true,
 576          skipBlankLines: false
 577        });
 578      } else if (options.autoDecreaseIndent && NonWhitespaceRegExp.test(text)) {
 579        this.editor.autoDecreaseIndentForBufferRow(newBufferRange.start.row);
 580      }
 581  
 582      const autoscroll =
 583        options.autoscroll != null ? options.autoscroll : this.isLastSelection();
 584      if (autoscroll) this.autoscroll();
 585  
 586      return newBufferRange;
 587    }
 588  
 589    // Public: Removes the first character before the selection if the selection
 590    // is empty otherwise it deletes the selection.
 591    //
 592    // * `options` (optional) {Object}
 593    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 594    backspace(options = {}) {
 595      if (!this.ensureWritable('backspace', options)) return;
 596      if (this.isEmpty()) this.selectLeft();
 597      this.deleteSelectedText(options);
 598    }
 599  
 600    // Public: Removes the selection or, if nothing is selected, then all
 601    // characters from the start of the selection back to the previous word
 602    // boundary.
 603    //
 604    // * `options` (optional) {Object}
 605    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 606    deleteToPreviousWordBoundary(options = {}) {
 607      if (!this.ensureWritable('deleteToPreviousWordBoundary', options)) return;
 608      if (this.isEmpty()) this.selectToPreviousWordBoundary();
 609      this.deleteSelectedText(options);
 610    }
 611  
 612    // Public: Removes the selection or, if nothing is selected, then all
 613    // characters from the start of the selection up to the next word
 614    // boundary.
 615    //
 616    // * `options` (optional) {Object}
 617    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 618    deleteToNextWordBoundary(options = {}) {
 619      if (!this.ensureWritable('deleteToNextWordBoundary', options)) return;
 620      if (this.isEmpty()) this.selectToNextWordBoundary();
 621      this.deleteSelectedText(options);
 622    }
 623  
 624    // Public: Removes from the start of the selection to the beginning of the
 625    // current word if the selection is empty otherwise it deletes the selection.
 626    //
 627    // * `options` (optional) {Object}
 628    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 629    deleteToBeginningOfWord(options = {}) {
 630      if (!this.ensureWritable('deleteToBeginningOfWord', options)) return;
 631      if (this.isEmpty()) this.selectToBeginningOfWord();
 632      this.deleteSelectedText(options);
 633    }
 634  
 635    // Public: Removes from the beginning of the line which the selection begins on
 636    // all the way through to the end of the selection.
 637    //
 638    // * `options` (optional) {Object}
 639    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 640    deleteToBeginningOfLine(options = {}) {
 641      if (!this.ensureWritable('deleteToBeginningOfLine', options)) return;
 642      if (this.isEmpty() && this.cursor.isAtBeginningOfLine()) {
 643        this.selectLeft();
 644      } else {
 645        this.selectToBeginningOfLine();
 646      }
 647      this.deleteSelectedText(options);
 648    }
 649  
 650    // Public: Removes the selection or the next character after the start of the
 651    // selection if the selection is empty.
 652    //
 653    // * `options` (optional) {Object}
 654    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 655    delete(options = {}) {
 656      if (!this.ensureWritable('delete', options)) return;
 657      if (this.isEmpty()) this.selectRight();
 658      this.deleteSelectedText(options);
 659    }
 660  
 661    // Public: If the selection is empty, removes all text from the cursor to the
 662    // end of the line. If the cursor is already at the end of the line, it
 663    // removes the following newline. If the selection isn't empty, only deletes
 664    // the contents of the selection.
 665    //
 666    // * `options` (optional) {Object}
 667    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 668    deleteToEndOfLine(options = {}) {
 669      if (!this.ensureWritable('deleteToEndOfLine', options)) return;
 670      if (this.isEmpty()) {
 671        if (this.cursor.isAtEndOfLine()) {
 672          this.delete(options);
 673          return;
 674        }
 675        this.selectToEndOfLine();
 676      }
 677      this.deleteSelectedText(options);
 678    }
 679  
 680    // Public: Removes the selection or all characters from the start of the
 681    // selection to the end of the current word if nothing is selected.
 682    //
 683    // * `options` (optional) {Object}
 684    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 685    deleteToEndOfWord(options = {}) {
 686      if (!this.ensureWritable('deleteToEndOfWord', options)) return;
 687      if (this.isEmpty()) this.selectToEndOfWord();
 688      this.deleteSelectedText(options);
 689    }
 690  
 691    // Public: Removes the selection or all characters from the start of the
 692    // selection to the end of the current word if nothing is selected.
 693    //
 694    // * `options` (optional) {Object}
 695    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 696    deleteToBeginningOfSubword(options = {}) {
 697      if (!this.ensureWritable('deleteToBeginningOfSubword', options)) return;
 698      if (this.isEmpty()) this.selectToPreviousSubwordBoundary();
 699      this.deleteSelectedText(options);
 700    }
 701  
 702    // Public: Removes the selection or all characters from the start of the
 703    // selection to the end of the current word if nothing is selected.
 704    //
 705    // * `options` (optional) {Object}
 706    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 707    deleteToEndOfSubword(options = {}) {
 708      if (!this.ensureWritable('deleteToEndOfSubword', options)) return;
 709      if (this.isEmpty()) this.selectToNextSubwordBoundary();
 710      this.deleteSelectedText(options);
 711    }
 712  
 713    // Public: Removes only the selected text.
 714    //
 715    // * `options` (optional) {Object}
 716    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 717    deleteSelectedText(options = {}) {
 718      if (!this.ensureWritable('deleteSelectedText', options)) return;
 719      const bufferRange = this.getBufferRange();
 720      if (!bufferRange.isEmpty()) this.editor.buffer.delete(bufferRange);
 721      if (this.cursor) this.cursor.setBufferPosition(bufferRange.start);
 722    }
 723  
 724    // Public: Removes the line at the beginning of the selection if the selection
 725    // is empty unless the selection spans multiple lines in which case all lines
 726    // are removed.
 727    //
 728    // * `options` (optional) {Object}
 729    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 730    deleteLine(options = {}) {
 731      if (!this.ensureWritable('deleteLine', options)) return;
 732      const range = this.getBufferRange();
 733      if (range.isEmpty()) {
 734        const start = this.cursor.getScreenRow();
 735        const range = this.editor.bufferRowsForScreenRows(start, start + 1);
 736        if (range[1] > range[0]) {
 737          this.editor.buffer.deleteRows(range[0], range[1] - 1);
 738        } else {
 739          this.editor.buffer.deleteRow(range[0]);
 740        }
 741      } else {
 742        const start = range.start.row;
 743        let end = range.end.row;
 744        if (end !== this.editor.buffer.getLastRow() && range.end.column === 0)
 745          end--;
 746        this.editor.buffer.deleteRows(start, end);
 747      }
 748      this.cursor.setBufferPosition({
 749        row: this.cursor.getBufferRow(),
 750        column: range.start.column
 751      });
 752    }
 753  
 754    // Public: Joins the current line with the one below it. Lines will
 755    // be separated by a single space.
 756    //
 757    // If there selection spans more than one line, all the lines are joined together.
 758    //
 759    // * `options` (optional) {Object}
 760    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 761    joinLines(options = {}) {
 762      if (!this.ensureWritable('joinLines', options)) return;
 763      let joinMarker;
 764      const selectedRange = this.getBufferRange();
 765      if (selectedRange.isEmpty()) {
 766        if (selectedRange.start.row === this.editor.buffer.getLastRow()) return;
 767      } else {
 768        joinMarker = this.editor.markBufferRange(selectedRange, {
 769          invalidate: 'never'
 770        });
 771      }
 772  
 773      const rowCount = Math.max(1, selectedRange.getRowCount() - 1);
 774      for (let i = 0; i < rowCount; i++) {
 775        this.cursor.setBufferPosition([selectedRange.start.row]);
 776        this.cursor.moveToEndOfLine();
 777  
 778        // Remove trailing whitespace from the current line
 779        const scanRange = this.cursor.getCurrentLineBufferRange();
 780        let trailingWhitespaceRange = null;
 781        this.editor.scanInBufferRange(/[ \t]+$/, scanRange, ({ range }) => {
 782          trailingWhitespaceRange = range;
 783        });
 784        if (trailingWhitespaceRange) {
 785          this.setBufferRange(trailingWhitespaceRange);
 786          this.deleteSelectedText(options);
 787        }
 788  
 789        const currentRow = selectedRange.start.row;
 790        const nextRow = currentRow + 1;
 791        const insertSpace =
 792          nextRow <= this.editor.buffer.getLastRow() &&
 793          this.editor.buffer.lineLengthForRow(nextRow) > 0 &&
 794          this.editor.buffer.lineLengthForRow(currentRow) > 0;
 795        if (insertSpace) this.insertText(' ', options);
 796  
 797        this.cursor.moveToEndOfLine();
 798  
 799        // Remove leading whitespace from the line below
 800        this.modifySelection(() => {
 801          this.cursor.moveRight();
 802          this.cursor.moveToFirstCharacterOfLine();
 803        });
 804        this.deleteSelectedText(options);
 805  
 806        if (insertSpace) this.cursor.moveLeft();
 807      }
 808  
 809      if (joinMarker) {
 810        const newSelectedRange = joinMarker.getBufferRange();
 811        this.setBufferRange(newSelectedRange);
 812        joinMarker.destroy();
 813      }
 814    }
 815  
 816    // Public: Removes one level of indent from the currently selected rows.
 817    //
 818    // * `options` (optional) {Object}
 819    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 820    outdentSelectedRows(options = {}) {
 821      if (!this.ensureWritable('outdentSelectedRows', options)) return;
 822      const [start, end] = this.getBufferRowRange();
 823      const { buffer } = this.editor;
 824      const leadingTabRegex = new RegExp(
 825        `^( {1,${this.editor.getTabLength()}}|\t)`
 826      );
 827      for (let row = start; row <= end; row++) {
 828        const match = buffer.lineForRow(row).match(leadingTabRegex);
 829        if (match && match[0].length > 0) {
 830          buffer.delete([[row, 0], [row, match[0].length]]);
 831        }
 832      }
 833    }
 834  
 835    // Public: Sets the indentation level of all selected rows to values suggested
 836    // by the relevant grammars.
 837    //
 838    // * `options` (optional) {Object}
 839    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 840    autoIndentSelectedRows(options = {}) {
 841      if (!this.ensureWritable('autoIndentSelectedRows', options)) return;
 842      const [start, end] = this.getBufferRowRange();
 843      return this.editor.autoIndentBufferRows(start, end);
 844    }
 845  
 846    // Public: Wraps the selected lines in comments if they aren't currently part
 847    // of a comment.
 848    //
 849    // Removes the comment if they are currently wrapped in a comment.
 850    //
 851    // * `options` (optional) {Object}
 852    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 853    toggleLineComments(options = {}) {
 854      if (!this.ensureWritable('toggleLineComments', options)) return;
 855      let bufferRowRange = this.getBufferRowRange() || [null, null];
 856      this.editor.toggleLineCommentsForBufferRows(...bufferRowRange, {
 857        correctSelection: true,
 858        selection: this
 859      });
 860    }
 861  
 862    // Public: Cuts the selection until the end of the screen line.
 863    //
 864    // * `maintainClipboard` {Boolean}
 865    // * `options` (optional) {Object}
 866    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 867    cutToEndOfLine(maintainClipboard, options = {}) {
 868      if (!this.ensureWritable('cutToEndOfLine', options)) return;
 869      if (this.isEmpty()) this.selectToEndOfLine();
 870      return this.cut(maintainClipboard, false, options.bypassReadOnly);
 871    }
 872  
 873    // Public: Cuts the selection until the end of the buffer line.
 874    //
 875    // * `maintainClipboard` {Boolean}
 876    // * `options` (optional) {Object}
 877    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 878    cutToEndOfBufferLine(maintainClipboard, options = {}) {
 879      if (!this.ensureWritable('cutToEndOfBufferLine', options)) return;
 880      if (this.isEmpty()) this.selectToEndOfBufferLine();
 881      this.cut(maintainClipboard, false, options.bypassReadOnly);
 882    }
 883  
 884    // Public: Copies the selection to the clipboard and then deletes it.
 885    //
 886    // * `maintainClipboard` {Boolean} (default: false) See {::copy}
 887    // * `fullLine` {Boolean} (default: false) See {::copy}
 888    // * `bypassReadOnly` {Boolean} (default: false) Must be `true` to modify text within a read-only editor.
 889    cut(maintainClipboard = false, fullLine = false, bypassReadOnly = false) {
 890      if (!this.ensureWritable('cut', { bypassReadOnly })) return;
 891      this.copy(maintainClipboard, fullLine);
 892      this.delete({ bypassReadOnly });
 893    }
 894  
 895    // Public: Copies the current selection to the clipboard.
 896    //
 897    // * `maintainClipboard` {Boolean} if `true`, a specific metadata property
 898    //   is created to store each content copied to the clipboard. The clipboard
 899    //   `text` still contains the concatenation of the clipboard with the
 900    //   current selection. (default: false)
 901    // * `fullLine` {Boolean} if `true`, the copied text will always be pasted
 902    //   at the beginning of the line containing the cursor, regardless of the
 903    //   cursor's horizontal position. (default: false)
 904    copy(maintainClipboard = false, fullLine = false) {
 905      if (this.isEmpty()) return;
 906      const { start, end } = this.getBufferRange();
 907      const selectionText = this.editor.getTextInRange([start, end]);
 908      const precedingText = this.editor.getTextInRange([[start.row, 0], start]);
 909      const startLevel = this.editor.indentLevelForLine(precedingText);
 910  
 911      if (maintainClipboard) {
 912        let {
 913          text: clipboardText,
 914          metadata
 915        } = this.editor.constructor.clipboard.readWithMetadata();
 916        if (!metadata) metadata = {};
 917        if (!metadata.selections) {
 918          metadata.selections = [
 919            {
 920              text: clipboardText,
 921              indentBasis: metadata.indentBasis,
 922              fullLine: metadata.fullLine
 923            }
 924          ];
 925        }
 926        metadata.selections.push({
 927          text: selectionText,
 928          indentBasis: startLevel,
 929          fullLine
 930        });
 931        this.editor.constructor.clipboard.write(
 932          [clipboardText, selectionText].join('\n'),
 933          metadata
 934        );
 935      } else {
 936        this.editor.constructor.clipboard.write(selectionText, {
 937          indentBasis: startLevel,
 938          fullLine
 939        });
 940      }
 941    }
 942  
 943    // Public: Creates a fold containing the current selection.
 944    fold() {
 945      const range = this.getBufferRange();
 946      if (!range.isEmpty()) {
 947        this.editor.foldBufferRange(range);
 948        this.cursor.setBufferPosition(range.end);
 949      }
 950    }
 951  
 952    // Private: Increase the indentation level of the given text by given number
 953    // of levels. Leaves the first line unchanged.
 954    adjustIndent(lines, indentAdjustment) {
 955      for (let i = 0; i < lines.length; i++) {
 956        const line = lines[i];
 957        if (indentAdjustment === 0 || line === '') {
 958          continue;
 959        } else if (indentAdjustment > 0) {
 960          lines[i] = this.editor.buildIndentString(indentAdjustment) + line;
 961        } else {
 962          const currentIndentLevel = this.editor.indentLevelForLine(lines[i]);
 963          const indentLevel = Math.max(0, currentIndentLevel + indentAdjustment);
 964          lines[i] = line.replace(
 965            /^[\t ]+/,
 966            this.editor.buildIndentString(indentLevel)
 967          );
 968        }
 969      }
 970    }
 971  
 972    // Indent the current line(s).
 973    //
 974    // If the selection is empty, indents the current line if the cursor precedes
 975    // non-whitespace characters, and otherwise inserts a tab. If the selection is
 976    // non empty, calls {::indentSelectedRows}.
 977    //
 978    // * `options` (optional) {Object} with the keys:
 979    //   * `autoIndent` If `true`, the line is indented to an automatically-inferred
 980    //     level. Otherwise, {TextEditor::getTabText} is inserted.
 981    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
 982    indent({ autoIndent, bypassReadOnly } = {}) {
 983      if (!this.ensureWritable('indent', { bypassReadOnly })) return;
 984      const { row } = this.cursor.getBufferPosition();
 985  
 986      if (this.isEmpty()) {
 987        this.cursor.skipLeadingWhitespace();
 988        const desiredIndent = this.editor.suggestedIndentForBufferRow(row);
 989        let delta = desiredIndent - this.cursor.getIndentLevel();
 990  
 991        if (autoIndent && delta > 0) {
 992          if (!this.editor.getSoftTabs()) delta = Math.max(delta, 1);
 993          this.insertText(this.editor.buildIndentString(delta), {
 994            bypassReadOnly
 995          });
 996        } else {
 997          this.insertText(
 998            this.editor.buildIndentString(1, this.cursor.getBufferColumn()),
 999            { bypassReadOnly }
1000          );
1001        }
1002      } else {
1003        this.indentSelectedRows({ bypassReadOnly });
1004      }
1005    }
1006  
1007    // Public: If the selection spans multiple rows, indent all of them.
1008    //
1009    // * `options` (optional) {Object} with the keys:
1010    //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
1011    indentSelectedRows(options = {}) {
1012      if (!this.ensureWritable('indentSelectedRows', options)) return;
1013      const [start, end] = this.getBufferRowRange();
1014      for (let row = start; row <= end; row++) {
1015        if (this.editor.buffer.lineLengthForRow(row) !== 0) {
1016          this.editor.buffer.insert([row, 0], this.editor.getTabText());
1017        }
1018      }
1019    }
1020  
1021    /*
1022    Section: Managing multiple selections
1023    */
1024  
1025    // Public: Moves the selection down one row.
1026    addSelectionBelow() {
1027      const range = this.getGoalScreenRange().copy();
1028      const nextRow = range.end.row + 1;
1029  
1030      for (
1031        let row = nextRow, end = this.editor.getLastScreenRow();
1032        row <= end;
1033        row++
1034      ) {
1035        range.start.row = row;
1036        range.end.row = row;
1037        const clippedRange = this.editor.clipScreenRange(range, {
1038          skipSoftWrapIndentation: true
1039        });
1040  
1041        if (range.isEmpty()) {
1042          if (range.end.column > 0 && clippedRange.end.column === 0) continue;
1043        } else {
1044          if (clippedRange.isEmpty()) continue;
1045        }
1046  
1047        const containingSelections = this.editor.selectionsMarkerLayer.findMarkers(
1048          { containsScreenRange: clippedRange }
1049        );
1050        if (containingSelections.length === 0) {
1051          const selection = this.editor.addSelectionForScreenRange(clippedRange);
1052          selection.setGoalScreenRange(range);
1053        }
1054  
1055        break;
1056      }
1057    }
1058  
1059    // Public: Moves the selection up one row.
1060    addSelectionAbove() {
1061      const range = this.getGoalScreenRange().copy();
1062      const previousRow = range.end.row - 1;
1063  
1064      for (let row = previousRow; row >= 0; row--) {
1065        range.start.row = row;
1066        range.end.row = row;
1067        const clippedRange = this.editor.clipScreenRange(range, {
1068          skipSoftWrapIndentation: true
1069        });
1070  
1071        if (range.isEmpty()) {
1072          if (range.end.column > 0 && clippedRange.end.column === 0) continue;
1073        } else {
1074          if (clippedRange.isEmpty()) continue;
1075        }
1076  
1077        const containingSelections = this.editor.selectionsMarkerLayer.findMarkers(
1078          { containsScreenRange: clippedRange }
1079        );
1080        if (containingSelections.length === 0) {
1081          const selection = this.editor.addSelectionForScreenRange(clippedRange);
1082          selection.setGoalScreenRange(range);
1083        }
1084  
1085        break;
1086      }
1087    }
1088  
1089    // Public: Combines the given selection into this selection and then destroys
1090    // the given selection.
1091    //
1092    // * `otherSelection` A {Selection} to merge with.
1093    // * `options` (optional) {Object} options matching those found in {::setBufferRange}.
1094    merge(otherSelection, options = {}) {
1095      const myGoalScreenRange = this.getGoalScreenRange();
1096      const otherGoalScreenRange = otherSelection.getGoalScreenRange();
1097  
1098      if (myGoalScreenRange && otherGoalScreenRange) {
1099        options.goalScreenRange = myGoalScreenRange.union(otherGoalScreenRange);
1100      } else {
1101        options.goalScreenRange = myGoalScreenRange || otherGoalScreenRange;
1102      }
1103  
1104      const bufferRange = this.getBufferRange().union(
1105        otherSelection.getBufferRange()
1106      );
1107      this.setBufferRange(
1108        bufferRange,
1109        Object.assign({ autoscroll: false }, options)
1110      );
1111      otherSelection.destroy();
1112    }
1113  
1114    /*
1115    Section: Comparing to other selections
1116    */
1117  
1118    // Public: Compare this selection's buffer range to another selection's buffer
1119    // range.
1120    //
1121    // See {Range::compare} for more details.
1122    //
1123    // * `otherSelection` A {Selection} to compare against
1124    compare(otherSelection) {
1125      return this.marker.compare(otherSelection.marker);
1126    }
1127  
1128    /*
1129    Section: Private Utilities
1130    */
1131  
1132    setGoalScreenRange(range) {
1133      this.goalScreenRange = Range.fromObject(range);
1134    }
1135  
1136    getGoalScreenRange() {
1137      return this.goalScreenRange || this.getScreenRange();
1138    }
1139  
1140    markerDidChange(e) {
1141      const {
1142        oldHeadBufferPosition,
1143        oldTailBufferPosition,
1144        newHeadBufferPosition
1145      } = e;
1146      const {
1147        oldHeadScreenPosition,
1148        oldTailScreenPosition,
1149        newHeadScreenPosition
1150      } = e;
1151      const { textChanged } = e;
1152  
1153      if (!oldHeadScreenPosition.isEqual(newHeadScreenPosition)) {
1154        this.cursor.goalColumn = null;
1155        const cursorMovedEvent = {
1156          oldBufferPosition: oldHeadBufferPosition,
1157          oldScreenPosition: oldHeadScreenPosition,
1158          newBufferPosition: newHeadBufferPosition,
1159          newScreenPosition: newHeadScreenPosition,
1160          textChanged,
1161          cursor: this.cursor
1162        };
1163        this.cursor.emitter.emit('did-change-position', cursorMovedEvent);
1164        this.editor.cursorMoved(cursorMovedEvent);
1165      }
1166  
1167      const rangeChangedEvent = {
1168        oldBufferRange: new Range(oldHeadBufferPosition, oldTailBufferPosition),
1169        oldScreenRange: new Range(oldHeadScreenPosition, oldTailScreenPosition),
1170        newBufferRange: this.getBufferRange(),
1171        newScreenRange: this.getScreenRange(),
1172        selection: this
1173      };
1174      this.emitter.emit('did-change-range', rangeChangedEvent);
1175      this.editor.selectionRangeChanged(rangeChangedEvent);
1176    }
1177  
1178    markerDidDestroy() {
1179      if (this.editor.isDestroyed()) return;
1180  
1181      this.destroyed = true;
1182      this.cursor.destroyed = true;
1183  
1184      this.editor.removeSelection(this);
1185  
1186      this.cursor.emitter.emit('did-destroy');
1187      this.emitter.emit('did-destroy');
1188  
1189      this.cursor.emitter.dispose();
1190      this.emitter.dispose();
1191    }
1192  
1193    finalize() {
1194      if (
1195        !this.initialScreenRange ||
1196        !this.initialScreenRange.isEqual(this.getScreenRange())
1197      ) {
1198        this.initialScreenRange = null;
1199      }
1200      if (this.isEmpty()) {
1201        this.wordwise = false;
1202        this.linewise = false;
1203      }
1204    }
1205  
1206    autoscroll(options) {
1207      if (this.marker.hasTail()) {
1208        this.editor.scrollToScreenRange(
1209          this.getScreenRange(),
1210          Object.assign({ reversed: this.isReversed() }, options)
1211        );
1212      } else {
1213        this.cursor.autoscroll(options);
1214      }
1215    }
1216  
1217    clearAutoscroll() {}
1218  
1219    modifySelection(fn) {
1220      this.retainSelection = true;
1221      this.plantTail();
1222      fn();
1223      this.retainSelection = false;
1224    }
1225  
1226    // Sets the marker's tail to the same position as the marker's head.
1227    //
1228    // This only works if there isn't already a tail position.
1229    //
1230    // Returns a {Point} representing the new tail position.
1231    plantTail() {
1232      this.marker.plantTail();
1233    }
1234  };