monaco-editor-core
Version:
A browser based code editor
889 lines (888 loc) • 30.1 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class Scanner {
constructor() {
this.value = '';
this.pos = 0;
}
static { this._table = {
[36 /* CharCode.DollarSign */]: 0 /* TokenType.Dollar */,
[58 /* CharCode.Colon */]: 1 /* TokenType.Colon */,
[44 /* CharCode.Comma */]: 2 /* TokenType.Comma */,
[123 /* CharCode.OpenCurlyBrace */]: 3 /* TokenType.CurlyOpen */,
[125 /* CharCode.CloseCurlyBrace */]: 4 /* TokenType.CurlyClose */,
[92 /* CharCode.Backslash */]: 5 /* TokenType.Backslash */,
[47 /* CharCode.Slash */]: 6 /* TokenType.Forwardslash */,
[124 /* CharCode.Pipe */]: 7 /* TokenType.Pipe */,
[43 /* CharCode.Plus */]: 11 /* TokenType.Plus */,
[45 /* CharCode.Dash */]: 12 /* TokenType.Dash */,
[63 /* CharCode.QuestionMark */]: 13 /* TokenType.QuestionMark */,
}; }
static isDigitCharacter(ch) {
return ch >= 48 /* CharCode.Digit0 */ && ch <= 57 /* CharCode.Digit9 */;
}
static isVariableCharacter(ch) {
return ch === 95 /* CharCode.Underline */
|| (ch >= 97 /* CharCode.a */ && ch <= 122 /* CharCode.z */)
|| (ch >= 65 /* CharCode.A */ && ch <= 90 /* CharCode.Z */);
}
text(value) {
this.value = value;
this.pos = 0;
}
tokenText(token) {
return this.value.substr(token.pos, token.len);
}
next() {
if (this.pos >= this.value.length) {
return { type: 14 /* TokenType.EOF */, pos: this.pos, len: 0 };
}
const pos = this.pos;
let len = 0;
let ch = this.value.charCodeAt(pos);
let type;
// static types
type = Scanner._table[ch];
if (typeof type === 'number') {
this.pos += 1;
return { type, pos, len: 1 };
}
// number
if (Scanner.isDigitCharacter(ch)) {
type = 8 /* TokenType.Int */;
do {
len += 1;
ch = this.value.charCodeAt(pos + len);
} while (Scanner.isDigitCharacter(ch));
this.pos += len;
return { type, pos, len };
}
// variable name
if (Scanner.isVariableCharacter(ch)) {
type = 9 /* TokenType.VariableName */;
do {
ch = this.value.charCodeAt(pos + (++len));
} while (Scanner.isVariableCharacter(ch) || Scanner.isDigitCharacter(ch));
this.pos += len;
return { type, pos, len };
}
// format
type = 10 /* TokenType.Format */;
do {
len += 1;
ch = this.value.charCodeAt(pos + len);
} while (!isNaN(ch)
&& typeof Scanner._table[ch] === 'undefined' // not static token
&& !Scanner.isDigitCharacter(ch) // not number
&& !Scanner.isVariableCharacter(ch) // not variable
);
this.pos += len;
return { type, pos, len };
}
}
export class Marker {
constructor() {
this._children = [];
}
appendChild(child) {
if (child instanceof Text && this._children[this._children.length - 1] instanceof Text) {
// this and previous child are text -> merge them
this._children[this._children.length - 1].value += child.value;
}
else {
// normal adoption of child
child.parent = this;
this._children.push(child);
}
return this;
}
replace(child, others) {
const { parent } = child;
const idx = parent.children.indexOf(child);
const newChildren = parent.children.slice(0);
newChildren.splice(idx, 1, ...others);
parent._children = newChildren;
(function _fixParent(children, parent) {
for (const child of children) {
child.parent = parent;
_fixParent(child.children, child);
}
})(others, parent);
}
get children() {
return this._children;
}
get rightMostDescendant() {
if (this._children.length > 0) {
return this._children[this._children.length - 1].rightMostDescendant;
}
return this;
}
get snippet() {
let candidate = this;
while (true) {
if (!candidate) {
return undefined;
}
if (candidate instanceof TextmateSnippet) {
return candidate;
}
candidate = candidate.parent;
}
}
toString() {
return this.children.reduce((prev, cur) => prev + cur.toString(), '');
}
len() {
return 0;
}
}
export class Text extends Marker {
constructor(value) {
super();
this.value = value;
}
toString() {
return this.value;
}
len() {
return this.value.length;
}
clone() {
return new Text(this.value);
}
}
export class TransformableMarker extends Marker {
}
export class Placeholder extends TransformableMarker {
static compareByIndex(a, b) {
if (a.index === b.index) {
return 0;
}
else if (a.isFinalTabstop) {
return 1;
}
else if (b.isFinalTabstop) {
return -1;
}
else if (a.index < b.index) {
return -1;
}
else if (a.index > b.index) {
return 1;
}
else {
return 0;
}
}
constructor(index) {
super();
this.index = index;
}
get isFinalTabstop() {
return this.index === 0;
}
get choice() {
return this._children.length === 1 && this._children[0] instanceof Choice
? this._children[0]
: undefined;
}
clone() {
const ret = new Placeholder(this.index);
if (this.transform) {
ret.transform = this.transform.clone();
}
ret._children = this.children.map(child => child.clone());
return ret;
}
}
export class Choice extends Marker {
constructor() {
super(...arguments);
this.options = [];
}
appendChild(marker) {
if (marker instanceof Text) {
marker.parent = this;
this.options.push(marker);
}
return this;
}
toString() {
return this.options[0].value;
}
len() {
return this.options[0].len();
}
clone() {
const ret = new Choice();
this.options.forEach(ret.appendChild, ret);
return ret;
}
}
export class Transform extends Marker {
constructor() {
super(...arguments);
this.regexp = new RegExp('');
}
resolve(value) {
const _this = this;
let didMatch = false;
let ret = value.replace(this.regexp, function () {
didMatch = true;
return _this._replace(Array.prototype.slice.call(arguments, 0, -2));
});
// when the regex didn't match and when the transform has
// else branches, then run those
if (!didMatch && this._children.some(child => child instanceof FormatString && Boolean(child.elseValue))) {
ret = this._replace([]);
}
return ret;
}
_replace(groups) {
let ret = '';
for (const marker of this._children) {
if (marker instanceof FormatString) {
let value = groups[marker.index] || '';
value = marker.resolve(value);
ret += value;
}
else {
ret += marker.toString();
}
}
return ret;
}
toString() {
return '';
}
clone() {
const ret = new Transform();
ret.regexp = new RegExp(this.regexp.source, '' + (this.regexp.ignoreCase ? 'i' : '') + (this.regexp.global ? 'g' : ''));
ret._children = this.children.map(child => child.clone());
return ret;
}
}
export class FormatString extends Marker {
constructor(index, shorthandName, ifValue, elseValue) {
super();
this.index = index;
this.shorthandName = shorthandName;
this.ifValue = ifValue;
this.elseValue = elseValue;
}
resolve(value) {
if (this.shorthandName === 'upcase') {
return !value ? '' : value.toLocaleUpperCase();
}
else if (this.shorthandName === 'downcase') {
return !value ? '' : value.toLocaleLowerCase();
}
else if (this.shorthandName === 'capitalize') {
return !value ? '' : (value[0].toLocaleUpperCase() + value.substr(1));
}
else if (this.shorthandName === 'pascalcase') {
return !value ? '' : this._toPascalCase(value);
}
else if (this.shorthandName === 'camelcase') {
return !value ? '' : this._toCamelCase(value);
}
else if (Boolean(value) && typeof this.ifValue === 'string') {
return this.ifValue;
}
else if (!Boolean(value) && typeof this.elseValue === 'string') {
return this.elseValue;
}
else {
return value || '';
}
}
_toPascalCase(value) {
const match = value.match(/[a-z0-9]+/gi);
if (!match) {
return value;
}
return match.map(word => {
return word.charAt(0).toUpperCase() + word.substr(1);
})
.join('');
}
_toCamelCase(value) {
const match = value.match(/[a-z0-9]+/gi);
if (!match) {
return value;
}
return match.map((word, index) => {
if (index === 0) {
return word.charAt(0).toLowerCase() + word.substr(1);
}
return word.charAt(0).toUpperCase() + word.substr(1);
})
.join('');
}
clone() {
const ret = new FormatString(this.index, this.shorthandName, this.ifValue, this.elseValue);
return ret;
}
}
export class Variable extends TransformableMarker {
constructor(name) {
super();
this.name = name;
}
resolve(resolver) {
let value = resolver.resolve(this);
if (this.transform) {
value = this.transform.resolve(value || '');
}
if (value !== undefined) {
this._children = [new Text(value)];
return true;
}
return false;
}
clone() {
const ret = new Variable(this.name);
if (this.transform) {
ret.transform = this.transform.clone();
}
ret._children = this.children.map(child => child.clone());
return ret;
}
}
function walk(marker, visitor) {
const stack = [...marker];
while (stack.length > 0) {
const marker = stack.shift();
const recurse = visitor(marker);
if (!recurse) {
break;
}
stack.unshift(...marker.children);
}
}
export class TextmateSnippet extends Marker {
get placeholderInfo() {
if (!this._placeholders) {
// fill in placeholders
const all = [];
let last;
this.walk(function (candidate) {
if (candidate instanceof Placeholder) {
all.push(candidate);
last = !last || last.index < candidate.index ? candidate : last;
}
return true;
});
this._placeholders = { all, last };
}
return this._placeholders;
}
get placeholders() {
const { all } = this.placeholderInfo;
return all;
}
offset(marker) {
let pos = 0;
let found = false;
this.walk(candidate => {
if (candidate === marker) {
found = true;
return false;
}
pos += candidate.len();
return true;
});
if (!found) {
return -1;
}
return pos;
}
fullLen(marker) {
let ret = 0;
walk([marker], marker => {
ret += marker.len();
return true;
});
return ret;
}
enclosingPlaceholders(placeholder) {
const ret = [];
let { parent } = placeholder;
while (parent) {
if (parent instanceof Placeholder) {
ret.push(parent);
}
parent = parent.parent;
}
return ret;
}
resolveVariables(resolver) {
this.walk(candidate => {
if (candidate instanceof Variable) {
if (candidate.resolve(resolver)) {
this._placeholders = undefined;
}
}
return true;
});
return this;
}
appendChild(child) {
this._placeholders = undefined;
return super.appendChild(child);
}
replace(child, others) {
this._placeholders = undefined;
return super.replace(child, others);
}
clone() {
const ret = new TextmateSnippet();
this._children = this.children.map(child => child.clone());
return ret;
}
walk(visitor) {
walk(this.children, visitor);
}
}
export class SnippetParser {
constructor() {
this._scanner = new Scanner();
this._token = { type: 14 /* TokenType.EOF */, pos: 0, len: 0 };
}
static escape(value) {
return value.replace(/\$|}|\\/g, '\\$&');
}
static guessNeedsClipboard(template) {
return /\${?CLIPBOARD/.test(template);
}
parse(value, insertFinalTabstop, enforceFinalTabstop) {
const snippet = new TextmateSnippet();
this.parseFragment(value, snippet);
this.ensureFinalTabstop(snippet, enforceFinalTabstop ?? false, insertFinalTabstop ?? false);
return snippet;
}
parseFragment(value, snippet) {
const offset = snippet.children.length;
this._scanner.text(value);
this._token = this._scanner.next();
while (this._parse(snippet)) {
// nothing
}
// fill in values for placeholders. the first placeholder of an index
// that has a value defines the value for all placeholders with that index
const placeholderDefaultValues = new Map();
const incompletePlaceholders = [];
snippet.walk(marker => {
if (marker instanceof Placeholder) {
if (marker.isFinalTabstop) {
placeholderDefaultValues.set(0, undefined);
}
else if (!placeholderDefaultValues.has(marker.index) && marker.children.length > 0) {
placeholderDefaultValues.set(marker.index, marker.children);
}
else {
incompletePlaceholders.push(marker);
}
}
return true;
});
const fillInIncompletePlaceholder = (placeholder, stack) => {
const defaultValues = placeholderDefaultValues.get(placeholder.index);
if (!defaultValues) {
return;
}
const clone = new Placeholder(placeholder.index);
clone.transform = placeholder.transform;
for (const child of defaultValues) {
const newChild = child.clone();
clone.appendChild(newChild);
// "recurse" on children that are again placeholders
if (newChild instanceof Placeholder && placeholderDefaultValues.has(newChild.index) && !stack.has(newChild.index)) {
stack.add(newChild.index);
fillInIncompletePlaceholder(newChild, stack);
stack.delete(newChild.index);
}
}
snippet.replace(placeholder, [clone]);
};
const stack = new Set();
for (const placeholder of incompletePlaceholders) {
fillInIncompletePlaceholder(placeholder, stack);
}
return snippet.children.slice(offset);
}
ensureFinalTabstop(snippet, enforceFinalTabstop, insertFinalTabstop) {
if (enforceFinalTabstop || insertFinalTabstop && snippet.placeholders.length > 0) {
const finalTabstop = snippet.placeholders.find(p => p.index === 0);
if (!finalTabstop) {
// the snippet uses placeholders but has no
// final tabstop defined -> insert at the end
snippet.appendChild(new Placeholder(0));
}
}
}
_accept(type, value) {
if (type === undefined || this._token.type === type) {
const ret = !value ? true : this._scanner.tokenText(this._token);
this._token = this._scanner.next();
return ret;
}
return false;
}
_backTo(token) {
this._scanner.pos = token.pos + token.len;
this._token = token;
return false;
}
_until(type) {
const start = this._token;
while (this._token.type !== type) {
if (this._token.type === 14 /* TokenType.EOF */) {
return false;
}
else if (this._token.type === 5 /* TokenType.Backslash */) {
const nextToken = this._scanner.next();
if (nextToken.type !== 0 /* TokenType.Dollar */
&& nextToken.type !== 4 /* TokenType.CurlyClose */
&& nextToken.type !== 5 /* TokenType.Backslash */) {
return false;
}
}
this._token = this._scanner.next();
}
const value = this._scanner.value.substring(start.pos, this._token.pos).replace(/\\(\$|}|\\)/g, '$1');
this._token = this._scanner.next();
return value;
}
_parse(marker) {
return this._parseEscaped(marker)
|| this._parseTabstopOrVariableName(marker)
|| this._parseComplexPlaceholder(marker)
|| this._parseComplexVariable(marker)
|| this._parseAnything(marker);
}
// \$, \\, \} -> just text
_parseEscaped(marker) {
let value;
if (value = this._accept(5 /* TokenType.Backslash */, true)) {
// saw a backslash, append escaped token or that backslash
value = this._accept(0 /* TokenType.Dollar */, true)
|| this._accept(4 /* TokenType.CurlyClose */, true)
|| this._accept(5 /* TokenType.Backslash */, true)
|| value;
marker.appendChild(new Text(value));
return true;
}
return false;
}
// $foo -> variable, $1 -> tabstop
_parseTabstopOrVariableName(parent) {
let value;
const token = this._token;
const match = this._accept(0 /* TokenType.Dollar */)
&& (value = this._accept(9 /* TokenType.VariableName */, true) || this._accept(8 /* TokenType.Int */, true));
if (!match) {
return this._backTo(token);
}
parent.appendChild(/^\d+$/.test(value)
? new Placeholder(Number(value))
: new Variable(value));
return true;
}
// ${1:<children>}, ${1} -> placeholder
_parseComplexPlaceholder(parent) {
let index;
const token = this._token;
const match = this._accept(0 /* TokenType.Dollar */)
&& this._accept(3 /* TokenType.CurlyOpen */)
&& (index = this._accept(8 /* TokenType.Int */, true));
if (!match) {
return this._backTo(token);
}
const placeholder = new Placeholder(Number(index));
if (this._accept(1 /* TokenType.Colon */)) {
// ${1:<children>}
while (true) {
// ...} -> done
if (this._accept(4 /* TokenType.CurlyClose */)) {
parent.appendChild(placeholder);
return true;
}
if (this._parse(placeholder)) {
continue;
}
// fallback
parent.appendChild(new Text('${' + index + ':'));
placeholder.children.forEach(parent.appendChild, parent);
return true;
}
}
else if (placeholder.index > 0 && this._accept(7 /* TokenType.Pipe */)) {
// ${1|one,two,three|}
const choice = new Choice();
while (true) {
if (this._parseChoiceElement(choice)) {
if (this._accept(2 /* TokenType.Comma */)) {
// opt, -> more
continue;
}
if (this._accept(7 /* TokenType.Pipe */)) {
placeholder.appendChild(choice);
if (this._accept(4 /* TokenType.CurlyClose */)) {
// ..|} -> done
parent.appendChild(placeholder);
return true;
}
}
}
this._backTo(token);
return false;
}
}
else if (this._accept(6 /* TokenType.Forwardslash */)) {
// ${1/<regex>/<format>/<options>}
if (this._parseTransform(placeholder)) {
parent.appendChild(placeholder);
return true;
}
this._backTo(token);
return false;
}
else if (this._accept(4 /* TokenType.CurlyClose */)) {
// ${1}
parent.appendChild(placeholder);
return true;
}
else {
// ${1 <- missing curly or colon
return this._backTo(token);
}
}
_parseChoiceElement(parent) {
const token = this._token;
const values = [];
while (true) {
if (this._token.type === 2 /* TokenType.Comma */ || this._token.type === 7 /* TokenType.Pipe */) {
break;
}
let value;
if (value = this._accept(5 /* TokenType.Backslash */, true)) {
// \, \|, or \\
value = this._accept(2 /* TokenType.Comma */, true)
|| this._accept(7 /* TokenType.Pipe */, true)
|| this._accept(5 /* TokenType.Backslash */, true)
|| value;
}
else {
value = this._accept(undefined, true);
}
if (!value) {
// EOF
this._backTo(token);
return false;
}
values.push(value);
}
if (values.length === 0) {
this._backTo(token);
return false;
}
parent.appendChild(new Text(values.join('')));
return true;
}
// ${foo:<children>}, ${foo} -> variable
_parseComplexVariable(parent) {
let name;
const token = this._token;
const match = this._accept(0 /* TokenType.Dollar */)
&& this._accept(3 /* TokenType.CurlyOpen */)
&& (name = this._accept(9 /* TokenType.VariableName */, true));
if (!match) {
return this._backTo(token);
}
const variable = new Variable(name);
if (this._accept(1 /* TokenType.Colon */)) {
// ${foo:<children>}
while (true) {
// ...} -> done
if (this._accept(4 /* TokenType.CurlyClose */)) {
parent.appendChild(variable);
return true;
}
if (this._parse(variable)) {
continue;
}
// fallback
parent.appendChild(new Text('${' + name + ':'));
variable.children.forEach(parent.appendChild, parent);
return true;
}
}
else if (this._accept(6 /* TokenType.Forwardslash */)) {
// ${foo/<regex>/<format>/<options>}
if (this._parseTransform(variable)) {
parent.appendChild(variable);
return true;
}
this._backTo(token);
return false;
}
else if (this._accept(4 /* TokenType.CurlyClose */)) {
// ${foo}
parent.appendChild(variable);
return true;
}
else {
// ${foo <- missing curly or colon
return this._backTo(token);
}
}
_parseTransform(parent) {
// ...<regex>/<format>/<options>}
const transform = new Transform();
let regexValue = '';
let regexOptions = '';
// (1) /regex
while (true) {
if (this._accept(6 /* TokenType.Forwardslash */)) {
break;
}
let escaped;
if (escaped = this._accept(5 /* TokenType.Backslash */, true)) {
escaped = this._accept(6 /* TokenType.Forwardslash */, true) || escaped;
regexValue += escaped;
continue;
}
if (this._token.type !== 14 /* TokenType.EOF */) {
regexValue += this._accept(undefined, true);
continue;
}
return false;
}
// (2) /format
while (true) {
if (this._accept(6 /* TokenType.Forwardslash */)) {
break;
}
let escaped;
if (escaped = this._accept(5 /* TokenType.Backslash */, true)) {
escaped = this._accept(5 /* TokenType.Backslash */, true) || this._accept(6 /* TokenType.Forwardslash */, true) || escaped;
transform.appendChild(new Text(escaped));
continue;
}
if (this._parseFormatString(transform) || this._parseAnything(transform)) {
continue;
}
return false;
}
// (3) /option
while (true) {
if (this._accept(4 /* TokenType.CurlyClose */)) {
break;
}
if (this._token.type !== 14 /* TokenType.EOF */) {
regexOptions += this._accept(undefined, true);
continue;
}
return false;
}
try {
transform.regexp = new RegExp(regexValue, regexOptions);
}
catch (e) {
// invalid regexp
return false;
}
parent.transform = transform;
return true;
}
_parseFormatString(parent) {
const token = this._token;
if (!this._accept(0 /* TokenType.Dollar */)) {
return false;
}
let complex = false;
if (this._accept(3 /* TokenType.CurlyOpen */)) {
complex = true;
}
const index = this._accept(8 /* TokenType.Int */, true);
if (!index) {
this._backTo(token);
return false;
}
else if (!complex) {
// $1
parent.appendChild(new FormatString(Number(index)));
return true;
}
else if (this._accept(4 /* TokenType.CurlyClose */)) {
// ${1}
parent.appendChild(new FormatString(Number(index)));
return true;
}
else if (!this._accept(1 /* TokenType.Colon */)) {
this._backTo(token);
return false;
}
if (this._accept(6 /* TokenType.Forwardslash */)) {
// ${1:/upcase}
const shorthand = this._accept(9 /* TokenType.VariableName */, true);
if (!shorthand || !this._accept(4 /* TokenType.CurlyClose */)) {
this._backTo(token);
return false;
}
else {
parent.appendChild(new FormatString(Number(index), shorthand));
return true;
}
}
else if (this._accept(11 /* TokenType.Plus */)) {
// ${1:+<if>}
const ifValue = this._until(4 /* TokenType.CurlyClose */);
if (ifValue) {
parent.appendChild(new FormatString(Number(index), undefined, ifValue, undefined));
return true;
}
}
else if (this._accept(12 /* TokenType.Dash */)) {
// ${2:-<else>}
const elseValue = this._until(4 /* TokenType.CurlyClose */);
if (elseValue) {
parent.appendChild(new FormatString(Number(index), undefined, undefined, elseValue));
return true;
}
}
else if (this._accept(13 /* TokenType.QuestionMark */)) {
// ${2:?<if>:<else>}
const ifValue = this._until(1 /* TokenType.Colon */);
if (ifValue) {
const elseValue = this._until(4 /* TokenType.CurlyClose */);
if (elseValue) {
parent.appendChild(new FormatString(Number(index), undefined, ifValue, elseValue));
return true;
}
}
}
else {
// ${1:<else>}
const elseValue = this._until(4 /* TokenType.CurlyClose */);
if (elseValue) {
parent.appendChild(new FormatString(Number(index), undefined, undefined, elseValue));
return true;
}
}
this._backTo(token);
return false;
}
_parseAnything(marker) {
if (this._token.type !== 14 /* TokenType.EOF */) {
marker.appendChild(new Text(this._scanner.tokenText(this._token)));
this._accept(undefined);
return true;
}
return false;
}
}