UNPKG

monaco-editor-core

Version:

A browser based code editor

889 lines (888 loc) • 30.1 kB
/*--------------------------------------------------------------------------------------------- * 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; } }