Merge pull request #718 from nightwing/multiCursor
Multi cursor cleanup and docs
This commit is contained in:
commit
caf151e232
7 changed files with 174 additions and 109 deletions
|
|
@ -127,91 +127,109 @@ exports.commands = [{
|
||||||
name: "selectup",
|
name: "selectup",
|
||||||
bindKey: bindKey("Shift-Up", "Shift-Up"),
|
bindKey: bindKey("Shift-Up", "Shift-Up"),
|
||||||
exec: function(editor) { editor.getSelection().selectUp(); },
|
exec: function(editor) { editor.getSelection().selectUp(); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "golineup",
|
name: "golineup",
|
||||||
bindKey: bindKey("Up", "Up|Ctrl-P"),
|
bindKey: bindKey("Up", "Up|Ctrl-P"),
|
||||||
exec: function(editor, args) { editor.navigateUp(args.times); },
|
exec: function(editor, args) { editor.navigateUp(args.times); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "selecttoend",
|
name: "selecttoend",
|
||||||
bindKey: bindKey("Ctrl-Shift-End|Alt-Shift-Down", "Command-Shift-Down"),
|
bindKey: bindKey("Ctrl-Shift-End|Alt-Shift-Down", "Command-Shift-Down"),
|
||||||
exec: function(editor) { editor.getSelection().selectFileEnd(); },
|
exec: function(editor) { editor.getSelection().selectFileEnd(); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "gotoend",
|
name: "gotoend",
|
||||||
bindKey: bindKey("Ctrl-End|Ctrl-Down", "Command-End|Command-Down"),
|
bindKey: bindKey("Ctrl-End|Ctrl-Down", "Command-End|Command-Down"),
|
||||||
exec: function(editor) { editor.navigateFileEnd(); },
|
exec: function(editor) { editor.navigateFileEnd(); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "selectdown",
|
name: "selectdown",
|
||||||
bindKey: bindKey("Shift-Down", "Shift-Down"),
|
bindKey: bindKey("Shift-Down", "Shift-Down"),
|
||||||
exec: function(editor) { editor.getSelection().selectDown(); },
|
exec: function(editor) { editor.getSelection().selectDown(); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "golinedown",
|
name: "golinedown",
|
||||||
bindKey: bindKey("Down", "Down|Ctrl-N"),
|
bindKey: bindKey("Down", "Down|Ctrl-N"),
|
||||||
exec: function(editor, args) { editor.navigateDown(args.times); },
|
exec: function(editor, args) { editor.navigateDown(args.times); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "selectwordleft",
|
name: "selectwordleft",
|
||||||
bindKey: bindKey("Ctrl-Shift-Left", "Option-Shift-Left"),
|
bindKey: bindKey("Ctrl-Shift-Left", "Option-Shift-Left"),
|
||||||
exec: function(editor) { editor.getSelection().selectWordLeft(); },
|
exec: function(editor) { editor.getSelection().selectWordLeft(); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "gotowordleft",
|
name: "gotowordleft",
|
||||||
bindKey: bindKey("Ctrl-Left", "Option-Left"),
|
bindKey: bindKey("Ctrl-Left", "Option-Left"),
|
||||||
exec: function(editor) { editor.navigateWordLeft(); },
|
exec: function(editor) { editor.navigateWordLeft(); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "selecttolinestart",
|
name: "selecttolinestart",
|
||||||
bindKey: bindKey("Alt-Shift-Left", "Command-Shift-Left"),
|
bindKey: bindKey("Alt-Shift-Left", "Command-Shift-Left"),
|
||||||
exec: function(editor) { editor.getSelection().selectLineStart(); },
|
exec: function(editor) { editor.getSelection().selectLineStart(); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "gotolinestart",
|
name: "gotolinestart",
|
||||||
bindKey: bindKey("Alt-Left|Home", "Command-Left|Home|Ctrl-A"),
|
bindKey: bindKey("Alt-Left|Home", "Command-Left|Home|Ctrl-A"),
|
||||||
exec: function(editor) { editor.navigateLineStart(); },
|
exec: function(editor) { editor.navigateLineStart(); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "selectleft",
|
name: "selectleft",
|
||||||
bindKey: bindKey("Shift-Left", "Shift-Left"),
|
bindKey: bindKey("Shift-Left", "Shift-Left"),
|
||||||
exec: function(editor) { editor.getSelection().selectLeft(); },
|
exec: function(editor) { editor.getSelection().selectLeft(); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "gotoleft",
|
name: "gotoleft",
|
||||||
bindKey: bindKey("Left", "Left|Ctrl-B"),
|
bindKey: bindKey("Left", "Left|Ctrl-B"),
|
||||||
exec: function(editor, args) { editor.navigateLeft(args.times); },
|
exec: function(editor, args) { editor.navigateLeft(args.times); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "selectwordright",
|
name: "selectwordright",
|
||||||
bindKey: bindKey("Ctrl-Shift-Right", "Option-Shift-Right"),
|
bindKey: bindKey("Ctrl-Shift-Right", "Option-Shift-Right"),
|
||||||
exec: function(editor) { editor.getSelection().selectWordRight(); },
|
exec: function(editor) { editor.getSelection().selectWordRight(); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "gotowordright",
|
name: "gotowordright",
|
||||||
bindKey: bindKey("Ctrl-Right", "Option-Right"),
|
bindKey: bindKey("Ctrl-Right", "Option-Right"),
|
||||||
exec: function(editor) { editor.navigateWordRight(); },
|
exec: function(editor) { editor.navigateWordRight(); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "selecttolineend",
|
name: "selecttolineend",
|
||||||
bindKey: bindKey("Alt-Shift-Right", "Command-Shift-Right"),
|
bindKey: bindKey("Alt-Shift-Right", "Command-Shift-Right"),
|
||||||
exec: function(editor) { editor.getSelection().selectLineEnd(); },
|
exec: function(editor) { editor.getSelection().selectLineEnd(); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "gotolineend",
|
name: "gotolineend",
|
||||||
bindKey: bindKey("Alt-Right|End", "Command-Right|End|Ctrl-E"),
|
bindKey: bindKey("Alt-Right|End", "Command-Right|End|Ctrl-E"),
|
||||||
exec: function(editor) { editor.navigateLineEnd(); },
|
exec: function(editor) { editor.navigateLineEnd(); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "selectright",
|
name: "selectright",
|
||||||
bindKey: bindKey("Shift-Right", "Shift-Right"),
|
bindKey: bindKey("Shift-Right", "Shift-Right"),
|
||||||
exec: function(editor) { editor.getSelection().selectRight(); },
|
exec: function(editor) { editor.getSelection().selectRight(); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "gotoright",
|
name: "gotoright",
|
||||||
bindKey: bindKey("Right", "Right|Ctrl-F"),
|
bindKey: bindKey("Right", "Right|Ctrl-F"),
|
||||||
exec: function(editor, args) { editor.navigateRight(args.times); },
|
exec: function(editor, args) { editor.navigateRight(args.times); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "selectpagedown",
|
name: "selectpagedown",
|
||||||
|
|
@ -247,11 +265,13 @@ exports.commands = [{
|
||||||
name: "selectlinestart",
|
name: "selectlinestart",
|
||||||
bindKey: bindKey("Shift-Home", "Shift-Home"),
|
bindKey: bindKey("Shift-Home", "Shift-Home"),
|
||||||
exec: function(editor) { editor.getSelection().selectLineStart(); },
|
exec: function(editor) { editor.getSelection().selectLineStart(); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "selectlineend",
|
name: "selectlineend",
|
||||||
bindKey: bindKey("Shift-End", "Shift-End"),
|
bindKey: bindKey("Shift-End", "Shift-End"),
|
||||||
exec: function(editor) { editor.getSelection().selectLineEnd(); },
|
exec: function(editor) { editor.getSelection().selectLineEnd(); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
}, {
|
}, {
|
||||||
name: "togglerecording",
|
name: "togglerecording",
|
||||||
|
|
@ -267,18 +287,33 @@ exports.commands = [{
|
||||||
name: "jumptomatching",
|
name: "jumptomatching",
|
||||||
bindKey: bindKey("Ctrl-Shift-P", "Ctrl-Shift-P"),
|
bindKey: bindKey("Ctrl-Shift-P", "Ctrl-Shift-P"),
|
||||||
exec: function(editor) { editor.jumpToMatching(); },
|
exec: function(editor) { editor.jumpToMatching(); },
|
||||||
|
multiSelectAction: "forEach",
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
|
|
||||||
// commands disabled in readOnly mode
|
// commands disabled in readOnly mode
|
||||||
{
|
{
|
||||||
|
name: "cut",
|
||||||
|
exec: function(editor) {
|
||||||
|
var range = editor.getSelectionRange();
|
||||||
|
editor._emit("cut", range);
|
||||||
|
|
||||||
|
if (!editor.selection.isEmpty()) {
|
||||||
|
editor.session.remove(range);
|
||||||
|
editor.clearSelection();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
multiSelectAction: "forEach"
|
||||||
|
}, {
|
||||||
name: "removeline",
|
name: "removeline",
|
||||||
bindKey: bindKey("Ctrl-D", "Command-D"),
|
bindKey: bindKey("Ctrl-D", "Command-D"),
|
||||||
exec: function(editor) { editor.removeLines(); }
|
exec: function(editor) { editor.removeLines(); },
|
||||||
|
multiSelectAction: "forEach"
|
||||||
}, {
|
}, {
|
||||||
name: "togglecomment",
|
name: "togglecomment",
|
||||||
bindKey: bindKey("Ctrl-7", "Command-7"),
|
bindKey: bindKey("Ctrl-7", "Command-7"),
|
||||||
exec: function(editor) { editor.toggleCommentLines(); }
|
exec: function(editor) { editor.toggleCommentLines(); },
|
||||||
|
multiSelectAction: "forEach"
|
||||||
}, {
|
}, {
|
||||||
name: "replace",
|
name: "replace",
|
||||||
bindKey: bindKey("Ctrl-R", "Command-Option-F"),
|
bindKey: bindKey("Ctrl-R", "Command-Option-F"),
|
||||||
|
|
@ -330,62 +365,76 @@ exports.commands = [{
|
||||||
}, {
|
}, {
|
||||||
name: "del",
|
name: "del",
|
||||||
bindKey: bindKey("Delete", "Delete|Ctrl-D"),
|
bindKey: bindKey("Delete", "Delete|Ctrl-D"),
|
||||||
exec: function(editor) { editor.remove("right"); }
|
exec: function(editor) { editor.remove("right"); },
|
||||||
|
multiSelectAction: "forEach"
|
||||||
}, {
|
}, {
|
||||||
name: "backspace",
|
name: "backspace",
|
||||||
bindKey: bindKey(
|
bindKey: bindKey(
|
||||||
"Command-Backspace|Option-Backspace|Shift-Backspace|Backspace",
|
"Command-Backspace|Option-Backspace|Shift-Backspace|Backspace",
|
||||||
"Ctrl-Backspace|Command-Backspace|Shift-Backspace|Backspace|Ctrl-H"
|
"Ctrl-Backspace|Command-Backspace|Shift-Backspace|Backspace|Ctrl-H"
|
||||||
),
|
),
|
||||||
exec: function(editor) { editor.remove("left"); }
|
exec: function(editor) { editor.remove("left"); },
|
||||||
|
multiSelectAction: "forEach"
|
||||||
}, {
|
}, {
|
||||||
name: "removetolinestart",
|
name: "removetolinestart",
|
||||||
bindKey: bindKey("Alt-Backspace", "Command-Backspace"),
|
bindKey: bindKey("Alt-Backspace", "Command-Backspace"),
|
||||||
exec: function(editor) { editor.removeToLineStart(); }
|
exec: function(editor) { editor.removeToLineStart(); },
|
||||||
|
multiSelectAction: "forEach"
|
||||||
}, {
|
}, {
|
||||||
name: "removetolineend",
|
name: "removetolineend",
|
||||||
bindKey: bindKey("Alt-Delete", "Ctrl-K"),
|
bindKey: bindKey("Alt-Delete", "Ctrl-K"),
|
||||||
exec: function(editor) { editor.removeToLineEnd(); }
|
exec: function(editor) { editor.removeToLineEnd(); },
|
||||||
|
multiSelectAction: "forEach"
|
||||||
}, {
|
}, {
|
||||||
name: "removewordleft",
|
name: "removewordleft",
|
||||||
bindKey: bindKey("Ctrl-Backspace", "Alt-Backspace|Ctrl-Alt-Backspace"),
|
bindKey: bindKey("Ctrl-Backspace", "Alt-Backspace|Ctrl-Alt-Backspace"),
|
||||||
exec: function(editor) { editor.removeWordLeft(); }
|
exec: function(editor) { editor.removeWordLeft(); },
|
||||||
|
multiSelectAction: "forEach"
|
||||||
}, {
|
}, {
|
||||||
name: "removewordright",
|
name: "removewordright",
|
||||||
bindKey: bindKey("Ctrl-Delete", "Alt-Delete"),
|
bindKey: bindKey("Ctrl-Delete", "Alt-Delete"),
|
||||||
exec: function(editor) { editor.removeWordRight(); }
|
exec: function(editor) { editor.removeWordRight(); },
|
||||||
|
multiSelectAction: "forEach"
|
||||||
}, {
|
}, {
|
||||||
name: "outdent",
|
name: "outdent",
|
||||||
bindKey: bindKey("Shift-Tab", "Shift-Tab"),
|
bindKey: bindKey("Shift-Tab", "Shift-Tab"),
|
||||||
exec: function(editor) { editor.blockOutdent(); }
|
exec: function(editor) { editor.blockOutdent(); },
|
||||||
|
multiSelectAction: "forEach"
|
||||||
}, {
|
}, {
|
||||||
name: "indent",
|
name: "indent",
|
||||||
bindKey: bindKey("Tab", "Tab"),
|
bindKey: bindKey("Tab", "Tab"),
|
||||||
exec: function(editor) { editor.indent(); }
|
exec: function(editor) { editor.indent(); },
|
||||||
|
multiSelectAction: "forEach"
|
||||||
}, {
|
}, {
|
||||||
name: "insertstring",
|
name: "insertstring",
|
||||||
exec: function(editor, str) { editor.insert(str); }
|
exec: function(editor, str) { editor.insert(str); },
|
||||||
|
multiSelectAction: "forEach"
|
||||||
}, {
|
}, {
|
||||||
name: "inserttext",
|
name: "inserttext",
|
||||||
exec: function(editor, args) {
|
exec: function(editor, args) {
|
||||||
editor.insert(lang.stringRepeat(args.text || "", args.times || 1));
|
editor.insert(lang.stringRepeat(args.text || "", args.times || 1));
|
||||||
}
|
},
|
||||||
|
multiSelectAction: "forEach"
|
||||||
}, {
|
}, {
|
||||||
name: "splitline",
|
name: "splitline",
|
||||||
bindKey: bindKey(null, "Ctrl-O"),
|
bindKey: bindKey(null, "Ctrl-O"),
|
||||||
exec: function(editor) { editor.splitLine(); }
|
exec: function(editor) { editor.splitLine(); },
|
||||||
|
multiSelectAction: "forEach"
|
||||||
}, {
|
}, {
|
||||||
name: "transposeletters",
|
name: "transposeletters",
|
||||||
bindKey: bindKey("Ctrl-T", "Ctrl-T"),
|
bindKey: bindKey("Ctrl-T", "Ctrl-T"),
|
||||||
exec: function(editor) { editor.transposeLetters(); }
|
exec: function(editor) { editor.transposeLetters(); },
|
||||||
|
multiSelectAction: function(editor) {editor.transposeSelections(1); }
|
||||||
}, {
|
}, {
|
||||||
name: "touppercase",
|
name: "touppercase",
|
||||||
bindKey: bindKey("Ctrl-U", "Ctrl-U"),
|
bindKey: bindKey("Ctrl-U", "Ctrl-U"),
|
||||||
exec: function(editor) { editor.toUpperCase(); }
|
exec: function(editor) { editor.toUpperCase(); },
|
||||||
|
multiSelectAction: "forEach"
|
||||||
}, {
|
}, {
|
||||||
name: "tolowercase",
|
name: "tolowercase",
|
||||||
bindKey: bindKey("Ctrl-Shift-U", "Ctrl-Shift-U"),
|
bindKey: bindKey("Ctrl-Shift-U", "Ctrl-Shift-U"),
|
||||||
exec: function(editor) { editor.toLowerCase(); }
|
exec: function(editor) { editor.toLowerCase(); },
|
||||||
|
multiSelectAction: "forEach"
|
||||||
}];
|
}];
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -38,27 +38,6 @@
|
||||||
|
|
||||||
define(function(require, exports, module) {
|
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", "removeline",
|
|
||||||
"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
|
// commands to enter multiselect mode
|
||||||
exports.defaultCommands = [{
|
exports.defaultCommands = [{
|
||||||
name: "addCursorAbove",
|
name: "addCursorAbove",
|
||||||
|
|
|
||||||
|
|
@ -422,16 +422,7 @@ var Editor = function(renderer, session) {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onCut = function() {
|
this.onCut = function() {
|
||||||
if (this.$readOnly)
|
this.commands.exec("cut", this);
|
||||||
return;
|
|
||||||
|
|
||||||
var range = this.getSelectionRange();
|
|
||||||
this._emit("cut", range);
|
|
||||||
|
|
||||||
if (!this.selection.isEmpty()) {
|
|
||||||
this.session.remove(range);
|
|
||||||
this.clearSelection();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.insert = function(text) {
|
this.insert = function(text) {
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ var event = require("../lib/event");
|
||||||
|
|
||||||
// mouse
|
// mouse
|
||||||
function isSamePoint(p1, p2) {
|
function isSamePoint(p1, p2) {
|
||||||
return p1.row == p2.row && p1.column == p2.column
|
return p1.row == p2.row && p1.column == p2.column;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseDown(e) {
|
function onMouseDown(e) {
|
||||||
|
|
@ -72,12 +72,11 @@ function onMouseDown(e) {
|
||||||
var selection = editor.selection;
|
var selection = editor.selection;
|
||||||
var isMultiSelect = editor.inMultiSelectMode;
|
var isMultiSelect = editor.inMultiSelectMode;
|
||||||
var pos = e.getDocumentPosition();
|
var pos = e.getDocumentPosition();
|
||||||
var cursor = selection.getCursor()
|
var cursor = selection.getCursor();
|
||||||
var inSelection = e.inSelection() || (selection.isEmpty() && isSamePoint(pos, cursor));
|
var inSelection = e.inSelection() || (selection.isEmpty() && isSamePoint(pos, cursor));
|
||||||
|
|
||||||
|
|
||||||
var mouseX = e.pageX, mouseY = e.pageY;
|
var mouseX = e.pageX, mouseY = e.pageY;
|
||||||
var clickX = mouseX, clickY = mouseY;
|
|
||||||
var onMouseSelection = function(e) {
|
var onMouseSelection = function(e) {
|
||||||
mouseX = event.getDocumentX(e);
|
mouseX = event.getDocumentX(e);
|
||||||
mouseY = event.getDocumentY(e);
|
mouseY = event.getDocumentY(e);
|
||||||
|
|
@ -99,20 +98,18 @@ function onMouseDown(e) {
|
||||||
editor.removeSelectionMarkers(rectSel);
|
editor.removeSelectionMarkers(rectSel);
|
||||||
rectSel = selection.rectangularRangeBlock(screenCursor, screenAnchor);
|
rectSel = selection.rectangularRangeBlock(screenCursor, screenAnchor);
|
||||||
rectSel.forEach(editor.addSelectionMarker, editor);
|
rectSel.forEach(editor.addSelectionMarker, editor);
|
||||||
editor.updateSelectionMarkers()
|
editor.updateSelectionMarkers();
|
||||||
};
|
};
|
||||||
|
|
||||||
var session = editor.session;
|
var session = editor.session;
|
||||||
var anchor = selection.getCursor();
|
|
||||||
var screenAnchor = editor.renderer.pixelToScreenCoordinates(mouseX, mouseY);
|
var screenAnchor = editor.renderer.pixelToScreenCoordinates(mouseX, mouseY);
|
||||||
var clippedAnchor = session.documentToScreenPosition(anchor);
|
|
||||||
var screenCursor = screenAnchor;
|
var screenCursor = screenAnchor;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (ctrl && !shift && !alt && button == 0) {
|
if (ctrl && !shift && !alt && button == 0) {
|
||||||
if (!isMultiSelect && inSelection)
|
if (!isMultiSelect && inSelection)
|
||||||
return // dragging
|
return; // dragging
|
||||||
|
|
||||||
if (!isMultiSelect)
|
if (!isMultiSelect)
|
||||||
selection.addRange(selection.toOrientedRange());
|
selection.addRange(selection.toOrientedRange());
|
||||||
|
|
@ -130,7 +127,7 @@ function onMouseDown(e) {
|
||||||
});
|
});
|
||||||
|
|
||||||
} else if (!shift && alt && button == 0) {
|
} else if (!shift && alt && button == 0) {
|
||||||
e.stop()
|
e.stop();
|
||||||
|
|
||||||
if (isMultiSelect && !ctrl)
|
if (isMultiSelect && !ctrl)
|
||||||
selection.toSingleRange();
|
selection.toSingleRange();
|
||||||
|
|
@ -146,13 +143,13 @@ function onMouseDown(e) {
|
||||||
clearInterval(timerId);
|
clearInterval(timerId);
|
||||||
editor.removeSelectionMarkers(rectSel);
|
editor.removeSelectionMarkers(rectSel);
|
||||||
for (var i = 0; i < rectSel.length; i++)
|
for (var i = 0; i < rectSel.length; i++)
|
||||||
selection.addRange(rectSel[i])
|
selection.addRange(rectSel[i]);
|
||||||
};
|
};
|
||||||
|
|
||||||
onSelectionInterval = blockSelect;
|
var onSelectionInterval = blockSelect;
|
||||||
|
|
||||||
event.capture(editor.container, onMouseSelection, onMouseSelectionEnd);
|
event.capture(editor.container, onMouseSelection, onMouseSelectionEnd);
|
||||||
var timerId = setInterval(function() {onSelectionInterval()}, 20);
|
var timerId = setInterval(function() {onSelectionInterval();}, 20);
|
||||||
|
|
||||||
return e.preventDefault();
|
return e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,14 +57,14 @@ function find(session, needle, dir) {
|
||||||
|
|
||||||
// extend EditSession
|
// extend EditSession
|
||||||
var EditSession = require("./edit_session").EditSession;
|
var EditSession = require("./edit_session").EditSession;
|
||||||
;(function() {
|
(function() {
|
||||||
this.getSelectionMarkers = function() {
|
this.getSelectionMarkers = function() {
|
||||||
return this.$selectionMarkers;
|
return this.$selectionMarkers;
|
||||||
};
|
};
|
||||||
}).call(EditSession.prototype);
|
}).call(EditSession.prototype);
|
||||||
|
|
||||||
// extend Selection
|
// extend Selection
|
||||||
;(function() {
|
(function() {
|
||||||
// list of ranges in reverse addition order
|
// list of ranges in reverse addition order
|
||||||
this.ranges = null;
|
this.ranges = null;
|
||||||
|
|
||||||
|
|
@ -72,8 +72,10 @@ var EditSession = require("./edit_session").EditSession;
|
||||||
this.rangeList = null;
|
this.rangeList = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* add a range to selection entering multiselect mode if necessary
|
* Selection.addRange(Range) -> Void
|
||||||
*/
|
*
|
||||||
|
* adds a range to selection entering multiselect mode if necessary
|
||||||
|
**/
|
||||||
this.addRange = function(range) {
|
this.addRange = function(range) {
|
||||||
if (!this.inMultiSelectMode && this.rangeCount == 0) {
|
if (!this.inMultiSelectMode && this.rangeCount == 0) {
|
||||||
var oldRange = this.toOrientedRange();
|
var oldRange = this.toOrientedRange();
|
||||||
|
|
@ -112,15 +114,26 @@ var EditSession = require("./edit_session").EditSession;
|
||||||
|
|
||||||
range && this.fromOrientedRange(range);
|
range && this.fromOrientedRange(range);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selection.addRange(pos) -> Range
|
||||||
|
* pos: {row, column}
|
||||||
|
*
|
||||||
|
* removes range containing pos (if exists)
|
||||||
|
**/
|
||||||
this.substractPoint = function(pos) {
|
this.substractPoint = function(pos) {
|
||||||
var removed = this.rangeList.substractPoint(pos);
|
var removed = this.rangeList.substractPoint(pos);
|
||||||
if (removed) {
|
if (removed) {
|
||||||
this.$onRemoveRange(removed);
|
this.$onRemoveRange(removed);
|
||||||
return removed[0]
|
return removed[0];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selection.mergeOverlappingRanges() -> Void
|
||||||
|
*
|
||||||
|
* merges overlapping ranges ensuring consistency after changes
|
||||||
|
**/
|
||||||
this.mergeOverlappingRanges = function() {
|
this.mergeOverlappingRanges = function() {
|
||||||
var removed = this.rangeList.merge();
|
var removed = this.rangeList.merge();
|
||||||
if (removed.length)
|
if (removed.length)
|
||||||
|
|
@ -139,7 +152,7 @@ var EditSession = require("./edit_session").EditSession;
|
||||||
this.$onRemoveRange = function(removed) {
|
this.$onRemoveRange = function(removed) {
|
||||||
this.rangeCount = this.rangeList.ranges.length;
|
this.rangeCount = this.rangeList.ranges.length;
|
||||||
if (this.rangeCount == 1 && this.inMultiSelectMode) {
|
if (this.rangeCount == 1 && this.inMultiSelectMode) {
|
||||||
var lastRange = this.rangeList.ranges.pop()
|
var lastRange = this.rangeList.ranges.pop();
|
||||||
removed.push(lastRange);
|
removed.push(lastRange);
|
||||||
this.rangeCount = 0;
|
this.rangeCount = 0;
|
||||||
}
|
}
|
||||||
|
|
@ -195,6 +208,7 @@ var EditSession = require("./edit_session").EditSession;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Selection.rectangularRangeBlock(screenCursor, screenAnchor, includeEmptyLines) -> [Range]
|
||||||
* gets list of ranges composing rectangular block on the screen
|
* gets list of ranges composing rectangular block on the screen
|
||||||
* @includeEmptyLines if true includes ranges inside the block which
|
* @includeEmptyLines if true includes ranges inside the block which
|
||||||
* are empty becuase of the clipping
|
* are empty becuase of the clipping
|
||||||
|
|
@ -241,6 +255,7 @@ var EditSession = require("./edit_session").EditSession;
|
||||||
range.cursor = xBackwards ? range.start : range.end;
|
range.cursor = xBackwards ? range.start : range.end;
|
||||||
rectSel.push(range);
|
rectSel.push(range);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (yBackwards)
|
if (yBackwards)
|
||||||
rectSel.reverse();
|
rectSel.reverse();
|
||||||
|
|
||||||
|
|
@ -265,12 +280,23 @@ var EditSession = require("./edit_session").EditSession;
|
||||||
|
|
||||||
// extend Editor
|
// extend Editor
|
||||||
var Editor = require("./editor").Editor;
|
var Editor = require("./editor").Editor;
|
||||||
;(function() {
|
(function() {
|
||||||
|
/**
|
||||||
|
* Editor.updateSelectionMarkers() -> Void
|
||||||
|
*
|
||||||
|
* updates cursor and marker layers
|
||||||
|
**/
|
||||||
this.updateSelectionMarkers = function() {
|
this.updateSelectionMarkers = function() {
|
||||||
this.renderer.updateCursor();
|
this.renderer.updateCursor();
|
||||||
this.renderer.updateBackMarkers();
|
this.renderer.updateBackMarkers();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editor.addSelectionMarker(orientedRange) -> Range
|
||||||
|
* - orientedRange: range with cursor
|
||||||
|
*
|
||||||
|
* adds selection and cursor
|
||||||
|
**/
|
||||||
this.addSelectionMarker = function(orientedRange) {
|
this.addSelectionMarker = function(orientedRange) {
|
||||||
if (!orientedRange.cursor)
|
if (!orientedRange.cursor)
|
||||||
orientedRange.cursor = orientedRange.end;
|
orientedRange.cursor = orientedRange.end;
|
||||||
|
|
@ -291,7 +317,7 @@ var Editor = require("./editor").Editor;
|
||||||
this.session.removeMarker(range.marker);
|
this.session.removeMarker(range.marker);
|
||||||
var index = this.session.$selectionMarkers.indexOf(range);
|
var index = this.session.$selectionMarkers.indexOf(range);
|
||||||
if (index != -1)
|
if (index != -1)
|
||||||
this.session.$selectionMarkers.splice(index, 1)
|
this.session.$selectionMarkers.splice(index, 1);
|
||||||
}
|
}
|
||||||
this.session.selectionMarkerCount = this.session.$selectionMarkers.length;
|
this.session.selectionMarkerCount = this.session.$selectionMarkers.length;
|
||||||
};
|
};
|
||||||
|
|
@ -301,11 +327,13 @@ var Editor = require("./editor").Editor;
|
||||||
this.renderer.updateCursor();
|
this.renderer.updateCursor();
|
||||||
this.renderer.updateBackMarkers();
|
this.renderer.updateBackMarkers();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$onRemoveRange = function(e) {
|
this.$onRemoveRange = function(e) {
|
||||||
this.removeSelectionMarkers(e.ranges);
|
this.removeSelectionMarkers(e.ranges);
|
||||||
this.renderer.updateCursor();
|
this.renderer.updateCursor();
|
||||||
this.renderer.updateBackMarkers();
|
this.renderer.updateBackMarkers();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$onMultiSelect = function(e) {
|
this.$onMultiSelect = function(e) {
|
||||||
if (this.inMultiSelectMode)
|
if (this.inMultiSelectMode)
|
||||||
return;
|
return;
|
||||||
|
|
@ -318,6 +346,7 @@ var Editor = require("./editor").Editor;
|
||||||
this.renderer.updateCursor();
|
this.renderer.updateCursor();
|
||||||
this.renderer.updateBackMarkers();
|
this.renderer.updateBackMarkers();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$onSingleSelect = function(e) {
|
this.$onSingleSelect = function(e) {
|
||||||
if (this.session.multiSelect.inVirtualMode)
|
if (this.session.multiSelect.inVirtualMode)
|
||||||
return;
|
return;
|
||||||
|
|
@ -349,11 +378,17 @@ var Editor = require("./editor").Editor;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editor.forEachSelection(cmd, args) -> Void
|
||||||
|
* - cmd: command to execute
|
||||||
|
* - args: arguments to the command
|
||||||
|
*
|
||||||
|
* executes command for each selection range
|
||||||
|
**/
|
||||||
this.forEachSelection = function(cmd, args) {
|
this.forEachSelection = function(cmd, args) {
|
||||||
if (this.inVirtualSelectionMode)
|
if (this.inVirtualSelectionMode)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var session = this.session;
|
var session = this.session;
|
||||||
var selection = this.selection;
|
var selection = this.selection;
|
||||||
var rangeList = selection.rangeList;
|
var rangeList = selection.rangeList;
|
||||||
|
|
@ -379,14 +414,18 @@ var Editor = require("./editor").Editor;
|
||||||
this.onCursorChange();
|
this.onCursorChange();
|
||||||
this.onSelectionChange();
|
this.onSelectionChange();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editor.exitMultiSelectMode() -> Void
|
||||||
|
*
|
||||||
|
* removes all selections except the last added one.
|
||||||
|
**/
|
||||||
this.exitMultiSelectMode = function() {
|
this.exitMultiSelectMode = function() {
|
||||||
if (this.inVirtualSelectionMode)
|
if (this.inVirtualSelectionMode)
|
||||||
return;
|
return;
|
||||||
this.multiSelect.toSingleRange();
|
this.multiSelect.toSingleRange();
|
||||||
};
|
};
|
||||||
|
|
||||||
// todo route copy/cut/paste through commandmanager
|
|
||||||
this.getCopyText = function() {
|
this.getCopyText = function() {
|
||||||
var text = "";
|
var text = "";
|
||||||
if (this.inMultiSelectMode) {
|
if (this.inMultiSelectMode) {
|
||||||
|
|
@ -403,26 +442,15 @@ var Editor = require("./editor").Editor;
|
||||||
return text;
|
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
|
// commands
|
||||||
|
/**
|
||||||
|
* Editor.selectMoreLines(dir, skip) -> Void
|
||||||
|
* - dir: -1 up, 1 down
|
||||||
|
* - skip: remove active selection range if true
|
||||||
|
*
|
||||||
|
* adds cursor above or bellow active cursor
|
||||||
|
**/
|
||||||
this.selectMoreLines = function(dir, skip) {
|
this.selectMoreLines = function(dir, skip) {
|
||||||
var range = this.selection.toOrientedRange();
|
var range = this.selection.toOrientedRange();
|
||||||
var isBackwards = range.cursor == range.end;
|
var isBackwards = range.cursor == range.end;
|
||||||
|
|
@ -452,9 +480,6 @@ var Editor = require("./editor").Editor;
|
||||||
if (!this.selection.inMultiSelectMode) {
|
if (!this.selection.inMultiSelectMode) {
|
||||||
this.selection.addRange(range);
|
this.selection.addRange(range);
|
||||||
} else {
|
} else {
|
||||||
var allRanges = this.selection.rangeList.ranges;
|
|
||||||
// remove range if at end
|
|
||||||
// var atEdge = range.isEqual(allRanges[dir == 1 ? 0 : allRanges.length - 1]);
|
|
||||||
if (skip)
|
if (skip)
|
||||||
var toRemove = range.cursor;
|
var toRemove = range.cursor;
|
||||||
}
|
}
|
||||||
|
|
@ -462,14 +487,20 @@ var Editor = require("./editor").Editor;
|
||||||
this.selection.addRange(newRange);
|
this.selection.addRange(newRange);
|
||||||
if (toRemove)
|
if (toRemove)
|
||||||
this.selection.substractPoint(toRemove);
|
this.selection.substractPoint(toRemove);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editor.transposeSelections(dir) -> Void
|
||||||
|
* - dir: direction to rotate selections
|
||||||
|
*
|
||||||
|
* contents
|
||||||
|
* empty ranges are expanded to word
|
||||||
|
**/
|
||||||
this.transposeSelections = function(dir) {
|
this.transposeSelections = function(dir) {
|
||||||
var session = this.session;
|
var session = this.session;
|
||||||
var sel = session.multiSelect;
|
var sel = session.multiSelect;
|
||||||
var all = sel.ranges;
|
var all = sel.ranges;
|
||||||
|
|
||||||
var words = [];
|
|
||||||
for (var i = all.length; i--; ) {
|
for (var i = all.length; i--; ) {
|
||||||
var range = all[i];
|
var range = all[i];
|
||||||
if (range.isEmpty()) {
|
if (range.isEmpty()) {
|
||||||
|
|
@ -479,8 +510,13 @@ var Editor = require("./editor").Editor;
|
||||||
range.end.row = tmp.end.row;
|
range.end.row = tmp.end.row;
|
||||||
range.end.column = tmp.end.column;
|
range.end.column = tmp.end.column;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
words.unshift(this.session.getTextRange(range));
|
sel.mergeOverlappingRanges();
|
||||||
|
|
||||||
|
var words = [];
|
||||||
|
for (var i = all.length; i--; ) {
|
||||||
|
var range = all[i];
|
||||||
|
words.unshift(session.getTextRange(range));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dir < 0)
|
if (dir < 0)
|
||||||
|
|
@ -497,6 +533,14 @@ var Editor = require("./editor").Editor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editor.selectMore(dir, skip) -> Void
|
||||||
|
* - dir: 1 next, -1 previous
|
||||||
|
* - skip: remove active selection range if true
|
||||||
|
*
|
||||||
|
* finds next occurence of text in active selection
|
||||||
|
* and adds it to the selections
|
||||||
|
**/
|
||||||
this.selectMore = function (dir, skip) {
|
this.selectMore = function (dir, skip) {
|
||||||
var session = this.session;
|
var session = this.session;
|
||||||
var sel = session.multiSelect;
|
var sel = session.multiSelect;
|
||||||
|
|
@ -516,12 +560,12 @@ var Editor = require("./editor").Editor;
|
||||||
}
|
}
|
||||||
if (skip)
|
if (skip)
|
||||||
this.multiSelect.substractPoint(range.cursor);
|
this.multiSelect.substractPoint(range.cursor);
|
||||||
}
|
};
|
||||||
}).call(Editor.prototype);
|
}).call(Editor.prototype);
|
||||||
|
|
||||||
|
|
||||||
function isSamePoint(p1, p2) {
|
function isSamePoint(p1, p2) {
|
||||||
return p1.row == p2.row && p1.column == p2.column
|
return p1.row == p2.row && p1.column == p2.column;
|
||||||
}
|
}
|
||||||
|
|
||||||
// patch
|
// patch
|
||||||
|
|
@ -562,7 +606,12 @@ exports.onSessionChange = function(e) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// adds multicursor support to editor instance
|
/**
|
||||||
|
* MultiSelect(editor) -> Void
|
||||||
|
*
|
||||||
|
* adds multiple selection support to the editor
|
||||||
|
* (note: should be called only once for each editor instance)
|
||||||
|
**/
|
||||||
function MultiSelect(editor) {
|
function MultiSelect(editor) {
|
||||||
editor.$onAddRange = editor.$onAddRange.bind(editor);
|
editor.$onAddRange = editor.$onAddRange.bind(editor);
|
||||||
editor.$onRemoveRange = editor.$onRemoveRange.bind(editor);
|
editor.$onRemoveRange = editor.$onRemoveRange.bind(editor);
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,9 @@ module.exports = {
|
||||||
assert.ok(editor.inMultiSelectMode);
|
assert.ok(editor.inMultiSelectMode);
|
||||||
assert.equal(editor.selection.getAllRanges().length, 4);
|
assert.equal(editor.selection.getAllRanges().length, 4);
|
||||||
|
|
||||||
assert.equal(editor.getCopyText(), "wwww");
|
var newLine = editor.session.getDocument().getNewLineCharacter();
|
||||||
|
var copyText = "wwww".split("").join(newLine);
|
||||||
|
assert.equal(editor.getCopyText(), copyText);
|
||||||
exec("insertstring", 1, "a");
|
exec("insertstring", 1, "a");
|
||||||
exec("backspace", 2);
|
exec("backspace", 2);
|
||||||
assert.equal(editor.session.getValue(), "w1.w2\ntt\ntt");
|
assert.equal(editor.session.getValue(), "w1.w2\ntt\ntt");
|
||||||
|
|
|
||||||
|
|
@ -38,16 +38,14 @@
|
||||||
define(function(require, exports, module) {
|
define(function(require, exports, module) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var oop = require("./lib/oop");
|
|
||||||
var EventEmitter = require("./lib/event_emitter").EventEmitter;
|
|
||||||
|
|
||||||
var RangeList = function(startRow, startColumn, endRow, endColumn) {
|
var RangeList = function() {
|
||||||
this.ranges = [];
|
this.ranges = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
this.comparePoints = function(p1, p2) {
|
this.comparePoints = function(p1, p2) {
|
||||||
return p1.row - p2.row || p1.column - p2.column
|
return p1.row - p2.row || p1.column - p2.column;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.pointIndex = function(pos, startIndex) {
|
this.pointIndex = function(pos, startIndex) {
|
||||||
|
|
@ -67,7 +65,7 @@ var RangeList = function(startRow, startColumn, endRow, endColumn) {
|
||||||
|
|
||||||
return -i-1;
|
return -i-1;
|
||||||
}
|
}
|
||||||
return -i - 1
|
return -i - 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.add = function(range) {
|
this.add = function(range) {
|
||||||
|
|
@ -88,7 +86,7 @@ var RangeList = function(startRow, startColumn, endRow, endColumn) {
|
||||||
this.addList = function(list) {
|
this.addList = function(list) {
|
||||||
var removed = [];
|
var removed = [];
|
||||||
for (var i = list.length; i--; ) {
|
for (var i = list.length; i--; ) {
|
||||||
removed.push.call(removed, this.add(list[i]))
|
removed.push.call(removed, this.add(list[i]));
|
||||||
}
|
}
|
||||||
return removed;
|
return removed;
|
||||||
};
|
};
|
||||||
|
|
@ -108,7 +106,7 @@ var RangeList = function(startRow, startColumn, endRow, endColumn) {
|
||||||
for (var i = 1; i < list.length; i++) {
|
for (var i = 1; i < list.length; i++) {
|
||||||
range = next;
|
range = next;
|
||||||
next = list[i];
|
next = list[i];
|
||||||
var cmp = this.comparePoints(range.end, next.start)
|
var cmp = this.comparePoints(range.end, next.start);
|
||||||
if (cmp < 0)
|
if (cmp < 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|
@ -180,8 +178,8 @@ var RangeList = function(startRow, startColumn, endRow, endColumn) {
|
||||||
this.detach = function() {
|
this.detach = function() {
|
||||||
if (!this.session)
|
if (!this.session)
|
||||||
return;
|
return;
|
||||||
this.session.removeListener('change', this.onChange)
|
this.session.removeListener('change', this.onChange);
|
||||||
this.session = null
|
this.session = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$onChange = function(e) {
|
this.$onChange = function(e) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue