
Previously, we were at an ACE editor published between 1.1.8 and 1.1.9. This caused multiple issues and was especially a problem for the upcoming pair programming feature. Further, updating ace is a long-time priority, see https://github.com/openHPI/codeocean/issues/250. Now, we are not yet updating to the latest version, but rather to the next minor version. This already contains breaking changes, and we are currently interested to keep the number of changes as low as possible. Further updating ACE might be still a future task. The new ACE version 1.2.0 is taken from this tag: https://github.com/ajaxorg/ace-builds/releases/tag/v1.2.0. We are using the src build (not minified, not in the noconflict version), since the same was used before as well. Further, we need to change our migration for storing editor events. Since the table is not yet used (in production), we also update the enum.
1190 lines
41 KiB
JavaScript
1190 lines
41 KiB
JavaScript
define("ace/snippets",["require","exports","module","ace/lib/oop","ace/lib/event_emitter","ace/lib/lang","ace/range","ace/anchor","ace/keyboard/hash_handler","ace/tokenizer","ace/lib/dom","ace/editor"], function(require, exports, module) {
|
|
"use strict";
|
|
var oop = require("./lib/oop");
|
|
var EventEmitter = require("./lib/event_emitter").EventEmitter;
|
|
var lang = require("./lib/lang");
|
|
var Range = require("./range").Range;
|
|
var Anchor = require("./anchor").Anchor;
|
|
var HashHandler = require("./keyboard/hash_handler").HashHandler;
|
|
var Tokenizer = require("./tokenizer").Tokenizer;
|
|
var comparePoints = Range.comparePoints;
|
|
|
|
var SnippetManager = function() {
|
|
this.snippetMap = {};
|
|
this.snippetNameMap = {};
|
|
};
|
|
|
|
(function() {
|
|
oop.implement(this, EventEmitter);
|
|
|
|
this.getTokenizer = function() {
|
|
function TabstopToken(str, _, stack) {
|
|
str = str.substr(1);
|
|
if (/^\d+$/.test(str) && !stack.inFormatString)
|
|
return [{tabstopId: parseInt(str, 10)}];
|
|
return [{text: str}];
|
|
}
|
|
function escape(ch) {
|
|
return "(?:[^\\\\" + ch + "]|\\\\.)";
|
|
}
|
|
SnippetManager.$tokenizer = new Tokenizer({
|
|
start: [
|
|
{regex: /:/, onMatch: function(val, state, stack) {
|
|
if (stack.length && stack[0].expectIf) {
|
|
stack[0].expectIf = false;
|
|
stack[0].elseBranch = stack[0];
|
|
return [stack[0]];
|
|
}
|
|
return ":";
|
|
}},
|
|
{regex: /\\./, onMatch: function(val, state, stack) {
|
|
var ch = val[1];
|
|
if (ch == "}" && stack.length) {
|
|
val = ch;
|
|
}else if ("`$\\".indexOf(ch) != -1) {
|
|
val = ch;
|
|
} else if (stack.inFormatString) {
|
|
if (ch == "n")
|
|
val = "\n";
|
|
else if (ch == "t")
|
|
val = "\n";
|
|
else if ("ulULE".indexOf(ch) != -1) {
|
|
val = {changeCase: ch, local: ch > "a"};
|
|
}
|
|
}
|
|
|
|
return [val];
|
|
}},
|
|
{regex: /}/, onMatch: function(val, state, stack) {
|
|
return [stack.length ? stack.shift() : val];
|
|
}},
|
|
{regex: /\$(?:\d+|\w+)/, onMatch: TabstopToken},
|
|
{regex: /\$\{[\dA-Z_a-z]+/, onMatch: function(str, state, stack) {
|
|
var t = TabstopToken(str.substr(1), state, stack);
|
|
stack.unshift(t[0]);
|
|
return t;
|
|
}, next: "snippetVar"},
|
|
{regex: /\n/, token: "newline", merge: false}
|
|
],
|
|
snippetVar: [
|
|
{regex: "\\|" + escape("\\|") + "*\\|", onMatch: function(val, state, stack) {
|
|
stack[0].choices = val.slice(1, -1).split(",");
|
|
}, next: "start"},
|
|
{regex: "/(" + escape("/") + "+)/(?:(" + escape("/") + "*)/)(\\w*):?",
|
|
onMatch: function(val, state, stack) {
|
|
var ts = stack[0];
|
|
ts.fmtString = val;
|
|
|
|
val = this.splitRegex.exec(val);
|
|
ts.guard = val[1];
|
|
ts.fmt = val[2];
|
|
ts.flag = val[3];
|
|
return "";
|
|
}, next: "start"},
|
|
{regex: "`" + escape("`") + "*`", onMatch: function(val, state, stack) {
|
|
stack[0].code = val.splice(1, -1);
|
|
return "";
|
|
}, next: "start"},
|
|
{regex: "\\?", onMatch: function(val, state, stack) {
|
|
if (stack[0])
|
|
stack[0].expectIf = true;
|
|
}, next: "start"},
|
|
{regex: "([^:}\\\\]|\\\\.)*:?", token: "", next: "start"}
|
|
],
|
|
formatString: [
|
|
{regex: "/(" + escape("/") + "+)/", token: "regex"},
|
|
{regex: "", onMatch: function(val, state, stack) {
|
|
stack.inFormatString = true;
|
|
}, next: "start"}
|
|
]
|
|
});
|
|
SnippetManager.prototype.getTokenizer = function() {
|
|
return SnippetManager.$tokenizer;
|
|
};
|
|
return SnippetManager.$tokenizer;
|
|
};
|
|
|
|
this.tokenizeTmSnippet = function(str, startState) {
|
|
return this.getTokenizer().getLineTokens(str, startState).tokens.map(function(x) {
|
|
return x.value || x;
|
|
});
|
|
};
|
|
|
|
this.$getDefaultValue = function(editor, name) {
|
|
if (/^[A-Z]\d+$/.test(name)) {
|
|
var i = name.substr(1);
|
|
return (this.variables[name[0] + "__"] || {})[i];
|
|
}
|
|
if (/^\d+$/.test(name)) {
|
|
return (this.variables.__ || {})[name];
|
|
}
|
|
name = name.replace(/^TM_/, "");
|
|
|
|
if (!editor)
|
|
return;
|
|
var s = editor.session;
|
|
switch(name) {
|
|
case "CURRENT_WORD":
|
|
var r = s.getWordRange();
|
|
case "SELECTION":
|
|
case "SELECTED_TEXT":
|
|
return s.getTextRange(r);
|
|
case "CURRENT_LINE":
|
|
return s.getLine(editor.getCursorPosition().row);
|
|
case "PREV_LINE": // not possible in textmate
|
|
return s.getLine(editor.getCursorPosition().row - 1);
|
|
case "LINE_INDEX":
|
|
return editor.getCursorPosition().column;
|
|
case "LINE_NUMBER":
|
|
return editor.getCursorPosition().row + 1;
|
|
case "SOFT_TABS":
|
|
return s.getUseSoftTabs() ? "YES" : "NO";
|
|
case "TAB_SIZE":
|
|
return s.getTabSize();
|
|
case "FILENAME":
|
|
case "FILEPATH":
|
|
return "";
|
|
case "FULLNAME":
|
|
return "Ace";
|
|
}
|
|
};
|
|
this.variables = {};
|
|
this.getVariableValue = function(editor, varName) {
|
|
if (this.variables.hasOwnProperty(varName))
|
|
return this.variables[varName](editor, varName) || "";
|
|
return this.$getDefaultValue(editor, varName) || "";
|
|
};
|
|
this.tmStrFormat = function(str, ch, editor) {
|
|
var flag = ch.flag || "";
|
|
var re = ch.guard;
|
|
re = new RegExp(re, flag.replace(/[^gi]/, ""));
|
|
var fmtTokens = this.tokenizeTmSnippet(ch.fmt, "formatString");
|
|
var _self = this;
|
|
var formatted = str.replace(re, function() {
|
|
_self.variables.__ = arguments;
|
|
var fmtParts = _self.resolveVariables(fmtTokens, editor);
|
|
var gChangeCase = "E";
|
|
for (var i = 0; i < fmtParts.length; i++) {
|
|
var ch = fmtParts[i];
|
|
if (typeof ch == "object") {
|
|
fmtParts[i] = "";
|
|
if (ch.changeCase && ch.local) {
|
|
var next = fmtParts[i + 1];
|
|
if (next && typeof next == "string") {
|
|
if (ch.changeCase == "u")
|
|
fmtParts[i] = next[0].toUpperCase();
|
|
else
|
|
fmtParts[i] = next[0].toLowerCase();
|
|
fmtParts[i + 1] = next.substr(1);
|
|
}
|
|
} else if (ch.changeCase) {
|
|
gChangeCase = ch.changeCase;
|
|
}
|
|
} else if (gChangeCase == "U") {
|
|
fmtParts[i] = ch.toUpperCase();
|
|
} else if (gChangeCase == "L") {
|
|
fmtParts[i] = ch.toLowerCase();
|
|
}
|
|
}
|
|
return fmtParts.join("");
|
|
});
|
|
this.variables.__ = null;
|
|
return formatted;
|
|
};
|
|
|
|
this.resolveVariables = function(snippet, editor) {
|
|
var result = [];
|
|
for (var i = 0; i < snippet.length; i++) {
|
|
var ch = snippet[i];
|
|
if (typeof ch == "string") {
|
|
result.push(ch);
|
|
} else if (typeof ch != "object") {
|
|
continue;
|
|
} else if (ch.skip) {
|
|
gotoNext(ch);
|
|
} else if (ch.processed < i) {
|
|
continue;
|
|
} else if (ch.text) {
|
|
var value = this.getVariableValue(editor, ch.text);
|
|
if (value && ch.fmtString)
|
|
value = this.tmStrFormat(value, ch);
|
|
ch.processed = i;
|
|
if (ch.expectIf == null) {
|
|
if (value) {
|
|
result.push(value);
|
|
gotoNext(ch);
|
|
}
|
|
} else {
|
|
if (value) {
|
|
ch.skip = ch.elseBranch;
|
|
} else
|
|
gotoNext(ch);
|
|
}
|
|
} else if (ch.tabstopId != null) {
|
|
result.push(ch);
|
|
} else if (ch.changeCase != null) {
|
|
result.push(ch);
|
|
}
|
|
}
|
|
function gotoNext(ch) {
|
|
var i1 = snippet.indexOf(ch, i + 1);
|
|
if (i1 != -1)
|
|
i = i1;
|
|
}
|
|
return result;
|
|
};
|
|
|
|
this.insertSnippetForSelection = function(editor, snippetText) {
|
|
var cursor = editor.getCursorPosition();
|
|
var line = editor.session.getLine(cursor.row);
|
|
var tabString = editor.session.getTabString();
|
|
var indentString = line.match(/^\s*/)[0];
|
|
|
|
if (cursor.column < indentString.length)
|
|
indentString = indentString.slice(0, cursor.column);
|
|
|
|
var tokens = this.tokenizeTmSnippet(snippetText);
|
|
tokens = this.resolveVariables(tokens, editor);
|
|
tokens = tokens.map(function(x) {
|
|
if (x == "\n")
|
|
return x + indentString;
|
|
if (typeof x == "string")
|
|
return x.replace(/\t/g, tabString);
|
|
return x;
|
|
});
|
|
var tabstops = [];
|
|
tokens.forEach(function(p, i) {
|
|
if (typeof p != "object")
|
|
return;
|
|
var id = p.tabstopId;
|
|
var ts = tabstops[id];
|
|
if (!ts) {
|
|
ts = tabstops[id] = [];
|
|
ts.index = id;
|
|
ts.value = "";
|
|
}
|
|
if (ts.indexOf(p) !== -1)
|
|
return;
|
|
ts.push(p);
|
|
var i1 = tokens.indexOf(p, i + 1);
|
|
if (i1 === -1)
|
|
return;
|
|
|
|
var value = tokens.slice(i + 1, i1);
|
|
var isNested = value.some(function(t) {return typeof t === "object"});
|
|
if (isNested && !ts.value) {
|
|
ts.value = value;
|
|
} else if (value.length && (!ts.value || typeof ts.value !== "string")) {
|
|
ts.value = value.join("");
|
|
}
|
|
});
|
|
tabstops.forEach(function(ts) {ts.length = 0});
|
|
var expanding = {};
|
|
function copyValue(val) {
|
|
var copy = [];
|
|
for (var i = 0; i < val.length; i++) {
|
|
var p = val[i];
|
|
if (typeof p == "object") {
|
|
if (expanding[p.tabstopId])
|
|
continue;
|
|
var j = val.lastIndexOf(p, i - 1);
|
|
p = copy[j] || {tabstopId: p.tabstopId};
|
|
}
|
|
copy[i] = p;
|
|
}
|
|
return copy;
|
|
}
|
|
for (var i = 0; i < tokens.length; i++) {
|
|
var p = tokens[i];
|
|
if (typeof p != "object")
|
|
continue;
|
|
var id = p.tabstopId;
|
|
var i1 = tokens.indexOf(p, i + 1);
|
|
if (expanding[id]) {
|
|
if (expanding[id] === p)
|
|
expanding[id] = null;
|
|
continue;
|
|
}
|
|
|
|
var ts = tabstops[id];
|
|
var arg = typeof ts.value == "string" ? [ts.value] : copyValue(ts.value);
|
|
arg.unshift(i + 1, Math.max(0, i1 - i));
|
|
arg.push(p);
|
|
expanding[id] = p;
|
|
tokens.splice.apply(tokens, arg);
|
|
|
|
if (ts.indexOf(p) === -1)
|
|
ts.push(p);
|
|
}
|
|
var row = 0, column = 0;
|
|
var text = "";
|
|
tokens.forEach(function(t) {
|
|
if (typeof t === "string") {
|
|
if (t[0] === "\n"){
|
|
column = t.length - 1;
|
|
row ++;
|
|
} else
|
|
column += t.length;
|
|
text += t;
|
|
} else {
|
|
if (!t.start)
|
|
t.start = {row: row, column: column};
|
|
else
|
|
t.end = {row: row, column: column};
|
|
}
|
|
});
|
|
var range = editor.getSelectionRange();
|
|
var end = editor.session.replace(range, text);
|
|
|
|
var tabstopManager = new TabstopManager(editor);
|
|
var selectionId = editor.inVirtualSelectionMode && editor.selection.index;
|
|
tabstopManager.addTabstops(tabstops, range.start, end, selectionId);
|
|
};
|
|
|
|
this.insertSnippet = function(editor, snippetText) {
|
|
var self = this;
|
|
if (editor.inVirtualSelectionMode)
|
|
return self.insertSnippetForSelection(editor, snippetText);
|
|
|
|
editor.forEachSelection(function() {
|
|
self.insertSnippetForSelection(editor, snippetText);
|
|
}, null, {keepOrder: true});
|
|
|
|
if (editor.tabstopManager)
|
|
editor.tabstopManager.tabNext();
|
|
};
|
|
|
|
this.$getScope = function(editor) {
|
|
var scope = editor.session.$mode.$id || "";
|
|
scope = scope.split("/").pop();
|
|
if (scope === "html" || scope === "php") {
|
|
if (scope === "php" && !editor.session.$mode.inlinePhp)
|
|
scope = "html";
|
|
var c = editor.getCursorPosition();
|
|
var state = editor.session.getState(c.row);
|
|
if (typeof state === "object") {
|
|
state = state[0];
|
|
}
|
|
if (state.substring) {
|
|
if (state.substring(0, 3) == "js-")
|
|
scope = "javascript";
|
|
else if (state.substring(0, 4) == "css-")
|
|
scope = "css";
|
|
else if (state.substring(0, 4) == "php-")
|
|
scope = "php";
|
|
}
|
|
}
|
|
|
|
return scope;
|
|
};
|
|
|
|
this.getActiveScopes = function(editor) {
|
|
var scope = this.$getScope(editor);
|
|
var scopes = [scope];
|
|
var snippetMap = this.snippetMap;
|
|
if (snippetMap[scope] && snippetMap[scope].includeScopes) {
|
|
scopes.push.apply(scopes, snippetMap[scope].includeScopes);
|
|
}
|
|
scopes.push("_");
|
|
return scopes;
|
|
};
|
|
|
|
this.expandWithTab = function(editor, options) {
|
|
var self = this;
|
|
var result = editor.forEachSelection(function() {
|
|
return self.expandSnippetForSelection(editor, options);
|
|
}, null, {keepOrder: true});
|
|
if (result && editor.tabstopManager)
|
|
editor.tabstopManager.tabNext();
|
|
return result;
|
|
};
|
|
|
|
this.expandSnippetForSelection = function(editor, options) {
|
|
var cursor = editor.getCursorPosition();
|
|
var line = editor.session.getLine(cursor.row);
|
|
var before = line.substring(0, cursor.column);
|
|
var after = line.substr(cursor.column);
|
|
|
|
var snippetMap = this.snippetMap;
|
|
var snippet;
|
|
this.getActiveScopes(editor).some(function(scope) {
|
|
var snippets = snippetMap[scope];
|
|
if (snippets)
|
|
snippet = this.findMatchingSnippet(snippets, before, after);
|
|
return !!snippet;
|
|
}, this);
|
|
if (!snippet)
|
|
return false;
|
|
if (options && options.dryRun)
|
|
return true;
|
|
editor.session.doc.removeInLine(cursor.row,
|
|
cursor.column - snippet.replaceBefore.length,
|
|
cursor.column + snippet.replaceAfter.length
|
|
);
|
|
|
|
this.variables.M__ = snippet.matchBefore;
|
|
this.variables.T__ = snippet.matchAfter;
|
|
this.insertSnippetForSelection(editor, snippet.content);
|
|
|
|
this.variables.M__ = this.variables.T__ = null;
|
|
return true;
|
|
};
|
|
|
|
this.findMatchingSnippet = function(snippetList, before, after) {
|
|
for (var i = snippetList.length; i--;) {
|
|
var s = snippetList[i];
|
|
if (s.startRe && !s.startRe.test(before))
|
|
continue;
|
|
if (s.endRe && !s.endRe.test(after))
|
|
continue;
|
|
if (!s.startRe && !s.endRe)
|
|
continue;
|
|
|
|
s.matchBefore = s.startRe ? s.startRe.exec(before) : [""];
|
|
s.matchAfter = s.endRe ? s.endRe.exec(after) : [""];
|
|
s.replaceBefore = s.triggerRe ? s.triggerRe.exec(before)[0] : "";
|
|
s.replaceAfter = s.endTriggerRe ? s.endTriggerRe.exec(after)[0] : "";
|
|
return s;
|
|
}
|
|
};
|
|
|
|
this.snippetMap = {};
|
|
this.snippetNameMap = {};
|
|
this.register = function(snippets, scope) {
|
|
var snippetMap = this.snippetMap;
|
|
var snippetNameMap = this.snippetNameMap;
|
|
var self = this;
|
|
|
|
if (!snippets)
|
|
snippets = [];
|
|
|
|
function wrapRegexp(src) {
|
|
if (src && !/^\^?\(.*\)\$?$|^\\b$/.test(src))
|
|
src = "(?:" + src + ")";
|
|
|
|
return src || "";
|
|
}
|
|
function guardedRegexp(re, guard, opening) {
|
|
re = wrapRegexp(re);
|
|
guard = wrapRegexp(guard);
|
|
if (opening) {
|
|
re = guard + re;
|
|
if (re && re[re.length - 1] != "$")
|
|
re = re + "$";
|
|
} else {
|
|
re = re + guard;
|
|
if (re && re[0] != "^")
|
|
re = "^" + re;
|
|
}
|
|
return new RegExp(re);
|
|
}
|
|
|
|
function addSnippet(s) {
|
|
if (!s.scope)
|
|
s.scope = scope || "_";
|
|
scope = s.scope;
|
|
if (!snippetMap[scope]) {
|
|
snippetMap[scope] = [];
|
|
snippetNameMap[scope] = {};
|
|
}
|
|
|
|
var map = snippetNameMap[scope];
|
|
if (s.name) {
|
|
var old = map[s.name];
|
|
if (old)
|
|
self.unregister(old);
|
|
map[s.name] = s;
|
|
}
|
|
snippetMap[scope].push(s);
|
|
|
|
if (s.tabTrigger && !s.trigger) {
|
|
if (!s.guard && /^\w/.test(s.tabTrigger))
|
|
s.guard = "\\b";
|
|
s.trigger = lang.escapeRegExp(s.tabTrigger);
|
|
}
|
|
|
|
s.startRe = guardedRegexp(s.trigger, s.guard, true);
|
|
s.triggerRe = new RegExp(s.trigger, "", true);
|
|
|
|
s.endRe = guardedRegexp(s.endTrigger, s.endGuard, true);
|
|
s.endTriggerRe = new RegExp(s.endTrigger, "", true);
|
|
}
|
|
|
|
if (snippets && snippets.content)
|
|
addSnippet(snippets);
|
|
else if (Array.isArray(snippets))
|
|
snippets.forEach(addSnippet);
|
|
|
|
this._signal("registerSnippets", {scope: scope});
|
|
};
|
|
this.unregister = function(snippets, scope) {
|
|
var snippetMap = this.snippetMap;
|
|
var snippetNameMap = this.snippetNameMap;
|
|
|
|
function removeSnippet(s) {
|
|
var nameMap = snippetNameMap[s.scope||scope];
|
|
if (nameMap && nameMap[s.name]) {
|
|
delete nameMap[s.name];
|
|
var map = snippetMap[s.scope||scope];
|
|
var i = map && map.indexOf(s);
|
|
if (i >= 0)
|
|
map.splice(i, 1);
|
|
}
|
|
}
|
|
if (snippets.content)
|
|
removeSnippet(snippets);
|
|
else if (Array.isArray(snippets))
|
|
snippets.forEach(removeSnippet);
|
|
};
|
|
this.parseSnippetFile = function(str) {
|
|
str = str.replace(/\r/g, "");
|
|
var list = [], snippet = {};
|
|
var re = /^#.*|^({[\s\S]*})\s*$|^(\S+) (.*)$|^((?:\n*\t.*)+)/gm;
|
|
var m;
|
|
while (m = re.exec(str)) {
|
|
if (m[1]) {
|
|
try {
|
|
snippet = JSON.parse(m[1]);
|
|
list.push(snippet);
|
|
} catch (e) {}
|
|
} if (m[4]) {
|
|
snippet.content = m[4].replace(/^\t/gm, "");
|
|
list.push(snippet);
|
|
snippet = {};
|
|
} else {
|
|
var key = m[2], val = m[3];
|
|
if (key == "regex") {
|
|
var guardRe = /\/((?:[^\/\\]|\\.)*)|$/g;
|
|
snippet.guard = guardRe.exec(val)[1];
|
|
snippet.trigger = guardRe.exec(val)[1];
|
|
snippet.endTrigger = guardRe.exec(val)[1];
|
|
snippet.endGuard = guardRe.exec(val)[1];
|
|
} else if (key == "snippet") {
|
|
snippet.tabTrigger = val.match(/^\S*/)[0];
|
|
if (!snippet.name)
|
|
snippet.name = val;
|
|
} else {
|
|
snippet[key] = val;
|
|
}
|
|
}
|
|
}
|
|
return list;
|
|
};
|
|
this.getSnippetByName = function(name, editor) {
|
|
var snippetMap = this.snippetNameMap;
|
|
var snippet;
|
|
this.getActiveScopes(editor).some(function(scope) {
|
|
var snippets = snippetMap[scope];
|
|
if (snippets)
|
|
snippet = snippets[name];
|
|
return !!snippet;
|
|
}, this);
|
|
return snippet;
|
|
};
|
|
|
|
}).call(SnippetManager.prototype);
|
|
|
|
|
|
var TabstopManager = function(editor) {
|
|
if (editor.tabstopManager)
|
|
return editor.tabstopManager;
|
|
editor.tabstopManager = this;
|
|
this.$onChange = this.onChange.bind(this);
|
|
this.$onChangeSelection = lang.delayedCall(this.onChangeSelection.bind(this)).schedule;
|
|
this.$onChangeSession = this.onChangeSession.bind(this);
|
|
this.$onAfterExec = this.onAfterExec.bind(this);
|
|
this.attach(editor);
|
|
};
|
|
(function() {
|
|
this.attach = function(editor) {
|
|
this.index = 0;
|
|
this.ranges = [];
|
|
this.tabstops = [];
|
|
this.$openTabstops = null;
|
|
this.selectedTabstop = null;
|
|
|
|
this.editor = editor;
|
|
this.editor.on("change", this.$onChange);
|
|
this.editor.on("changeSelection", this.$onChangeSelection);
|
|
this.editor.on("changeSession", this.$onChangeSession);
|
|
this.editor.commands.on("afterExec", this.$onAfterExec);
|
|
this.editor.keyBinding.addKeyboardHandler(this.keyboardHandler);
|
|
};
|
|
this.detach = function() {
|
|
this.tabstops.forEach(this.removeTabstopMarkers, this);
|
|
this.ranges = null;
|
|
this.tabstops = null;
|
|
this.selectedTabstop = null;
|
|
this.editor.removeListener("change", this.$onChange);
|
|
this.editor.removeListener("changeSelection", this.$onChangeSelection);
|
|
this.editor.removeListener("changeSession", this.$onChangeSession);
|
|
this.editor.commands.removeListener("afterExec", this.$onAfterExec);
|
|
this.editor.keyBinding.removeKeyboardHandler(this.keyboardHandler);
|
|
this.editor.tabstopManager = null;
|
|
this.editor = null;
|
|
};
|
|
|
|
this.onChange = function(delta) {
|
|
var changeRange = delta;
|
|
var isRemove = delta.action[0] == "r";
|
|
var start = delta.start;
|
|
var end = delta.end;
|
|
var startRow = start.row;
|
|
var endRow = end.row;
|
|
var lineDif = endRow - startRow;
|
|
var colDiff = end.column - start.column;
|
|
|
|
if (isRemove) {
|
|
lineDif = -lineDif;
|
|
colDiff = -colDiff;
|
|
}
|
|
if (!this.$inChange && isRemove) {
|
|
var ts = this.selectedTabstop;
|
|
var changedOutside = ts && !ts.some(function(r) {
|
|
return comparePoints(r.start, start) <= 0 && comparePoints(r.end, end) >= 0;
|
|
});
|
|
if (changedOutside)
|
|
return this.detach();
|
|
}
|
|
var ranges = this.ranges;
|
|
for (var i = 0; i < ranges.length; i++) {
|
|
var r = ranges[i];
|
|
if (r.end.row < start.row)
|
|
continue;
|
|
|
|
if (isRemove && comparePoints(start, r.start) < 0 && comparePoints(end, r.end) > 0) {
|
|
this.removeRange(r);
|
|
i--;
|
|
continue;
|
|
}
|
|
|
|
if (r.start.row == startRow && r.start.column > start.column)
|
|
r.start.column += colDiff;
|
|
if (r.end.row == startRow && r.end.column >= start.column)
|
|
r.end.column += colDiff;
|
|
if (r.start.row >= startRow)
|
|
r.start.row += lineDif;
|
|
if (r.end.row >= startRow)
|
|
r.end.row += lineDif;
|
|
|
|
if (comparePoints(r.start, r.end) > 0)
|
|
this.removeRange(r);
|
|
}
|
|
if (!ranges.length)
|
|
this.detach();
|
|
};
|
|
this.updateLinkedFields = function() {
|
|
var ts = this.selectedTabstop;
|
|
if (!ts || !ts.hasLinkedRanges)
|
|
return;
|
|
this.$inChange = true;
|
|
var session = this.editor.session;
|
|
var text = session.getTextRange(ts.firstNonLinked);
|
|
for (var i = ts.length; i--;) {
|
|
var range = ts[i];
|
|
if (!range.linked)
|
|
continue;
|
|
var fmt = exports.snippetManager.tmStrFormat(text, range.original);
|
|
session.replace(range, fmt);
|
|
}
|
|
this.$inChange = false;
|
|
};
|
|
this.onAfterExec = function(e) {
|
|
if (e.command && !e.command.readOnly)
|
|
this.updateLinkedFields();
|
|
};
|
|
this.onChangeSelection = function() {
|
|
if (!this.editor)
|
|
return;
|
|
var lead = this.editor.selection.lead;
|
|
var anchor = this.editor.selection.anchor;
|
|
var isEmpty = this.editor.selection.isEmpty();
|
|
for (var i = this.ranges.length; i--;) {
|
|
if (this.ranges[i].linked)
|
|
continue;
|
|
var containsLead = this.ranges[i].contains(lead.row, lead.column);
|
|
var containsAnchor = isEmpty || this.ranges[i].contains(anchor.row, anchor.column);
|
|
if (containsLead && containsAnchor)
|
|
return;
|
|
}
|
|
this.detach();
|
|
};
|
|
this.onChangeSession = function() {
|
|
this.detach();
|
|
};
|
|
this.tabNext = function(dir) {
|
|
var max = this.tabstops.length;
|
|
var index = this.index + (dir || 1);
|
|
index = Math.min(Math.max(index, 1), max);
|
|
if (index == max)
|
|
index = 0;
|
|
this.selectTabstop(index);
|
|
if (index === 0)
|
|
this.detach();
|
|
};
|
|
this.selectTabstop = function(index) {
|
|
this.$openTabstops = null;
|
|
var ts = this.tabstops[this.index];
|
|
if (ts)
|
|
this.addTabstopMarkers(ts);
|
|
this.index = index;
|
|
ts = this.tabstops[this.index];
|
|
if (!ts || !ts.length)
|
|
return;
|
|
|
|
this.selectedTabstop = ts;
|
|
if (!this.editor.inVirtualSelectionMode) {
|
|
var sel = this.editor.multiSelect;
|
|
sel.toSingleRange(ts.firstNonLinked.clone());
|
|
for (var i = ts.length; i--;) {
|
|
if (ts.hasLinkedRanges && ts[i].linked)
|
|
continue;
|
|
sel.addRange(ts[i].clone(), true);
|
|
}
|
|
if (sel.ranges[0])
|
|
sel.addRange(sel.ranges[0].clone());
|
|
} else {
|
|
this.editor.selection.setRange(ts.firstNonLinked);
|
|
}
|
|
|
|
this.editor.keyBinding.addKeyboardHandler(this.keyboardHandler);
|
|
};
|
|
this.addTabstops = function(tabstops, start, end) {
|
|
if (!this.$openTabstops)
|
|
this.$openTabstops = [];
|
|
if (!tabstops[0]) {
|
|
var p = Range.fromPoints(end, end);
|
|
moveRelative(p.start, start);
|
|
moveRelative(p.end, start);
|
|
tabstops[0] = [p];
|
|
tabstops[0].index = 0;
|
|
}
|
|
|
|
var i = this.index;
|
|
var arg = [i + 1, 0];
|
|
var ranges = this.ranges;
|
|
tabstops.forEach(function(ts, index) {
|
|
var dest = this.$openTabstops[index] || ts;
|
|
|
|
for (var i = ts.length; i--;) {
|
|
var p = ts[i];
|
|
var range = Range.fromPoints(p.start, p.end || p.start);
|
|
movePoint(range.start, start);
|
|
movePoint(range.end, start);
|
|
range.original = p;
|
|
range.tabstop = dest;
|
|
ranges.push(range);
|
|
if (dest != ts)
|
|
dest.unshift(range);
|
|
else
|
|
dest[i] = range;
|
|
if (p.fmtString) {
|
|
range.linked = true;
|
|
dest.hasLinkedRanges = true;
|
|
} else if (!dest.firstNonLinked)
|
|
dest.firstNonLinked = range;
|
|
}
|
|
if (!dest.firstNonLinked)
|
|
dest.hasLinkedRanges = false;
|
|
if (dest === ts) {
|
|
arg.push(dest);
|
|
this.$openTabstops[index] = dest;
|
|
}
|
|
this.addTabstopMarkers(dest);
|
|
}, this);
|
|
|
|
if (arg.length > 2) {
|
|
if (this.tabstops.length)
|
|
arg.push(arg.splice(2, 1)[0]);
|
|
this.tabstops.splice.apply(this.tabstops, arg);
|
|
}
|
|
};
|
|
|
|
this.addTabstopMarkers = function(ts) {
|
|
var session = this.editor.session;
|
|
ts.forEach(function(range) {
|
|
if (!range.markerId)
|
|
range.markerId = session.addMarker(range, "ace_snippet-marker", "text");
|
|
});
|
|
};
|
|
this.removeTabstopMarkers = function(ts) {
|
|
var session = this.editor.session;
|
|
ts.forEach(function(range) {
|
|
session.removeMarker(range.markerId);
|
|
range.markerId = null;
|
|
});
|
|
};
|
|
this.removeRange = function(range) {
|
|
var i = range.tabstop.indexOf(range);
|
|
range.tabstop.splice(i, 1);
|
|
i = this.ranges.indexOf(range);
|
|
this.ranges.splice(i, 1);
|
|
this.editor.session.removeMarker(range.markerId);
|
|
if (!range.tabstop.length) {
|
|
i = this.tabstops.indexOf(range.tabstop);
|
|
if (i != -1)
|
|
this.tabstops.splice(i, 1);
|
|
if (!this.tabstops.length)
|
|
this.detach();
|
|
}
|
|
};
|
|
|
|
this.keyboardHandler = new HashHandler();
|
|
this.keyboardHandler.bindKeys({
|
|
"Tab": function(ed) {
|
|
if (exports.snippetManager && exports.snippetManager.expandWithTab(ed)) {
|
|
return;
|
|
}
|
|
|
|
ed.tabstopManager.tabNext(1);
|
|
},
|
|
"Shift-Tab": function(ed) {
|
|
ed.tabstopManager.tabNext(-1);
|
|
},
|
|
"Esc": function(ed) {
|
|
ed.tabstopManager.detach();
|
|
},
|
|
"Return": function(ed) {
|
|
return false;
|
|
}
|
|
});
|
|
}).call(TabstopManager.prototype);
|
|
|
|
|
|
|
|
var changeTracker = {};
|
|
changeTracker.onChange = Anchor.prototype.onChange;
|
|
changeTracker.setPosition = function(row, column) {
|
|
this.pos.row = row;
|
|
this.pos.column = column;
|
|
};
|
|
changeTracker.update = function(pos, delta, $insertRight) {
|
|
this.$insertRight = $insertRight;
|
|
this.pos = pos;
|
|
this.onChange(delta);
|
|
};
|
|
|
|
var movePoint = function(point, diff) {
|
|
if (point.row == 0)
|
|
point.column += diff.column;
|
|
point.row += diff.row;
|
|
};
|
|
|
|
var moveRelative = function(point, start) {
|
|
if (point.row == start.row)
|
|
point.column -= start.column;
|
|
point.row -= start.row;
|
|
};
|
|
|
|
|
|
require("./lib/dom").importCssString("\
|
|
.ace_snippet-marker {\
|
|
-moz-box-sizing: border-box;\
|
|
box-sizing: border-box;\
|
|
background: rgba(194, 193, 208, 0.09);\
|
|
border: 1px dotted rgba(211, 208, 235, 0.62);\
|
|
position: absolute;\
|
|
}");
|
|
|
|
exports.snippetManager = new SnippetManager();
|
|
|
|
|
|
var Editor = require("./editor").Editor;
|
|
(function() {
|
|
this.insertSnippet = function(content, options) {
|
|
return exports.snippetManager.insertSnippet(this, content, options);
|
|
};
|
|
this.expandSnippet = function(options) {
|
|
return exports.snippetManager.expandWithTab(this, options);
|
|
};
|
|
}).call(Editor.prototype);
|
|
|
|
});
|
|
|
|
define("ace/ext/emmet",["require","exports","module","ace/keyboard/hash_handler","ace/editor","ace/snippets","ace/range","resources","resources","range","tabStops","resources","utils","actions","ace/config","ace/config"], function(require, exports, module) {
|
|
"use strict";
|
|
var HashHandler = require("ace/keyboard/hash_handler").HashHandler;
|
|
var Editor = require("ace/editor").Editor;
|
|
var snippetManager = require("ace/snippets").snippetManager;
|
|
var Range = require("ace/range").Range;
|
|
var emmet, emmetPath;
|
|
function AceEmmetEditor() {}
|
|
|
|
AceEmmetEditor.prototype = {
|
|
setupContext: function(editor) {
|
|
this.ace = editor;
|
|
this.indentation = editor.session.getTabString();
|
|
if (!emmet)
|
|
emmet = window.emmet;
|
|
emmet.require("resources").setVariable("indentation", this.indentation);
|
|
this.$syntax = null;
|
|
this.$syntax = this.getSyntax();
|
|
},
|
|
getSelectionRange: function() {
|
|
var range = this.ace.getSelectionRange();
|
|
var doc = this.ace.session.doc;
|
|
return {
|
|
start: doc.positionToIndex(range.start),
|
|
end: doc.positionToIndex(range.end)
|
|
};
|
|
},
|
|
createSelection: function(start, end) {
|
|
var doc = this.ace.session.doc;
|
|
this.ace.selection.setRange({
|
|
start: doc.indexToPosition(start),
|
|
end: doc.indexToPosition(end)
|
|
});
|
|
},
|
|
getCurrentLineRange: function() {
|
|
var ace = this.ace;
|
|
var row = ace.getCursorPosition().row;
|
|
var lineLength = ace.session.getLine(row).length;
|
|
var index = ace.session.doc.positionToIndex({row: row, column: 0});
|
|
return {
|
|
start: index,
|
|
end: index + lineLength
|
|
};
|
|
},
|
|
getCaretPos: function(){
|
|
var pos = this.ace.getCursorPosition();
|
|
return this.ace.session.doc.positionToIndex(pos);
|
|
},
|
|
setCaretPos: function(index){
|
|
var pos = this.ace.session.doc.indexToPosition(index);
|
|
this.ace.selection.moveToPosition(pos);
|
|
},
|
|
getCurrentLine: function() {
|
|
var row = this.ace.getCursorPosition().row;
|
|
return this.ace.session.getLine(row);
|
|
},
|
|
replaceContent: function(value, start, end, noIndent) {
|
|
if (end == null)
|
|
end = start == null ? this.getContent().length : start;
|
|
if (start == null)
|
|
start = 0;
|
|
|
|
var editor = this.ace;
|
|
var doc = editor.session.doc;
|
|
var range = Range.fromPoints(doc.indexToPosition(start), doc.indexToPosition(end));
|
|
editor.session.remove(range);
|
|
|
|
range.end = range.start;
|
|
|
|
value = this.$updateTabstops(value);
|
|
snippetManager.insertSnippet(editor, value);
|
|
},
|
|
getContent: function(){
|
|
return this.ace.getValue();
|
|
},
|
|
getSyntax: function() {
|
|
if (this.$syntax)
|
|
return this.$syntax;
|
|
var syntax = this.ace.session.$modeId.split("/").pop();
|
|
if (syntax == "html" || syntax == "php") {
|
|
var cursor = this.ace.getCursorPosition();
|
|
var state = this.ace.session.getState(cursor.row);
|
|
if (typeof state != "string")
|
|
state = state[0];
|
|
if (state) {
|
|
state = state.split("-");
|
|
if (state.length > 1)
|
|
syntax = state[0];
|
|
else if (syntax == "php")
|
|
syntax = "html";
|
|
}
|
|
}
|
|
return syntax;
|
|
},
|
|
getProfileName: function() {
|
|
switch(this.getSyntax()) {
|
|
case "css": return "css";
|
|
case "xml":
|
|
case "xsl":
|
|
return "xml";
|
|
case "html":
|
|
var profile = emmet.require("resources").getVariable("profile");
|
|
if (!profile)
|
|
profile = this.ace.session.getLines(0,2).join("").search(/<!DOCTYPE[^>]+XHTML/i) != -1 ? "xhtml": "html";
|
|
return profile;
|
|
}
|
|
return "xhtml";
|
|
},
|
|
prompt: function(title) {
|
|
return prompt(title);
|
|
},
|
|
getSelection: function() {
|
|
return this.ace.session.getTextRange();
|
|
},
|
|
getFilePath: function() {
|
|
return "";
|
|
},
|
|
$updateTabstops: function(value) {
|
|
var base = 1000;
|
|
var zeroBase = 0;
|
|
var lastZero = null;
|
|
var range = emmet.require('range');
|
|
var ts = emmet.require('tabStops');
|
|
var settings = emmet.require('resources').getVocabulary("user");
|
|
var tabstopOptions = {
|
|
tabstop: function(data) {
|
|
var group = parseInt(data.group, 10);
|
|
var isZero = group === 0;
|
|
if (isZero)
|
|
group = ++zeroBase;
|
|
else
|
|
group += base;
|
|
|
|
var placeholder = data.placeholder;
|
|
if (placeholder) {
|
|
placeholder = ts.processText(placeholder, tabstopOptions);
|
|
}
|
|
|
|
var result = '${' + group + (placeholder ? ':' + placeholder : '') + '}';
|
|
|
|
if (isZero) {
|
|
lastZero = range.create(data.start, result);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
escape: function(ch) {
|
|
if (ch == '$') return '\\$';
|
|
if (ch == '\\') return '\\\\';
|
|
return ch;
|
|
}
|
|
};
|
|
|
|
value = ts.processText(value, tabstopOptions);
|
|
|
|
if (settings.variables['insert_final_tabstop'] && !/\$\{0\}$/.test(value)) {
|
|
value += '${0}';
|
|
} else if (lastZero) {
|
|
value = emmet.require('utils').replaceSubstring(value, '${0}', lastZero);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
};
|
|
|
|
|
|
var keymap = {
|
|
expand_abbreviation: {"mac": "ctrl+alt+e", "win": "alt+e"},
|
|
match_pair_outward: {"mac": "ctrl+d", "win": "ctrl+,"},
|
|
match_pair_inward: {"mac": "ctrl+j", "win": "ctrl+shift+0"},
|
|
matching_pair: {"mac": "ctrl+alt+j", "win": "alt+j"},
|
|
next_edit_point: "alt+right",
|
|
prev_edit_point: "alt+left",
|
|
toggle_comment: {"mac": "command+/", "win": "ctrl+/"},
|
|
split_join_tag: {"mac": "shift+command+'", "win": "shift+ctrl+`"},
|
|
remove_tag: {"mac": "command+'", "win": "shift+ctrl+;"},
|
|
evaluate_math_expression: {"mac": "shift+command+y", "win": "shift+ctrl+y"},
|
|
increment_number_by_1: "ctrl+up",
|
|
decrement_number_by_1: "ctrl+down",
|
|
increment_number_by_01: "alt+up",
|
|
decrement_number_by_01: "alt+down",
|
|
increment_number_by_10: {"mac": "alt+command+up", "win": "shift+alt+up"},
|
|
decrement_number_by_10: {"mac": "alt+command+down", "win": "shift+alt+down"},
|
|
select_next_item: {"mac": "shift+command+.", "win": "shift+ctrl+."},
|
|
select_previous_item: {"mac": "shift+command+,", "win": "shift+ctrl+,"},
|
|
reflect_css_value: {"mac": "shift+command+r", "win": "shift+ctrl+r"},
|
|
|
|
encode_decode_data_url: {"mac": "shift+ctrl+d", "win": "ctrl+'"},
|
|
expand_abbreviation_with_tab: "Tab",
|
|
wrap_with_abbreviation: {"mac": "shift+ctrl+a", "win": "shift+ctrl+a"}
|
|
};
|
|
|
|
var editorProxy = new AceEmmetEditor();
|
|
exports.commands = new HashHandler();
|
|
exports.runEmmetCommand = function(editor) {
|
|
try {
|
|
editorProxy.setupContext(editor);
|
|
if (editorProxy.getSyntax() == "php")
|
|
return false;
|
|
var actions = emmet.require("actions");
|
|
|
|
if (this.action == "expand_abbreviation_with_tab") {
|
|
if (!editor.selection.isEmpty())
|
|
return false;
|
|
}
|
|
|
|
if (this.action == "wrap_with_abbreviation") {
|
|
return setTimeout(function() {
|
|
actions.run("wrap_with_abbreviation", editorProxy);
|
|
}, 0);
|
|
}
|
|
|
|
var pos = editor.selection.lead;
|
|
var token = editor.session.getTokenAt(pos.row, pos.column);
|
|
if (token && /\btag\b/.test(token.type))
|
|
return false;
|
|
|
|
var result = actions.run(this.action, editorProxy);
|
|
} catch(e) {
|
|
editor._signal("changeStatus", typeof e == "string" ? e : e.message);
|
|
console.log(e);
|
|
result = false;
|
|
}
|
|
return result;
|
|
};
|
|
|
|
for (var command in keymap) {
|
|
exports.commands.addCommand({
|
|
name: "emmet:" + command,
|
|
action: command,
|
|
bindKey: keymap[command],
|
|
exec: exports.runEmmetCommand,
|
|
multiSelectAction: "forEach"
|
|
});
|
|
}
|
|
|
|
exports.updateCommands = function(editor, enabled) {
|
|
if (enabled) {
|
|
editor.keyBinding.addKeyboardHandler(exports.commands);
|
|
} else {
|
|
editor.keyBinding.removeKeyboardHandler(exports.commands);
|
|
}
|
|
};
|
|
|
|
exports.isSupportedMode = function(modeId) {
|
|
return modeId && /css|less|scss|sass|stylus|html|php|twig|ejs|handlebars/.test(modeId);
|
|
};
|
|
|
|
var onChangeMode = function(e, target) {
|
|
var editor = target;
|
|
if (!editor)
|
|
return;
|
|
var enabled = exports.isSupportedMode(editor.session.$modeId);
|
|
if (e.enableEmmet === false)
|
|
enabled = false;
|
|
if (enabled) {
|
|
if (typeof emmetPath == "string") {
|
|
require("ace/config").loadModule(emmetPath, function() {
|
|
emmetPath = null;
|
|
});
|
|
}
|
|
}
|
|
exports.updateCommands(editor, enabled);
|
|
};
|
|
|
|
exports.AceEmmetEditor = AceEmmetEditor;
|
|
require("ace/config").defineOptions(Editor.prototype, "editor", {
|
|
enableEmmet: {
|
|
set: function(val) {
|
|
this[val ? "on" : "removeListener"]("changeMode", onChangeMode);
|
|
onChangeMode({enableEmmet: !!val}, this);
|
|
},
|
|
value: true
|
|
}
|
|
});
|
|
|
|
exports.setCore = function(e) {
|
|
if (typeof e == "string")
|
|
emmetPath = e;
|
|
else
|
|
emmet = e;
|
|
};
|
|
});
|
|
(function() {
|
|
window.require(["ace/ext/emmet"], function() {});
|
|
})();
|
|
|