diff --git a/demo/kitchen-sink/demo.js b/demo/kitchen-sink/demo.js index 242b87a4..0a857a45 100644 --- a/demo/kitchen-sink/demo.js +++ b/demo/kitchen-sink/demo.js @@ -576,4 +576,7 @@ commands.addCommand({ } }); +// add multiple cursor support to editor +require("ace/multi_cursor").MultiCursor(env.editor) + }); diff --git a/lib/ace/multi_cursor.js b/lib/ace/multi_cursor.js new file mode 100644 index 00000000..d5e930ee --- /dev/null +++ b/lib/ace/multi_cursor.js @@ -0,0 +1,470 @@ +/* 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) { + +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"); + +function VirtualSelection(selection) { + this.session = selection.session; + this.doc = selection.doc; + + this.clearSelection(); + this.selectionLead = this.doc.createAnchor(0, 0); + this.selectionAnchor = this.doc.createAnchor(0, 0); + + this.selectionLead.detach() + this.selectionAnchor.detach() + + this.fromRange = function(r) { + this.setSelectionRange(r, r.cursor == r.start) + } + + this.toRange = function(r) { + var r1 = this.getRange() + + r.start.column = r1.start.column + r.start.row = r1.start.row + r.end.column = r1.end.column + r.end.row = r1.end.row + + r.cursor = this.isBackwards() ? r.start : r.end; + } +} +VirtualSelection.prototype = Selection.prototype; + + +function forEachSelection(editor, cmd, args) { + if (editor.session.multiSelection.inVirtualMode) + return + var session = editor.session + var selection = editor.selection + var rangeList = selection.rangeList + + var sh = new VirtualSelection(selection) + editor.session.multiSelection.inVirtualMode = true + for (var i = rangeList.ranges.length; i--;) { + sh.fromRange(rangeList.ranges[i]) + editor.selection = session.selection = sh + cmd.exec(editor, args || {}) + + sh.toRange(rangeList.ranges[i]) + } + + rangeList.merge() + editor.selection = session.selection = selection + editor.session.multiSelection.inVirtualMode = false + + selection.setSelectionRange(selection.rangeList.all[0]) + editor.renderer.updateCursor(); + editor.renderer.updateBackMarkers(); + + if (selection.rangeCount == 1 && editor.inMultiSelectMode) + exitMultiSelectMode(editor) +} + +function exec(command, editor, args) { + if (typeof command === 'string') + command = this.commands[command]; + + if (!command) + return false; + + if (editor && editor.$readOnly && !command.readOnly) + return false; + + if (/^(remove|insert|select|go|jumptomatching|del|backspace)/.test(command.name)) { + forEachSelection(editor, command, args) + }else + command.exec(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.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.selection.rangeList.dettach(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.setSelectionRange(orientedRange, orientedRange.cursor == orientedRange.start) + 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); + 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 + } + 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) +} +// commands +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" + } +}]; + +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; + } + + 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) { + var tmpSel = selection.getRange(); + tmpSel.cursor = selection.isBackwards() ? tmpSel.start : tmpSel.end; + addSelectionRange(editor, tmpSel) + } + 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.getRange(); + tmpSel.cursor = selection.isBackwards() ? tmpSel.start : tmpSel.end; + + 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 = 5 + } + + + 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]) + }; + + 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(); + } +} + +function altSelect(ev) { + +} + +// MultiCursor +function MultiCursor(editor) { + initSession(editor.session); + editor.on("changeSession", function(e) { + initSession(e.session) + }.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)); + + editor.on("mousedown", onMouseDown); + editor.commands.addCommands(exports.defaultCommands); +} + + + +exports.MultiCursor = MultiCursor; + +}); \ No newline at end of file