UNPKG

coc.nvim

Version:

LSP based intellisense engine for neovim & vim8.

1,019 lines 33.5 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); const vscode_languageserver_protocol_1 = require("vscode-languageserver-protocol"); const logger = require('../util/logger')('snippets-parser'); class Scanner { constructor() { this.text(''); } static isDigitCharacter(ch) { return ch >= 48 /* Digit0 */ && ch <= 57 /* Digit9 */; } static isVariableCharacter(ch) { return ch === 95 /* Underline */ || (ch >= 97 /* a */ && ch <= 122 /* z */) || (ch >= 65 /* A */ && ch <= 90 /* 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 /* EOF */, pos: this.pos, len: 0 }; } let 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 /* 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 /* 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 /* 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 }; } } Scanner._table = { [36 /* DollarSign */]: 0 /* Dollar */, [58 /* Colon */]: 1 /* Colon */, [44 /* Comma */]: 2 /* Comma */, [123 /* OpenCurlyBrace */]: 3 /* CurlyOpen */, [125 /* CloseCurlyBrace */]: 4 /* CurlyClose */, [92 /* Backslash */]: 5 /* Backslash */, [47 /* Slash */]: 6 /* Forwardslash */, [124 /* Pipe */]: 7 /* Pipe */, [43 /* Plus */]: 11 /* Plus */, [45 /* Dash */]: 12 /* Dash */, [63 /* QuestionMark */]: 13 /* QuestionMark */, }; exports.Scanner = Scanner; 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; } setOnlyChild(child) { child.parent = this; this._children = [child]; } 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 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; } get next() { let { parent } = this; let { children } = parent; let idx = children.indexOf(this); return children[idx + 1]; } } exports.Marker = Marker; class Text extends Marker { constructor(value) { super(); this.value = value; } static escape(value) { return value.replace(/\$|}|\\/g, '\\$&'); } toString() { return this.value; } toTextmateString() { return Text.escape(this.value); } len() { return this.value.length; } clone() { return new Text(this.value); } } exports.Text = Text; class TransformableMarker extends Marker { } exports.TransformableMarker = TransformableMarker; class Placeholder extends TransformableMarker { constructor(index) { super(); this.index = index; } 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; } } get isFinalTabstop() { return this.index === 0; } get choice() { return this._children.length === 1 && this._children[0] instanceof Choice ? this._children[0] : undefined; } toTextmateString() { let transformString = ''; if (this.transform) { transformString = this.transform.toTextmateString(); } if (this.children.length === 0 && !this.transform) { return `\$${this.index}`; } else if (this.children.length === 0) { return `\${${this.index}${transformString}}`; } else if (this.choice) { return `\${${this.index}|${this.choice.toTextmateString()}|${transformString}}`; } else { return `\${${this.index}:${this.children.map(child => child.toTextmateString()).join('')}${transformString}}`; } } clone() { let ret = new Placeholder(this.index); if (this.transform) { ret.transform = this.transform.clone(); } ret._children = this.children.map(child => child.clone()); return ret; } } exports.Placeholder = Placeholder; 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; } toTextmateString() { return this.options .map(option => option.value.replace(/\||,/g, '\\$&')) .join(','); } len() { return this.options[0].len(); } clone() { let ret = new Choice(); for (let opt of this.options) { ret.appendChild(opt); } return ret; } } exports.Choice = Choice; class Transform extends Marker { resolve(value) { let didMatch = false; let ret = value.replace(this.regexp, (...args) => { didMatch = true; return this._replace(args.slice(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 ''; } toTextmateString() { return `/${this.regexp.source}/${this.children.map(c => c.toTextmateString())}/${(this.regexp.ignoreCase ? 'i' : '') + (this.regexp.global ? 'g' : '')}`; } clone() { let 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; } } exports.Transform = Transform; 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 (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-z]+/gi); if (!match) { return value; } return match.map(word => { return word.charAt(0).toUpperCase() + word.substr(1).toLowerCase(); }) .join(''); } toTextmateString() { let value = '${'; value += this.index; if (this.shorthandName) { value += `:/${this.shorthandName}`; } else if (this.ifValue && this.elseValue) { value += `:?${this.ifValue}:${this.elseValue}`; } else if (this.ifValue) { value += `:+${this.ifValue}`; } else if (this.elseValue) { value += `:-${this.elseValue}`; } value += '}'; return value; } clone() { let ret = new FormatString(this.index, this.shorthandName, this.ifValue, this.elseValue); return ret; } } exports.FormatString = FormatString; class Variable extends TransformableMarker { constructor(name) { super(); this.name = name; } resolve(resolver) { let value = resolver.resolve(this); if (value && value.indexOf('\n') !== -1) { // get indent of previous Text child let { children } = this.parent; let idx = children.indexOf(this); let previous = children[idx - 1]; if (previous && previous instanceof Text) { let ms = previous.value.match(/\n([ \t]*)$/); if (ms) { let newLines = value.split('\n').map((s, i) => { return i == 0 ? s : ms[1] + s.replace(/^\s*/, ''); }); value = newLines.join('\n'); } } } if (this.transform) { value = this.transform.resolve(value || ''); } if (value !== undefined) { this._children = [new Text(value)]; return true; } return false; } toTextmateString() { let transformString = ''; if (this.transform) { transformString = this.transform.toTextmateString(); } if (this.children.length === 0) { return `\${${this.name}${transformString}}`; } else { return `\${${this.name}:${this.children.map(child => child.toTextmateString()).join('')}${transformString}}`; } } 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; } } exports.Variable = Variable; 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); } } class TextmateSnippet extends Marker { get placeholderInfo() { if (!this._placeholders) { // fill in placeholders let all = []; let last; this.walk(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; } get maxIndexNumber() { let { placeholders } = this; return placeholders.reduce((curr, p) => { return Math.max(curr, p.index); }, 0); } get minIndexNumber() { let { placeholders } = this; let nums = placeholders.map(p => p.index); nums.sort((a, b) => a - b); if (nums.length > 1 && nums[0] == 0) return nums[1]; return nums[0] || 0; } insertSnippet(snippet, id, range) { let placeholder = this.placeholders[id]; if (!placeholder) return; let { index } = placeholder; const document = vscode_languageserver_protocol_1.TextDocument.create('untitled:/1', 'snippet', 0, placeholder.toString()); snippet = vscode_languageserver_protocol_1.TextDocument.applyEdits(document, [{ range, newText: snippet.replace(/\$0$/, '') }]); let nested = new SnippetParser().parse(snippet, false); let maxIndexAdded = nested.maxIndexNumber; let totalAdd = maxIndexAdded + -1; for (let p of nested.placeholders) { if (p.isFinalTabstop) { p.index = maxIndexAdded + index + 1; } else { p.index = p.index + index; } } this.walk(m => { if (m instanceof Placeholder && m.index > index) { m.index = m.index + totalAdd + 1; } return true; }); this.replace(placeholder, nested.children); return index + 1; } updatePlaceholder(id, val) { const placeholder = this.placeholders[id]; for (let p of this.placeholders) { if (p.index == placeholder.index) { let child = p.children[0]; let newText = p.transform ? p.transform.resolve(val) : val; if (child) { p.setOnlyChild(new Text(newText)); } else { p.appendChild(new Text(newText)); } } } this._placeholders = undefined; } /** * newText after update with value */ getPlaceholderText(id, value) { const placeholder = this.placeholders[id]; if (!placeholder) return value; return placeholder.transform ? placeholder.transform.resolve(value) : value; } 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) { let 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); } toTextmateString() { return this.children.reduce((prev, cur) => prev + cur.toTextmateString(), ''); } clone() { let ret = new TextmateSnippet(); this._children = this.children.map(child => child.clone()); return ret; } walk(visitor) { walk(this.children, visitor); } } exports.TextmateSnippet = TextmateSnippet; class SnippetParser { constructor() { this._scanner = new Scanner(); } static escape(value) { return value.replace(/\$|}|\\/g, '\\$&'); } text(value) { return this.parse(value).toString(); } parse(value, insertFinalTabstop) { this._scanner.text(value); this._token = this._scanner.next(); const snippet = new TextmateSnippet(); 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; }); for (const placeholder of incompletePlaceholders) { if (placeholderDefaultValues.has(placeholder.index)) { const clone = new Placeholder(placeholder.index); clone.transform = placeholder.transform; for (const child of placeholderDefaultValues.get(placeholder.index)) { let marker = child.clone(); if (clone.transform) { if (marker instanceof Text) { marker = new Text(clone.transform.resolve(marker.value)); } else { for (let child of marker.children) { if (child instanceof Text) { marker.replace(child, [new Text(clone.transform.resolve(child.value))]); break; } } } } clone.appendChild(marker); } snippet.replace(placeholder, [clone]); } } if (!placeholderDefaultValues.has(0) && insertFinalTabstop) { // the snippet uses placeholders but has no // final tabstop defined -> insert at the end snippet.appendChild(new Placeholder(0)); } return snippet; } _accept(type, value) { if (type === undefined || this._token.type === type) { let 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) { if (this._token.type === 14 /* EOF */) { return false; } let start = this._token; while (this._token.type !== type) { this._token = this._scanner.next(); if (this._token.type === 14 /* EOF */) { return false; } } let value = this._scanner.value.substring(start.pos, this._token.pos); 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 /* Backslash */, true)) { // tslint:disable-line // saw a backslash, append escaped token or that backslash value = this._accept(0 /* Dollar */, true) || this._accept(4 /* CurlyClose */, true) || this._accept(5 /* 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 /* Dollar */) && (value = this._accept(9 /* VariableName */, true) || this._accept(8 /* 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 /* Dollar */) && this._accept(3 /* CurlyOpen */) && (index = this._accept(8 /* Int */, true)); if (!match) { return this._backTo(token); } const placeholder = new Placeholder(Number(index)); if (this._accept(1 /* Colon */)) { // ${1:<children>} while (true) { // ...} -> done if (this._accept(4 /* 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 /* Pipe */)) { // ${1|one,two,three|} const choice = new Choice(); while (true) { if (this._parseChoiceElement(choice)) { if (this._accept(2 /* Comma */)) { // opt, -> more continue; } if (this._accept(7 /* Pipe */)) { placeholder.appendChild(choice); if (this._accept(4 /* CurlyClose */)) { // ..|} -> done parent.appendChild(placeholder); return true; } } } this._backTo(token); return false; } } else if (this._accept(6 /* Forwardslash */)) { // ${1/<regex>/<format>/<options>} if (this._parseTransform(placeholder)) { parent.appendChild(placeholder); return true; } this._backTo(token); return false; } else if (this._accept(4 /* 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 /* Comma */ || this._token.type === 7 /* Pipe */) { break; } let value; if (value = this._accept(5 /* Backslash */, true)) { // tslint:disable-line // \, \|, or \\ value = this._accept(2 /* Comma */, true) || this._accept(7 /* Pipe */, true) || this._accept(5 /* 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 /* Dollar */) && this._accept(3 /* CurlyOpen */) && (name = this._accept(9 /* VariableName */, true)); if (!match) { return this._backTo(token); } const variable = new Variable(name); if (this._accept(1 /* Colon */)) { // ${foo:<children>} while (true) { // ...} -> done if (this._accept(4 /* 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 /* Forwardslash */)) { // ${foo/<regex>/<format>/<options>} if (this._parseTransform(variable)) { parent.appendChild(variable); return true; } this._backTo(token); return false; } else if (this._accept(4 /* CurlyClose */)) { // ${foo} parent.appendChild(variable); return true; } else { // ${foo <- missing curly or colon return this._backTo(token); } } _parseTransform(parent) { // ...<regex>/<format>/<options>} let transform = new Transform(); let regexValue = ''; let regexOptions = ''; // (1) /regex while (true) { if (this._accept(6 /* Forwardslash */)) { break; } let escaped; if (escaped = this._accept(5 /* Backslash */, true)) { // tslint:disable-line escaped = this._accept(6 /* Forwardslash */, true) || escaped; regexValue += escaped; continue; } if (this._token.type !== 14 /* EOF */) { regexValue += this._accept(undefined, true); continue; } return false; } // (2) /format while (true) { if (this._accept(6 /* Forwardslash */)) { break; } let escaped; if (escaped = this._accept(5 /* Backslash */, true)) { // tslint:disable-line escaped = this._accept(6 /* 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 /* CurlyClose */)) { break; } if (this._token.type !== 14 /* 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 /* Dollar */)) { return false; } let complex = false; if (this._accept(3 /* CurlyOpen */)) { complex = true; } let index = this._accept(8 /* 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 /* CurlyClose */)) { // ${1} parent.appendChild(new FormatString(Number(index))); return true; } else if (!this._accept(1 /* Colon */)) { this._backTo(token); return false; } if (this._accept(6 /* Forwardslash */)) { // ${1:/upcase} let shorthand = this._accept(9 /* VariableName */, true); if (!shorthand || !this._accept(4 /* CurlyClose */)) { this._backTo(token); return false; } else { parent.appendChild(new FormatString(Number(index), shorthand)); return true; } } else if (this._accept(11 /* Plus */)) { // ${1:+<if>} let ifValue = this._until(4 /* CurlyClose */); if (ifValue) { parent.appendChild(new FormatString(Number(index), undefined, ifValue, undefined)); return true; } } else if (this._accept(12 /* Dash */)) { // ${2:-<else>} let elseValue = this._until(4 /* CurlyClose */); if (elseValue) { parent.appendChild(new FormatString(Number(index), undefined, undefined, elseValue)); return true; } } else if (this._accept(13 /* QuestionMark */)) { // ${2:?<if>:<else>} let ifValue = this._until(1 /* Colon */); if (ifValue) { let elseValue = this._until(4 /* CurlyClose */); if (elseValue) { parent.appendChild(new FormatString(Number(index), undefined, ifValue, elseValue)); return true; } } } else { // ${1:<else>} let elseValue = this._until(4 /* 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 /* EOF */) { marker.appendChild(new Text(this._scanner.tokenText(this._token))); this._accept(undefined); return true; } return false; } } exports.SnippetParser = SnippetParser; //# sourceMappingURL=parser.js.map