multi_select.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977
  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. var RangeList = require("./range_list").RangeList;
  32. var Range = require("./range").Range;
  33. var Selection = require("./selection").Selection;
  34. var onMouseDown = require("./mouse/multi_select_handler").onMouseDown;
  35. var event = require("./lib/event");
  36. var lang = require("./lib/lang");
  37. var commands = require("./commands/multi_select_commands");
  38. exports.commands = commands.defaultCommands.concat(commands.multiSelectCommands);
  39. // Todo: session.find or editor.findVolatile that returns range
  40. var Search = require("./search").Search;
  41. var search = new Search();
  42. function find(session, needle, dir) {
  43. search.$options.wrap = true;
  44. search.$options.needle = needle;
  45. search.$options.backwards = dir == -1;
  46. return search.find(session);
  47. }
  48. // extend EditSession
  49. var EditSession = require("./edit_session").EditSession;
  50. (function() {
  51. this.getSelectionMarkers = function() {
  52. return this.$selectionMarkers;
  53. };
  54. }).call(EditSession.prototype);
  55. // extend Selection
  56. (function() {
  57. // list of ranges in reverse addition order
  58. this.ranges = null;
  59. // automatically sorted list of ranges
  60. this.rangeList = null;
  61. /**
  62. * Adds a range to a selection by entering multiselect mode, if necessary.
  63. * @param {Range} range The new range to add
  64. * @param {Boolean} $blockChangeEvents Whether or not to block changing events
  65. * @method Selection.addRange
  66. **/
  67. this.addRange = function(range, $blockChangeEvents) {
  68. if (!range)
  69. return;
  70. if (!this.inMultiSelectMode && this.rangeCount === 0) {
  71. var oldRange = this.toOrientedRange();
  72. this.rangeList.add(oldRange);
  73. this.rangeList.add(range);
  74. if (this.rangeList.ranges.length != 2) {
  75. this.rangeList.removeAll();
  76. return $blockChangeEvents || this.fromOrientedRange(range);
  77. }
  78. this.rangeList.removeAll();
  79. this.rangeList.add(oldRange);
  80. this.$onAddRange(oldRange);
  81. }
  82. if (!range.cursor)
  83. range.cursor = range.end;
  84. var removed = this.rangeList.add(range);
  85. this.$onAddRange(range);
  86. if (removed.length)
  87. this.$onRemoveRange(removed);
  88. if (this.rangeCount > 1 && !this.inMultiSelectMode) {
  89. this._signal("multiSelect");
  90. this.inMultiSelectMode = true;
  91. this.session.$undoSelect = false;
  92. this.rangeList.attach(this.session);
  93. }
  94. return $blockChangeEvents || this.fromOrientedRange(range);
  95. };
  96. /**
  97. * @method Selection.toSingleRange
  98. **/
  99. this.toSingleRange = function(range) {
  100. range = range || this.ranges[0];
  101. var removed = this.rangeList.removeAll();
  102. if (removed.length)
  103. this.$onRemoveRange(removed);
  104. range && this.fromOrientedRange(range);
  105. };
  106. /**
  107. * Removes a Range containing pos (if it exists).
  108. * @param {Range} pos The position to remove, as a `{row, column}` object
  109. * @method Selection.substractPoint
  110. **/
  111. this.substractPoint = function(pos) {
  112. var removed = this.rangeList.substractPoint(pos);
  113. if (removed) {
  114. this.$onRemoveRange(removed);
  115. return removed[0];
  116. }
  117. };
  118. /**
  119. * Merges overlapping ranges ensuring consistency after changes
  120. * @method Selection.mergeOverlappingRanges
  121. **/
  122. this.mergeOverlappingRanges = function() {
  123. var removed = this.rangeList.merge();
  124. if (removed.length)
  125. this.$onRemoveRange(removed);
  126. else if(this.ranges[0])
  127. this.fromOrientedRange(this.ranges[0]);
  128. };
  129. this.$onAddRange = function(range) {
  130. this.rangeCount = this.rangeList.ranges.length;
  131. this.ranges.unshift(range);
  132. this._signal("addRange", {range: range});
  133. };
  134. this.$onRemoveRange = function(removed) {
  135. this.rangeCount = this.rangeList.ranges.length;
  136. if (this.rangeCount == 1 && this.inMultiSelectMode) {
  137. var lastRange = this.rangeList.ranges.pop();
  138. removed.push(lastRange);
  139. this.rangeCount = 0;
  140. }
  141. for (var i = removed.length; i--; ) {
  142. var index = this.ranges.indexOf(removed[i]);
  143. this.ranges.splice(index, 1);
  144. }
  145. this._signal("removeRange", {ranges: removed});
  146. if (this.rangeCount === 0 && this.inMultiSelectMode) {
  147. this.inMultiSelectMode = false;
  148. this._signal("singleSelect");
  149. this.session.$undoSelect = true;
  150. this.rangeList.detach(this.session);
  151. }
  152. lastRange = lastRange || this.ranges[0];
  153. if (lastRange && !lastRange.isEqual(this.getRange()))
  154. this.fromOrientedRange(lastRange);
  155. };
  156. // adds multicursor support to selection
  157. this.$initRangeList = function() {
  158. if (this.rangeList)
  159. return;
  160. this.rangeList = new RangeList();
  161. this.ranges = [];
  162. this.rangeCount = 0;
  163. };
  164. /**
  165. * Returns a concatenation of all the ranges.
  166. * @returns {Array}
  167. * @method Selection.getAllRanges
  168. **/
  169. this.getAllRanges = function() {
  170. return this.rangeCount ? this.rangeList.ranges.concat() : [this.getRange()];
  171. };
  172. /**
  173. * Splits all the ranges into lines.
  174. * @method Selection.splitIntoLines
  175. **/
  176. this.splitIntoLines = function () {
  177. if (this.rangeCount > 1) {
  178. var ranges = this.rangeList.ranges;
  179. var lastRange = ranges[ranges.length - 1];
  180. var range = Range.fromPoints(ranges[0].start, lastRange.end);
  181. this.toSingleRange();
  182. this.setSelectionRange(range, lastRange.cursor == lastRange.start);
  183. } else {
  184. var range = this.getRange();
  185. var isBackwards = this.isBackwards();
  186. var startRow = range.start.row;
  187. var endRow = range.end.row;
  188. if (startRow == endRow) {
  189. if (isBackwards)
  190. var start = range.end, end = range.start;
  191. else
  192. var start = range.start, end = range.end;
  193. this.addRange(Range.fromPoints(end, end));
  194. this.addRange(Range.fromPoints(start, start));
  195. return;
  196. }
  197. var rectSel = [];
  198. var r = this.getLineRange(startRow, true);
  199. r.start.column = range.start.column;
  200. rectSel.push(r);
  201. for (var i = startRow + 1; i < endRow; i++)
  202. rectSel.push(this.getLineRange(i, true));
  203. r = this.getLineRange(endRow, true);
  204. r.end.column = range.end.column;
  205. rectSel.push(r);
  206. rectSel.forEach(this.addRange, this);
  207. }
  208. };
  209. /**
  210. * @method Selection.toggleBlockSelection
  211. **/
  212. this.toggleBlockSelection = function () {
  213. if (this.rangeCount > 1) {
  214. var ranges = this.rangeList.ranges;
  215. var lastRange = ranges[ranges.length - 1];
  216. var range = Range.fromPoints(ranges[0].start, lastRange.end);
  217. this.toSingleRange();
  218. this.setSelectionRange(range, lastRange.cursor == lastRange.start);
  219. } else {
  220. var cursor = this.session.documentToScreenPosition(this.selectionLead);
  221. var anchor = this.session.documentToScreenPosition(this.selectionAnchor);
  222. var rectSel = this.rectangularRangeBlock(cursor, anchor);
  223. rectSel.forEach(this.addRange, this);
  224. }
  225. };
  226. /**
  227. *
  228. * Gets list of ranges composing rectangular block on the screen
  229. *
  230. * @param {Cursor} screenCursor The cursor to use
  231. * @param {Anchor} screenAnchor The anchor to use
  232. * @param {Boolean} includeEmptyLines If true, this includes ranges inside the block which are empty due to clipping
  233. * @returns {Range}
  234. * @method Selection.rectangularRangeBlock
  235. **/
  236. this.rectangularRangeBlock = function(screenCursor, screenAnchor, includeEmptyLines) {
  237. var rectSel = [];
  238. var xBackwards = screenCursor.column < screenAnchor.column;
  239. if (xBackwards) {
  240. var startColumn = screenCursor.column;
  241. var endColumn = screenAnchor.column;
  242. } else {
  243. var startColumn = screenAnchor.column;
  244. var endColumn = screenCursor.column;
  245. }
  246. var yBackwards = screenCursor.row < screenAnchor.row;
  247. if (yBackwards) {
  248. var startRow = screenCursor.row;
  249. var endRow = screenAnchor.row;
  250. } else {
  251. var startRow = screenAnchor.row;
  252. var endRow = screenCursor.row;
  253. }
  254. if (startColumn < 0)
  255. startColumn = 0;
  256. if (startRow < 0)
  257. startRow = 0;
  258. if (startRow == endRow)
  259. includeEmptyLines = true;
  260. for (var row = startRow; row <= endRow; row++) {
  261. var range = Range.fromPoints(
  262. this.session.screenToDocumentPosition(row, startColumn),
  263. this.session.screenToDocumentPosition(row, endColumn)
  264. );
  265. if (range.isEmpty()) {
  266. if (docEnd && isSamePoint(range.end, docEnd))
  267. break;
  268. var docEnd = range.end;
  269. }
  270. range.cursor = xBackwards ? range.start : range.end;
  271. rectSel.push(range);
  272. }
  273. if (yBackwards)
  274. rectSel.reverse();
  275. if (!includeEmptyLines) {
  276. var end = rectSel.length - 1;
  277. while (rectSel[end].isEmpty() && end > 0)
  278. end--;
  279. if (end > 0) {
  280. var start = 0;
  281. while (rectSel[start].isEmpty())
  282. start++;
  283. }
  284. for (var i = end; i >= start; i--) {
  285. if (rectSel[i].isEmpty())
  286. rectSel.splice(i, 1);
  287. }
  288. }
  289. return rectSel;
  290. };
  291. }).call(Selection.prototype);
  292. // extend Editor
  293. var Editor = require("./editor").Editor;
  294. (function() {
  295. /**
  296. *
  297. * Updates the cursor and marker layers.
  298. * @method Editor.updateSelectionMarkers
  299. *
  300. **/
  301. this.updateSelectionMarkers = function() {
  302. this.renderer.updateCursor();
  303. this.renderer.updateBackMarkers();
  304. };
  305. /**
  306. * Adds the selection and cursor.
  307. * @param {Range} orientedRange A range containing a cursor
  308. * @returns {Range}
  309. * @method Editor.addSelectionMarker
  310. **/
  311. this.addSelectionMarker = function(orientedRange) {
  312. if (!orientedRange.cursor)
  313. orientedRange.cursor = orientedRange.end;
  314. var style = this.getSelectionStyle();
  315. orientedRange.marker = this.session.addMarker(orientedRange, "ace_selection", style);
  316. this.session.$selectionMarkers.push(orientedRange);
  317. this.session.selectionMarkerCount = this.session.$selectionMarkers.length;
  318. return orientedRange;
  319. };
  320. /**
  321. * Removes the selection marker.
  322. * @param {Range} range The selection range added with [[Editor.addSelectionMarker `addSelectionMarker()`]].
  323. * @method Editor.removeSelectionMarker
  324. **/
  325. this.removeSelectionMarker = function(range) {
  326. if (!range.marker)
  327. return;
  328. this.session.removeMarker(range.marker);
  329. var index = this.session.$selectionMarkers.indexOf(range);
  330. if (index != -1)
  331. this.session.$selectionMarkers.splice(index, 1);
  332. this.session.selectionMarkerCount = this.session.$selectionMarkers.length;
  333. };
  334. this.removeSelectionMarkers = function(ranges) {
  335. var markerList = this.session.$selectionMarkers;
  336. for (var i = ranges.length; i--; ) {
  337. var range = ranges[i];
  338. if (!range.marker)
  339. continue;
  340. this.session.removeMarker(range.marker);
  341. var index = markerList.indexOf(range);
  342. if (index != -1)
  343. markerList.splice(index, 1);
  344. }
  345. this.session.selectionMarkerCount = markerList.length;
  346. };
  347. this.$onAddRange = function(e) {
  348. this.addSelectionMarker(e.range);
  349. this.renderer.updateCursor();
  350. this.renderer.updateBackMarkers();
  351. };
  352. this.$onRemoveRange = function(e) {
  353. this.removeSelectionMarkers(e.ranges);
  354. this.renderer.updateCursor();
  355. this.renderer.updateBackMarkers();
  356. };
  357. this.$onMultiSelect = function(e) {
  358. if (this.inMultiSelectMode)
  359. return;
  360. this.inMultiSelectMode = true;
  361. this.setStyle("ace_multiselect");
  362. this.keyBinding.addKeyboardHandler(commands.keyboardHandler);
  363. this.commands.setDefaultHandler("exec", this.$onMultiSelectExec);
  364. this.renderer.updateCursor();
  365. this.renderer.updateBackMarkers();
  366. };
  367. this.$onSingleSelect = function(e) {
  368. if (this.session.multiSelect.inVirtualMode)
  369. return;
  370. this.inMultiSelectMode = false;
  371. this.unsetStyle("ace_multiselect");
  372. this.keyBinding.removeKeyboardHandler(commands.keyboardHandler);
  373. this.commands.removeDefaultHandler("exec", this.$onMultiSelectExec);
  374. this.renderer.updateCursor();
  375. this.renderer.updateBackMarkers();
  376. this._emit("changeSelection");
  377. };
  378. this.$onMultiSelectExec = function(e) {
  379. var command = e.command;
  380. var editor = e.editor;
  381. if (!editor.multiSelect)
  382. return;
  383. if (!command.multiSelectAction) {
  384. var result = command.exec(editor, e.args || {});
  385. editor.multiSelect.addRange(editor.multiSelect.toOrientedRange());
  386. editor.multiSelect.mergeOverlappingRanges();
  387. } else if (command.multiSelectAction == "forEach") {
  388. result = editor.forEachSelection(command, e.args);
  389. } else if (command.multiSelectAction == "forEachLine") {
  390. result = editor.forEachSelection(command, e.args, true);
  391. } else if (command.multiSelectAction == "single") {
  392. editor.exitMultiSelectMode();
  393. result = command.exec(editor, e.args || {});
  394. } else {
  395. result = command.multiSelectAction(editor, e.args || {});
  396. }
  397. return result;
  398. };
  399. /**
  400. * Executes a command for each selection range.
  401. * @param {Object} cmd The command to execute
  402. * @param {String} args Any arguments for the command
  403. * @method Editor.forEachSelection
  404. **/
  405. this.forEachSelection = function(cmd, args, options) {
  406. if (this.inVirtualSelectionMode)
  407. return;
  408. var keepOrder = options && options.keepOrder;
  409. var $byLines = options == true || options && options.$byLines
  410. var session = this.session;
  411. var selection = this.selection;
  412. var rangeList = selection.rangeList;
  413. var ranges = (keepOrder ? selection : rangeList).ranges;
  414. var result;
  415. if (!ranges.length)
  416. return cmd.exec ? cmd.exec(this, args || {}) : cmd(this, args || {});
  417. var reg = selection._eventRegistry;
  418. selection._eventRegistry = {};
  419. var tmpSel = new Selection(session);
  420. this.inVirtualSelectionMode = true;
  421. for (var i = ranges.length; i--;) {
  422. if ($byLines) {
  423. while (i > 0 && ranges[i].start.row == ranges[i - 1].end.row)
  424. i--;
  425. }
  426. tmpSel.fromOrientedRange(ranges[i]);
  427. tmpSel.index = i;
  428. this.selection = session.selection = tmpSel;
  429. var cmdResult = cmd.exec ? cmd.exec(this, args || {}) : cmd(this, args || {});
  430. if (!result && cmdResult !== undefined)
  431. result = cmdResult;
  432. tmpSel.toOrientedRange(ranges[i]);
  433. }
  434. tmpSel.detach();
  435. this.selection = session.selection = selection;
  436. this.inVirtualSelectionMode = false;
  437. selection._eventRegistry = reg;
  438. selection.mergeOverlappingRanges();
  439. var anim = this.renderer.$scrollAnimation;
  440. this.onCursorChange();
  441. this.onSelectionChange();
  442. if (anim && anim.from == anim.to)
  443. this.renderer.animateScrolling(anim.from);
  444. return result;
  445. };
  446. /**
  447. * Removes all the selections except the last added one.
  448. * @method Editor.exitMultiSelectMode
  449. **/
  450. this.exitMultiSelectMode = function() {
  451. if (!this.inMultiSelectMode || this.inVirtualSelectionMode)
  452. return;
  453. this.multiSelect.toSingleRange();
  454. };
  455. this.getSelectedText = function() {
  456. var text = "";
  457. if (this.inMultiSelectMode && !this.inVirtualSelectionMode) {
  458. var ranges = this.multiSelect.rangeList.ranges;
  459. var buf = [];
  460. for (var i = 0; i < ranges.length; i++) {
  461. buf.push(this.session.getTextRange(ranges[i]));
  462. }
  463. var nl = this.session.getDocument().getNewLineCharacter();
  464. text = buf.join(nl);
  465. if (text.length == (buf.length - 1) * nl.length)
  466. text = "";
  467. } else if (!this.selection.isEmpty()) {
  468. text = this.session.getTextRange(this.getSelectionRange());
  469. }
  470. return text;
  471. };
  472. this.$checkMultiselectChange = function(e, anchor) {
  473. if (this.inMultiSelectMode && !this.inVirtualSelectionMode) {
  474. var range = this.multiSelect.ranges[0];
  475. if (this.multiSelect.isEmpty() && anchor == this.multiSelect.anchor)
  476. return;
  477. var pos = anchor == this.multiSelect.anchor
  478. ? range.cursor == range.start ? range.end : range.start
  479. : range.cursor;
  480. if (pos.row != anchor.row
  481. || this.session.$clipPositionToDocument(pos.row, pos.column).column != anchor.column)
  482. this.multiSelect.toSingleRange(this.multiSelect.toOrientedRange());
  483. }
  484. };
  485. /**
  486. * Finds and selects all the occurences of `needle`.
  487. * @param {String} The text to find
  488. * @param {Object} The search options
  489. * @param {Boolean} keeps
  490. *
  491. * @returns {Number} The cumulative count of all found matches
  492. * @method Editor.findAll
  493. **/
  494. this.findAll = function(needle, options, additive) {
  495. options = options || {};
  496. options.needle = needle || options.needle;
  497. if (options.needle == undefined) {
  498. var range = this.selection.isEmpty()
  499. ? this.selection.getWordRange()
  500. : this.selection.getRange();
  501. options.needle = this.session.getTextRange(range);
  502. }
  503. this.$search.set(options);
  504. var ranges = this.$search.findAll(this.session);
  505. if (!ranges.length)
  506. return 0;
  507. this.$blockScrolling += 1;
  508. var selection = this.multiSelect;
  509. if (!additive)
  510. selection.toSingleRange(ranges[0]);
  511. for (var i = ranges.length; i--; )
  512. selection.addRange(ranges[i], true);
  513. // keep old selection as primary if possible
  514. if (range && selection.rangeList.rangeAtPoint(range.start))
  515. selection.addRange(range, true);
  516. this.$blockScrolling -= 1;
  517. return ranges.length;
  518. };
  519. /**
  520. * Adds a cursor above or below the active cursor.
  521. *
  522. * @param {Number} dir The direction of lines to select: -1 for up, 1 for down
  523. * @param {Boolean} skip If `true`, removes the active selection range
  524. *
  525. * @method Editor.selectMoreLines
  526. */
  527. this.selectMoreLines = function(dir, skip) {
  528. var range = this.selection.toOrientedRange();
  529. var isBackwards = range.cursor == range.end;
  530. var screenLead = this.session.documentToScreenPosition(range.cursor);
  531. if (this.selection.$desiredColumn)
  532. screenLead.column = this.selection.$desiredColumn;
  533. var lead = this.session.screenToDocumentPosition(screenLead.row + dir, screenLead.column);
  534. if (!range.isEmpty()) {
  535. var screenAnchor = this.session.documentToScreenPosition(isBackwards ? range.end : range.start);
  536. var anchor = this.session.screenToDocumentPosition(screenAnchor.row + dir, screenAnchor.column);
  537. } else {
  538. var anchor = lead;
  539. }
  540. if (isBackwards) {
  541. var newRange = Range.fromPoints(lead, anchor);
  542. newRange.cursor = newRange.start;
  543. } else {
  544. var newRange = Range.fromPoints(anchor, lead);
  545. newRange.cursor = newRange.end;
  546. }
  547. newRange.desiredColumn = screenLead.column;
  548. if (!this.selection.inMultiSelectMode) {
  549. this.selection.addRange(range);
  550. } else {
  551. if (skip)
  552. var toRemove = range.cursor;
  553. }
  554. this.selection.addRange(newRange);
  555. if (toRemove)
  556. this.selection.substractPoint(toRemove);
  557. };
  558. /**
  559. * Transposes the selected ranges.
  560. * @param {Number} dir The direction to rotate selections
  561. * @method Editor.transposeSelections
  562. **/
  563. this.transposeSelections = function(dir) {
  564. var session = this.session;
  565. var sel = session.multiSelect;
  566. var all = sel.ranges;
  567. for (var i = all.length; i--; ) {
  568. var range = all[i];
  569. if (range.isEmpty()) {
  570. var tmp = session.getWordRange(range.start.row, range.start.column);
  571. range.start.row = tmp.start.row;
  572. range.start.column = tmp.start.column;
  573. range.end.row = tmp.end.row;
  574. range.end.column = tmp.end.column;
  575. }
  576. }
  577. sel.mergeOverlappingRanges();
  578. var words = [];
  579. for (var i = all.length; i--; ) {
  580. var range = all[i];
  581. words.unshift(session.getTextRange(range));
  582. }
  583. if (dir < 0)
  584. words.unshift(words.pop());
  585. else
  586. words.push(words.shift());
  587. for (var i = all.length; i--; ) {
  588. var range = all[i];
  589. var tmp = range.clone();
  590. session.replace(range, words[i]);
  591. range.start.row = tmp.start.row;
  592. range.start.column = tmp.start.column;
  593. }
  594. };
  595. /**
  596. * Finds the next occurence of text in an active selection and adds it to the selections.
  597. * @param {Number} dir The direction of lines to select: -1 for up, 1 for down
  598. * @param {Boolean} skip If `true`, removes the active selection range
  599. * @method Editor.selectMore
  600. **/
  601. this.selectMore = function(dir, skip, stopAtFirst) {
  602. var session = this.session;
  603. var sel = session.multiSelect;
  604. var range = sel.toOrientedRange();
  605. if (range.isEmpty()) {
  606. range = session.getWordRange(range.start.row, range.start.column);
  607. range.cursor = dir == -1 ? range.start : range.end;
  608. this.multiSelect.addRange(range);
  609. if (stopAtFirst)
  610. return;
  611. }
  612. var needle = session.getTextRange(range);
  613. var newRange = find(session, needle, dir);
  614. if (newRange) {
  615. newRange.cursor = dir == -1 ? newRange.start : newRange.end;
  616. this.$blockScrolling += 1;
  617. this.session.unfold(newRange);
  618. this.multiSelect.addRange(newRange);
  619. this.$blockScrolling -= 1;
  620. this.renderer.scrollCursorIntoView(null, 0.5);
  621. }
  622. if (skip)
  623. this.multiSelect.substractPoint(range.cursor);
  624. };
  625. /**
  626. * Aligns the cursors or selected text.
  627. * @method Editor.alignCursors
  628. **/
  629. this.alignCursors = function() {
  630. var session = this.session;
  631. var sel = session.multiSelect;
  632. var ranges = sel.ranges;
  633. // filter out ranges on same row
  634. var row = -1;
  635. var sameRowRanges = ranges.filter(function(r) {
  636. if (r.cursor.row == row)
  637. return true;
  638. row = r.cursor.row;
  639. });
  640. if (!ranges.length || sameRowRanges.length == ranges.length - 1) {
  641. var range = this.selection.getRange();
  642. var fr = range.start.row, lr = range.end.row;
  643. var guessRange = fr == lr;
  644. if (guessRange) {
  645. var max = this.session.getLength();
  646. var line;
  647. do {
  648. line = this.session.getLine(lr);
  649. } while (/[=:]/.test(line) && ++lr < max);
  650. do {
  651. line = this.session.getLine(fr);
  652. } while (/[=:]/.test(line) && --fr > 0);
  653. if (fr < 0) fr = 0;
  654. if (lr >= max) lr = max - 1;
  655. }
  656. var lines = this.session.removeFullLines(fr, lr);
  657. lines = this.$reAlignText(lines, guessRange);
  658. this.session.insert({row: fr, column: 0}, lines.join("\n") + "\n");
  659. if (!guessRange) {
  660. range.start.column = 0;
  661. range.end.column = lines[lines.length - 1].length;
  662. }
  663. this.selection.setRange(range);
  664. } else {
  665. sameRowRanges.forEach(function(r) {
  666. sel.substractPoint(r.cursor);
  667. });
  668. var maxCol = 0;
  669. var minSpace = Infinity;
  670. var spaceOffsets = ranges.map(function(r) {
  671. var p = r.cursor;
  672. var line = session.getLine(p.row);
  673. var spaceOffset = line.substr(p.column).search(/\S/g);
  674. if (spaceOffset == -1)
  675. spaceOffset = 0;
  676. if (p.column > maxCol)
  677. maxCol = p.column;
  678. if (spaceOffset < minSpace)
  679. minSpace = spaceOffset;
  680. return spaceOffset;
  681. });
  682. ranges.forEach(function(r, i) {
  683. var p = r.cursor;
  684. var l = maxCol - p.column;
  685. var d = spaceOffsets[i] - minSpace;
  686. if (l > d)
  687. session.insert(p, lang.stringRepeat(" ", l - d));
  688. else
  689. session.remove(new Range(p.row, p.column, p.row, p.column - l + d));
  690. r.start.column = r.end.column = maxCol;
  691. r.start.row = r.end.row = p.row;
  692. r.cursor = r.end;
  693. });
  694. sel.fromOrientedRange(ranges[0]);
  695. this.renderer.updateCursor();
  696. this.renderer.updateBackMarkers();
  697. }
  698. };
  699. this.$reAlignText = function(lines, forceLeft) {
  700. var isLeftAligned = true, isRightAligned = true;
  701. var startW, textW, endW;
  702. return lines.map(function(line) {
  703. var m = line.match(/(\s*)(.*?)(\s*)([=:].*)/);
  704. if (!m)
  705. return [line];
  706. if (startW == null) {
  707. startW = m[1].length;
  708. textW = m[2].length;
  709. endW = m[3].length;
  710. return m;
  711. }
  712. if (startW + textW + endW != m[1].length + m[2].length + m[3].length)
  713. isRightAligned = false;
  714. if (startW != m[1].length)
  715. isLeftAligned = false;
  716. if (startW > m[1].length)
  717. startW = m[1].length;
  718. if (textW < m[2].length)
  719. textW = m[2].length;
  720. if (endW > m[3].length)
  721. endW = m[3].length;
  722. return m;
  723. }).map(forceLeft ? alignLeft :
  724. isLeftAligned ? isRightAligned ? alignRight : alignLeft : unAlign);
  725. function spaces(n) {
  726. return lang.stringRepeat(" ", n);
  727. }
  728. function alignLeft(m) {
  729. return !m[2] ? m[0] : spaces(startW) + m[2]
  730. + spaces(textW - m[2].length + endW)
  731. + m[4].replace(/^([=:])\s+/, "$1 ");
  732. }
  733. function alignRight(m) {
  734. return !m[2] ? m[0] : spaces(startW + textW - m[2].length) + m[2]
  735. + spaces(endW, " ")
  736. + m[4].replace(/^([=:])\s+/, "$1 ");
  737. }
  738. function unAlign(m) {
  739. return !m[2] ? m[0] : spaces(startW) + m[2]
  740. + spaces(endW)
  741. + m[4].replace(/^([=:])\s+/, "$1 ");
  742. }
  743. };
  744. }).call(Editor.prototype);
  745. function isSamePoint(p1, p2) {
  746. return p1.row == p2.row && p1.column == p2.column;
  747. }
  748. // patch
  749. // adds multicursor support to a session
  750. exports.onSessionChange = function(e) {
  751. var session = e.session;
  752. if (session && !session.multiSelect) {
  753. session.$selectionMarkers = [];
  754. session.selection.$initRangeList();
  755. session.multiSelect = session.selection;
  756. }
  757. this.multiSelect = session && session.multiSelect;
  758. var oldSession = e.oldSession;
  759. if (oldSession) {
  760. oldSession.multiSelect.off("addRange", this.$onAddRange);
  761. oldSession.multiSelect.off("removeRange", this.$onRemoveRange);
  762. oldSession.multiSelect.off("multiSelect", this.$onMultiSelect);
  763. oldSession.multiSelect.off("singleSelect", this.$onSingleSelect);
  764. oldSession.multiSelect.lead.off("change", this.$checkMultiselectChange);
  765. oldSession.multiSelect.anchor.off("change", this.$checkMultiselectChange);
  766. }
  767. if (session) {
  768. session.multiSelect.on("addRange", this.$onAddRange);
  769. session.multiSelect.on("removeRange", this.$onRemoveRange);
  770. session.multiSelect.on("multiSelect", this.$onMultiSelect);
  771. session.multiSelect.on("singleSelect", this.$onSingleSelect);
  772. session.multiSelect.lead.on("change", this.$checkMultiselectChange);
  773. session.multiSelect.anchor.on("change", this.$checkMultiselectChange);
  774. }
  775. if (session && this.inMultiSelectMode != session.selection.inMultiSelectMode) {
  776. if (session.selection.inMultiSelectMode)
  777. this.$onMultiSelect();
  778. else
  779. this.$onSingleSelect();
  780. }
  781. };
  782. // MultiSelect(editor)
  783. // adds multiple selection support to the editor
  784. // (note: should be called only once for each editor instance)
  785. function MultiSelect(editor) {
  786. if (editor.$multiselectOnSessionChange)
  787. return;
  788. editor.$onAddRange = editor.$onAddRange.bind(editor);
  789. editor.$onRemoveRange = editor.$onRemoveRange.bind(editor);
  790. editor.$onMultiSelect = editor.$onMultiSelect.bind(editor);
  791. editor.$onSingleSelect = editor.$onSingleSelect.bind(editor);
  792. editor.$multiselectOnSessionChange = exports.onSessionChange.bind(editor);
  793. editor.$checkMultiselectChange = editor.$checkMultiselectChange.bind(editor);
  794. editor.$multiselectOnSessionChange(editor);
  795. editor.on("changeSession", editor.$multiselectOnSessionChange);
  796. editor.on("mousedown", onMouseDown);
  797. editor.commands.addCommands(commands.defaultCommands);
  798. addAltCursorListeners(editor);
  799. }
  800. function addAltCursorListeners(editor){
  801. var el = editor.textInput.getElement();
  802. var altCursor = false;
  803. event.addListener(el, "keydown", function(e) {
  804. var altDown = e.keyCode == 18 && !(e.ctrlKey || e.shiftKey || e.metaKey);
  805. if (editor.$blockSelectEnabled && altDown) {
  806. if (!altCursor) {
  807. editor.renderer.setMouseCursor("crosshair");
  808. altCursor = true;
  809. }
  810. } else if (altCursor) {
  811. reset();
  812. }
  813. });
  814. event.addListener(el, "keyup", reset);
  815. event.addListener(el, "blur", reset);
  816. function reset(e) {
  817. if (altCursor) {
  818. editor.renderer.setMouseCursor("");
  819. altCursor = false;
  820. // TODO disable menu poping up
  821. // e && e.preventDefault()
  822. }
  823. }
  824. }
  825. exports.MultiSelect = MultiSelect;
  826. require("./config").defineOptions(Editor.prototype, "editor", {
  827. enableMultiselect: {
  828. set: function(val) {
  829. MultiSelect(this);
  830. if (val) {
  831. this.on("changeSession", this.$multiselectOnSessionChange);
  832. this.on("mousedown", onMouseDown);
  833. } else {
  834. this.off("changeSession", this.$multiselectOnSessionChange);
  835. this.off("mousedown", onMouseDown);
  836. }
  837. },
  838. value: true
  839. },
  840. enableBlockSelect: {
  841. set: function(val) {
  842. this.$blockSelectEnabled = val;
  843. },
  844. value: true
  845. }
  846. });
  847. });