/* vim:ts=4:sts=4:sw=4: * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Ajax.org Code Editor (ACE). * * The Initial Developer of the Original Code is * Ajax.org B.V. * Portions created by the Initial Developer are Copyright (C) 2010 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Harutyun Amirjanyan * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ define(function(require, exports, module) { var RangeList = require("./range_list").RangeList; var Range = require("./range").Range; var Selection = require("./selection").Selection; var onMouseDown = require("./mouse/multi_select_handler").onMouseDown; var event = require("./lib/event"); var commands = require("./commands/multi_select_commands"); exports.commands = commands.defaultCommands.concat(commands.multiSelectCommands); // Todo: session.find or editor.findVolatile that returns range var Search = require("./search").Search; var search = new Search(); function find(session, needle, dir) { search.$options.wrap = true; search.$options.needle = needle; search.$options.backwards = dir == -1; return search.find(session); } // extend EditSession var EditSession = require("./edit_session").EditSession; (function() { this.getSelectionMarkers = function() { return this.$selectionMarkers; }; }).call(EditSession.prototype); // extend Selection (function() { // list of ranges in reverse addition order this.ranges = null; // automatically sorted list of ranges this.rangeList = null; /** extension * Selection.addRange(range, $blockChangeEvents) * - range (Range): The new range to add * - $blockChangeEvents (Boolean): Whether or not to block changing events * * Adds a range to a selection by entering multiselect mode, if necessary. **/ this.addRange = function(range, $blockChangeEvents) { if (!range) return; if (!this.inMultiSelectMode && this.rangeCount == 0) { var oldRange = this.toOrientedRange(); if (range.intersects(oldRange)) return $blockChangeEvents || this.fromOrientedRange(range); this.rangeList.add(oldRange); this.$onAddRange(oldRange); } if (!range.cursor) range.cursor = range.end; var removed = this.rangeList.add(range); this.$onAddRange(range); if (removed.length) this.$onRemoveRange(removed); if (this.rangeCount > 1 && !this.inMultiSelectMode) { this._emit("multiSelect"); this.inMultiSelectMode = true; this.session.$undoSelect = false; this.rangeList.attach(this.session); } return $blockChangeEvents || this.fromOrientedRange(range); }; this.toSingleRange = function(range) { range = range || this.ranges[0]; var removed = this.rangeList.removeAll(); if (removed.length) this.$onRemoveRange(removed); range && this.fromOrientedRange(range); }; /** extension * Selection.substractPoint(pos) -> Range * - pos (Range): The position to remove, as a `{row, column}` object * * Removes a Range containing pos (if it exists). **/ this.substractPoint = function(pos) { var removed = this.rangeList.substractPoint(pos); if (removed) { this.$onRemoveRange(removed); return removed[0]; } }; /** extension * Selection.mergeOverlappingRanges() * * Merges overlapping ranges ensuring consistency after changes **/ this.mergeOverlappingRanges = function() { var removed = this.rangeList.merge(); if (removed.length) this.$onRemoveRange(removed); else if(this.ranges[0]) this.fromOrientedRange(this.ranges[0]); }; this.$onAddRange = function(range) { this.rangeCount = this.rangeList.ranges.length; this.ranges.unshift(range); this._emit("addRange", {range: range}); }; this.$onRemoveRange = function(removed) { this.rangeCount = this.rangeList.ranges.length; if (this.rangeCount == 1 && this.inMultiSelectMode) { var lastRange = this.rangeList.ranges.pop(); removed.push(lastRange); this.rangeCount = 0; } for (var i = removed.length; i--; ) { var index = this.ranges.indexOf(removed[i]); this.ranges.splice(index, 1); } this._emit("removeRange", {ranges: removed}); if (this.rangeCount == 0 && this.inMultiSelectMode) { this.inMultiSelectMode = false; this._emit("singleSelect"); this.session.$undoSelect = true; this.rangeList.detach(this.session); } lastRange = lastRange || this.ranges[0]; if (lastRange && !lastRange.isEqual(this.getRange())) this.fromOrientedRange(lastRange); }; // adds multicursor support to selection this.$initRangeList = function() { if (this.rangeList) return; this.rangeList = new RangeList(); this.ranges = []; this.rangeCount = 0; }; this.getAllRanges = function() { return this.rangeList.ranges.concat(); }; this.splitIntoLines = function () { if (this.rangeCount > 1) { var ranges = this.rangeList.ranges; var lastRange = ranges[ranges.length - 1]; var range = Range.fromPoints(ranges[0].start, lastRange.end); this.toSingleRange(); this.setSelectionRange(range, lastRange.cursor == lastRange.start); } else { var range = this.getRange(); var startRow = range.start.row; var endRow = range.end.row; if (startRow == endRow) return; var rectSel = []; var r = this.getLineRange(startRow, true); r.start.column = range.start.column; rectSel.push(r); for (var i = startRow + 1; i < endRow; i++) rectSel.push(this.getLineRange(i, true)); r = this.getLineRange(endRow, true); r.end.column = range.end.column; rectSel.push(r); rectSel.forEach(this.addRange, this); } }; this.toggleBlockSelection = function () { if (this.rangeCount > 1) { var ranges = this.rangeList.ranges; var lastRange = ranges[ranges.length - 1]; var range = Range.fromPoints(ranges[0].start, lastRange.end); this.toSingleRange(); this.setSelectionRange(range, lastRange.cursor == lastRange.start); } else { var cursor = this.session.documentToScreenPosition(this.selectionLead); var anchor = this.session.documentToScreenPosition(this.selectionAnchor); var rectSel = this.rectangularRangeBlock(cursor, anchor); rectSel.forEach(this.addRange, this); } }; /** extension * Selection.rectangularRangeBlock(screenCursor, screenAnchor, includeEmptyLines) -> Range * - screenCursor (Cursor): The cursor to use * - screenAnchor (Anchor): The anchor to use * - includeEmptyLins (Boolean): If true, this includes ranges inside the block which are empty due to clipping * * Gets list of ranges composing rectangular block on the screen * */ this.rectangularRangeBlock = function(screenCursor, screenAnchor, includeEmptyLines) { var rectSel = []; var xBackwards = screenCursor.column < screenAnchor.column; if (xBackwards) { var startColumn = screenCursor.column; var endColumn = screenAnchor.column; } else { var startColumn = screenAnchor.column; var endColumn = screenCursor.column; } var yBackwards = screenCursor.row < screenAnchor.row; if (yBackwards) { var startRow = screenCursor.row; var endRow = screenAnchor.row; } else { var startRow = screenAnchor.row; var endRow = screenCursor.row; } if (startColumn < 0) startColumn = 0; if (startRow < 0) startRow = 0; if (startRow == endRow) includeEmptyLines = true; for (var row = startRow; row <= endRow; row++) { var range = Range.fromPoints( this.session.screenToDocumentPosition(row, startColumn), this.session.screenToDocumentPosition(row, endColumn) ); if (range.isEmpty()) { if (docEnd && isSamePoint(range.end, docEnd)) break; var docEnd = range.end; } range.cursor = xBackwards ? range.start : range.end; rectSel.push(range); } if (yBackwards) rectSel.reverse(); if (!includeEmptyLines) { var end = rectSel.length - 1; while (rectSel[end].isEmpty() && end > 0) end--; if (end > 0) { var start = 0; while (rectSel[start].isEmpty()) start++; } for (var i = end; i >= start; i--) { if (rectSel[i].isEmpty()) rectSel.splice(i, 1); } } return rectSel; }; }).call(Selection.prototype); // extend Editor var Editor = require("./editor").Editor; (function() { /** extension * Editor.updateSelectionMarkers() * * Updates the cursor and marker layers. **/ this.updateSelectionMarkers = function() { this.renderer.updateCursor(); this.renderer.updateBackMarkers(); }; /** extension * Editor.addSelectionMarker(orientedRange) -> Range * - orientedRange (Range): A range containing a cursor * * Adds the selection and cursor. **/ this.addSelectionMarker = function(orientedRange) { if (!orientedRange.cursor) orientedRange.cursor = orientedRange.end; var style = this.getSelectionStyle(); orientedRange.marker = this.session.addMarker(orientedRange, "ace_selection", style); this.session.$selectionMarkers.push(orientedRange); this.session.selectionMarkerCount = this.session.$selectionMarkers.length; return orientedRange; }; /** extension * Editor.removeSelectionMarker(range) * - range (Range): The selection range added with [[Editor.addSelectionMarker `addSelectionMarker()`]]. * * Removes the selection marker. **/ this.removeSelectionMarker = function(range) { if (!range.marker) return; this.session.removeMarker(range.marker); var index = this.session.$selectionMarkers.indexOf(range); if (index != -1) this.session.$selectionMarkers.splice(index, 1); this.session.selectionMarkerCount = this.session.$selectionMarkers.length; }; this.removeSelectionMarkers = function(ranges) { var markerList = this.session.$selectionMarkers; for (var i = ranges.length; i--; ) { var range = ranges[i]; if (!range.marker) continue; this.session.removeMarker(range.marker); var index = markerList.indexOf(range); if (index != -1) markerList.splice(index, 1); } this.session.selectionMarkerCount = markerList.length; }; this.$onAddRange = function(e) { this.addSelectionMarker(e.range); this.renderer.updateCursor(); this.renderer.updateBackMarkers(); }; this.$onRemoveRange = function(e) { this.removeSelectionMarkers(e.ranges); this.renderer.updateCursor(); this.renderer.updateBackMarkers(); }; this.$onMultiSelect = function(e) { if (this.inMultiSelectMode) return; this.inMultiSelectMode = true; this.setStyle("multiselect"); this.keyBinding.addKeyboardHandler(commands.keyboardHandler); this.commands.on("exec", this.$onMultiSelectExec); this.renderer.updateCursor(); this.renderer.updateBackMarkers(); }; this.$onSingleSelect = function(e) { if (this.session.multiSelect.inVirtualMode) return; this.inMultiSelectMode = false; this.unsetStyle("multiselect"); this.keyBinding.removeKeyboardHandler(commands.keyboardHandler); this.commands.removeEventListener("exec", this.$onMultiSelectExec); this.renderer.updateCursor(); this.renderer.updateBackMarkers(); }; this.$onMultiSelectExec = function(e) { var command = e.command; var editor = e.editor; if (!editor.multiSelect) return; if (!command.multiSelectAction) { command.exec(editor, e.args || {}); editor.multiSelect.addRange(editor.multiSelect.toOrientedRange()); editor.multiSelect.mergeOverlappingRanges(); } else if (command.multiSelectAction == "forEach") { editor.forEachSelection(command, e.args); } else if (command.multiSelectAction == "single") { editor.exitMultiSelectMode(); command.exec(editor, e.args || {}); } else { command.multiSelectAction(editor, e.args || {}); } e.preventDefault(); }; /** extension * Editor.forEachSelection(cmd, args) * - cmd (String): The command to execute * - args (String): Any arguments for the command * * Executes a command for each selection range. **/ this.forEachSelection = function(cmd, args) { if (this.inVirtualSelectionMode) return; var session = this.session; var selection = this.selection; var rangeList = selection.rangeList; var reg = selection._eventRegistry; selection._eventRegistry = {}; var tmpSel = new Selection(session); this.inVirtualSelectionMode = true; for (var i = rangeList.ranges.length; i--;) { tmpSel.fromOrientedRange(rangeList.ranges[i]); this.selection = session.selection = tmpSel; cmd.exec(this, args || {}); tmpSel.toOrientedRange(rangeList.ranges[i]); } tmpSel.detach(); this.selection = session.selection = selection; this.inVirtualSelectionMode = false; selection._eventRegistry = reg; selection.mergeOverlappingRanges(); this.onCursorChange(); this.onSelectionChange(); }; /** extension * Editor.exitMultiSelectMode() -> Void * * Removes all the selections except the last added one. **/ this.exitMultiSelectMode = function() { if (this.inVirtualSelectionMode) return; this.multiSelect.toSingleRange(); }; this.getCopyText = function() { var text = ""; if (this.inMultiSelectMode) { var ranges = this.multiSelect.rangeList.ranges; text = []; for (var i = 0; i < ranges.length; i++) { text.push(this.session.getTextRange(ranges[i])); } text = text.join(this.session.getDocument().getNewLineCharacter()); } else if (!this.selection.isEmpty()) { text = this.session.getTextRange(this.getSelectionRange()); } return text; }; this.onPaste = function(text) { this._emit("paste", text); if (!this.inMultiSelectMode) return this.insert(text); var lines = text.split(/\r\n|\r|\n/); var ranges = this.selection.rangeList.ranges; if (lines.length > ranges.length || (lines.length <= 2 || !lines[1])) return this.commands.exec("insertstring", this, text); for (var i = ranges.length; i--; ) { var range = ranges[i]; if (!range.isEmpty()) this.session.remove(range); this.session.insert(range.start, lines[i]); } }; /** extension * Editor.findAll(needle, options, additive) -> Number * - needle (String): The text to find * - options (Object): The search options * - additive (Boolean): keeps * * Finds and selects all the occurences of `needle`. **/ this.findAll = function(needle, options, additive) { options = options || {}; options.needle = needle || options.needle; this.$search.set(options); var ranges = this.$search.findAll(this.session); if (!ranges.length) return 0; this.$blockScrolling += 1; var selection = this.multiSelect; if (!additive) selection.toSingleRange(ranges[0]); for (var i = ranges.length; i--; ) selection.addRange(ranges[i], true); this.$blockScrolling -= 1; return ranges.length; }; // commands /** extension * Editor.selectMoreLines(dir, skip) * - dir (Number): The direction of lines to select: -1 for up, 1 for down * - skip (Boolean): If `true`, removes the active selection range * * Adds a cursor above or below the active cursor. **/ this.selectMoreLines = function(dir, skip) { var range = this.selection.toOrientedRange(); var isBackwards = range.cursor == range.end; var screenLead = this.session.documentToScreenPosition(range.cursor); if (this.selection.$desiredColumn) screenLead.column = this.selection.$desiredColumn; var lead = this.session.screenToDocumentPosition(screenLead.row + dir, screenLead.column); if (!range.isEmpty()) { var screenAnchor = this.session.documentToScreenPosition(isBackwards ? range.end : range.start); var anchor = this.session.screenToDocumentPosition(screenAnchor.row + dir, screenAnchor.column); } else { var anchor = lead; } if (isBackwards) { var newRange = Range.fromPoints(lead, anchor); newRange.cursor = newRange.start; } else { var newRange = Range.fromPoints(anchor, lead); newRange.cursor = newRange.end; } newRange.desiredColumn = screenLead.column; if (!this.selection.inMultiSelectMode) { this.selection.addRange(range); } else { if (skip) var toRemove = range.cursor; } this.selection.addRange(newRange); if (toRemove) this.selection.substractPoint(toRemove); }; /** extension * Editor.transposeSelections(dir) * - dir (Number): The direction to rotate selections * * Transposes the selected ranges. **/ this.transposeSelections = function(dir) { var session = this.session; var sel = session.multiSelect; var all = sel.ranges; for (var i = all.length; i--; ) { var range = all[i]; if (range.isEmpty()) { var tmp = session.getWordRange(range.start.row, range.start.column); range.start.row = tmp.start.row; range.start.column = tmp.start.column; range.end.row = tmp.end.row; range.end.column = tmp.end.column; } } sel.mergeOverlappingRanges(); var words = []; for (var i = all.length; i--; ) { var range = all[i]; words.unshift(session.getTextRange(range)); } if (dir < 0) words.unshift(words.pop()); else words.push(words.shift()); for (var i = all.length; i--; ) { var range = all[i]; var tmp = range.clone(); session.replace(range, words[i]); range.start.row = tmp.start.row; range.start.column = tmp.start.column; } } /** extension * Editor.selectMore(dir, skip) * - dir (Number): The direction of lines to select: -1 for up, 1 for down * - skip (Boolean): If `true`, removes the active selection range * * Finds the next occurence of text in an active selection and adds it to the selections. **/ this.selectMore = function (dir, skip) { var session = this.session; var sel = session.multiSelect; var range = sel.toOrientedRange(); if (range.isEmpty()) { var range = session.getWordRange(range.start.row, range.start.column); range.cursor = range.end; this.multiSelect.addRange(range); } var needle = session.getTextRange(range); var newRange = find(session, needle, dir); if (newRange) { newRange.cursor = dir == -1 ? newRange.start : newRange.end; this.multiSelect.addRange(newRange); } if (skip) this.multiSelect.substractPoint(range.cursor); }; }).call(Editor.prototype); function isSamePoint(p1, p2) { return p1.row == p2.row && p1.column == p2.column; } // patch // adds multicursor support to a session exports.onSessionChange = function(e) { var session = e.session; if (!session.multiSelect) { session.$selectionMarkers = []; session.selection.$initRangeList(); session.multiSelect = session.selection; } this.multiSelect = session.multiSelect; var oldSession = e.oldSession; if (oldSession) { // todo use events if (oldSession.multiSelect && oldSession.multiSelect.editor == this) oldSession.multiSelect.editor = null; session.multiSelect.removeEventListener("addRange", this.$onAddRange); session.multiSelect.removeEventListener("removeRange", this.$onRemoveRange); session.multiSelect.removeEventListener("multiSelect", this.$onMultiSelect); session.multiSelect.removeEventListener("singleSelect", this.$onSingleSelect); } session.multiSelect.on("addRange", this.$onAddRange); session.multiSelect.on("removeRange", this.$onRemoveRange); session.multiSelect.on("multiSelect", this.$onMultiSelect); session.multiSelect.on("singleSelect", this.$onSingleSelect); // this.$onSelectionChange = this.onSelectionChange.bind(this); if (this.inMultiSelectMode != session.selection.inMultiSelectMode) { if (session.selection.inMultiSelectMode) this.$onMultiSelect(); else this.$onSingleSelect(); } }; // MultiSelect(editor) // adds multiple selection support to the editor // (note: should be called only once for each editor instance) function MultiSelect(editor) { editor.$onAddRange = editor.$onAddRange.bind(editor); editor.$onRemoveRange = editor.$onRemoveRange.bind(editor); editor.$onMultiSelect = editor.$onMultiSelect.bind(editor); editor.$onSingleSelect = editor.$onSingleSelect.bind(editor); exports.onSessionChange.call(editor, editor); editor.on("changeSession", exports.onSessionChange.bind(editor)); editor.on("mousedown", onMouseDown); editor.commands.addCommands(commands.defaultCommands); addAltCursorListeners(editor); } function addAltCursorListeners(editor){ var el = editor.textInput.getElement(); var altCursor = false; var contentEl = editor.renderer.content; event.addListener(el, "keydown", function(e) { if (e.keyCode == 18 && !(e.ctrlKey || e.shiftKey || e.metaKey)) { if (!altCursor) { contentEl.style.cursor = "crosshair"; altCursor = true; } } else if (altCursor) { contentEl.style.cursor = ""; } }); event.addListener(el, "keyup", reset); event.addListener(el, "blur", reset); function reset() { if (altCursor) { contentEl.style.cursor = ""; altCursor = false; } } } exports.MultiSelect = MultiSelect; });