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