commit
b1bb0b9ccc
22 changed files with 361 additions and 221 deletions
|
|
@ -240,12 +240,9 @@ var BackgroundTokenizer = function(tokenizer, editor) {
|
||||||
var data = this.tokenizer.getLineTokens(line, state);
|
var data = this.tokenizer.getLineTokens(line, state);
|
||||||
if (overflow) {
|
if (overflow) {
|
||||||
data.tokens.push(overflow);
|
data.tokens.push(overflow);
|
||||||
data.state = null;
|
data.state = "start";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.state == "start" && this.states[row] == null)
|
|
||||||
this.states[row] = "start";
|
|
||||||
|
|
||||||
if (this.states[row] !== data.state) {
|
if (this.states[row] !== data.state) {
|
||||||
this.states[row] = data.state;
|
this.states[row] = data.state;
|
||||||
this.lines[row + 1] = null;
|
this.lines[row + 1] = null;
|
||||||
|
|
|
||||||
92
lib/ace/background_tokenizer_test.js
Normal file
92
lib/ace/background_tokenizer_test.js
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
/* ***** 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 <harutyun@c9.io>
|
||||||
|
*
|
||||||
|
* 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 ***** */
|
||||||
|
|
||||||
|
if (typeof process !== "undefined") {
|
||||||
|
require("amd-loader");
|
||||||
|
}
|
||||||
|
|
||||||
|
define(function(require, exports, module) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var EditSession = require("./edit_session").EditSession;
|
||||||
|
var JavaScriptMode = require("./mode/javascript").Mode;
|
||||||
|
var Range = require("./range").Range;
|
||||||
|
var assert = require("./test/assertions");
|
||||||
|
|
||||||
|
function forceTokenize(session){
|
||||||
|
for (var i = 0, l = session.getLength(); i < l; i++)
|
||||||
|
session.getTokens(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
function testStates(session, states) {
|
||||||
|
for (var i = 0, l = session.getLength(); i < l; i++)
|
||||||
|
assert.equal(session.bgTokenizer.states[i], states[i])
|
||||||
|
assert.ok(l == states.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
|
||||||
|
"test background tokenizer update on session change" : function() {
|
||||||
|
var doc = new EditSession([
|
||||||
|
"/*",
|
||||||
|
"*/",
|
||||||
|
"var juhu"
|
||||||
|
]);
|
||||||
|
doc.setMode("ace/mode/javascript")
|
||||||
|
|
||||||
|
forceTokenize(doc)
|
||||||
|
testStates(doc, ["comment", "start", "start"])
|
||||||
|
|
||||||
|
doc.remove(new Range(0,2,1,2))
|
||||||
|
testStates(doc, [null, "start"])
|
||||||
|
|
||||||
|
forceTokenize(doc)
|
||||||
|
testStates(doc, ["comment", "comment"])
|
||||||
|
|
||||||
|
doc.insert({row:0, column:2}, "\n*/")
|
||||||
|
testStates(doc, [undefined, undefined, "comment"])
|
||||||
|
|
||||||
|
forceTokenize(doc)
|
||||||
|
testStates(doc, ["comment", "start", "start"])
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof module !== "undefined" && module === require.main) {
|
||||||
|
require("asyncjs").test.testcase(module.exports).exec()
|
||||||
|
}
|
||||||
|
|
@ -51,16 +51,11 @@ oop.inherits(CommandManager, HashHandler);
|
||||||
if (editor && editor.$readOnly && !command.readOnly)
|
if (editor && editor.$readOnly && !command.readOnly)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
try {
|
var retvalue = this._emit("exec", {
|
||||||
var retvalue = this._emit("exec", {
|
editor: editor,
|
||||||
editor: editor,
|
command: command,
|
||||||
command: command,
|
args: args
|
||||||
args: args
|
});
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
window.console && window.console.log(e);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return retvalue === false ? false : true;
|
return retvalue === false ? false : true;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -79,12 +79,15 @@ exports.defaultCommands = [{
|
||||||
exec: function(editor) { editor.selectMore(1, true); },
|
exec: function(editor) { editor.selectMore(1, true); },
|
||||||
bindKey: {win: "Ctrl-Alt-Shift-Right", mac: "Ctrl-Alt-Shift-Right"},
|
bindKey: {win: "Ctrl-Alt-Shift-Right", mac: "Ctrl-Alt-Shift-Right"},
|
||||||
readonly: true
|
readonly: true
|
||||||
}, {
|
}, {
|
||||||
name: "splitIntoLines",
|
name: "splitIntoLines",
|
||||||
exec: function(editor) { editor.multiSelect.splitIntoLines(); },
|
exec: function(editor) { editor.multiSelect.splitIntoLines(); },
|
||||||
bindKey: {win: "Ctrl-Shift-L", mac: "Ctrl-Shift-L"},
|
bindKey: {win: "Ctrl-Shift-L", mac: "Ctrl-Shift-L"},
|
||||||
readonly: true
|
readonly: true
|
||||||
}, {
|
}];
|
||||||
|
|
||||||
|
// commands active in multiselect mode
|
||||||
|
exports.multiSelectCommands = [{
|
||||||
name: "singleSelection",
|
name: "singleSelection",
|
||||||
bindKey: "esc",
|
bindKey: "esc",
|
||||||
exec: function(editor) { editor.exitMultiSelectMode(); },
|
exec: function(editor) { editor.exitMultiSelectMode(); },
|
||||||
|
|
@ -92,10 +95,7 @@ exports.defaultCommands = [{
|
||||||
isAvailable: function(editor) {return editor.inMultiSelectMode}
|
isAvailable: function(editor) {return editor.inMultiSelectMode}
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// commands active in multiselect mode
|
|
||||||
exports.multiEditCommands = {"singleSelection": "esc"};
|
|
||||||
|
|
||||||
var HashHandler = require("../keyboard/hash_handler").HashHandler;
|
var HashHandler = require("../keyboard/hash_handler").HashHandler;
|
||||||
exports.keyboardHandler = new HashHandler(exports.multiEditCommands);
|
exports.keyboardHandler = new HashHandler(exports.multiSelectCommands);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -55,14 +55,14 @@ var options = {
|
||||||
|
|
||||||
exports.get = function(key) {
|
exports.get = function(key) {
|
||||||
if (!options.hasOwnProperty(key))
|
if (!options.hasOwnProperty(key))
|
||||||
throw new Error("Unknown confik key: " + key);
|
throw new Error("Unknown config key: " + key);
|
||||||
|
|
||||||
return options[key];
|
return options[key];
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.set = function(key, value) {
|
exports.set = function(key, value) {
|
||||||
if (!options.hasOwnProperty(key))
|
if (!options.hasOwnProperty(key))
|
||||||
throw new Error("Unknown confik key: " + key);
|
throw new Error("Unknown config key: " + key);
|
||||||
|
|
||||||
options[key] = value;
|
options[key] = value;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,6 @@
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace_gutter_active_line {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ace_scroller.horscroll {
|
.ace_scroller.horscroll {
|
||||||
box-shadow: 17px 0 16px -16px rgba(0, 0, 0, 0.4) inset;
|
box-shadow: 17px 0 16px -16px rgba(0, 0, 0, 0.4) inset;
|
||||||
}
|
}
|
||||||
|
|
@ -294,23 +288,19 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace_fade-fold-widgets .ace_fold-widget {
|
.ace_fade-fold-widgets .ace_fold-widget {
|
||||||
-moz-transition: 0.5s opacity;
|
-moz-transition: opacity 0.4s ease 0.05s;
|
||||||
-webkit-transition: 0.5s opacity;
|
-webkit-transition: opacity 0.4s ease 0.05s;
|
||||||
-o-transition: 0.5s opacity;
|
-o-transition: opacity 0.4s ease 0.05s;
|
||||||
-ms-transition: 0.5s opacity;
|
-ms-transition: opacity 0.4s ease 0.05s;
|
||||||
transition: 0.5s opacity;
|
transition: opacity 0.4s ease 0.05s;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace_fade-fold-widgets:hover .ace_fold-widget {
|
.ace_fade-fold-widgets:hover .ace_fold-widget {
|
||||||
-moz-transition-duration: 0.05s;
|
-moz-transition: opacity 0.05s ease 0.05s;
|
||||||
-webkit-transition-duration: 0.05s;
|
-webkit-transition: opacity 0.05s ease 0.05s;
|
||||||
-o-transition-duration: 0.05s;
|
-o-transition: opacity 0.05s ease 0.05s;
|
||||||
-ms-transition-duration: 0.05s;
|
-ms-transition: opacity 0.05s ease 0.05s;
|
||||||
transition-duration: 0.05s;
|
transition: opacity 0.05s ease 0.05s;
|
||||||
-moz-transition-delay: 0.2s;
|
|
||||||
-webkit-transition-delay: 0.2s;
|
|
||||||
-o-transition-delay: 0.2s;
|
|
||||||
-ms-transition-delay: 0.2s;
|
|
||||||
transition-delay: 0.2s;
|
|
||||||
opacity:1;
|
opacity:1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,35 @@ var Editor = function(renderer, session) {
|
||||||
return this.session;
|
return this.session;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** related to: Document.setValue
|
||||||
|
* Editor.setValue(val [,dontSelect]) -> String
|
||||||
|
* - val (String): The new value to set for the document
|
||||||
|
* - cursorPos (number): 0: selectAll, -1 document start, 1 end
|
||||||
|
*
|
||||||
|
* Sets the current document to `val`.
|
||||||
|
**/
|
||||||
|
this.setValue = function(val, cursorPos) {
|
||||||
|
this.session.doc.setValue(val);
|
||||||
|
|
||||||
|
if (!cursorPos)
|
||||||
|
this.selectAll();
|
||||||
|
else if (cursorPos == 1)
|
||||||
|
this.navigateFileEnd();
|
||||||
|
else if (cursorPos == -1)
|
||||||
|
this.navigateFileStart();
|
||||||
|
|
||||||
|
return val;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** related to: EditSession.getValue
|
||||||
|
* Editor.getValue() -> String
|
||||||
|
*
|
||||||
|
* Returns the current session's content.
|
||||||
|
**/
|
||||||
|
this.getValue = function() {
|
||||||
|
return this.session.getValue();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editor.getSelection() -> String
|
* Editor.getSelection() -> String
|
||||||
*
|
*
|
||||||
|
|
@ -249,8 +278,8 @@ var Editor = function(renderer, session) {
|
||||||
*
|
*
|
||||||
* {:VirtualRenderer.onResize}
|
* {:VirtualRenderer.onResize}
|
||||||
**/
|
**/
|
||||||
this.resize = function() {
|
this.resize = function(force) {
|
||||||
this.renderer.onResize();
|
this.renderer.onResize(force);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -541,7 +570,7 @@ var Editor = function(renderer, session) {
|
||||||
* Emitted when a breakpoint changes.
|
* Emitted when a breakpoint changes.
|
||||||
**/
|
**/
|
||||||
this.onChangeBreakpoint = function() {
|
this.onChangeBreakpoint = function() {
|
||||||
this.renderer.setBreakpoints(this.session.getBreakpoints());
|
this.renderer.updateBreakpoints();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -57,9 +57,8 @@ function HashHandler(config, platform) {
|
||||||
|
|
||||||
this.commands[command.name] = command;
|
this.commands[command.name] = command;
|
||||||
|
|
||||||
if (command.bindKey) {
|
if (command.bindKey)
|
||||||
this._buildKeyHash(command);
|
this._buildKeyHash(command);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.removeCommand = function(command) {
|
this.removeCommand = function(command) {
|
||||||
|
|
@ -81,6 +80,10 @@ function HashHandler(config, platform) {
|
||||||
this.bindKey = function(key, command) {
|
this.bindKey = function(key, command) {
|
||||||
if(!key)
|
if(!key)
|
||||||
return;
|
return;
|
||||||
|
if (typeof command == "function") {
|
||||||
|
this.addCommand({exec: command, bindKey: key, name: key});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var ckb = this.commmandKeyBinding;
|
var ckb = this.commmandKeyBinding;
|
||||||
key.split("|").forEach(function(keyPart) {
|
key.split("|").forEach(function(keyPart) {
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,8 @@ var KeyBinding = function(editor) {
|
||||||
else
|
else
|
||||||
success = toExecute.passEvent != true;
|
success = toExecute.passEvent != true;
|
||||||
|
|
||||||
if (success && e)
|
// do not stop input events to not break repeating
|
||||||
|
if (success && e && hashId != -1)
|
||||||
event.stopEvent(e);
|
event.stopEvent(e);
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,9 @@ exports.handler = {
|
||||||
editor.removeListener("click", exports.onCursorMove);
|
editor.removeListener("click", exports.onCursorMove);
|
||||||
util.noMode(editor);
|
util.noMode(editor);
|
||||||
util.currentMode = "normal";
|
util.currentMode = "normal";
|
||||||
}
|
},
|
||||||
|
|
||||||
|
actions: cmds.actions
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,18 +52,6 @@ var MOTION = 3;
|
||||||
var ACTION = 4;
|
var ACTION = 4;
|
||||||
var HMARGIN = 8; // Minimum amount of line separation between margins;
|
var HMARGIN = 8; // Minimum amount of line separation between margins;
|
||||||
|
|
||||||
exports.searchStore = {
|
|
||||||
current: "",
|
|
||||||
options: {
|
|
||||||
needle: "",
|
|
||||||
backwards: false,
|
|
||||||
wrap: true,
|
|
||||||
caseSensitive: false,
|
|
||||||
wholeWord: false,
|
|
||||||
regExp: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var repeat = function repeat(fn, count, args) {
|
var repeat = function repeat(fn, count, args) {
|
||||||
while (0 < count--)
|
while (0 < count--)
|
||||||
fn.apply(this, args);
|
fn.apply(this, args);
|
||||||
|
|
@ -88,7 +76,7 @@ var ensureScrollMargin = function(editor) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var actions = {
|
var actions = exports.actions = {
|
||||||
"z": {
|
"z": {
|
||||||
param: true,
|
param: true,
|
||||||
fn: function(editor, range, count, param) {
|
fn: function(editor, range, count, param) {
|
||||||
|
|
@ -288,16 +276,17 @@ var actions = {
|
||||||
},
|
},
|
||||||
":": {
|
":": {
|
||||||
fn: function(editor, range, count, param) {
|
fn: function(editor, range, count, param) {
|
||||||
editor.blur();
|
// not implemented
|
||||||
txtConsoleInput.focus();
|
|
||||||
txtConsoleInput.setValue(":");
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/": {
|
"/": {
|
||||||
fn: function(editor, range, count, param) {
|
fn: function(editor, range, count, param) {
|
||||||
editor.blur();
|
// not implemented
|
||||||
txtConsoleInput.focus();
|
}
|
||||||
txtConsoleInput.setValue("/");
|
},
|
||||||
|
"?": {
|
||||||
|
fn: function(editor, range, count, param) {
|
||||||
|
// not implemented
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
".": {
|
".": {
|
||||||
|
|
|
||||||
|
|
@ -72,11 +72,7 @@ var Gutter = function(parentEl) {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.removeGutterDecoration = function(row, className){
|
this.removeGutterDecoration = function(row, className){
|
||||||
this.$decorations[row] = this.$decorations[row].replace(" " + className, "");
|
this.$decorations[row] = (this.$decorations[row] || "").replace(" " + className, "");
|
||||||
};
|
|
||||||
|
|
||||||
this.setBreakpoints = function(rows) {
|
|
||||||
this.$breakpoints = rows.concat();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setAnnotations = function(annotations) {
|
this.setAnnotations = function(annotations) {
|
||||||
|
|
@ -116,6 +112,7 @@ var Gutter = function(parentEl) {
|
||||||
var fold = this.session.getNextFoldLine(i);
|
var fold = this.session.getNextFoldLine(i);
|
||||||
var foldStart = fold ? fold.start.row : Infinity;
|
var foldStart = fold ? fold.start.row : Infinity;
|
||||||
var foldWidgets = this.$showFoldWidgets && this.session.foldWidgets;
|
var foldWidgets = this.$showFoldWidgets && this.session.foldWidgets;
|
||||||
|
var breakpoints = this.session.$breakpoints;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if(i > foldStart) {
|
if(i > foldStart) {
|
||||||
|
|
@ -129,10 +126,10 @@ var Gutter = function(parentEl) {
|
||||||
var annotation = this.$annotations[i] || emptyAnno;
|
var annotation = this.$annotations[i] || emptyAnno;
|
||||||
html.push("<div class='ace_gutter-cell",
|
html.push("<div class='ace_gutter-cell",
|
||||||
this.$decorations[i] || "",
|
this.$decorations[i] || "",
|
||||||
this.$breakpoints[i] ? " ace_breakpoint " : " ",
|
breakpoints[i] ? " ace_breakpoint " : " ",
|
||||||
annotation.className,
|
annotation.className,
|
||||||
"' title='", annotation.text.join("\n"),
|
"' title='", annotation.text.join("\n"),
|
||||||
"' style='height:", config.lineHeight, "px;'>", (i+1));
|
"' style='height:", this.session.getRowLength(i) * config.lineHeight, "px;'>", (i+1));
|
||||||
|
|
||||||
if (foldWidgets) {
|
if (foldWidgets) {
|
||||||
var c = foldWidgets[i];
|
var c = foldWidgets[i];
|
||||||
|
|
@ -147,15 +144,18 @@ var Gutter = function(parentEl) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var wrappedRowLength = this.session.getRowLength(i) - 1;
|
|
||||||
while (wrappedRowLength--) {
|
|
||||||
html.push("</div><div class='ace_gutter-cell' style='height:", config.lineHeight, "px'>\xA6");
|
|
||||||
}
|
|
||||||
|
|
||||||
html.push("</div>");
|
html.push("</div>");
|
||||||
|
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.session.$useWrapMode)
|
||||||
|
html.push(
|
||||||
|
"<div class='ace_gutter-cell' style='pointer-events:none;opacity:0'>",
|
||||||
|
this.session.getLength() - 1,
|
||||||
|
"</div>"
|
||||||
|
);
|
||||||
|
|
||||||
this.element = dom.setInnerHtml(this.element, html.join(""));
|
this.element = dom.setInnerHtml(this.element, html.join(""));
|
||||||
this.element.style.height = config.minHeight + "px";
|
this.element.style.height = config.minHeight + "px";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,10 @@ var ClojureHighlightRules = function() {
|
||||||
}, {
|
}, {
|
||||||
token : "string", // single line
|
token : "string", // single line
|
||||||
regex : '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'
|
regex : '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'
|
||||||
|
}, {
|
||||||
|
token : "string", // multi line
|
||||||
|
regex : '["](?:(?:\\\\.)|(?:[^"\\\\]))*?$',
|
||||||
|
next: "string"
|
||||||
}, {
|
}, {
|
||||||
token : "string", // symbol
|
token : "string", // symbol
|
||||||
regex : "[:](?:[a-zA-Z]|\\d)+"
|
regex : "[:](?:[a-zA-Z]|\\d)+"
|
||||||
|
|
@ -210,6 +214,17 @@ var ClojureHighlightRules = function() {
|
||||||
merge : true,
|
merge : true,
|
||||||
regex : ".+"
|
regex : ".+"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"string" : [
|
||||||
|
{
|
||||||
|
token : "string",
|
||||||
|
merge : true,
|
||||||
|
regex : "\\\\."
|
||||||
|
}, {
|
||||||
|
token : "string",
|
||||||
|
regex : '[^"\\\\]*?"',
|
||||||
|
next : "start"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ var CSharpHighlightRules = function() {
|
||||||
token : "comment",
|
token : "comment",
|
||||||
regex : "\\/\\/.*$"
|
regex : "\\/\\/.*$"
|
||||||
},
|
},
|
||||||
new DocCommentHighlightRules().getStartRule("doc-start"),
|
DocCommentHighlightRules.getStartRule("doc-start"),
|
||||||
{
|
{
|
||||||
token : "comment", // multi line comment
|
token : "comment", // multi line comment
|
||||||
regex : "\\/\\*",
|
regex : "\\/\\*",
|
||||||
|
|
@ -95,7 +95,7 @@ var CSharpHighlightRules = function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.embedRules(DocCommentHighlightRules, "doc-",
|
this.embedRules(DocCommentHighlightRules, "doc-",
|
||||||
[ new DocCommentHighlightRules().getEndRule("start") ]);
|
[ DocCommentHighlightRules.getEndRule("start") ]);
|
||||||
};
|
};
|
||||||
|
|
||||||
oop.inherits(CSharpHighlightRules, TextHighlightRules);
|
oop.inherits(CSharpHighlightRules, TextHighlightRules);
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,9 @@ define(function(require, exports, module) {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.embedRules(DocCommentHighlightRules, "doc-",
|
||||||
|
[ DocCommentHighlightRules.getEndRule("start") ]);
|
||||||
}
|
}
|
||||||
oop.inherits(GolangHighlightRules, TextHighlightRules);
|
oop.inherits(GolangHighlightRules, TextHighlightRules);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ function GutterHandler(mouseHandler) {
|
||||||
}
|
}
|
||||||
|
|
||||||
mouseHandler.captureMouse(e, "selectByLines");
|
mouseHandler.captureMouse(e, "selectByLines");
|
||||||
|
return e.preventDefault();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,16 +42,12 @@ define(function(require, exports, module) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var dom = require("../lib/dom");
|
var dom = require("../lib/dom");
|
||||||
var BrowserFocus = require("../lib/browser_focus").BrowserFocus;
|
var useragent = require("../lib/useragent");
|
||||||
|
|
||||||
|
|
||||||
var DRAG_OFFSET = 5; // pixels
|
var DRAG_OFFSET = 5; // pixels
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function DefaultHandlers(mouseHandler) {
|
function DefaultHandlers(mouseHandler) {
|
||||||
mouseHandler.$clickSelection = null;
|
mouseHandler.$clickSelection = null;
|
||||||
mouseHandler.browserFocus = new BrowserFocus();
|
|
||||||
|
|
||||||
var editor = mouseHandler.editor;
|
var editor = mouseHandler.editor;
|
||||||
editor.setDefaultHandler("mousedown", this.onMouseDown.bind(mouseHandler));
|
editor.setDefaultHandler("mousedown", this.onMouseDown.bind(mouseHandler));
|
||||||
|
|
@ -69,14 +65,16 @@ function DefaultHandlers(mouseHandler) {
|
||||||
|
|
||||||
mouseHandler.selectByLines = this.extendSelectionBy.bind(mouseHandler, "getLineRange");
|
mouseHandler.selectByLines = this.extendSelectionBy.bind(mouseHandler, "getLineRange");
|
||||||
mouseHandler.selectByWords = this.extendSelectionBy.bind(mouseHandler, "getWordRange");
|
mouseHandler.selectByWords = this.extendSelectionBy.bind(mouseHandler, "getWordRange");
|
||||||
|
|
||||||
|
mouseHandler.$focusWaitTimout = 250;
|
||||||
}
|
}
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
|
|
||||||
this.onMouseDown = function(ev) {
|
this.onMouseDown = function(ev) {
|
||||||
this.mousedownEvent = ev;
|
|
||||||
var inSelection = ev.inSelection();
|
var inSelection = ev.inSelection();
|
||||||
var pos = ev.getDocumentPosition();
|
var pos = ev.getDocumentPosition();
|
||||||
|
this.mousedownEvent = ev;
|
||||||
var editor = this.editor;
|
var editor = this.editor;
|
||||||
var _self = this;
|
var _self = this;
|
||||||
|
|
||||||
|
|
@ -99,7 +97,10 @@ function DefaultHandlers(mouseHandler) {
|
||||||
// selection
|
// selection
|
||||||
if (inSelection && !editor.isFocused()) {
|
if (inSelection && !editor.isFocused()) {
|
||||||
editor.focus();
|
editor.focus();
|
||||||
return;
|
if (this.$focusWaitTimout && !this.$clickSelection) {
|
||||||
|
// todo start select after focusWaitTimout passes
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inSelection || this.$clickSelection || ev.getShiftKey()) {
|
if (!inSelection || this.$clickSelection || ev.getShiftKey()) {
|
||||||
|
|
@ -116,7 +117,8 @@ function DefaultHandlers(mouseHandler) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.captureMouse(ev)
|
this.captureMouse(ev);
|
||||||
|
return ev.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.startSelect = function(pos) {
|
this.startSelect = function(pos) {
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,8 @@ var Range = require("./range").Range;
|
||||||
var Selection = require("./selection").Selection;
|
var Selection = require("./selection").Selection;
|
||||||
var onMouseDown = require("./mouse/multi_select_handler").onMouseDown;
|
var onMouseDown = require("./mouse/multi_select_handler").onMouseDown;
|
||||||
var event = require("./lib/event");
|
var event = require("./lib/event");
|
||||||
exports.commands = require("./commands/multi_select_commands");
|
var commands = require("./commands/multi_select_commands");
|
||||||
|
exports.commands = commands.defaultCommands.concat(commands.multiSelectCommands);
|
||||||
|
|
||||||
// Todo: session.find or editor.findVolatile that returns range
|
// Todo: session.find or editor.findVolatile that returns range
|
||||||
var Search = require("./search").Search;
|
var Search = require("./search").Search;
|
||||||
|
|
@ -397,7 +398,7 @@ var Editor = require("./editor").Editor;
|
||||||
this.inMultiSelectMode = true;
|
this.inMultiSelectMode = true;
|
||||||
|
|
||||||
this.setStyle("multiselect");
|
this.setStyle("multiselect");
|
||||||
this.keyBinding.addKeyboardHandler(exports.commands.keyboardHandler);
|
this.keyBinding.addKeyboardHandler(commands.keyboardHandler);
|
||||||
this.commands.on("exec", this.$onMultiSelectExec);
|
this.commands.on("exec", this.$onMultiSelectExec);
|
||||||
|
|
||||||
this.renderer.updateCursor();
|
this.renderer.updateCursor();
|
||||||
|
|
@ -410,7 +411,7 @@ var Editor = require("./editor").Editor;
|
||||||
this.inMultiSelectMode = false;
|
this.inMultiSelectMode = false;
|
||||||
|
|
||||||
this.unsetStyle("multiselect");
|
this.unsetStyle("multiselect");
|
||||||
this.keyBinding.removeKeyboardHandler(exports.commands.keyboardHandler);
|
this.keyBinding.removeKeyboardHandler(commands.keyboardHandler);
|
||||||
|
|
||||||
this.commands.removeEventListener("exec", this.$onMultiSelectExec);
|
this.commands.removeEventListener("exec", this.$onMultiSelectExec);
|
||||||
this.renderer.updateCursor();
|
this.renderer.updateCursor();
|
||||||
|
|
@ -420,6 +421,8 @@ var Editor = require("./editor").Editor;
|
||||||
this.$onMultiSelectExec = function(e) {
|
this.$onMultiSelectExec = function(e) {
|
||||||
var command = e.command;
|
var command = e.command;
|
||||||
var editor = e.editor;
|
var editor = e.editor;
|
||||||
|
if (!editor.multiSelect)
|
||||||
|
return;
|
||||||
if (!command.multiSelectAction) {
|
if (!command.multiSelectAction) {
|
||||||
command.exec(editor, e.args || {});
|
command.exec(editor, e.args || {});
|
||||||
editor.multiSelect.addRange(editor.multiSelect.toOrientedRange());
|
editor.multiSelect.addRange(editor.multiSelect.toOrientedRange());
|
||||||
|
|
@ -724,7 +727,7 @@ function MultiSelect(editor) {
|
||||||
editor.on("changeSession", exports.onSessionChange.bind(editor));
|
editor.on("changeSession", exports.onSessionChange.bind(editor));
|
||||||
|
|
||||||
editor.on("mousedown", onMouseDown);
|
editor.on("mousedown", onMouseDown);
|
||||||
editor.commands.addCommands(exports.commands.defaultCommands);
|
editor.commands.addCommands(commands.defaultCommands);
|
||||||
|
|
||||||
addAltCursorListeners(editor);
|
addAltCursorListeners(editor);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -236,8 +236,8 @@ var Selection = function(session) {
|
||||||
**/
|
**/
|
||||||
this.selectAll = function() {
|
this.selectAll = function() {
|
||||||
var lastRow = this.doc.getLength() - 1;
|
var lastRow = this.doc.getLength() - 1;
|
||||||
this.setSelectionAnchor(lastRow, this.doc.getLine(lastRow).length);
|
this.setSelectionAnchor(0, 0);
|
||||||
this.moveCursorTo(0, 0);
|
this.moveCursorTo(lastRow, this.doc.getLine(lastRow).length);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -248,6 +248,7 @@ var Selection = function(session) {
|
||||||
* Sets the selection to the provided range.
|
* Sets the selection to the provided range.
|
||||||
*
|
*
|
||||||
**/
|
**/
|
||||||
|
this.setRange =
|
||||||
this.setSelectionRange = function(range, reverse) {
|
this.setSelectionRange = function(range, reverse) {
|
||||||
if (reverse) {
|
if (reverse) {
|
||||||
this.setSelectionAnchor(range.end.row, range.end.column);
|
this.setSelectionAnchor(range.end.row, range.end.column);
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ var log = document.getElementById("log")
|
||||||
|
|
||||||
var testNames = [
|
var testNames = [
|
||||||
"ace/anchor_test",
|
"ace/anchor_test",
|
||||||
|
"ace/background_tokenizer_test",
|
||||||
"ace/commands/command_manager_test",
|
"ace/commands/command_manager_test",
|
||||||
"ace/document_test",
|
"ace/document_test",
|
||||||
"ace/edit_session_test",
|
"ace/edit_session_test",
|
||||||
|
|
@ -65,7 +66,11 @@ if (location.search)
|
||||||
testNames = location.search.substr(1).split(",")
|
testNames = location.search.substr(1).split(",")
|
||||||
|
|
||||||
require(testNames, function() {
|
require(testNames, function() {
|
||||||
var tests = testNames.map(require);
|
var tests = testNames.map(function(x) {
|
||||||
|
var module = require(x);
|
||||||
|
module.href = x;
|
||||||
|
return module;
|
||||||
|
});
|
||||||
|
|
||||||
async.list(tests)
|
async.list(tests)
|
||||||
.expand(function(test) {
|
.expand(function(test) {
|
||||||
|
|
@ -73,6 +78,12 @@ require(testNames, function() {
|
||||||
}, AsyncTest.TestGenerator)
|
}, AsyncTest.TestGenerator)
|
||||||
.run()
|
.run()
|
||||||
.each(function(test, next) {
|
.each(function(test, next) {
|
||||||
|
if (test.index == 1 && test.context.href) {
|
||||||
|
var href = test.context.href;
|
||||||
|
var node = document.createElement("div");
|
||||||
|
node.innerHTML = "<a href='?" + href + "'>" + href.replace(/^ace\//, "") + "</a>";
|
||||||
|
log.appendChild(node);
|
||||||
|
}
|
||||||
var node = document.createElement("div");
|
var node = document.createElement("div");
|
||||||
node.className = test.passed ? "passed" : "failed";
|
node.className = test.passed ? "passed" : "failed";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ MockRenderer.prototype.updateBackMarkers = function() {
|
||||||
MockRenderer.prototype.updateFrontMarkers = function() {
|
MockRenderer.prototype.updateFrontMarkers = function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
MockRenderer.prototype.setBreakpoints = function() {
|
MockRenderer.prototype.updateBreakpoints = function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
MockRenderer.prototype.onResize = function() {
|
MockRenderer.prototype.onResize = function() {
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,6 @@ var VirtualRenderer = function(container, theme) {
|
||||||
this.content.className = "ace_content";
|
this.content.className = "ace_content";
|
||||||
this.scroller.appendChild(this.content);
|
this.scroller.appendChild(this.content);
|
||||||
|
|
||||||
this.setHighlightGutterLine(true);
|
|
||||||
this.$gutterLayer = new GutterLayer(this.$gutter);
|
this.$gutterLayer = new GutterLayer(this.$gutter);
|
||||||
this.$gutterLayer.on("changeGutterWidth", this.onResize.bind(this, true));
|
this.$gutterLayer.on("changeGutterWidth", this.onResize.bind(this, true));
|
||||||
this.setFadeFoldWidgets(true);
|
this.setFadeFoldWidgets(true);
|
||||||
|
|
@ -449,31 +448,38 @@ var VirtualRenderer = function(container, theme) {
|
||||||
dom.removeCssClass(this.$gutter, "ace_fade-fold-widgets");
|
dom.removeCssClass(this.$gutter, "ace_fade-fold-widgets");
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$highlightGutterLine = false;
|
this.$highlightGutterLine = true;
|
||||||
this.setHighlightGutterLine = function(shouldHighlight) {
|
this.setHighlightGutterLine = function(shouldHighlight) {
|
||||||
if (this.$highlightGutterLine == shouldHighlight)
|
if (this.$highlightGutterLine == shouldHighlight)
|
||||||
return;
|
return;
|
||||||
this.$highlightGutterLine = shouldHighlight;
|
this.$highlightGutterLine = shouldHighlight;
|
||||||
|
|
||||||
|
this.$loop.schedule(this.CHANGE_GUTTER);
|
||||||
if (!this.$gutterLineHighlight) {
|
|
||||||
this.$gutterLineHighlight = dom.createElement("div");
|
|
||||||
this.$gutterLineHighlight.className = "ace_gutter_active_line";
|
|
||||||
this.$gutter.appendChild(this.$gutterLineHighlight);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$gutterLineHighlight.style.display = shouldHighlight ? "" : "none";
|
|
||||||
this.$updateGutterLineHighlight();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getHighlightGutterLine = function() {
|
this.getHighlightGutterLine = function() {
|
||||||
return this.$highlightGutterLine;
|
return this.$highlightGutterLine;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$updateGutterLineHighlight = function() {
|
this.$updateGutterLineHighlight = function(gutterReady) {
|
||||||
this.$gutterLineHighlight.style.top = this.$cursorLayer.$pixelPos.top - this.layerConfig.offset + "px";
|
var i = this.session.selection.lead.row;
|
||||||
this.$gutterLineHighlight.style.height = this.layerConfig.lineHeight + "px";
|
if (i == this.$gutterLineHighlight)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!gutterReady) {
|
||||||
|
var ch = this.$gutterLayer.element.children
|
||||||
|
var oldEl = ch[this.$gutterLineHighlight - this.layerConfig.firstRow];
|
||||||
|
if (oldEl)
|
||||||
|
dom.removeCssClass(oldEl, "ace_gutter_active_line");
|
||||||
|
|
||||||
|
var newEl = ch[i - this.layerConfig.firstRow];
|
||||||
|
if (newEl)
|
||||||
|
dom.addCssClass(newEl, "ace_gutter_active_line");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$gutterLayer.removeGutterDecoration(this.$gutterLineHighlight, "ace_gutter_active_line");
|
||||||
|
this.$gutterLayer.addGutterDecoration(i, "ace_gutter_active_line");
|
||||||
|
this.$gutterLineHighlight = i;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$updatePrintMargin = function() {
|
this.$updatePrintMargin = function() {
|
||||||
|
|
@ -667,13 +673,15 @@ var VirtualRenderer = function(container, theme) {
|
||||||
// update scrollbar first to not lose scroll position when gutter calls resize
|
// update scrollbar first to not lose scroll position when gutter calls resize
|
||||||
this.$updateScrollBar();
|
this.$updateScrollBar();
|
||||||
this.$textLayer.update(this.layerConfig);
|
this.$textLayer.update(this.layerConfig);
|
||||||
if (this.showGutter)
|
if (this.showGutter) {
|
||||||
|
if (this.$highlightGutterLine)
|
||||||
|
this.$updateGutterLineHighlight(true);
|
||||||
this.$gutterLayer.update(this.layerConfig);
|
this.$gutterLayer.update(this.layerConfig);
|
||||||
|
}
|
||||||
this.$markerBack.update(this.layerConfig);
|
this.$markerBack.update(this.layerConfig);
|
||||||
this.$markerFront.update(this.layerConfig);
|
this.$markerFront.update(this.layerConfig);
|
||||||
this.$cursorLayer.update(this.layerConfig);
|
this.$cursorLayer.update(this.layerConfig);
|
||||||
this.$moveTextAreaToCursor();
|
this.$moveTextAreaToCursor();
|
||||||
this.$highlightGutterLine && this.$updateGutterLineHighlight();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -685,13 +693,15 @@ var VirtualRenderer = function(container, theme) {
|
||||||
else
|
else
|
||||||
this.$textLayer.scrollLines(this.layerConfig);
|
this.$textLayer.scrollLines(this.layerConfig);
|
||||||
|
|
||||||
if (this.showGutter)
|
if (this.showGutter) {
|
||||||
|
if (this.$highlightGutterLine)
|
||||||
|
this.$updateGutterLineHighlight(true);
|
||||||
this.$gutterLayer.update(this.layerConfig);
|
this.$gutterLayer.update(this.layerConfig);
|
||||||
|
}
|
||||||
this.$markerBack.update(this.layerConfig);
|
this.$markerBack.update(this.layerConfig);
|
||||||
this.$markerFront.update(this.layerConfig);
|
this.$markerFront.update(this.layerConfig);
|
||||||
this.$cursorLayer.update(this.layerConfig);
|
this.$cursorLayer.update(this.layerConfig);
|
||||||
this.$moveTextAreaToCursor();
|
this.$moveTextAreaToCursor();
|
||||||
this.$highlightGutterLine && this.$updateGutterLineHighlight();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -714,7 +724,7 @@ var VirtualRenderer = function(container, theme) {
|
||||||
if (changes & this.CHANGE_CURSOR) {
|
if (changes & this.CHANGE_CURSOR) {
|
||||||
this.$cursorLayer.update(this.layerConfig);
|
this.$cursorLayer.update(this.layerConfig);
|
||||||
this.$moveTextAreaToCursor();
|
this.$moveTextAreaToCursor();
|
||||||
this.$highlightGutterLine && this.$updateGutterLineHighlight();
|
this.$highlightGutterLine && this.$updateGutterLineHighlight(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_FRONT)) {
|
if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_FRONT)) {
|
||||||
|
|
@ -810,10 +820,6 @@ var VirtualRenderer = function(container, theme) {
|
||||||
|
|
||||||
var layerConfig = this.layerConfig;
|
var layerConfig = this.layerConfig;
|
||||||
|
|
||||||
// if the update changes the width of the document do a full redraw
|
|
||||||
if (layerConfig.width != this.$getLongestLine())
|
|
||||||
return this.$textLayer.update(layerConfig);
|
|
||||||
|
|
||||||
if (firstRow > layerConfig.lastRow + 1) { return; }
|
if (firstRow > layerConfig.lastRow + 1) { return; }
|
||||||
if (lastRow < layerConfig.firstRow) { return; }
|
if (lastRow < layerConfig.firstRow) { return; }
|
||||||
|
|
||||||
|
|
@ -883,13 +889,11 @@ var VirtualRenderer = function(container, theme) {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VirtualRenderer.setBreakpoints(rows) -> Void
|
* VirtualRenderer.updateBreakpoints() -> Void
|
||||||
* - rows (Array): An array containg row numbers
|
|
||||||
*
|
*
|
||||||
* Sets a breakpoint for every row number indicated on `rows`.
|
* Redraw breakpoints.
|
||||||
**/
|
**/
|
||||||
this.setBreakpoints = function(rows) {
|
this.updateBreakpoints = function(rows) {
|
||||||
this.$gutterLayer.setBreakpoints(rows);
|
|
||||||
this.$loop.schedule(this.CHANGE_GUTTER);
|
this.$loop.schedule(this.CHANGE_GUTTER);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1081,12 +1085,14 @@ var VirtualRenderer = function(container, theme) {
|
||||||
_self.session.setScrollTop(steps.shift());
|
_self.session.setScrollTop(steps.shift());
|
||||||
// trick session to think it's already scrolled to not loose toValue
|
// trick session to think it's already scrolled to not loose toValue
|
||||||
_self.session.$scrollTop = toValue;
|
_self.session.$scrollTop = toValue;
|
||||||
} else {
|
} else if (toValue != null) {
|
||||||
_self.$inScrollAnimation = false;
|
|
||||||
clearInterval(_self.$timer);
|
|
||||||
|
|
||||||
_self.session.$scrollTop = -1;
|
_self.session.$scrollTop = -1;
|
||||||
_self.session.setScrollTop(toValue);
|
_self.session.setScrollTop(toValue);
|
||||||
|
toValue = null;
|
||||||
|
} else {
|
||||||
|
// do this on separate step to not get spurious scroll event from scrollbar
|
||||||
|
_self.$timer = clearInterval(_self.$timer);
|
||||||
|
_self.$inScrollAnimation = false;
|
||||||
callback && callback();
|
callback && callback();
|
||||||
}
|
}
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue