cursor.js 26 KB

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