multi_select.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972
  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(ace_require, exports, module) {
  31. var RangeList = ace_require("./range_list").RangeList;
  32. var Range = ace_require("./range").Range;
  33. var Selection = ace_require("./selection").Selection;
  34. var onMouseDown = ace_require("./mouse/multi_select_handler").onMouseDown;
  35. var event = ace_require("./lib/event");
  36. var lang = ace_require("./lib/lang");
  37. var commands = ace_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 = ace_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 = ace_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. };
  127. this.$onAddRange = function(range) {
  128. this.rangeCount = this.rangeList.ranges.length;
  129. this.ranges.unshift(range);
  130. this._signal("addRange", {range: range});
  131. };
  132. this.$onRemoveRange = function(removed) {
  133. this.rangeCount = this.rangeList.ranges.length;
  134. if (this.rangeCount == 1 && this.inMultiSelectMode) {
  135. var lastRange = this.rangeList.ranges.pop();
  136. removed.push(lastRange);
  137. this.rangeCount = 0;
  138. }
  139. for (var i = removed.length; i--; ) {
  140. var index = this.ranges.indexOf(removed[i]);
  141. this.ranges.splice(index, 1);
  142. }
  143. this._signal("removeRange", {ranges: removed});
  144. if (this.rangeCount === 0 && this.inMultiSelectMode) {
  145. this.inMultiSelectMode = false;
  146. this._signal("singleSelect");
  147. this.session.$undoSelect = true;
  148. this.rangeList.detach(this.session);
  149. }
  150. lastRange = lastRange || this.ranges[0];
  151. if (lastRange && !lastRange.isEqual(this.getRange()))
  152. this.fromOrientedRange(lastRange);
  153. };
  154. // adds multicursor support to selection
  155. this.$initRangeList = function() {
  156. if (this.rangeList)
  157. return;
  158. this.rangeList = new RangeList();
  159. this.ranges = [];
  160. this.rangeCount = 0;
  161. };
  162. /**
  163. * Returns a concatenation of all the ranges.
  164. * @returns {Array}
  165. * @method Selection.getAllRanges
  166. **/
  167. this.getAllRanges = function() {
  168. return this.rangeCount ? this.rangeList.ranges.concat() : [this.getRange()];
  169. };
  170. /**
  171. * Splits all the ranges into lines.
  172. * @method Selection.splitIntoLines
  173. **/
  174. this.splitIntoLines = function () {
  175. var ranges = this.ranges.length ? this.ranges : [this.getRange()];
  176. var newRanges = [];
  177. for (var i = 0; i < ranges.length; i++) {
  178. var range = ranges[i];
  179. var row = range.start.row;
  180. var endRow = range.end.row;
  181. if (row === endRow) {
  182. newRanges.push(range.clone());
  183. } else {
  184. newRanges.push(new Range(row, range.start.column, row, this.session.getLine(row).length));
  185. while (++row < endRow)
  186. newRanges.push(this.getLineRange(row, true));
  187. newRanges.push(new Range(endRow, 0, endRow, range.end.column));
  188. }
  189. if (i == 0 && !this.isBackwards())
  190. newRanges = newRanges.reverse();
  191. }
  192. this.toSingleRange();
  193. for (var i = newRanges.length; i--;)
  194. this.addRange(newRanges[i]);
  195. };
  196. this.joinSelections = function () {
  197. var ranges = this.rangeList.ranges;
  198. var lastRange = ranges[ranges.length - 1];
  199. var range = Range.fromPoints(ranges[0].start, lastRange.end);
  200. this.toSingleRange();
  201. this.setSelectionRange(range, lastRange.cursor == lastRange.start);
  202. };
  203. /**
  204. * @method Selection.toggleBlockSelection
  205. **/
  206. this.toggleBlockSelection = function () {
  207. if (this.rangeCount > 1) {
  208. var ranges = this.rangeList.ranges;
  209. var lastRange = ranges[ranges.length - 1];
  210. var range = Range.fromPoints(ranges[0].start, lastRange.end);
  211. this.toSingleRange();
  212. this.setSelectionRange(range, lastRange.cursor == lastRange.start);
  213. } else {
  214. var cursor = this.session.documentToScreenPosition(this.cursor);
  215. var anchor = this.session.documentToScreenPosition(this.anchor);
  216. var rectSel = this.rectangularRangeBlock(cursor, anchor);
  217. rectSel.forEach(this.addRange, this);
  218. }
  219. };
  220. /**
  221. *
  222. * Gets list of ranges composing rectangular block on the screen
  223. *
  224. * @param {Cursor} screenCursor The cursor to use
  225. * @param {Anchor} screenAnchor The anchor to use
  226. * @param {Boolean} includeEmptyLines If true, this includes ranges inside the block which are empty due to clipping
  227. * @returns {Range}
  228. * @method Selection.rectangularRangeBlock
  229. **/
  230. this.rectangularRangeBlock = function(screenCursor, screenAnchor, includeEmptyLines) {
  231. var rectSel = [];
  232. var xBackwards = screenCursor.column < screenAnchor.column;
  233. if (xBackwards) {
  234. var startColumn = screenCursor.column;
  235. var endColumn = screenAnchor.column;
  236. var startOffsetX = screenCursor.offsetX;
  237. var endOffsetX = screenAnchor.offsetX;
  238. } else {
  239. var startColumn = screenAnchor.column;
  240. var endColumn = screenCursor.column;
  241. var startOffsetX = screenAnchor.offsetX;
  242. var endOffsetX = screenCursor.offsetX;
  243. }
  244. var yBackwards = screenCursor.row < screenAnchor.row;
  245. if (yBackwards) {
  246. var startRow = screenCursor.row;
  247. var endRow = screenAnchor.row;
  248. } else {
  249. var startRow = screenAnchor.row;
  250. var endRow = screenCursor.row;
  251. }
  252. if (startColumn < 0)
  253. startColumn = 0;
  254. if (startRow < 0)
  255. startRow = 0;
  256. if (startRow == endRow)
  257. includeEmptyLines = true;
  258. var docEnd;
  259. for (var row = startRow; row <= endRow; row++) {
  260. var range = Range.fromPoints(
  261. this.session.screenToDocumentPosition(row, startColumn, startOffsetX),
  262. this.session.screenToDocumentPosition(row, endColumn, endOffsetX)
  263. );
  264. if (range.isEmpty()) {
  265. if (docEnd && isSamePoint(range.end, docEnd))
  266. break;
  267. docEnd = range.end;
  268. }
  269. range.cursor = xBackwards ? range.start : range.end;
  270. rectSel.push(range);
  271. }
  272. if (yBackwards)
  273. rectSel.reverse();
  274. if (!includeEmptyLines) {
  275. var end = rectSel.length - 1;
  276. while (rectSel[end].isEmpty() && end > 0)
  277. end--;
  278. if (end > 0) {
  279. var start = 0;
  280. while (rectSel[start].isEmpty())
  281. start++;
  282. }
  283. for (var i = end; i >= start; i--) {
  284. if (rectSel[i].isEmpty())
  285. rectSel.splice(i, 1);
  286. }
  287. }
  288. return rectSel;
  289. };
  290. }).call(Selection.prototype);
  291. // extend Editor
  292. var Editor = ace_require("./editor").Editor;
  293. (function() {
  294. /**
  295. *
  296. * Updates the cursor and marker layers.
  297. * @method Editor.updateSelectionMarkers
  298. *
  299. **/
  300. this.updateSelectionMarkers = function() {
  301. this.renderer.updateCursor();
  302. this.renderer.updateBackMarkers();
  303. };
  304. /**
  305. * Adds the selection and cursor.
  306. * @param {Range} orientedRange A range containing a cursor
  307. * @returns {Range}
  308. * @method Editor.addSelectionMarker
  309. **/
  310. this.addSelectionMarker = function(orientedRange) {
  311. if (!orientedRange.cursor)
  312. orientedRange.cursor = orientedRange.end;
  313. var style = this.getSelectionStyle();
  314. orientedRange.marker = this.session.addMarker(orientedRange, "ace_selection", style);
  315. this.session.$selectionMarkers.push(orientedRange);
  316. this.session.selectionMarkerCount = this.session.$selectionMarkers.length;
  317. return orientedRange;
  318. };
  319. /**
  320. * Removes the selection marker.
  321. * @param {Range} range The selection range added with [[Editor.addSelectionMarker `addSelectionMarker()`]].
  322. * @method Editor.removeSelectionMarker
  323. **/
  324. this.removeSelectionMarker = function(range) {
  325. if (!range.marker)
  326. return;
  327. this.session.removeMarker(range.marker);
  328. var index = this.session.$selectionMarkers.indexOf(range);
  329. if (index != -1)
  330. this.session.$selectionMarkers.splice(index, 1);
  331. this.session.selectionMarkerCount = this.session.$selectionMarkers.length;
  332. };
  333. this.removeSelectionMarkers = function(ranges) {
  334. var markerList = this.session.$selectionMarkers;
  335. for (var i = ranges.length; i--; ) {
  336. var range = ranges[i];
  337. if (!range.marker)
  338. continue;
  339. this.session.removeMarker(range.marker);
  340. var index = markerList.indexOf(range);
  341. if (index != -1)
  342. markerList.splice(index, 1);
  343. }
  344. this.session.selectionMarkerCount = markerList.length;
  345. };
  346. this.$onAddRange = function(e) {
  347. this.addSelectionMarker(e.range);
  348. this.renderer.updateCursor();
  349. this.renderer.updateBackMarkers();
  350. };
  351. this.$onRemoveRange = function(e) {
  352. this.removeSelectionMarkers(e.ranges);
  353. this.renderer.updateCursor();
  354. this.renderer.updateBackMarkers();
  355. };
  356. this.$onMultiSelect = function(e) {
  357. if (this.inMultiSelectMode)
  358. return;
  359. this.inMultiSelectMode = true;
  360. this.setStyle("ace_multiselect");
  361. this.keyBinding.addKeyboardHandler(commands.keyboardHandler);
  362. this.commands.setDefaultHandler("exec", this.$onMultiSelectExec);
  363. this.renderer.updateCursor();
  364. this.renderer.updateBackMarkers();
  365. };
  366. this.$onSingleSelect = function(e) {
  367. if (this.session.multiSelect.inVirtualMode)
  368. return;
  369. this.inMultiSelectMode = false;
  370. this.unsetStyle("ace_multiselect");
  371. this.keyBinding.removeKeyboardHandler(commands.keyboardHandler);
  372. this.commands.removeDefaultHandler("exec", this.$onMultiSelectExec);
  373. this.renderer.updateCursor();
  374. this.renderer.updateBackMarkers();
  375. this._emit("changeSelection");
  376. };
  377. this.$onMultiSelectExec = function(e) {
  378. var command = e.command;
  379. var editor = e.editor;
  380. if (!editor.multiSelect)
  381. return;
  382. if (!command.multiSelectAction) {
  383. var result = command.exec(editor, e.args || {});
  384. editor.multiSelect.addRange(editor.multiSelect.toOrientedRange());
  385. editor.multiSelect.mergeOverlappingRanges();
  386. } else if (command.multiSelectAction == "forEach") {
  387. result = editor.forEachSelection(command, e.args);
  388. } else if (command.multiSelectAction == "forEachLine") {
  389. result = editor.forEachSelection(command, e.args, true);
  390. } else if (command.multiSelectAction == "single") {
  391. editor.exitMultiSelectMode();
  392. result = command.exec(editor, e.args || {});
  393. } else {
  394. result = command.multiSelectAction(editor, e.args || {});
  395. }
  396. return result;
  397. };
  398. /**
  399. * Executes a command for each selection range.
  400. * @param {Object} cmd The command to execute
  401. * @param {String} args Any arguments for the command
  402. * @method Editor.forEachSelection
  403. **/
  404. this.forEachSelection = function(cmd, args, options) {
  405. if (this.inVirtualSelectionMode)
  406. return;
  407. var keepOrder = options && options.keepOrder;
  408. var $byLines = options == true || options && options.$byLines;
  409. var session = this.session;
  410. var selection = this.selection;
  411. var rangeList = selection.rangeList;
  412. var ranges = (keepOrder ? selection : rangeList).ranges;
  413. var result;
  414. if (!ranges.length)
  415. return cmd.exec ? cmd.exec(this, args || {}) : cmd(this, args || {});
  416. var reg = selection._eventRegistry;
  417. selection._eventRegistry = {};
  418. var tmpSel = new Selection(session);
  419. this.inVirtualSelectionMode = true;
  420. for (var i = ranges.length; i--;) {
  421. if ($byLines) {
  422. while (i > 0 && ranges[i].start.row == ranges[i - 1].end.row)
  423. i--;
  424. }
  425. tmpSel.fromOrientedRange(ranges[i]);
  426. tmpSel.index = i;
  427. this.selection = session.selection = tmpSel;
  428. var cmdResult = cmd.exec ? cmd.exec(this, args || {}) : cmd(this, args || {});
  429. if (!result && cmdResult !== undefined)
  430. result = cmdResult;
  431. tmpSel.toOrientedRange(ranges[i]);
  432. }
  433. tmpSel.detach();
  434. this.selection = session.selection = selection;
  435. this.inVirtualSelectionMode = false;
  436. selection._eventRegistry = reg;
  437. selection.mergeOverlappingRanges();
  438. if (selection.ranges[0])
  439. selection.fromOrientedRange(selection.ranges[0]);
  440. var anim = this.renderer.$scrollAnimation;
  441. this.onCursorChange();
  442. this.onSelectionChange();
  443. if (anim && anim.from == anim.to)
  444. this.renderer.animateScrolling(anim.from);
  445. return result;
  446. };
  447. /**
  448. * Removes all the selections except the last added one.
  449. * @method Editor.exitMultiSelectMode
  450. **/
  451. this.exitMultiSelectMode = function() {
  452. if (!this.inMultiSelectMode || this.inVirtualSelectionMode)
  453. return;
  454. this.multiSelect.toSingleRange();
  455. };
  456. this.getSelectedText = function() {
  457. var text = "";
  458. if (this.inMultiSelectMode && !this.inVirtualSelectionMode) {
  459. var ranges = this.multiSelect.rangeList.ranges;
  460. var buf = [];
  461. for (var i = 0; i < ranges.length; i++) {
  462. buf.push(this.session.getTextRange(ranges[i]));
  463. }
  464. var nl = this.session.getDocument().getNewLineCharacter();
  465. text = buf.join(nl);
  466. if (text.length == (buf.length - 1) * nl.length)
  467. text = "";
  468. } else if (!this.selection.isEmpty()) {
  469. text = this.session.getTextRange(this.getSelectionRange());
  470. }
  471. return text;
  472. };
  473. this.$checkMultiselectChange = function(e, anchor) {
  474. if (this.inMultiSelectMode && !this.inVirtualSelectionMode) {
  475. var range = this.multiSelect.ranges[0];
  476. if (this.multiSelect.isEmpty() && anchor == this.multiSelect.anchor)
  477. return;
  478. var pos = anchor == this.multiSelect.anchor
  479. ? range.cursor == range.start ? range.end : range.start
  480. : range.cursor;
  481. if (pos.row != anchor.row
  482. || this.session.$clipPositionToDocument(pos.row, pos.column).column != anchor.column)
  483. this.multiSelect.toSingleRange(this.multiSelect.toOrientedRange());
  484. else
  485. this.multiSelect.mergeOverlappingRanges();
  486. }
  487. };
  488. /**
  489. * Finds and selects all the occurrences of `needle`.
  490. * @param {String} The text to find
  491. * @param {Object} The search options
  492. * @param {Boolean} keeps
  493. *
  494. * @returns {Number} The cumulative count of all found matches
  495. * @method Editor.findAll
  496. **/
  497. this.findAll = function(needle, options, additive) {
  498. options = options || {};
  499. options.needle = needle || options.needle;
  500. if (options.needle == undefined) {
  501. var range = this.selection.isEmpty()
  502. ? this.selection.getWordRange()
  503. : this.selection.getRange();
  504. options.needle = this.session.getTextRange(range);
  505. }
  506. this.$search.set(options);
  507. var ranges = this.$search.findAll(this.session);
  508. if (!ranges.length)
  509. return 0;
  510. var selection = this.multiSelect;
  511. if (!additive)
  512. selection.toSingleRange(ranges[0]);
  513. for (var i = ranges.length; i--; )
  514. selection.addRange(ranges[i], true);
  515. // keep old selection as primary if possible
  516. if (range && selection.rangeList.rangeAtPoint(range.start))
  517. selection.addRange(range, true);
  518. return ranges.length;
  519. };
  520. /**
  521. * Adds a cursor above or below the active cursor.
  522. *
  523. * @param {Number} dir The direction of lines to select: -1 for up, 1 for down
  524. * @param {Boolean} skip If `true`, removes the active selection range
  525. *
  526. * @method Editor.selectMoreLines
  527. */
  528. this.selectMoreLines = function(dir, skip) {
  529. var range = this.selection.toOrientedRange();
  530. var isBackwards = range.cursor == range.end;
  531. var screenLead = this.session.documentToScreenPosition(range.cursor);
  532. if (this.selection.$desiredColumn)
  533. screenLead.column = this.selection.$desiredColumn;
  534. var lead = this.session.screenToDocumentPosition(screenLead.row + dir, screenLead.column);
  535. if (!range.isEmpty()) {
  536. var screenAnchor = this.session.documentToScreenPosition(isBackwards ? range.end : range.start);
  537. var anchor = this.session.screenToDocumentPosition(screenAnchor.row + dir, screenAnchor.column);
  538. } else {
  539. var anchor = lead;
  540. }
  541. if (isBackwards) {
  542. var newRange = Range.fromPoints(lead, anchor);
  543. newRange.cursor = newRange.start;
  544. } else {
  545. var newRange = Range.fromPoints(anchor, lead);
  546. newRange.cursor = newRange.end;
  547. }
  548. newRange.desiredColumn = screenLead.column;
  549. if (!this.selection.inMultiSelectMode) {
  550. this.selection.addRange(range);
  551. } else {
  552. if (skip)
  553. var toRemove = range.cursor;
  554. }
  555. this.selection.addRange(newRange);
  556. if (toRemove)
  557. this.selection.substractPoint(toRemove);
  558. };
  559. /**
  560. * Transposes the selected ranges.
  561. * @param {Number} dir The direction to rotate selections
  562. * @method Editor.transposeSelections
  563. **/
  564. this.transposeSelections = function(dir) {
  565. var session = this.session;
  566. var sel = session.multiSelect;
  567. var all = sel.ranges;
  568. for (var i = all.length; i--; ) {
  569. var range = all[i];
  570. if (range.isEmpty()) {
  571. var tmp = session.getWordRange(range.start.row, range.start.column);
  572. range.start.row = tmp.start.row;
  573. range.start.column = tmp.start.column;
  574. range.end.row = tmp.end.row;
  575. range.end.column = tmp.end.column;
  576. }
  577. }
  578. sel.mergeOverlappingRanges();
  579. var words = [];
  580. for (var i = all.length; i--; ) {
  581. var range = all[i];
  582. words.unshift(session.getTextRange(range));
  583. }
  584. if (dir < 0)
  585. words.unshift(words.pop());
  586. else
  587. words.push(words.shift());
  588. for (var i = all.length; i--; ) {
  589. var range = all[i];
  590. var tmp = range.clone();
  591. session.replace(range, words[i]);
  592. range.start.row = tmp.start.row;
  593. range.start.column = tmp.start.column;
  594. }
  595. sel.fromOrientedRange(sel.ranges[0]);
  596. };
  597. /**
  598. * Finds the next occurrence of text in an active selection and adds it to the selections.
  599. * @param {Number} dir The direction of lines to select: -1 for up, 1 for down
  600. * @param {Boolean} skip If `true`, removes the active selection range
  601. * @method Editor.selectMore
  602. **/
  603. this.selectMore = function(dir, skip, stopAtFirst) {
  604. var session = this.session;
  605. var sel = session.multiSelect;
  606. var range = sel.toOrientedRange();
  607. if (range.isEmpty()) {
  608. range = session.getWordRange(range.start.row, range.start.column);
  609. range.cursor = dir == -1 ? range.start : range.end;
  610. this.multiSelect.addRange(range);
  611. if (stopAtFirst)
  612. return;
  613. }
  614. var needle = session.getTextRange(range);
  615. var newRange = find(session, needle, dir);
  616. if (newRange) {
  617. newRange.cursor = dir == -1 ? newRange.start : newRange.end;
  618. this.session.unfold(newRange);
  619. this.multiSelect.addRange(newRange);
  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. if (!editor.textInput) return;
  802. var el = editor.textInput.getElement();
  803. var altCursor = false;
  804. event.addListener(el, "keydown", function(e) {
  805. var altDown = e.keyCode == 18 && !(e.ctrlKey || e.shiftKey || e.metaKey);
  806. if (editor.$blockSelectEnabled && altDown) {
  807. if (!altCursor) {
  808. editor.renderer.setMouseCursor("crosshair");
  809. altCursor = true;
  810. }
  811. } else if (altCursor) {
  812. reset();
  813. }
  814. }, editor);
  815. event.addListener(el, "keyup", reset, editor);
  816. event.addListener(el, "blur", reset, editor);
  817. function reset(e) {
  818. if (altCursor) {
  819. editor.renderer.setMouseCursor("");
  820. altCursor = false;
  821. // TODO disable menu popping up
  822. // e && e.preventDefault()
  823. }
  824. }
  825. }
  826. exports.MultiSelect = MultiSelect;
  827. ace_require("./config").defineOptions(Editor.prototype, "editor", {
  828. enableMultiselect: {
  829. set: function(val) {
  830. MultiSelect(this);
  831. if (val) {
  832. this.on("changeSession", this.$multiselectOnSessionChange);
  833. this.on("mousedown", onMouseDown);
  834. } else {
  835. this.off("changeSession", this.$multiselectOnSessionChange);
  836. this.off("mousedown", onMouseDown);
  837. }
  838. },
  839. value: true
  840. },
  841. enableBlockSelect: {
  842. set: function(val) {
  843. this.$blockSelectEnabled = val;
  844. },
  845. value: true
  846. }
  847. });
  848. });