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 };