multi_select.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956
  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._emit("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._emit("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._emit("removeRange", {ranges: removed});
  146. if (this.rangeCount == 0 && this.inMultiSelectMode) {
  147. this.inMultiSelectMode = false;
  148. this._emit("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. };
  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 {String} 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, $byLines) {
  405. if (this.inVirtualSelectionMode)
  406. return;
  407. var session = this.session;
  408. var selection = this.selection;
  409. var rangeList = selection.rangeList;
  410. var result;
  411. var reg = selection._eventRegistry;
  412. selection._eventRegistry = {};
  413. var tmpSel = new Selection(session);
  414. this.inVirtualSelectionMode = true;
  415. for (var i = rangeList.ranges.length; i--;) {
  416. if ($byLines) {
  417. while (i > 0 && rangeList.ranges[i].start.row == rangeList.ranges[i - 1].end.row)
  418. i--;
  419. }
  420. tmpSel.fromOrientedRange(rangeList.ranges[i]);
  421. this.selection = session.selection = tmpSel;
  422. var cmdResult = cmd.exec(this, args || {});
  423. if (!result == undefined)
  424. result = cmdResult;
  425. tmpSel.toOrientedRange(rangeList.ranges[i]);
  426. }
  427. tmpSel.detach();
  428. this.selection = session.selection = selection;
  429. this.inVirtualSelectionMode = false;
  430. selection._eventRegistry = reg;
  431. selection.mergeOverlappingRanges();
  432. var anim = this.renderer.$scrollAnimation;
  433. this.onCursorChange();
  434. this.onSelectionChange();
  435. if (anim && anim.from == anim.to)
  436. this.renderer.animateScrolling(anim.from);
  437. return result;
  438. };
  439. /**
  440. * Removes all the selections except the last added one.
  441. * @method Editor.exitMultiSelectMode
  442. **/
  443. this.exitMultiSelectMode = function() {
  444. if (!this.inMultiSelectMode || this.inVirtualSelectionMode)
  445. return;
  446. this.multiSelect.toSingleRange();
  447. };
  448. this.getSelectedText = function() {
  449. var text = "";
  450. if (this.inMultiSelectMode && !this.inVirtualSelectionMode) {
  451. var ranges = this.multiSelect.rangeList.ranges;
  452. var buf = [];
  453. for (var i = 0; i < ranges.length; i++) {
  454. buf.push(this.session.getTextRange(ranges[i]));
  455. }
  456. var nl = this.session.getDocument().getNewLineCharacter();
  457. text = buf.join(nl);
  458. if (text.length == (buf.length - 1) * nl.length)
  459. text = "";
  460. } else if (!this.selection.isEmpty()) {
  461. text = this.session.getTextRange(this.getSelectionRange());
  462. }
  463. return text;
  464. };
  465. // todo this should change when paste becomes a command
  466. this.onPaste = function(text) {
  467. if (this.$readOnly)
  468. return;
  469. this._signal("paste", text);
  470. if (!this.inMultiSelectMode || this.inVirtualSelectionMode)
  471. return this.insert(text);
  472. var lines = text.split(/\r\n|\r|\n/);
  473. var ranges = this.selection.rangeList.ranges;
  474. if (lines.length > ranges.length || lines.length < 2 || !lines[1])
  475. return this.commands.exec("insertstring", this, text);
  476. for (var i = ranges.length; i--;) {
  477. var range = ranges[i];
  478. if (!range.isEmpty())
  479. this.session.remove(range);
  480. this.session.insert(range.start, lines[i]);
  481. }
  482. };
  483. /**
  484. * Finds and selects all the occurences of `needle`.
  485. * @param {String} The text to find
  486. * @param {Object} The search options
  487. * @param {Boolean} keeps
  488. *
  489. * @returns {Number} The cumulative count of all found matches
  490. * @method Editor.findAll
  491. **/
  492. this.findAll = function(needle, options, additive) {
  493. options = options || {};
  494. options.needle = needle || options.needle;
  495. this.$search.set(options);
  496. var ranges = this.$search.findAll(this.session);
  497. if (!ranges.length)
  498. return 0;
  499. this.$blockScrolling += 1;
  500. var selection = this.multiSelect;
  501. if (!additive)
  502. selection.toSingleRange(ranges[0]);
  503. for (var i = ranges.length; i--; )
  504. selection.addRange(ranges[i], true);
  505. this.$blockScrolling -= 1;
  506. return ranges.length;
  507. };
  508. /**
  509. * Adds a cursor above or below the active cursor.
  510. *
  511. * @param {Number} dir The direction of lines to select: -1 for up, 1 for down
  512. * @param {Boolean} skip If `true`, removes the active selection range
  513. *
  514. * @method Editor.selectMoreLines
  515. */
  516. this.selectMoreLines = function(dir, skip) {
  517. var range = this.selection.toOrientedRange();
  518. var isBackwards = range.cursor == range.end;
  519. var screenLead = this.session.documentToScreenPosition(range.cursor);
  520. if (this.selection.$desiredColumn)
  521. screenLead.column = this.selection.$desiredColumn;
  522. var lead = this.session.screenToDocumentPosition(screenLead.row + dir, screenLead.column);
  523. if (!range.isEmpty()) {
  524. var screenAnchor = this.session.documentToScreenPosition(isBackwards ? range.end : range.start);
  525. var anchor = this.session.screenToDocumentPosition(screenAnchor.row + dir, screenAnchor.column);
  526. } else {
  527. var anchor = lead;
  528. }
  529. if (isBackwards) {
  530. var newRange = Range.fromPoints(lead, anchor);
  531. newRange.cursor = newRange.start;
  532. } else {
  533. var newRange = Range.fromPoints(anchor, lead);
  534. newRange.cursor = newRange.end;
  535. }
  536. newRange.desiredColumn = screenLead.column;
  537. if (!this.selection.inMultiSelectMode) {
  538. this.selection.addRange(range);
  539. } else {
  540. if (skip)
  541. var toRemove = range.cursor;
  542. }
  543. this.selection.addRange(newRange);
  544. if (toRemove)
  545. this.selection.substractPoint(toRemove);
  546. };
  547. /**
  548. * Transposes the selected ranges.
  549. * @param {Number} dir The direction to rotate selections
  550. * @method Editor.transposeSelections
  551. **/
  552. this.transposeSelections = function(dir) {
  553. var session = this.session;
  554. var sel = session.multiSelect;
  555. var all = sel.ranges;
  556. for (var i = all.length; i--; ) {
  557. var range = all[i];
  558. if (range.isEmpty()) {
  559. var tmp = session.getWordRange(range.start.row, range.start.column);
  560. range.start.row = tmp.start.row;
  561. range.start.column = tmp.start.column;
  562. range.end.row = tmp.end.row;
  563. range.end.column = tmp.end.column;
  564. }
  565. }
  566. sel.mergeOverlappingRanges();
  567. var words = [];
  568. for (var i = all.length; i--; ) {
  569. var range = all[i];
  570. words.unshift(session.getTextRange(range));
  571. }
  572. if (dir < 0)
  573. words.unshift(words.pop());
  574. else
  575. words.push(words.shift());
  576. for (var i = all.length; i--; ) {
  577. var range = all[i];
  578. var tmp = range.clone();
  579. session.replace(range, words[i]);
  580. range.start.row = tmp.start.row;
  581. range.start.column = tmp.start.column;
  582. }
  583. };
  584. /**
  585. * Finds the next occurence of text in an active selection and adds it to the selections.
  586. * @param {Number} dir The direction of lines to select: -1 for up, 1 for down
  587. * @param {Boolean} skip If `true`, removes the active selection range
  588. * @method Editor.selectMore
  589. **/
  590. this.selectMore = function(dir, skip) {
  591. var session = this.session;
  592. var sel = session.multiSelect;
  593. var range = sel.toOrientedRange();
  594. if (range.isEmpty()) {
  595. range = session.getWordRange(range.start.row, range.start.column);
  596. range.cursor = dir == -1 ? range.start : range.end;
  597. this.multiSelect.addRange(range);
  598. // todo add option for sublime like behavior
  599. // return;
  600. }
  601. var needle = session.getTextRange(range);
  602. var newRange = find(session, needle, dir);
  603. if (newRange) {
  604. newRange.cursor = dir == -1 ? newRange.start : newRange.end;
  605. this.$blockScrolling += 1;
  606. this.session.unfold(newRange);
  607. this.multiSelect.addRange(newRange);
  608. this.$blockScrolling -= 1;
  609. this.renderer.scrollCursorIntoView(null, 0.5);
  610. }
  611. if (skip)
  612. this.multiSelect.substractPoint(range.cursor);
  613. };
  614. /**
  615. * Aligns the cursors or selected text.
  616. * @method Editor.alignCursors
  617. **/
  618. this.alignCursors = function() {
  619. var session = this.session;
  620. var sel = session.multiSelect;
  621. var ranges = sel.ranges;
  622. if (!ranges.length) {
  623. var range = this.selection.getRange();
  624. var fr = range.start.row, lr = range.end.row;
  625. var guessRange = fr == lr;
  626. if (guessRange) {
  627. var max = this.session.getLength();
  628. var line;
  629. do {
  630. line = this.session.getLine(lr);
  631. } while (/[=:]/.test(line) && ++lr < max);
  632. do {
  633. line = this.session.getLine(fr);
  634. } while (/[=:]/.test(line) && --fr > 0);
  635. if (fr < 0) fr = 0;
  636. if (lr >= max) lr = max - 1;
  637. }
  638. var lines = this.session.doc.removeLines(fr, lr);
  639. lines = this.$reAlignText(lines, guessRange);
  640. this.session.doc.insert({row: fr, column: 0}, lines.join("\n") + "\n");
  641. if (!guessRange) {
  642. range.start.column = 0;
  643. range.end.column = lines[lines.length - 1].length;
  644. }
  645. this.selection.setRange(range);
  646. } else {
  647. // filter out ranges on same row
  648. var row = -1;
  649. var sameRowRanges = ranges.filter(function(r) {
  650. if (r.cursor.row == row)
  651. return true;
  652. row = r.cursor.row;
  653. });
  654. sel.$onRemoveRange(sameRowRanges);
  655. var maxCol = 0;
  656. var minSpace = Infinity;
  657. var spaceOffsets = ranges.map(function(r) {
  658. var p = r.cursor;
  659. var line = session.getLine(p.row);
  660. var spaceOffset = line.substr(p.column).search(/\S/g);
  661. if (spaceOffset == -1)
  662. spaceOffset = 0;
  663. if (p.column > maxCol)
  664. maxCol = p.column;
  665. if (spaceOffset < minSpace)
  666. minSpace = spaceOffset;
  667. return spaceOffset;
  668. });
  669. ranges.forEach(function(r, i) {
  670. var p = r.cursor;
  671. var l = maxCol - p.column;
  672. var d = spaceOffsets[i] - minSpace;
  673. if (l > d)
  674. session.insert(p, lang.stringRepeat(" ", l - d));
  675. else
  676. session.remove(new Range(p.row, p.column, p.row, p.column - l + d));
  677. r.start.column = r.end.column = maxCol;
  678. r.start.row = r.end.row = p.row;
  679. r.cursor = r.end;
  680. });
  681. sel.fromOrientedRange(ranges[0]);
  682. this.renderer.updateCursor();
  683. this.renderer.updateBackMarkers();
  684. }
  685. };
  686. this.$reAlignText = function(lines, forceLeft) {
  687. var isLeftAligned = true, isRightAligned = true;
  688. var startW, textW, endW;
  689. return lines.map(function(line) {
  690. var m = line.match(/(\s*)(.*?)(\s*)([=:].*)/);
  691. if (!m)
  692. return [line];
  693. if (startW == null) {
  694. startW = m[1].length;
  695. textW = m[2].length;
  696. endW = m[3].length;
  697. return m;
  698. }
  699. if (startW + textW + endW != m[1].length + m[2].length + m[3].length)
  700. isRightAligned = false;
  701. if (startW != m[1].length)
  702. isLeftAligned = false;
  703. if (startW > m[1].length)
  704. startW = m[1].length;
  705. if (textW < m[2].length)
  706. textW = m[2].length;
  707. if (endW > m[3].length)
  708. endW = m[3].length;
  709. return m;
  710. }).map(forceLeft ? alignLeft :
  711. isLeftAligned ? isRightAligned ? alignRight : alignLeft : unAlign);
  712. function spaces(n) {
  713. return lang.stringRepeat(" ", n);
  714. }
  715. function alignLeft(m) {
  716. return !m[2] ? m[0] : spaces(startW) + m[2]
  717. + spaces(textW - m[2].length + endW)
  718. + m[4].replace(/^([=:])\s+/, "$1 ")
  719. }
  720. function alignRight(m) {
  721. return !m[2] ? m[0] : spaces(startW + textW - m[2].length) + m[2]
  722. + spaces(endW, " ")
  723. + m[4].replace(/^([=:])\s+/, "$1 ")
  724. }
  725. function unAlign(m) {
  726. return !m[2] ? m[0] : spaces(startW) + m[2]
  727. + spaces(endW)
  728. + m[4].replace(/^([=:])\s+/, "$1 ")
  729. }
  730. }
  731. }).call(Editor.prototype);
  732. function isSamePoint(p1, p2) {
  733. return p1.row == p2.row && p1.column == p2.column;
  734. }
  735. // patch
  736. // adds multicursor support to a session
  737. exports.onSessionChange = function(e) {
  738. var session = e.session;
  739. if (!session.multiSelect) {
  740. session.$selectionMarkers = [];
  741. session.selection.$initRangeList();
  742. session.multiSelect = session.selection;
  743. }
  744. this.multiSelect = session.multiSelect;
  745. var oldSession = e.oldSession;
  746. if (oldSession) {
  747. oldSession.multiSelect.removeEventListener("addRange", this.$onAddRange);
  748. oldSession.multiSelect.removeEventListener("removeRange", this.$onRemoveRange);
  749. oldSession.multiSelect.removeEventListener("multiSelect", this.$onMultiSelect);
  750. oldSession.multiSelect.removeEventListener("singleSelect", this.$onSingleSelect);
  751. }
  752. session.multiSelect.on("addRange", this.$onAddRange);
  753. session.multiSelect.on("removeRange", this.$onRemoveRange);
  754. session.multiSelect.on("multiSelect", this.$onMultiSelect);
  755. session.multiSelect.on("singleSelect", this.$onSingleSelect);
  756. // this.$onSelectionChange = this.onSelectionChange.bind(this);
  757. if (this.inMultiSelectMode != session.selection.inMultiSelectMode) {
  758. if (session.selection.inMultiSelectMode)
  759. this.$onMultiSelect();
  760. else
  761. this.$onSingleSelect();
  762. }
  763. };
  764. // MultiSelect(editor)
  765. // adds multiple selection support to the editor
  766. // (note: should be called only once for each editor instance)
  767. function MultiSelect(editor) {
  768. if (editor.$multiselectOnSessionChange)
  769. return;
  770. editor.$onAddRange = editor.$onAddRange.bind(editor);
  771. editor.$onRemoveRange = editor.$onRemoveRange.bind(editor);
  772. editor.$onMultiSelect = editor.$onMultiSelect.bind(editor);
  773. editor.$onSingleSelect = editor.$onSingleSelect.bind(editor);
  774. editor.$multiselectOnSessionChange = exports.onSessionChange.bind(editor);
  775. editor.$multiselectOnSessionChange(editor);
  776. editor.on("changeSession", editor.$multiselectOnSessionChange);
  777. editor.on("mousedown", onMouseDown);
  778. editor.commands.addCommands(commands.defaultCommands);
  779. addAltCursorListeners(editor);
  780. }
  781. function addAltCursorListeners(editor){
  782. var el = editor.textInput.getElement();
  783. var altCursor = false;
  784. event.addListener(el, "keydown", function(e) {
  785. if (e.keyCode == 18 && !(e.ctrlKey || e.shiftKey || e.metaKey)) {
  786. if (!altCursor) {
  787. editor.renderer.setMouseCursor("crosshair");
  788. altCursor = true;
  789. }
  790. } else if (altCursor) {
  791. reset();
  792. }
  793. });
  794. event.addListener(el, "keyup", reset);
  795. event.addListener(el, "blur", reset);
  796. function reset(e) {
  797. if (altCursor) {
  798. editor.renderer.setMouseCursor("");
  799. altCursor = false;
  800. // TODO disable menu poping up
  801. // e && e.preventDefault()
  802. }
  803. }
  804. }
  805. exports.MultiSelect = MultiSelect;
  806. require("./config").defineOptions(Editor.prototype, "editor", {
  807. enableMultiselect: {
  808. set: function(val) {
  809. MultiSelect(this);
  810. if (val) {
  811. this.on("changeSession", this.$multiselectOnSessionChange);
  812. this.on("mousedown", onMouseDown);
  813. } else {
  814. this.off("changeSession", this.$multiselectOnSessionChange);
  815. this.off("mousedown", onMouseDown);
  816. }
  817. },
  818. value: true
  819. }
  820. })
  821. });