selection.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920
  1. /* ***** BEGIN LICENSE BLOCK *****
  2. * Distributed under the BSD license:
  3. *
  4. * Copyright (c) 2010, Ajax.org B.V.
  5. * All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions are met:
  9. * * Redistributions of source code must retain the above copyright
  10. * notice, this list of conditions and the following disclaimer.
  11. * * Redistributions in binary form must reproduce the above copyright
  12. * notice, this list of conditions and the following disclaimer in the
  13. * documentation and/or other materials provided with the distribution.
  14. * * Neither the name of Ajax.org B.V. nor the
  15. * names of its contributors may be used to endorse or promote products
  16. * derived from this software without specific prior written permission.
  17. *
  18. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  19. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  24. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  27. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *
  29. * ***** END LICENSE BLOCK ***** */
  30. define(function(require, exports, module) {
  31. "use strict";
  32. var oop = require("./lib/oop");
  33. var lang = require("./lib/lang");
  34. var EventEmitter = require("./lib/event_emitter").EventEmitter;
  35. var Range = require("./range").Range;
  36. /**
  37. *
  38. * Contains the cursor position and the text selection of an edit session.
  39. *
  40. * The row/columns used in the selection are in document coordinates representing ths coordinates as thez appear in the document before applying soft wrap and folding.
  41. * @class Selection
  42. **/
  43. /**
  44. * Emitted when the cursor position changes.
  45. * @event changeCursor
  46. *
  47. *
  48. *
  49. **/
  50. /**
  51. * Emitted when the cursor selection changes.
  52. * @event changeSelection
  53. *
  54. *
  55. *
  56. **/
  57. /**
  58. * Creates a new `Selection` object.
  59. * @param {EditSession} session The session to use
  60. *
  61. *
  62. *
  63. * @constructor
  64. **/
  65. var Selection = function(session) {
  66. this.session = session;
  67. this.doc = session.getDocument();
  68. this.clearSelection();
  69. this.lead = this.selectionLead = this.doc.createAnchor(0, 0);
  70. this.anchor = this.selectionAnchor = this.doc.createAnchor(0, 0);
  71. var self = this;
  72. this.lead.on("change", function(e) {
  73. self._emit("changeCursor");
  74. if (!self.$isEmpty)
  75. self._emit("changeSelection");
  76. if (!self.$keepDesiredColumnOnChange && e.old.column != e.value.column)
  77. self.$desiredColumn = null;
  78. });
  79. this.selectionAnchor.on("change", function() {
  80. if (!self.$isEmpty)
  81. self._emit("changeSelection");
  82. });
  83. };
  84. (function() {
  85. oop.implement(this, EventEmitter);
  86. /**
  87. *
  88. * Returns `true` if the selection is empty.
  89. * @returns {Boolean}
  90. **/
  91. this.isEmpty = function() {
  92. return (this.$isEmpty || (
  93. this.anchor.row == this.lead.row &&
  94. this.anchor.column == this.lead.column
  95. ));
  96. };
  97. /**
  98. * Returns `true` if the selection is a multi-line.
  99. * @returns {Boolean}
  100. **/
  101. this.isMultiLine = function() {
  102. if (this.isEmpty()) {
  103. return false;
  104. }
  105. return this.getRange().isMultiLine();
  106. };
  107. /**
  108. * Gets the current position of the cursor.
  109. * @returns {Number}
  110. **/
  111. this.getCursor = function() {
  112. return this.lead.getPosition();
  113. };
  114. /**
  115. * Sets the row and column position of the anchor. This function also emits the `'changeSelection'` event.
  116. * @param {Number} row The new row
  117. * @param {Number} column The new column
  118. *
  119. *
  120. **/
  121. this.setSelectionAnchor = function(row, column) {
  122. this.anchor.setPosition(row, column);
  123. if (this.$isEmpty) {
  124. this.$isEmpty = false;
  125. this._emit("changeSelection");
  126. }
  127. };
  128. /**
  129. * Returns an object containing the `row` and `column` of the calling selection anchor.
  130. *
  131. * @returns {Object}
  132. * @related Anchor.getPosition
  133. **/
  134. this.getSelectionAnchor = function() {
  135. if (this.$isEmpty)
  136. return this.getSelectionLead()
  137. else
  138. return this.anchor.getPosition();
  139. };
  140. /**
  141. *
  142. * Returns an object containing the `row` and `column` of the calling selection lead.
  143. * @returns {Object}
  144. **/
  145. this.getSelectionLead = function() {
  146. return this.lead.getPosition();
  147. };
  148. /**
  149. * Shifts the selection up (or down, if [[Selection.isBackwards `isBackwards()`]] is true) the given number of columns.
  150. * @param {Number} columns The number of columns to shift by
  151. *
  152. *
  153. *
  154. **/
  155. this.shiftSelection = function(columns) {
  156. if (this.$isEmpty) {
  157. this.moveCursorTo(this.lead.row, this.lead.column + columns);
  158. return;
  159. };
  160. var anchor = this.getSelectionAnchor();
  161. var lead = this.getSelectionLead();
  162. var isBackwards = this.isBackwards();
  163. if (!isBackwards || anchor.column !== 0)
  164. this.setSelectionAnchor(anchor.row, anchor.column + columns);
  165. if (isBackwards || lead.column !== 0) {
  166. this.$moveSelection(function() {
  167. this.moveCursorTo(lead.row, lead.column + columns);
  168. });
  169. }
  170. };
  171. /**
  172. * Returns `true` if the selection is going backwards in the document.
  173. * @returns {Boolean}
  174. **/
  175. this.isBackwards = function() {
  176. var anchor = this.anchor;
  177. var lead = this.lead;
  178. return (anchor.row > lead.row || (anchor.row == lead.row && anchor.column > lead.column));
  179. };
  180. /**
  181. * [Returns the [[Range]] for the selected text.]{: #Selection.getRange}
  182. * @returns {Range}
  183. **/
  184. this.getRange = function() {
  185. var anchor = this.anchor;
  186. var lead = this.lead;
  187. if (this.isEmpty())
  188. return Range.fromPoints(lead, lead);
  189. if (this.isBackwards()) {
  190. return Range.fromPoints(lead, anchor);
  191. }
  192. else {
  193. return Range.fromPoints(anchor, lead);
  194. }
  195. };
  196. /**
  197. * [Empties the selection (by de-selecting it). This function also emits the `'changeSelection'` event.]{: #Selection.clearSelection}
  198. **/
  199. this.clearSelection = function() {
  200. if (!this.$isEmpty) {
  201. this.$isEmpty = true;
  202. this._emit("changeSelection");
  203. }
  204. };
  205. /**
  206. * Selects all the text in the document.
  207. **/
  208. this.selectAll = function() {
  209. var lastRow = this.doc.getLength() - 1;
  210. this.setSelectionAnchor(0, 0);
  211. this.moveCursorTo(lastRow, this.doc.getLine(lastRow).length);
  212. };
  213. /**
  214. * Sets the selection to the provided range.
  215. * @param {Range} range The range of text to select
  216. * @param {Boolean} reverse Indicates if the range should go backwards (`true`) or not
  217. *
  218. *
  219. * @method setSelectionRange
  220. * @alias setRange
  221. **/
  222. this.setRange =
  223. this.setSelectionRange = function(range, reverse) {
  224. if (reverse) {
  225. this.setSelectionAnchor(range.end.row, range.end.column);
  226. this.selectTo(range.start.row, range.start.column);
  227. } else {
  228. this.setSelectionAnchor(range.start.row, range.start.column);
  229. this.selectTo(range.end.row, range.end.column);
  230. }
  231. if (this.getRange().isEmpty())
  232. this.$isEmpty = true;
  233. this.$desiredColumn = null;
  234. };
  235. this.$moveSelection = function(mover) {
  236. var lead = this.lead;
  237. if (this.$isEmpty)
  238. this.setSelectionAnchor(lead.row, lead.column);
  239. mover.call(this);
  240. };
  241. /**
  242. * Moves the selection cursor to the indicated row and column.
  243. * @param {Number} row The row to select to
  244. * @param {Number} column The column to select to
  245. *
  246. *
  247. *
  248. **/
  249. this.selectTo = function(row, column) {
  250. this.$moveSelection(function() {
  251. this.moveCursorTo(row, column);
  252. });
  253. };
  254. /**
  255. * Moves the selection cursor to the row and column indicated by `pos`.
  256. * @param {Object} pos An object containing the row and column
  257. *
  258. *
  259. *
  260. **/
  261. this.selectToPosition = function(pos) {
  262. this.$moveSelection(function() {
  263. this.moveCursorToPosition(pos);
  264. });
  265. };
  266. /**
  267. *
  268. * Moves the selection up one row.
  269. **/
  270. this.selectUp = function() {
  271. this.$moveSelection(this.moveCursorUp);
  272. };
  273. /**
  274. *
  275. * Moves the selection down one row.
  276. **/
  277. this.selectDown = function() {
  278. this.$moveSelection(this.moveCursorDown);
  279. };
  280. /**
  281. *
  282. *
  283. * Moves the selection right one column.
  284. **/
  285. this.selectRight = function() {
  286. this.$moveSelection(this.moveCursorRight);
  287. };
  288. /**
  289. *
  290. * Moves the selection left one column.
  291. **/
  292. this.selectLeft = function() {
  293. this.$moveSelection(this.moveCursorLeft);
  294. };
  295. /**
  296. *
  297. * Moves the selection to the beginning of the current line.
  298. **/
  299. this.selectLineStart = function() {
  300. this.$moveSelection(this.moveCursorLineStart);
  301. };
  302. /**
  303. *
  304. * Moves the selection to the end of the current line.
  305. **/
  306. this.selectLineEnd = function() {
  307. this.$moveSelection(this.moveCursorLineEnd);
  308. };
  309. /**
  310. *
  311. * Moves the selection to the end of the file.
  312. **/
  313. this.selectFileEnd = function() {
  314. this.$moveSelection(this.moveCursorFileEnd);
  315. };
  316. /**
  317. *
  318. * Moves the selection to the start of the file.
  319. **/
  320. this.selectFileStart = function() {
  321. this.$moveSelection(this.moveCursorFileStart);
  322. };
  323. /**
  324. *
  325. * Moves the selection to the first word on the right.
  326. **/
  327. this.selectWordRight = function() {
  328. this.$moveSelection(this.moveCursorWordRight);
  329. };
  330. /**
  331. *
  332. * Moves the selection to the first word on the left.
  333. **/
  334. this.selectWordLeft = function() {
  335. this.$moveSelection(this.moveCursorWordLeft);
  336. };
  337. /**
  338. * Moves the selection to highlight the entire word.
  339. * @related EditSession.getWordRange
  340. **/
  341. this.getWordRange = function(row, column) {
  342. if (typeof column == "undefined") {
  343. var cursor = row || this.lead;
  344. row = cursor.row;
  345. column = cursor.column;
  346. }
  347. return this.session.getWordRange(row, column);
  348. };
  349. /**
  350. *
  351. * Selects an entire word boundary.
  352. **/
  353. this.selectWord = function() {
  354. this.setSelectionRange(this.getWordRange());
  355. };
  356. /**
  357. * Selects a word, including its right whitespace.
  358. * @related EditSession.getAWordRange
  359. **/
  360. this.selectAWord = function() {
  361. var cursor = this.getCursor();
  362. var range = this.session.getAWordRange(cursor.row, cursor.column);
  363. this.setSelectionRange(range);
  364. };
  365. this.getLineRange = function(row, excludeLastChar) {
  366. var rowStart = typeof row == "number" ? row : this.lead.row;
  367. var rowEnd;
  368. var foldLine = this.session.getFoldLine(rowStart);
  369. if (foldLine) {
  370. rowStart = foldLine.start.row;
  371. rowEnd = foldLine.end.row;
  372. } else {
  373. rowEnd = rowStart;
  374. }
  375. if (excludeLastChar === true)
  376. return new Range(rowStart, 0, rowEnd, this.session.getLine(rowEnd).length);
  377. else
  378. return new Range(rowStart, 0, rowEnd + 1, 0);
  379. };
  380. /**
  381. * Selects the entire line.
  382. **/
  383. this.selectLine = function() {
  384. this.setSelectionRange(this.getLineRange());
  385. };
  386. /**
  387. *
  388. * Moves the cursor up one row.
  389. **/
  390. this.moveCursorUp = function() {
  391. this.moveCursorBy(-1, 0);
  392. };
  393. /**
  394. *
  395. * Moves the cursor down one row.
  396. **/
  397. this.moveCursorDown = function() {
  398. this.moveCursorBy(1, 0);
  399. };
  400. /**
  401. *
  402. * Moves the cursor left one column.
  403. **/
  404. this.moveCursorLeft = function() {
  405. var cursor = this.lead.getPosition(),
  406. fold;
  407. if (fold = this.session.getFoldAt(cursor.row, cursor.column, -1)) {
  408. this.moveCursorTo(fold.start.row, fold.start.column);
  409. } else if (cursor.column == 0) {
  410. // cursor is a line (start
  411. if (cursor.row > 0) {
  412. this.moveCursorTo(cursor.row - 1, this.doc.getLine(cursor.row - 1).length);
  413. }
  414. }
  415. else {
  416. var tabSize = this.session.getTabSize();
  417. if (this.session.isTabStop(cursor) && this.doc.getLine(cursor.row).slice(cursor.column-tabSize, cursor.column).split(" ").length-1 == tabSize)
  418. this.moveCursorBy(0, -tabSize);
  419. else
  420. this.moveCursorBy(0, -1);
  421. }
  422. };
  423. /**
  424. *
  425. * Moves the cursor right one column.
  426. **/
  427. this.moveCursorRight = function() {
  428. var cursor = this.lead.getPosition(),
  429. fold;
  430. if (fold = this.session.getFoldAt(cursor.row, cursor.column, 1)) {
  431. this.moveCursorTo(fold.end.row, fold.end.column);
  432. }
  433. else if (this.lead.column == this.doc.getLine(this.lead.row).length) {
  434. if (this.lead.row < this.doc.getLength() - 1) {
  435. this.moveCursorTo(this.lead.row + 1, 0);
  436. }
  437. }
  438. else {
  439. var tabSize = this.session.getTabSize();
  440. var cursor = this.lead;
  441. if (this.session.isTabStop(cursor) && this.doc.getLine(cursor.row).slice(cursor.column, cursor.column+tabSize).split(" ").length-1 == tabSize)
  442. this.moveCursorBy(0, tabSize);
  443. else
  444. this.moveCursorBy(0, 1);
  445. }
  446. };
  447. /**
  448. *
  449. * Moves the cursor to the start of the line.
  450. **/
  451. this.moveCursorLineStart = function() {
  452. var row = this.lead.row;
  453. var column = this.lead.column;
  454. var screenRow = this.session.documentToScreenRow(row, column);
  455. // Determ the doc-position of the first character at the screen line.
  456. var firstColumnPosition = this.session.screenToDocumentPosition(screenRow, 0);
  457. // Determ the line
  458. var beforeCursor = this.session.getDisplayLine(
  459. row, null, firstColumnPosition.row,
  460. firstColumnPosition.column
  461. );
  462. var leadingSpace = beforeCursor.match(/^\s*/);
  463. // TODO find better way for emacs mode to override selection behaviors
  464. if (leadingSpace[0].length != column && !this.session.$useEmacsStyleLineStart)
  465. firstColumnPosition.column += leadingSpace[0].length;
  466. this.moveCursorToPosition(firstColumnPosition);
  467. };
  468. /**
  469. *
  470. * Moves the cursor to the end of the line.
  471. **/
  472. this.moveCursorLineEnd = function() {
  473. var lead = this.lead;
  474. var lineEnd = this.session.getDocumentLastRowColumnPosition(lead.row, lead.column);
  475. if (this.lead.column == lineEnd.column) {
  476. var line = this.session.getLine(lineEnd.row);
  477. if (lineEnd.column == line.length) {
  478. var textEnd = line.search(/\s+$/);
  479. if (textEnd > 0)
  480. lineEnd.column = textEnd;
  481. }
  482. }
  483. this.moveCursorTo(lineEnd.row, lineEnd.column);
  484. };
  485. /**
  486. *
  487. * Moves the cursor to the end of the file.
  488. **/
  489. this.moveCursorFileEnd = function() {
  490. var row = this.doc.getLength() - 1;
  491. var column = this.doc.getLine(row).length;
  492. this.moveCursorTo(row, column);
  493. };
  494. /**
  495. *
  496. * Moves the cursor to the start of the file.
  497. **/
  498. this.moveCursorFileStart = function() {
  499. this.moveCursorTo(0, 0);
  500. };
  501. /**
  502. *
  503. * Moves the cursor to the word on the right.
  504. **/
  505. this.moveCursorLongWordRight = function() {
  506. var row = this.lead.row;
  507. var column = this.lead.column;
  508. var line = this.doc.getLine(row);
  509. var rightOfCursor = line.substring(column);
  510. var match;
  511. this.session.nonTokenRe.lastIndex = 0;
  512. this.session.tokenRe.lastIndex = 0;
  513. // skip folds
  514. var fold = this.session.getFoldAt(row, column, 1);
  515. if (fold) {
  516. this.moveCursorTo(fold.end.row, fold.end.column);
  517. return;
  518. }
  519. // first skip space
  520. if (match = this.session.nonTokenRe.exec(rightOfCursor)) {
  521. column += this.session.nonTokenRe.lastIndex;
  522. this.session.nonTokenRe.lastIndex = 0;
  523. rightOfCursor = line.substring(column);
  524. }
  525. // if at line end proceed with next line
  526. if (column >= line.length) {
  527. this.moveCursorTo(row, line.length);
  528. this.moveCursorRight();
  529. if (row < this.doc.getLength() - 1)
  530. this.moveCursorWordRight();
  531. return;
  532. }
  533. // advance to the end of the next token
  534. if (match = this.session.tokenRe.exec(rightOfCursor)) {
  535. column += this.session.tokenRe.lastIndex;
  536. this.session.tokenRe.lastIndex = 0;
  537. }
  538. this.moveCursorTo(row, column);
  539. };
  540. /**
  541. *
  542. * Moves the cursor to the word on the left.
  543. **/
  544. this.moveCursorLongWordLeft = function() {
  545. var row = this.lead.row;
  546. var column = this.lead.column;
  547. // skip folds
  548. var fold;
  549. if (fold = this.session.getFoldAt(row, column, -1)) {
  550. this.moveCursorTo(fold.start.row, fold.start.column);
  551. return;
  552. }
  553. var str = this.session.getFoldStringAt(row, column, -1);
  554. if (str == null) {
  555. str = this.doc.getLine(row).substring(0, column)
  556. }
  557. var leftOfCursor = lang.stringReverse(str);
  558. var match;
  559. this.session.nonTokenRe.lastIndex = 0;
  560. this.session.tokenRe.lastIndex = 0;
  561. // skip whitespace
  562. if (match = this.session.nonTokenRe.exec(leftOfCursor)) {
  563. column -= this.session.nonTokenRe.lastIndex;
  564. leftOfCursor = leftOfCursor.slice(this.session.nonTokenRe.lastIndex);
  565. this.session.nonTokenRe.lastIndex = 0;
  566. }
  567. // if at begin of the line proceed in line above
  568. if (column <= 0) {
  569. this.moveCursorTo(row, 0);
  570. this.moveCursorLeft();
  571. if (row > 0)
  572. this.moveCursorWordLeft();
  573. return;
  574. }
  575. // move to the begin of the word
  576. if (match = this.session.tokenRe.exec(leftOfCursor)) {
  577. column -= this.session.tokenRe.lastIndex;
  578. this.session.tokenRe.lastIndex = 0;
  579. }
  580. this.moveCursorTo(row, column);
  581. };
  582. this.$shortWordEndIndex = function(rightOfCursor) {
  583. var match, index = 0, ch;
  584. var whitespaceRe = /\s/;
  585. var tokenRe = this.session.tokenRe;
  586. tokenRe.lastIndex = 0;
  587. if (match = this.session.tokenRe.exec(rightOfCursor)) {
  588. index = this.session.tokenRe.lastIndex;
  589. } else {
  590. while ((ch = rightOfCursor[index]) && whitespaceRe.test(ch))
  591. index ++;
  592. if (index < 1) {
  593. tokenRe.lastIndex = 0;
  594. while ((ch = rightOfCursor[index]) && !tokenRe.test(ch)) {
  595. tokenRe.lastIndex = 0;
  596. index ++;
  597. if (whitespaceRe.test(ch)) {
  598. if (index > 2) {
  599. index--
  600. break;
  601. } else {
  602. while ((ch = rightOfCursor[index]) && whitespaceRe.test(ch))
  603. index ++;
  604. if (index > 2)
  605. break
  606. }
  607. }
  608. }
  609. }
  610. }
  611. tokenRe.lastIndex = 0;
  612. return index;
  613. };
  614. this.moveCursorShortWordRight = function() {
  615. var row = this.lead.row;
  616. var column = this.lead.column;
  617. var line = this.doc.getLine(row);
  618. var rightOfCursor = line.substring(column);
  619. var fold = this.session.getFoldAt(row, column, 1);
  620. if (fold)
  621. return this.moveCursorTo(fold.end.row, fold.end.column);
  622. if (column == line.length) {
  623. var l = this.doc.getLength();
  624. do {
  625. row++;
  626. rightOfCursor = this.doc.getLine(row)
  627. } while (row < l && /^\s*$/.test(rightOfCursor))
  628. if (!/^\s+/.test(rightOfCursor))
  629. rightOfCursor = ""
  630. column = 0;
  631. }
  632. var index = this.$shortWordEndIndex(rightOfCursor);
  633. this.moveCursorTo(row, column + index);
  634. };
  635. this.moveCursorShortWordLeft = function() {
  636. var row = this.lead.row;
  637. var column = this.lead.column;
  638. var fold;
  639. if (fold = this.session.getFoldAt(row, column, -1))
  640. return this.moveCursorTo(fold.start.row, fold.start.column);
  641. var line = this.session.getLine(row).substring(0, column);
  642. if (column == 0) {
  643. do {
  644. row--;
  645. line = this.doc.getLine(row);
  646. } while (row > 0 && /^\s*$/.test(line))
  647. column = line.length;
  648. if (!/\s+$/.test(line))
  649. line = ""
  650. }
  651. var leftOfCursor = lang.stringReverse(line);
  652. var index = this.$shortWordEndIndex(leftOfCursor);
  653. return this.moveCursorTo(row, column - index);
  654. };
  655. this.moveCursorWordRight = function() {
  656. if (this.session.$selectLongWords)
  657. this.moveCursorLongWordRight();
  658. else
  659. this.moveCursorShortWordRight();
  660. };
  661. this.moveCursorWordLeft = function() {
  662. if (this.session.$selectLongWords)
  663. this.moveCursorLongWordLeft();
  664. else
  665. this.moveCursorShortWordLeft();
  666. };
  667. /**
  668. * Moves the cursor to position indicated by the parameters. Negative numbers move the cursor backwards in the document.
  669. * @param {Number} rows The number of rows to move by
  670. * @param {Number} chars The number of characters to move by
  671. *
  672. *
  673. * @related EditSession.documentToScreenPosition
  674. **/
  675. this.moveCursorBy = function(rows, chars) {
  676. var screenPos = this.session.documentToScreenPosition(
  677. this.lead.row,
  678. this.lead.column
  679. );
  680. if (chars === 0) {
  681. if (this.$desiredColumn)
  682. screenPos.column = this.$desiredColumn;
  683. else
  684. this.$desiredColumn = screenPos.column;
  685. }
  686. var docPos = this.session.screenToDocumentPosition(screenPos.row + rows, screenPos.column);
  687. if (rows !== 0 && chars === 0 && docPos.row === this.lead.row && docPos.column === this.lead.column) {
  688. if (this.session.lineWidgets && this.session.lineWidgets[docPos.row])
  689. docPos.row++;
  690. }
  691. // move the cursor and update the desired column
  692. this.moveCursorTo(docPos.row, docPos.column + chars, chars === 0);
  693. };
  694. /**
  695. * Moves the selection to the position indicated by its `row` and `column`.
  696. * @param {Object} position The position to move to
  697. *
  698. *
  699. **/
  700. this.moveCursorToPosition = function(position) {
  701. this.moveCursorTo(position.row, position.column);
  702. };
  703. /**
  704. * Moves the cursor to the row and column provided. [If `preventUpdateDesiredColumn` is `true`, then the cursor stays in the same column position as its original point.]{: #preventUpdateBoolDesc}
  705. * @param {Number} row The row to move to
  706. * @param {Number} column The column to move to
  707. * @param {Boolean} keepDesiredColumn [If `true`, the cursor move does not respect the previous column]{: #preventUpdateBool}
  708. *
  709. *
  710. **/
  711. this.moveCursorTo = function(row, column, keepDesiredColumn) {
  712. // Ensure the row/column is not inside of a fold.
  713. var fold = this.session.getFoldAt(row, column, 1);
  714. if (fold) {
  715. row = fold.start.row;
  716. column = fold.start.column;
  717. }
  718. this.$keepDesiredColumnOnChange = true;
  719. this.lead.setPosition(row, column);
  720. this.$keepDesiredColumnOnChange = false;
  721. if (!keepDesiredColumn)
  722. this.$desiredColumn = null;
  723. };
  724. /**
  725. * Moves the cursor to the screen position indicated by row and column. {:preventUpdateBoolDesc}
  726. * @param {Number} row The row to move to
  727. * @param {Number} column The column to move to
  728. * @param {Boolean} keepDesiredColumn {:preventUpdateBool}
  729. *
  730. *
  731. **/
  732. this.moveCursorToScreen = function(row, column, keepDesiredColumn) {
  733. var pos = this.session.screenToDocumentPosition(row, column);
  734. this.moveCursorTo(pos.row, pos.column, keepDesiredColumn);
  735. };
  736. // remove listeners from document
  737. this.detach = function() {
  738. this.lead.detach();
  739. this.anchor.detach();
  740. this.session = this.doc = null;
  741. }
  742. this.fromOrientedRange = function(range) {
  743. this.setSelectionRange(range, range.cursor == range.start);
  744. this.$desiredColumn = range.desiredColumn || this.$desiredColumn;
  745. }
  746. this.toOrientedRange = function(range) {
  747. var r = this.getRange();
  748. if (range) {
  749. range.start.column = r.start.column;
  750. range.start.row = r.start.row;
  751. range.end.column = r.end.column;
  752. range.end.row = r.end.row;
  753. } else {
  754. range = r;
  755. }
  756. range.cursor = this.isBackwards() ? range.start : range.end;
  757. range.desiredColumn = this.$desiredColumn;
  758. return range;
  759. }
  760. this.toJSON = function() {
  761. if (this.rangeCount) {
  762. var data = this.ranges.map(function(r) {
  763. var r1 = r.clone();
  764. r1.isBackwards = r.cursor == r.start;
  765. return r1;
  766. });
  767. } else {
  768. var data = this.getRange();
  769. data.isBackwards = this.isBackwards();
  770. }
  771. return data;
  772. };
  773. this.fromJSON = function(data) {
  774. if (data.start == undefined) {
  775. if (this.rangeList) {
  776. this.toSingleRange(data[0]);
  777. for (var i = data.length; i--; ) {
  778. var r = Range.fromPoints(data[i].start, data[i].end);
  779. if (data.isBackwards)
  780. r.cursor = r.start;
  781. this.addRange(r, true);
  782. }
  783. return;
  784. } else
  785. data = data[0];
  786. }
  787. if (this.rangeList)
  788. this.toSingleRange(data);
  789. this.setSelectionRange(data, data.isBackwards);
  790. };
  791. this.isEqual = function(data) {
  792. if ((data.length || this.rangeCount) && data.length != this.rangeCount)
  793. return false;
  794. if (!data.length || !this.ranges)
  795. return this.getRange().isEqual(data);
  796. for (var i = this.ranges.length; i--; ) {
  797. if (!this.ranges[i].isEqual(data[i]))
  798. return false
  799. }
  800. return true;
  801. }
  802. }).call(Selection.prototype);
  803. exports.Selection = Selection;
  804. });