diff --git a/lib/ace/commands/multi_select_commands.js b/lib/ace/commands/multi_select_commands.js new file mode 100644 index 00000000..3ec6778b --- /dev/null +++ b/lib/ace/commands/multi_select_commands.js @@ -0,0 +1,111 @@ +/* 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): + * Fabian Jakobs + * + * 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) { + +// add multiSelectAction annotations to default commands +require("./default_commands").commands.forEach(function(command) { + var single = RegExp(["selectall"].join("|"), ""); + var mapOver = RegExp(["backspace", "del", + "golinedown", "golineup", "gotoend", "gotoleft", "gotolineend", "gotolinestart", + "gotoright", "gotostart", "gotowordleft", "gotowordright", + "indent", "insertstring", "inserttext", "jumptomatching", "outdent", + "removetolineend", "removetolinestart", "removewordleft", "removewordright", + "selectdown", "selectleft", "selectlineend", "selectlinestart", "selectright", + "selecttoend", "selecttolineend", "selecttolinestart", "selecttostart", + "selectup", "selectwordleft", "selectwordright", + "splitline", "tolowercase", "touppercase"].join("|"), ""); + + if (single.test(command.name)) + command.multiSelectAction = "single"; + else if (mapOver.test(command.name)) + command.multiSelectAction = "forEach"; + else if (command.name == "transposeletters") + command.multiSelectAction = function(editor) {editor.transposeSelections(1); } +}); + +// commands to enter multiselect mode +exports.defaultCommands = [{ + name: "addCursorAbove", + exec: function(editor) { editor.selectMoreLines(-1); }, + bindKey: {win: "Ctrl-Alt-Up", mac: "Ctrl-Alt-Up"}, + readonly: true +}, { + name: "addCursorBelow", + exec: function(editor) { editor.selectMoreLines(1); }, + bindKey: {win: "Ctrl-Alt-Down", mac: "Ctrl-Alt-Down"}, + readonly: true +}, { + name: "selectMoreBefore", + exec: function(editor) { editor.selectMore(-1); }, + bindKey: {win: "Ctrl-Alt-Left", mac: "Ctrl-Alt-Left"}, + readonly: true +}, { + name: "selectMoreAfter", + exec: function(editor) { editor.selectMore(1); }, + bindKey: {win: "Ctrl-Alt-Right", mac: "Ctrl-Alt-Right"}, + readonly: true +}, { + name: "selectNextBefore", + exec: function(editor) { editor.selectMore(-1, true); }, + bindKey: {win: "Ctrl-Alt-Shift-Left", mac: "Ctrl-Alt-Shift-Left"}, + readonly: true +}, { + name: "selectNextAfter", + exec: function(editor) { editor.selectMore(1, true); }, + bindKey: {win: "Ctrl-Alt-Shift-Right", mac: "Ctrl-Alt-Shift-Right"}, + readonly: true +}, { + name: "splitIntoLines", + exec: function(editor) { editor.multiSelect.splitIntoLines(); }, + bindKey: {win: "Ctrl-Shift-L", mac: "Ctrl-Shift-L"}, + readonly: true +}, ]; + +// commands active in multiselect mode +exports.multiEditCommands = [{ + name: "singleSelection", + bindKey: "esc", + exec: function(editor) { editor.exitMultiSelectMode(); }, + readonly: true +}]; + +var HashHandler = require("../keyboard/hash_handler").HashHandler; +exports.keyboardHandler = new HashHandler(exports.multiEditCommands); + +}); \ No newline at end of file diff --git a/lib/ace/layer/cursor.js b/lib/ace/layer/cursor.js index bf6c2901..36a085f3 100644 --- a/lib/ace/layer/cursor.js +++ b/lib/ace/layer/cursor.js @@ -66,7 +66,7 @@ var Cursor = function(parentEl) { this.addCursor = function() { var el = dom.createElement("div"); - var className = "ace_cursor" + var className = "ace_cursor"; if (!this.isVisible) className += " ace_hidden"; if (this.overwrite) @@ -81,8 +81,8 @@ var Cursor = function(parentEl) { this.removeCursor = function() { if (this.cursors.length > 1) { var el = this.cursors.pop(); - el.parentNode.removeChild(el) - return el + el.parentNode.removeChild(el); + return el; } }; @@ -141,8 +141,8 @@ var Cursor = function(parentEl) { this.update = function(config) { this.config = config; - if (this.session.selection.rangeCount > 1) { - var selections = this.session.selection.getAllRanges(); + if (this.session.selectionMarkerCount > 1) { + var selections = this.session.$selectionMarkers; var i = 0, sel, cursorIndex = 0; for (var i = selections.length; i--; ) { @@ -156,8 +156,8 @@ var Cursor = function(parentEl) { style.width = config.characterWidth + "px"; style.height = config.lineHeight + "px"; } - if (this.cursors.length > 1) - while (cursorIndex < this.cursors.length) + if (cursorIndex > 1) + while (this.cursors.length > cursorIndex) this.removeCursor(); } else { var pixelPos = this.getPixelPosition(null, true); diff --git a/lib/ace/mouse/multi_select_mouse_handler.js b/lib/ace/mouse/multi_select_mouse_handler.js new file mode 100644 index 00000000..5d8dcfd5 --- /dev/null +++ b/lib/ace/mouse/multi_select_mouse_handler.js @@ -0,0 +1,173 @@ +/* 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 event = require("../lib/event"); + + +// mouse +function isSamePoint(p1, p2) { + return p1.row == p2.row && p1.column == p2.column +} + +function onMouseDown(e) { + var ev = e.domEvent; + var alt = ev.altKey; + var shift = ev.shiftKey; + var ctrl = ev.ctrlKey; + var button = e.getButton(); + + if (!ctrl && !alt) { + if (e.editor.selection.rangeCount > 1) { + if (button == 0) { + e.editor.exitMultiSelectMode(); + } else if (button == 2) { + var editor = e.editor; + var selectionEmpty = editor.selection.isEmpty(); + editor.textInput.onContextMenu({x: e.clientX, y: e.clientY}, selectionEmpty); + event.capture(editor.container, function(){}, editor.textInput.onContextMenuClose); + e.stop(); + } + } + return; + } + + var editor = e.editor; + var selection = editor.selection; + var isMultiSelect = selection.rangeCount > 1 + var pos = e.getDocumentPosition(); + var rangeList = selection.rangeList; + var cursor = selection.getCursor() + var inSelection = e.inSelection() || (selection.isEmpty() && isSamePoint(pos, cursor)); + + if (ctrl && !shift && !alt && button == 0) { + if (!isMultiSelect && inSelection) + return // dragging + + if (!isMultiSelect) { + selection.addRange(selection.toOrientedRange()); + } + if (inSelection) + selection.clearSelection(); + + var helper = selection.toOrientedRange(); + editor.addSelectionMarker(helper); + + var oldRange = rangeList.rangeAtPoint(pos); + + event.capture(editor.container, function(){}, function() { + editor.removeSelectionMarkers([helper]); + var tmpSel = selection.toOrientedRange(); + + if (oldRange && tmpSel.isEmpty() && isSamePoint(oldRange.cursor, tmpSel.cursor)) { + if (selection.rangeCount > 1) { + range = editor.selection.rangeList.substractPoint(tmpSel.cursor); + var range = editor.selection.rangeList.all[0]; + if (range) + editor.selection.addRange(range); + return; + } + } + + selection.addRange(tmpSel); + }); + + //e.stop() + } else if (!shift && alt && button == 0) { + e.stop() + var mouseX = e.pageX, mouseY = e.pageY; + var onMouseSelection = function(e) { + mouseX = event.getDocumentX(e); + mouseY = event.getDocumentY(e); + }; + + if (isMultiSelect && !ctrl) { + selection.single(); + } + selection.moveCursorToPosition(pos); + selection.clearSelection(); + + var rectSel = []; + var session = editor.session; + + var onMouseSelectionEnd = function(e) { + clearInterval(timerId); + editor.removeSelectionMarkers(rectSel); + for (var i = 0; i < rectSel.length; i++) + selection.addRange(rectSel[i]) + }; + + var anchor = selection.getCursor(); + var screenAnchor = editor.renderer.pixelToScreenCoordinates(mouseX, mouseY); + var clippedAnchor = session.documentToScreenPosition(anchor); + var screenCursor = screenAnchor; + + var onSelectionInterval = function() { + var newCursor = editor.renderer.pixelToScreenCoordinates(mouseX, mouseY); + var cursor = session.screenToDocumentPosition(newCursor.row, newCursor.column); + + if (isSamePoint(screenCursor, newCursor) + && isSamePoint(cursor, selection.selectionLead)) + return; + screenCursor = newCursor; + + editor.selection.moveCursorToPosition(cursor); + editor.selection.clearSelection(); + editor.renderer.scrollCursorIntoView(); + + editor.removeSelectionMarkers(rectSel); + rectSel = selection.rectangularRangeBlock(screenCursor, screenAnchor); + rectSel.forEach(editor.addSelectionMarker, editor); + + editor.renderer.updateCursor(); + editor.renderer.updateBackMarkers(); + }; + + event.capture(editor.container, onMouseSelection, onMouseSelectionEnd); + var timerId = setInterval(onSelectionInterval, 20); + + return e.preventDefault(); + } +} + + + +exports.onMouseDown = onMouseDown; + +}); \ No newline at end of file diff --git a/lib/ace/multi_select.js b/lib/ace/multi_select.js index 5a425169..3b5a8bd0 100644 --- a/lib/ace/multi_select.js +++ b/lib/ace/multi_select.js @@ -42,43 +42,445 @@ var RangeList = require("./range_list").RangeList; var Range = require("./range").Range; var Selection = require("./selection").Selection; var Range = require("./range").Range; -var event = require("./lib/event"); +var onMouseDown = require("./mouse/multi_select_mouse_handler").onMouseDown; +exports.commands = require("./commands/multi_select_commands"); -function forEachSelection(editor, cmd, args) { - if (editor.session.multiSelection.inVirtualMode) - return - var session = editor.session - var selection = editor.selection - var rangeList = selection.rangeList +var Search = require("ace/search").Search +var search = new Search - var reg = selection._eventRegistry; - selection._eventRegistry = {}; - - var sh = new Selection(session); - editor.session.multiSelection.inVirtualMode = true; - for (var i = rangeList.ranges.length; i--;) { - sh.fromOrientedRange(rangeList.ranges[i]) - editor.selection = session.selection = sh - cmd.exec(editor, args || {}) - sh.toOrientedRange(rangeList.ranges[i]) - } - sh.detach(); - - rangeList.merge() - editor.selection = session.selection = selection; - editor.session.multiSelection.inVirtualMode = false; - selection._eventRegistry = reg; - - selection.fromOrientedRange(selection.rangeList.all[0]) - editor.renderer.updateCursor(); - editor.renderer.updateBackMarkers(); - - if (selection.rangeCount == 1 && editor.inMultiSelectMode) - exitMultiSelectMode(editor) +function find(session, needle, dir) { + search.$options.wrap = true; + search.$options.needle = needle; + search.$options.backwards = dir == -1; + return search.find(session) } -function exec(command, editor, args) { +// extend EditSession +var EditSession = require("./edit_session").EditSession; +;(function() { + this.getSelectionMarkers = function() { + return this.$selectionMarkers; + }; +}).call(EditSession.prototype); + +// extend Selection +;(function() { + this.addRange = function(range) { + if (!range.cursor) + range.cursor = range.end; + + if (this.rangeCount == 0) { + var oldRange = this.toOrientedRange(); + this.rangeList.add(oldRange); + this._emit("addRange", {range: oldRange}); + } + + this.rangeList.add(range); + this.rangeCount = this.rangeList.ranges.length; + + if (this.rangeCount > 1 && !this.inMultiSelectMode) { + this._emit("multiSelect"); + this.inMultiSelectMode = true; + } + + this.fromOrientedRange(range); + if (this.rangeCount >= 1) + this._emit("addRange", {range: range}); + }; + + this.single = function(range) { + range = range || this.rangeList.all[0]; + this.rangeList.removeAll(); + range && this.fromOrientedRange(range); + }; + + this.$onRemoveRange = function(e) { + this.rangeCount = this.rangeList.ranges.length; + this._emit("removeRange", e); + + if (this.rangeCount <= 1 && this.inMultiSelectMode) { + this.inMultiSelectMode = false; + this._emit("singleSelect"); + + if (this.rangeCount == 1) + this.single(); + } + }; + + // adds multicursor support to selection + this.$initRangeList = function() { + if (this.rangeList) + return; + + var rangeList = new RangeList; + // list of ranges in reverse addition order + // rangeList.all[0] is the same as selection.getRange + rangeList.all = []; + rangeList.on("add", function(e) { + rangeList.all.unshift(e.range) + }) + rangeList.on("remove", function(e) { + var ranges = e.ranges + for (var i = ranges.length; i--; ) { + var index = rangeList.all.indexOf(ranges[i]); + rangeList.all.splice(index, 1); + } + }); + + this.rangeList = rangeList; + this.cursor = this.selectionLead; + + this.rangeCount = 1; + this.rangeList.on("remove", this.$onRemoveRange.bind(this)); + }; + this.getAllRanges = function() { + return this.rangeList.ranges.concat(this.secondarySelections) + }; + + + 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.single() + this.setSelectionRange(range, lastRange.cursor == lastRange.start) + } else { + var cursor = this.getRange().toScreenRange(); + range + } + }; + + 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.single() + 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); + } + }; + + 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() { + 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 + }; + + this.removeSelectionMarkers = function(ranges) { + for (var i = ranges.length; i--; ) { + var range = ranges[i]; + if (!range.marker) + continue; + 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.$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(exports.commands.keyboardHandler); + // FixMe + this.commands.__SingleSelectionExec = this.commands.exec; + this.commands.exec = exports.exec; + this.renderer.updateCursor(); + this.renderer.updateBackMarkers(); + + this.session.$undoSelect = false; + this.selection.rangeList.attach(this.session); + }; + + this.$onSingleSelect = function(e) { + if (this.session.multiSelect.inVirtualMode) + return; + this.inMultiSelectMode = false; + + this.unsetStyle("multiselect"); + this.keyBinding.removeKeyboardHandler(exports.commands.keyboardHandler); + + this.commands.exec = this.commands.__SingleSelectionExec; + this.renderer.updateCursor(); + this.renderer.updateBackMarkers(); + + this.session.$undoSelect = true; + + this.selection.rangeList.detach(this.session); + }; + + 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; + rangeList.merge(); + + var lastRange = selection.rangeList.all[0] + lastRange && selection.fromOrientedRange(lastRange); + + selection._emit("changeSelection") + selection._emit("changeCursor") + this.renderer.updateCursor(); + this.renderer.updateBackMarkers(); + }; + + this.exitMultiSelectMode = function() { + if (this.inVirtualSelectionMode) + return; + this.multiSelect.single(); + }; + + // todo route copy/cut/paste through commandmanager + this.getCopyText = function() { + var text = ""; + if (this.inMultiSelectMode) { + var ranges = this.multiSelect.rangeList.ranges; + for (var i = 0; i < ranges.length; i++) { + text += this.session.getTextRange(ranges[i]); + } + } else if (!this.selection.isEmpty()) + text = this.session.getTextRange(this.getSelectionRange()); + + + return text; + }; + + this.onCut = function() { + var cmd = { + name: "cut", + exec: function(editor) { + var range = editor.getSelectionRange(); + editor._emit("cut", range); + + if (!editor.selection.isEmpty()) { + editor.session.remove(range); + editor.clearSelection(); + } + }, + readonly: true, + multiSelectAction: "forEach" + } + this.commands.exec(cmd, this) + }; + + // commands + + 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 { + var allRanges = this.selection.rangeList.ranges; + // remove range if at end + if (skip || range.isEequal(allRanges[dir == 1 ? 0 : allRanges.length - 1])) + var toRemove = range.cursor; + } + + this.selection.addRange(newRange); + if (toRemove) + this.selection.rangeList.substractPoint(toRemove); + } + + this.transposeSelections = function(dir) { + var session = this.session; + var sel = session.multiSelect; + var all = sel.rangeList.all; + + var words = []; + 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; + } + + words.unshift(this.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; + } + } + + this.selectMore = function (dir, skip) { + var session = this.session; + var sel = session.multiSelect; + var all = sel.rangeList.all; + + 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.rangeList.substractPoint(range.cursor); + } + + +}).call(Editor.prototype); + +// Todo emit event before exec? +exports.exec = function(command, editor, args) { if (typeof command === 'string') command = this.commands[command]; @@ -88,481 +490,67 @@ function exec(command, editor, args) { if (editor && editor.$readOnly && !command.readOnly) return false; - if (!command.multiCursor) { + if (!command.multiSelectAction) { command.exec(editor, args || {}); - } else if (command.multiCursor == "forEach") { - forEachSelection(editor, command, args) - } else if (command.multiCursor == "single") { - exitMultiSelectMode(editor); + } else if (command.multiSelectAction == "forEach") { + editor.forEachSelection(command, args); + } else if (command.multiSelectAction == "single") { + editor.exitMultiSelectMode(); command.exec(editor, args || {}); } else { - command.multiCursor(editor, args || {}); + command.multiSelectAction(editor, args || {}); } return true; }; -function enterMultiSelectMode(editor) { - if (editor.inMultiSelectMode) - return - editor.inMultiSelectMode = true - editor.setStyle("multiselect") - editor.keyBinding.addKeyboardHandler(exports.keyboardHandler); - editor.commands.__exec = editor.commands.exec - editor.commands.exec = exec - editor.session.$undoSelect = false - editor.selection.rangeList.attach(editor.session); -} -function exitMultiSelectMode(editor) { - if (editor.session.multiSelection.inVirtualMode) - return - editor.inMultiSelectMode = false; - editor.selection.secondarySelections = []; - editor.unsetStyle("multiselect"); - editor.selection.rangeList.removeAll(); - editor.keyBinding.removeKeyboardHandler(exports.keyboardHandler); - - editor.commands.exec = editor.commands.__exec; - editor.renderer.updateCursor(); - editor.renderer.updateBackMarkers(); - - editor.session.$undoSelect = true - - editor.selection.rangeList.detach(editor.session); -} - -function initSession(session) { - if (session.selection.rangeList) - return - session.multiSelection = session.selection - - var rangeList = new RangeList; - - rangeList.all = []; - rangeList.on("add", function(e) { - rangeList.all.unshift(e.range) - }) - - rangeList.on("remove", function(e) { - var ranges = e.ranges - for (var i = ranges.length; i--; ) { - var index = rangeList.all.indexOf(ranges[i]); - rangeList.all.splice(index, 1); - } - }); - - session.selection.rangeList = rangeList; - session.selection.cursor = session.selection.selectionLead; - session.selection.secondarySelections = []; - session.selection.getAllRanges = function() { - return this.rangeList.ranges.concat(this.secondarySelections) - }; - session.selection.rangeCount = 1; -} - -var addSelectionRange = function(editor, orientedRange) { - if (!editor.inMultiSelectMode) - enterMultiSelectMode(editor) - - if (!orientedRange.cursor) - orientedRange.cursor = orientedRange.end - - var style = editor.getSelectionStyle(); - orientedRange.marker = editor.session.addMarker(orientedRange, "ace_selection", style); - - // use this to not conflict with virtualSelections added by forEachSelection - var selection = editor.session.multiSelection; - selection.rangeList.add(orientedRange); - selection.rangeCount = selection.rangeList.all.length + selection.secondarySelections.length; - - selection.fromOrientedRange(orientedRange) - editor.renderer.updateCursor(); - editor.renderer.updateBackMarkers(); -}; - -function addCursorV(editor, dir){ - var range = editor.selection.getRange() - var isBackwards = editor.selection.isBackwards() - range.cursor = isBackwards ? range.start : range.end; - - var screenLead = editor.session.documentToScreenPosition(range.cursor); - if (editor.selection.$desiredColumn) - screenLead.column = editor.selection.$desiredColumn; - - var lead = editor.session.screenToDocumentPosition(screenLead.row + dir, screenLead.column); - - if (!range.isEmpty()) { - var screenAnchor = editor.session.documentToScreenPosition(isBackwards ? range.end : range.start); - var anchor = editor.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 (!editor.inMultiSelectMode) { - addSelectionRange(editor, range) - } else { - var allRanges = editor.selection.rangeList.ranges - // remove range if at end - if (range.isEequal(allRanges[dir == 1 ? 0 : allRanges.length - 1])) - var toRemove = range.cursor - } - addSelectionRange(editor, newRange) - if (toRemove) - editor.selection.rangeList.substractPoint(toRemove) -} - -function transposeSelections(editor, dir) { - var session = editor.session; - var sel = session.multiSelection; - var all = sel.rangeList.all; - - var words = []; - 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; - } - - words.unshift(editor.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; - } -} - -function splitIntoLines(editor) { - var sel = editor.session.multiSelection - if (sel.rangeCount > 1) { - var ranges = sel.rangeList.ranges; - var lastRange = ranges[ranges.length - 1] - var range = Range.fromPoints(ranges[0].start, lastRange.end) - - exitMultiSelectMode(editor) - sel.setSelectionRange(range, lastRange.cursor == lastRange.start) - } else { - - } -} - -var Search = require("ace/search").Search -var search = new Search - -function find(session, needle, dir) { - search.$options.wrap = false; - search.$options.needle = needle; - search.$options.backwards = dir == -1; - return search.find(session) -} -function addRange(editor, dir, skip) { - var session = editor.session; - var sel = session.multiSelection; - var all = sel.rangeList.all; - - var range = sel.getRange(); - if (range.isEmpty()) { - var tmp = session.getWordRange(range.start.row, range.start.column) - var offset = tmp.start.column - range.start.column - range = tmp; - } - var needle = session.getTextRange(range); - - if (skip) { - - } - - var newRange = find(session, needle, dir); - - newRange.cursor = dir == -1 ? newRange.start : newRange.end; - addSelectionRange(editor, newRange) -} -// commands -// add multicursor annotations to default commands -var defaultCommands = require("./commands/default_commands").commands; - -defaultCommands.forEach(function(command) { - var single = RegExp(["selectall"].join("|"), ""); - var mapOverCommands = RegExp(["backspace", "del", - "golinedown", "golineup", "gotoend", "gotoleft", "gotolineend", "gotolinestart", - "gotoright", "gotostart", "gotowordleft", "gotowordright", - "indent", "insertstring", "inserttext", "jumptomatching", "outdent", - "removetolineend", "removetolinestart", "removewordleft", "removewordright", - "selectdown", "selectleft", "selectlineend", "selectlinestart", "selectright", - "selecttoend", "selecttolineend", "selecttolinestart", "selecttostart", - "selectup", "selectwordleft", "selectwordright", - "splitline", "tolowercase", "touppercase"].join("|"), ""); - - if (single.test(command.name)) - command.multiCursor = "single"; - else if (mapOverCommands.test(command.name)) - command.multiCursor = "forEach"; - else if (command.name == "transposeletters") - command.multiCursor = transposeSelections -}); - -// commands to to enter multicursor mode -exports.defaultCommands = [{ - name: "addCursorAbove", - exec: function(editor) {addCursorV(editor, -1); }, - bindKey: {win: "Alt-Shift-Up", mac: "Alt-Shift-Up"} -}, { - name: "addCursorBelow", - exec: function(editor) {addCursorV(editor, 1); }, - bindKey: {win: "Alt-Shift-Down", mac: "Alt-Shift-Down"} -}, { - name: "selectMoreBefore", - exec: function(editor) {addRange(editor, -1); }, - bindKey: {win: "Ctrl-Alt-Up", mac: "Ctrl-Alt-Up"} -}, { - name: "selectMoreAfter", - exec: function(editor) {addRange(editor, 1); }, - bindKey: {win: "Ctrl-Alt-Down", mac: "Ctrl-Alt-Down"} -}, { - name: "selectNextBefore", - exec: function(editor) {addCursorV(editor, -1, true); }, - bindKey: {win: "Ctrl-Shift-PageUp", mac: "Ctrl-Shift-PageUp"} -}, { - name: "selectNextAfter", - exec: function(editor) {addCursorV(editor, 1, true); }, - bindKey: {win: "Ctrl-Shift-PageDown", mac: "Ctrl-Shift-PageDown"} -}, { - name: "splitIntoLines", - exec: function(editor) {splitIntoLines(editor); }, - bindKey: {win: "Ctrl-Shift-L", mac: "Ctrl-Shift-L"} -}, ]; - -// commands active when multiple cursors are present -exports.multiEditCommands = [{ - name: "singleSelection", - bindKey: "esc", - exec: function(editor) { - console.log(editor) - exitMultiSelectMode(editor) - }, -}]; - -var HashHandler = require("ace/keyboard/hash_handler").HashHandler; -exports.keyboardHandler = new HashHandler(exports.multiEditCommands); // mouse function isSamePoint(p1, p2) { return p1.row == p2.row && p1.column == p2.column } -function onMouseDown(e) { - var ev = e.domEvent; - var alt = ev.altKey; - var shift = ev.shiftKey; - var ctrl = ev.ctrlKey; - var button = e.getButton(); - if (!ctrl && !alt) { - if (e.editor.selection.rangeCount > 1) { - if (button == 0) { - exitMultiSelectMode(e.editor) - } else if (button == 2) { - var editor = e.editor; - var selectionEmpty = editor.selection.isEmpty() - editor.textInput.onContextMenu({x: e.clientX, y: e.clientY}, selectionEmpty); - event.capture(editor.container, function(){}, editor.textInput.onContextMenuClose); - e.stop(); - } - } - return; - } +// 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 editor = e.editor; - var selection = editor.selection; - var isMultiSelect = selection.rangeCount > 1 - var pos = e.getDocumentPosition(); - var rangeList = selection.rangeList; - var cursor = selection.getCursor() - var inSelection = e.inSelection() || (selection.isEmpty() && isSamePoint(pos, cursor)); + var oldSession = e.oldSession; + if (oldSession) { + // todo use events + if (oldSession.multiSelect && oldSession.multiSelect.editor == this) + oldSession.multiSelect.editor = null; - if (ctrl && !shift && !alt && button == 0) { - if (!isMultiSelect && inSelection) - return // dragging + session.multiSelect.removeEventListener("addRange", this.$onAddRange); + session.multiSelect.removeEventListener("removeRange", this.$onRemoveRange); + session.multiSelect.removeEventListener("multiSelect", this.$onMultiSelect); + session.multiSelect.removeEventListener("singleSelect", this.$onSingleSelect); + } - if (!isMultiSelect) { - addSelectionRange(editor, selection.toOrientedRange()) - } - if (inSelection) - selection.clearSelection(); - - selection.secondarySelections.push(selection) - selection.rangeCount++ - var oldRange = rangeList.substractPoint(pos) - - event.capture(editor.container, function(){}, function() { - var i = selection.secondarySelections.indexOf(selection); - if (i != -1) { - selection.rangeCount-- - selection.secondarySelections.splice(i, 1); - } - - var tmpSel = selection.toOrientedRange(); - - if (oldRange && oldRange.isEmpty() && tmpSel.isEequal(oldRange)) { - var range = selection.rangeList.all[0] - editor.selection.rangeList._emit("remove", {ranges: []}) - selection.setSelectionRange(range, range.cursor == range.start) - selection._emit("changeSelection"); - selection._emit("changeCursor"); - return; - } - - addSelectionRange(editor, tmpSel) - }); - - //e.stop() - } else if (!shift && alt && button == 0) { - e.stop() - var mouseX = e.pageX, mouseY = e.pageY; - var onMouseSelection = function(e) { - mouseX = event.getDocumentX(e); - mouseY = event.getDocumentY(e); - }; - - selection.moveCursorToPosition(pos); - selection.clearSelection(); - if (!isMultiSelect) { - enterMultiSelectMode(editor) - selection.rangeCount = Infinity - } - - - var rectSel = [] - selection.secondarySelections = rectSel - - var session = editor.session - var style = editor.getSelectionStyle(); - function addMarker(range) { - range.marker = session.addMarker(range, "ace_selection", style); - } - function removeMarker(range) { - session.removeMarker(range.marker); - } - - - - var onMouseSelectionEnd = function(e) { - clearInterval(timerId); - rectSel.forEach(removeMarker) - selection.secondarySelections = []; - for (var i = rectSel.length; i--; ) - addSelectionRange(editor, rectSel[i]) - - if (selection.rangeCount == Infinity) { - selection.rangeCount = selection.rangeList.all.length + selection.secondarySelections.length; - - if (selection.rangeCount <= 1) - exitMultiSelectMode(editor) - } - }; - - var anchor = selection.getCursor(); - var screenAnchor = session.documentToScreenPosition(anchor); - var screenCursor = screenAnchor - var screenLength = session.getScreenLength() - 1 - - var onSelectionInterval = function() { - var newCursor = editor.renderer.pixelToScreenCoordinates(mouseX, mouseY); - if (isSamePoint(screenCursor, newCursor)) - return - rectSel.forEach(removeMarker) - rectSel.splice(0, rectSel.length) - screenCursor = newCursor - 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 (endRow > screenLength) - endRow = screenLength - - for (var row = startRow; row <= endRow; row++) { - var r = Range.fromPoints( - session.screenToDocumentPosition(row, startColumn), - session.screenToDocumentPosition(row, endColumn) - ) - r.cursor = xBackwards ? r.start : r.end - rectSel.push(r) - } - - rectSel.forEach(addMarker) - var lastIndex = yBackwards ? 0 : rectSel.length - 1 - selection.moveCursorToPosition(rectSel[lastIndex].cursor); - selection.clearSelection() - editor.renderer.scrollCursorIntoView(); - - editor.renderer.updateCursor(); - editor.renderer.updateBackMarkers(); - }; - - event.capture(editor.container, onMouseSelection, onMouseSelectionEnd); - var timerId = setInterval(onSelectionInterval, 20); - - return e.preventDefault(); - } + session.multiSelect.on("addRange", this.$onAddRange); + session.multiSelect.on("removeRange", this.$onRemoveRange); + session.multiSelect.on("multiSelect", this.$onMultiSelect); + session.multiSelect.on("singleSelect", this.$onSingleSelect); } -// MultiCursor +// adds multicursor support to editor instance function MultiSelect(editor) { - initSession(editor.session); - editor.on("changeSession", function(e) { - initSession(e.session) - }.bind(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); - editor.selection.rangeList.on("remove", function(e) { - var ranges = e.ranges; - for (var i = ranges.length; i--; ) { - var range = ranges[i]; - if (range.marker != null) - this.session.removeMarker(range.marker); - - this.rangeCount --; - } - - if (this.rangeCount == 1 && editor.inMultiSelectMode) - exitMultiSelectMode(editor) - }.bind(editor.selection)); + exports.onSessionChange.call(editor, editor); + editor.on("changeSession", exports.onSessionChange.bind(editor)); editor.on("mousedown", onMouseDown); - editor.commands.addCommands(exports.defaultCommands); + editor.commands.addCommands(exports.commands.defaultCommands); } diff --git a/lib/ace/range_list.js b/lib/ace/range_list.js index 9777b5ab..f30b1756 100644 --- a/lib/ace/range_list.js +++ b/lib/ace/range_list.js @@ -85,9 +85,9 @@ var RangeList = function(startRow, startColumn, endRow, endColumn) { endIndex++; var removed = this.ranges.splice(startIndex, endIndex - startIndex, range); - this._emit("add", {range: range}); if (removed.length) this._emit("remove", {ranges: removed}); + this._emit("add", {range: range}); return startIndex; }; @@ -143,6 +143,12 @@ var RangeList = function(startRow, startColumn, endRow, endColumn) { return this.pointIndex(pos) >= 0; }; + this.rangeAtPoint = function(pos) { + var i = this.pointIndex(pos); + if (i >= 0) + return this.ranges[i]; + }; + this.clipRows = function(startRow, endRow) { var list = this.ranges;