mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
957 lines
32 KiB
JavaScript
957 lines
32 KiB
JavaScript
|
/* ***** BEGIN LICENSE BLOCK *****
|
||
|
* Distributed under the BSD license:
|
||
|
*
|
||
|
* Copyright (c) 2010, Ajax.org B.V.
|
||
|
* All rights reserved.
|
||
|
*
|
||
|
* Redistribution and use in source and binary forms, with or without
|
||
|
* modification, are permitted provided that the following conditions are met:
|
||
|
* * Redistributions of source code must retain the above copyright
|
||
|
* notice, this list of conditions and the following disclaimer.
|
||
|
* * Redistributions in binary form must reproduce the above copyright
|
||
|
* notice, this list of conditions and the following disclaimer in the
|
||
|
* documentation and/or other materials provided with the distribution.
|
||
|
* * Neither the name of Ajax.org B.V. nor the
|
||
|
* names of its contributors may be used to endorse or promote products
|
||
|
* derived from this software without specific prior written permission.
|
||
|
*
|
||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||
|
* DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
|
||
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||
|
*
|
||
|
* ***** 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 lang = require("./lib/lang");
|
||
|
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;
|
||
|
|
||
|
/**
|
||
|
* Adds a range to a selection by entering multiselect mode, if necessary.
|
||
|
* @param {Range} range The new range to add
|
||
|
* @param {Boolean} $blockChangeEvents Whether or not to block changing events
|
||
|
* @method Selection.addRange
|
||
|
**/
|
||
|
this.addRange = function(range, $blockChangeEvents) {
|
||
|
if (!range)
|
||
|
return;
|
||
|
|
||
|
if (!this.inMultiSelectMode && this.rangeCount == 0) {
|
||
|
var oldRange = this.toOrientedRange();
|
||
|
this.rangeList.add(oldRange);
|
||
|
this.rangeList.add(range);
|
||
|
if (this.rangeList.ranges.length != 2) {
|
||
|
this.rangeList.removeAll();
|
||
|
return $blockChangeEvents || this.fromOrientedRange(range);
|
||
|
}
|
||
|
this.rangeList.removeAll();
|
||
|
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);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* @method Selection.toSingleRange
|
||
|
**/
|
||
|
|
||
|
this.toSingleRange = function(range) {
|
||
|
range = range || this.ranges[0];
|
||
|
var removed = this.rangeList.removeAll();
|
||
|
if (removed.length)
|
||
|
this.$onRemoveRange(removed);
|
||
|
|
||
|
range && this.fromOrientedRange(range);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Removes a Range containing pos (if it exists).
|
||
|
* @param {Range} pos The position to remove, as a `{row, column}` object
|
||
|
* @method Selection.substractPoint
|
||
|
**/
|
||
|
this.substractPoint = function(pos) {
|
||
|
var removed = this.rangeList.substractPoint(pos);
|
||
|
if (removed) {
|
||
|
this.$onRemoveRange(removed);
|
||
|
return removed[0];
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Merges overlapping ranges ensuring consistency after changes
|
||
|
* @method Selection.mergeOverlappingRanges
|
||
|
**/
|
||
|
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;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Returns a concatenation of all the ranges.
|
||
|
* @returns {Array}
|
||
|
* @method Selection.getAllRanges
|
||
|
**/
|
||
|
this.getAllRanges = function() {
|
||
|
return this.rangeCount ? this.rangeList.ranges.concat() : [this.getRange()];
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Splits all the ranges into lines.
|
||
|
* @method Selection.splitIntoLines
|
||
|
**/
|
||
|
|
||
|
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 isBackwards = this.isBackwards();
|
||
|
var startRow = range.start.row;
|
||
|
var endRow = range.end.row;
|
||
|
if (startRow == endRow) {
|
||
|
if (isBackwards)
|
||
|
var start = range.end, end = range.start;
|
||
|
else
|
||
|
var start = range.start, end = range.end;
|
||
|
|
||
|
this.addRange(Range.fromPoints(end, end));
|
||
|
this.addRange(Range.fromPoints(start, start));
|
||
|
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);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* @method Selection.toggleBlockSelection
|
||
|
**/
|
||
|
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);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* Gets list of ranges composing rectangular block on the screen
|
||
|
*
|
||
|
* @param {Cursor} screenCursor The cursor to use
|
||
|
* @param {Anchor} screenAnchor The anchor to use
|
||
|
* @param {Boolean} includeEmptyLines If true, this includes ranges inside the block which are empty due to clipping
|
||
|
* @returns {Range}
|
||
|
* @method Selection.rectangularRangeBlock
|
||
|
**/
|
||
|
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() {
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* Updates the cursor and marker layers.
|
||
|
* @method Editor.updateSelectionMarkers
|
||
|
*
|
||
|
**/
|
||
|
this.updateSelectionMarkers = function() {
|
||
|
this.renderer.updateCursor();
|
||
|
this.renderer.updateBackMarkers();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Adds the selection and cursor.
|
||
|
* @param {Range} orientedRange A range containing a cursor
|
||
|
* @returns {Range}
|
||
|
* @method Editor.addSelectionMarker
|
||
|
**/
|
||
|
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;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Removes the selection marker.
|
||
|
* @param {Range} range The selection range added with [[Editor.addSelectionMarker `addSelectionMarker()`]].
|
||
|
* @method Editor.removeSelectionMarker
|
||
|
**/
|
||
|
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("ace_multiselect");
|
||
|
this.keyBinding.addKeyboardHandler(commands.keyboardHandler);
|
||
|
this.commands.setDefaultHandler("exec", this.$onMultiSelectExec);
|
||
|
|
||
|
this.renderer.updateCursor();
|
||
|
this.renderer.updateBackMarkers();
|
||
|
};
|
||
|
|
||
|
this.$onSingleSelect = function(e) {
|
||
|
if (this.session.multiSelect.inVirtualMode)
|
||
|
return;
|
||
|
this.inMultiSelectMode = false;
|
||
|
|
||
|
this.unsetStyle("ace_multiselect");
|
||
|
this.keyBinding.removeKeyboardHandler(commands.keyboardHandler);
|
||
|
|
||
|
this.commands.removeDefaultHandler("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) {
|
||
|
var result = command.exec(editor, e.args || {});
|
||
|
editor.multiSelect.addRange(editor.multiSelect.toOrientedRange());
|
||
|
editor.multiSelect.mergeOverlappingRanges();
|
||
|
} else if (command.multiSelectAction == "forEach") {
|
||
|
result = editor.forEachSelection(command, e.args);
|
||
|
} else if (command.multiSelectAction == "forEachLine") {
|
||
|
result = editor.forEachSelection(command, e.args, true);
|
||
|
} else if (command.multiSelectAction == "single") {
|
||
|
editor.exitMultiSelectMode();
|
||
|
result = command.exec(editor, e.args || {});
|
||
|
} else {
|
||
|
result = command.multiSelectAction(editor, e.args || {});
|
||
|
}
|
||
|
return result;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Executes a command for each selection range.
|
||
|
* @param {String} cmd The command to execute
|
||
|
* @param {String} args Any arguments for the command
|
||
|
* @method Editor.forEachSelection
|
||
|
**/
|
||
|
this.forEachSelection = function(cmd, args, $byLines) {
|
||
|
if (this.inVirtualSelectionMode)
|
||
|
return;
|
||
|
|
||
|
var session = this.session;
|
||
|
var selection = this.selection;
|
||
|
var rangeList = selection.rangeList;
|
||
|
var result;
|
||
|
|
||
|
var reg = selection._eventRegistry;
|
||
|
selection._eventRegistry = {};
|
||
|
|
||
|
var tmpSel = new Selection(session);
|
||
|
this.inVirtualSelectionMode = true;
|
||
|
for (var i = rangeList.ranges.length; i--;) {
|
||
|
if ($byLines) {
|
||
|
while (i > 0 && rangeList.ranges[i].start.row == rangeList.ranges[i - 1].end.row)
|
||
|
i--;
|
||
|
}
|
||
|
tmpSel.fromOrientedRange(rangeList.ranges[i]);
|
||
|
this.selection = session.selection = tmpSel;
|
||
|
var cmdResult = cmd.exec(this, args || {});
|
||
|
if (!result == undefined)
|
||
|
result = cmdResult;
|
||
|
tmpSel.toOrientedRange(rangeList.ranges[i]);
|
||
|
}
|
||
|
tmpSel.detach();
|
||
|
|
||
|
this.selection = session.selection = selection;
|
||
|
this.inVirtualSelectionMode = false;
|
||
|
selection._eventRegistry = reg;
|
||
|
selection.mergeOverlappingRanges();
|
||
|
|
||
|
var anim = this.renderer.$scrollAnimation;
|
||
|
this.onCursorChange();
|
||
|
this.onSelectionChange();
|
||
|
if (anim && anim.from == anim.to)
|
||
|
this.renderer.animateScrolling(anim.from);
|
||
|
|
||
|
return result;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Removes all the selections except the last added one.
|
||
|
* @method Editor.exitMultiSelectMode
|
||
|
**/
|
||
|
this.exitMultiSelectMode = function() {
|
||
|
if (!this.inMultiSelectMode || this.inVirtualSelectionMode)
|
||
|
return;
|
||
|
this.multiSelect.toSingleRange();
|
||
|
};
|
||
|
|
||
|
this.getSelectedText = function() {
|
||
|
var text = "";
|
||
|
if (this.inMultiSelectMode && !this.inVirtualSelectionMode) {
|
||
|
var ranges = this.multiSelect.rangeList.ranges;
|
||
|
var buf = [];
|
||
|
for (var i = 0; i < ranges.length; i++) {
|
||
|
buf.push(this.session.getTextRange(ranges[i]));
|
||
|
}
|
||
|
var nl = this.session.getDocument().getNewLineCharacter();
|
||
|
text = buf.join(nl);
|
||
|
if (text.length == (buf.length - 1) * nl.length)
|
||
|
text = "";
|
||
|
} else if (!this.selection.isEmpty()) {
|
||
|
text = this.session.getTextRange(this.getSelectionRange());
|
||
|
}
|
||
|
return text;
|
||
|
};
|
||
|
|
||
|
// todo this should change when paste becomes a command
|
||
|
this.onPaste = function(text) {
|
||
|
if (this.$readOnly)
|
||
|
return;
|
||
|
|
||
|
this._signal("paste", text);
|
||
|
if (!this.inMultiSelectMode || this.inVirtualSelectionMode)
|
||
|
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]);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Finds and selects all the occurences of `needle`.
|
||
|
* @param {String} The text to find
|
||
|
* @param {Object} The search options
|
||
|
* @param {Boolean} keeps
|
||
|
*
|
||
|
* @returns {Number} The cumulative count of all found matches
|
||
|
* @method Editor.findAll
|
||
|
**/
|
||
|
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;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Adds a cursor above or below the active cursor.
|
||
|
*
|
||
|
* @param {Number} dir The direction of lines to select: -1 for up, 1 for down
|
||
|
* @param {Boolean} skip If `true`, removes the active selection range
|
||
|
*
|
||
|
* @method Editor.selectMoreLines
|
||
|
*/
|
||
|
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);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Transposes the selected ranges.
|
||
|
* @param {Number} dir The direction to rotate selections
|
||
|
* @method Editor.transposeSelections
|
||
|
**/
|
||
|
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;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Finds the next occurence of text in an active selection and adds it to the selections.
|
||
|
* @param {Number} dir The direction of lines to select: -1 for up, 1 for down
|
||
|
* @param {Boolean} skip If `true`, removes the active selection range
|
||
|
* @method Editor.selectMore
|
||
|
**/
|
||
|
this.selectMore = function(dir, skip) {
|
||
|
var session = this.session;
|
||
|
var sel = session.multiSelect;
|
||
|
|
||
|
var range = sel.toOrientedRange();
|
||
|
if (range.isEmpty()) {
|
||
|
range = session.getWordRange(range.start.row, range.start.column);
|
||
|
range.cursor = dir == -1 ? range.start : range.end;
|
||
|
this.multiSelect.addRange(range);
|
||
|
// todo add option for sublime like behavior
|
||
|
// return;
|
||
|
}
|
||
|
var needle = session.getTextRange(range);
|
||
|
|
||
|
var newRange = find(session, needle, dir);
|
||
|
if (newRange) {
|
||
|
newRange.cursor = dir == -1 ? newRange.start : newRange.end;
|
||
|
this.$blockScrolling += 1;
|
||
|
this.session.unfold(newRange);
|
||
|
this.multiSelect.addRange(newRange);
|
||
|
this.$blockScrolling -= 1;
|
||
|
this.renderer.scrollCursorIntoView(null, 0.5);
|
||
|
}
|
||
|
if (skip)
|
||
|
this.multiSelect.substractPoint(range.cursor);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Aligns the cursors or selected text.
|
||
|
* @method Editor.alignCursors
|
||
|
**/
|
||
|
this.alignCursors = function() {
|
||
|
var session = this.session;
|
||
|
var sel = session.multiSelect;
|
||
|
var ranges = sel.ranges;
|
||
|
|
||
|
if (!ranges.length) {
|
||
|
var range = this.selection.getRange();
|
||
|
var fr = range.start.row, lr = range.end.row;
|
||
|
var guessRange = fr == lr;
|
||
|
if (guessRange) {
|
||
|
var max = this.session.getLength();
|
||
|
var line;
|
||
|
do {
|
||
|
line = this.session.getLine(lr);
|
||
|
} while (/[=:]/.test(line) && ++lr < max);
|
||
|
do {
|
||
|
line = this.session.getLine(fr);
|
||
|
} while (/[=:]/.test(line) && --fr > 0);
|
||
|
|
||
|
if (fr < 0) fr = 0;
|
||
|
if (lr >= max) lr = max - 1;
|
||
|
}
|
||
|
var lines = this.session.doc.removeLines(fr, lr);
|
||
|
lines = this.$reAlignText(lines, guessRange);
|
||
|
this.session.doc.insert({row: fr, column: 0}, lines.join("\n") + "\n");
|
||
|
if (!guessRange) {
|
||
|
range.start.column = 0;
|
||
|
range.end.column = lines[lines.length - 1].length;
|
||
|
}
|
||
|
this.selection.setRange(range);
|
||
|
} else {
|
||
|
// filter out ranges on same row
|
||
|
var row = -1;
|
||
|
var sameRowRanges = ranges.filter(function(r) {
|
||
|
if (r.cursor.row == row)
|
||
|
return true;
|
||
|
row = r.cursor.row;
|
||
|
});
|
||
|
sel.$onRemoveRange(sameRowRanges);
|
||
|
|
||
|
var maxCol = 0;
|
||
|
var minSpace = Infinity;
|
||
|
var spaceOffsets = ranges.map(function(r) {
|
||
|
var p = r.cursor;
|
||
|
var line = session.getLine(p.row);
|
||
|
var spaceOffset = line.substr(p.column).search(/\S/g);
|
||
|
if (spaceOffset == -1)
|
||
|
spaceOffset = 0;
|
||
|
|
||
|
if (p.column > maxCol)
|
||
|
maxCol = p.column;
|
||
|
if (spaceOffset < minSpace)
|
||
|
minSpace = spaceOffset;
|
||
|
return spaceOffset;
|
||
|
});
|
||
|
ranges.forEach(function(r, i) {
|
||
|
var p = r.cursor;
|
||
|
var l = maxCol - p.column;
|
||
|
var d = spaceOffsets[i] - minSpace;
|
||
|
if (l > d)
|
||
|
session.insert(p, lang.stringRepeat(" ", l - d));
|
||
|
else
|
||
|
session.remove(new Range(p.row, p.column, p.row, p.column - l + d));
|
||
|
|
||
|
r.start.column = r.end.column = maxCol;
|
||
|
r.start.row = r.end.row = p.row;
|
||
|
r.cursor = r.end;
|
||
|
});
|
||
|
sel.fromOrientedRange(ranges[0]);
|
||
|
this.renderer.updateCursor();
|
||
|
this.renderer.updateBackMarkers();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
this.$reAlignText = function(lines, forceLeft) {
|
||
|
var isLeftAligned = true, isRightAligned = true;
|
||
|
var startW, textW, endW;
|
||
|
|
||
|
return lines.map(function(line) {
|
||
|
var m = line.match(/(\s*)(.*?)(\s*)([=:].*)/);
|
||
|
if (!m)
|
||
|
return [line];
|
||
|
|
||
|
if (startW == null) {
|
||
|
startW = m[1].length;
|
||
|
textW = m[2].length;
|
||
|
endW = m[3].length;
|
||
|
return m;
|
||
|
}
|
||
|
|
||
|
if (startW + textW + endW != m[1].length + m[2].length + m[3].length)
|
||
|
isRightAligned = false;
|
||
|
if (startW != m[1].length)
|
||
|
isLeftAligned = false;
|
||
|
|
||
|
if (startW > m[1].length)
|
||
|
startW = m[1].length;
|
||
|
if (textW < m[2].length)
|
||
|
textW = m[2].length;
|
||
|
if (endW > m[3].length)
|
||
|
endW = m[3].length;
|
||
|
|
||
|
return m;
|
||
|
}).map(forceLeft ? alignLeft :
|
||
|
isLeftAligned ? isRightAligned ? alignRight : alignLeft : unAlign);
|
||
|
|
||
|
function spaces(n) {
|
||
|
return lang.stringRepeat(" ", n);
|
||
|
}
|
||
|
|
||
|
function alignLeft(m) {
|
||
|
return !m[2] ? m[0] : spaces(startW) + m[2]
|
||
|
+ spaces(textW - m[2].length + endW)
|
||
|
+ m[4].replace(/^([=:])\s+/, "$1 ")
|
||
|
}
|
||
|
function alignRight(m) {
|
||
|
return !m[2] ? m[0] : spaces(startW + textW - m[2].length) + m[2]
|
||
|
+ spaces(endW, " ")
|
||
|
+ m[4].replace(/^([=:])\s+/, "$1 ")
|
||
|
}
|
||
|
function unAlign(m) {
|
||
|
return !m[2] ? m[0] : spaces(startW) + m[2]
|
||
|
+ spaces(endW)
|
||
|
+ m[4].replace(/^([=:])\s+/, "$1 ")
|
||
|
}
|
||
|
}
|
||
|
}).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) {
|
||
|
oldSession.multiSelect.removeEventListener("addRange", this.$onAddRange);
|
||
|
oldSession.multiSelect.removeEventListener("removeRange", this.$onRemoveRange);
|
||
|
oldSession.multiSelect.removeEventListener("multiSelect", this.$onMultiSelect);
|
||
|
oldSession.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) {
|
||
|
if (editor.$multiselectOnSessionChange)
|
||
|
return;
|
||
|
editor.$onAddRange = editor.$onAddRange.bind(editor);
|
||
|
editor.$onRemoveRange = editor.$onRemoveRange.bind(editor);
|
||
|
editor.$onMultiSelect = editor.$onMultiSelect.bind(editor);
|
||
|
editor.$onSingleSelect = editor.$onSingleSelect.bind(editor);
|
||
|
editor.$multiselectOnSessionChange = exports.onSessionChange.bind(editor);
|
||
|
|
||
|
editor.$multiselectOnSessionChange(editor);
|
||
|
editor.on("changeSession", editor.$multiselectOnSessionChange);
|
||
|
|
||
|
editor.on("mousedown", onMouseDown);
|
||
|
editor.commands.addCommands(commands.defaultCommands);
|
||
|
|
||
|
addAltCursorListeners(editor);
|
||
|
}
|
||
|
|
||
|
function addAltCursorListeners(editor){
|
||
|
var el = editor.textInput.getElement();
|
||
|
var altCursor = false;
|
||
|
event.addListener(el, "keydown", function(e) {
|
||
|
if (e.keyCode == 18 && !(e.ctrlKey || e.shiftKey || e.metaKey)) {
|
||
|
if (!altCursor) {
|
||
|
editor.renderer.setMouseCursor("crosshair");
|
||
|
altCursor = true;
|
||
|
}
|
||
|
} else if (altCursor) {
|
||
|
reset();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
event.addListener(el, "keyup", reset);
|
||
|
event.addListener(el, "blur", reset);
|
||
|
function reset(e) {
|
||
|
if (altCursor) {
|
||
|
editor.renderer.setMouseCursor("");
|
||
|
altCursor = false;
|
||
|
// TODO disable menu poping up
|
||
|
// e && e.preventDefault()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
exports.MultiSelect = MultiSelect;
|
||
|
|
||
|
|
||
|
require("./config").defineOptions(Editor.prototype, "editor", {
|
||
|
enableMultiselect: {
|
||
|
set: function(val) {
|
||
|
MultiSelect(this);
|
||
|
if (val) {
|
||
|
this.on("changeSession", this.$multiselectOnSessionChange);
|
||
|
this.on("mousedown", onMouseDown);
|
||
|
} else {
|
||
|
this.off("changeSession", this.$multiselectOnSessionChange);
|
||
|
this.off("mousedown", onMouseDown);
|
||
|
}
|
||
|
},
|
||
|
value: true
|
||
|
}
|
||
|
})
|
||
|
|
||
|
|
||
|
|
||
|
});
|