typescript-lit-html-plugin
Version:
TypeScript language service plugin that adds IntelliSense for html tagged templates
342 lines • 14.3 kB
JavaScript
"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
//
// Original code forked from https://github.com/Quramy/ts-graphql-plugin
Object.defineProperty(exports, "__esModule", { value: true });
const vscode = require("vscode-languageserver-types");
const embeddedSupport_1 = require("./embeddedSupport");
const emptyCompletionList = {
isIncomplete: false,
items: [],
};
class CompletionsCache {
getCached(context, position) {
if (this._completions
&& context.fileName === this._cachedCompletionsFile
&& this._cachedCompletionsPosition && arePositionsEqual(position, this._cachedCompletionsPosition)
&& context.text === this._cachedCompletionsContent) {
return this._completions;
}
return undefined;
}
updateCached(context, position, completions) {
this._cachedCompletionsFile = context.fileName;
this._cachedCompletionsPosition = position;
this._cachedCompletionsContent = context.text;
this._completions = completions;
}
}
class HtmlTemplateLanguageService {
constructor(typescript, configuration, virtualDocumentProvider, htmlLanguageService, styledLanguageService, _logger) {
this.typescript = typescript;
this.configuration = configuration;
this.virtualDocumentProvider = virtualDocumentProvider;
this.htmlLanguageService = htmlLanguageService;
this.styledLanguageService = styledLanguageService;
this._completionsCache = new CompletionsCache();
}
getCompletionsAtPosition(context, position) {
const entry = this.getCompletionItems(context, position);
if (entry.type === 'styled') {
return entry.value;
}
return translateCompletionItemsToCompletionInfo(this.typescript, context, entry.value);
}
getCompletionEntryDetails(context, position, name) {
const entry = this.getCompletionItems(context, position);
if (entry.type === 'styled') {
return this.styledLanguageService.getCompletionEntryDetails(context, position, name);
}
const item = entry.value.items.find(x => x.label === name);
if (!item) {
return {
name,
kind: this.typescript.ScriptElementKind.unknown,
kindModifiers: '',
tags: [],
displayParts: toDisplayParts(name),
documentation: [],
};
}
return translateCompletionItemsToCompletionEntryDetails(this.typescript, item);
}
getQuickInfoAtPosition(context, position) {
const document = this.virtualDocumentProvider.createVirtualDocument(context);
const documentRegions = embeddedSupport_1.getDocumentRegions(this.htmlLanguageService, document);
const languageId = documentRegions.getLanguageAtPosition(position);
switch (languageId) {
case 'html':
const htmlDoc = this.htmlLanguageService.parseHTMLDocument(document);
const hover = this.htmlLanguageService.doHover(document, position, htmlDoc);
return hover ? this.translateHover(hover, position, context) : undefined;
case 'css':
return this.styledLanguageService.getQuickInfoAtPosition(context, position);
}
return undefined;
}
getFormattingEditsForRange(context, start, end, settings) {
if (!this.configuration.format.enabled) {
return [];
}
// Disable formatting for blocks that contain a style tag
//
// Formatting styled blocks gets complex since we want to preserve interpolations inside the output
// but we can't format content with `{` property.
// The best fix would be to add `style` to `contentUnformatted` but
// https://github.com/Microsoft/vscode-html-languageservice/issues/29 is causing problems and I'm not sure how
// to work around it well
if (context.text.match(/<style/g)) {
return [];
}
const document = this.virtualDocumentProvider.createVirtualDocument(context);
// Make sure we don't get rid of leading newline
const leading = context.text.match(/^\s*\n/);
if (leading) {
start += leading[0].length;
}
// or any trailing newlines
const trailing = context.text.match(/\n\s*$/);
if (trailing) {
end -= trailing[0].length;
}
if (end <= start) {
return [];
}
const range = this.toVsRange(context, start, end);
const edits = this.htmlLanguageService.format(document, range, {
tabSize: settings.tabSize,
insertSpaces: !!settings.convertTabsToSpaces,
wrapLineLength: 120,
unformatted: '',
contentUnformatted: 'pre,code,textarea',
indentInnerHtml: false,
preserveNewLines: true,
maxPreserveNewLines: undefined,
indentHandlebars: false,
endWithNewline: false,
extraLiners: 'head, body, /html',
wrapAttributes: 'auto',
});
return edits.map(vsedit => toTsTextChange(context, vsedit));
}
getSignatureHelpItemsAtPosition(_context, _position) {
// Html does not support sig help
return undefined;
}
getOutliningSpans(context) {
const document = this.virtualDocumentProvider.createVirtualDocument(context);
const ranges = this.htmlLanguageService.getFoldingRanges(document);
return ranges.map(range => this.translateOutliningSpan(context, range));
}
getSemanticDiagnostics(context) {
return this.styledLanguageService.getSemanticDiagnostics(context);
}
getSupportedCodeFixes() {
return this.styledLanguageService.getSupportedCodeFixes();
}
getCodeFixesAtPosition(context, start, end, errorCodes, format) {
return this.styledLanguageService.getCodeFixesAtPosition(context, start, end, errorCodes, format);
}
getReferencesAtPosition(context, position) {
const document = this.virtualDocumentProvider.createVirtualDocument(context);
const htmlDoc = this.htmlLanguageService.parseHTMLDocument(document);
const highlights = this.htmlLanguageService.findDocumentHighlights(document, position, htmlDoc);
return highlights.map(highlight => ({
fileName: context.fileName,
textSpan: toTsSpan(context, highlight.range),
}));
}
getJsxClosingTagAtPosition(context, position) {
const document = this.virtualDocumentProvider.createVirtualDocument(context);
const htmlDocument = this.htmlLanguageService.parseHTMLDocument(document);
const tagComplete = this.htmlLanguageService.doTagComplete(document, position, htmlDocument);
if (!tagComplete) {
return undefined;
}
// Html returns completions with snippet placeholders. Strip these out.
return {
newText: tagComplete.replace(/\$\d/g, ''),
};
}
toVsRange(context, start, end) {
return {
start: context.toPosition(start),
end: context.toPosition(end),
};
}
getCompletionItems(context, position) {
const cached = this._completionsCache.getCached(context, position);
if (cached) {
return cached;
}
const document = this.virtualDocumentProvider.createVirtualDocument(context);
const documentRegions = embeddedSupport_1.getDocumentRegions(this.htmlLanguageService, document);
const languageId = documentRegions.getLanguageAtPosition(position);
switch (languageId) {
case 'html': {
const htmlDoc = this.htmlLanguageService.parseHTMLDocument(document);
const htmlCompletions = {
type: 'html',
value: this.htmlLanguageService.doComplete(document, position, htmlDoc) || emptyCompletionList,
};
this._completionsCache.updateCached(context, position, htmlCompletions);
return htmlCompletions;
}
case 'css': {
const styledCompletions = {
type: 'styled',
value: this.styledLanguageService.getCompletionsAtPosition(context, position),
};
this._completionsCache.updateCached(context, position, styledCompletions);
return styledCompletions;
}
}
const completions = {
type: 'html',
value: emptyCompletionList,
};
this._completionsCache.updateCached(context, position, completions);
return completions;
}
translateHover(hover, position, context) {
const header = [];
const docs = [];
const convertPart = (hoverContents) => {
if (typeof hoverContents === 'string') {
docs.push({ kind: 'unknown', text: hoverContents });
}
else if (Array.isArray(hoverContents)) {
hoverContents.forEach(convertPart);
}
else {
header.push({ kind: 'unknown', text: hoverContents.value });
}
};
convertPart(hover.contents);
const start = context.toOffset(hover.range ? hover.range.start : position);
return {
kind: this.typescript.ScriptElementKind.string,
kindModifiers: '',
textSpan: {
start,
length: hover.range ? context.toOffset(hover.range.end) - start : 1,
},
displayParts: header,
documentation: docs,
tags: [],
};
}
translateOutliningSpan(context, range) {
const startOffset = context.toOffset({ line: range.startLine, character: range.startCharacter || 0 });
const endOffset = context.toOffset({ line: range.endLine, character: range.endCharacter || 0 });
const span = {
start: startOffset,
length: endOffset - startOffset,
};
return {
autoCollapse: false,
kind: this.typescript.OutliningSpanKind.Code,
bannerText: '',
textSpan: span,
hintSpan: span,
};
}
}
exports.default = HtmlTemplateLanguageService;
function translateCompletionItemsToCompletionInfo(typescript, context, items) {
return {
isGlobalCompletion: false,
isMemberCompletion: false,
isNewIdentifierLocation: false,
entries: items.items.map(x => translateCompetionEntry(typescript, context, x)),
};
}
function translateCompletionItemsToCompletionEntryDetails(typescript, item) {
return {
name: item.label,
kindModifiers: 'declare',
kind: item.kind ? translateionCompletionItemKind(typescript, item.kind) : typescript.ScriptElementKind.unknown,
displayParts: toDisplayParts(item.detail),
documentation: toDisplayParts(item.documentation),
tags: [],
};
}
function translateCompetionEntry(typescript, context, vsItem) {
const kind = vsItem.kind ? translateionCompletionItemKind(typescript, vsItem.kind) : typescript.ScriptElementKind.unknown;
const entry = {
name: vsItem.label,
kind,
sortText: '0',
};
if (vsItem.textEdit) {
entry.insertText = vsItem.textEdit.newText;
entry.replacementSpan = toTsSpan(context, vsItem.textEdit.range);
}
return entry;
}
function translateionCompletionItemKind(typescript, kind) {
switch (kind) {
case vscode.CompletionItemKind.Method:
return typescript.ScriptElementKind.memberFunctionElement;
case vscode.CompletionItemKind.Function:
return typescript.ScriptElementKind.functionElement;
case vscode.CompletionItemKind.Constructor:
return typescript.ScriptElementKind.constructorImplementationElement;
case vscode.CompletionItemKind.Field:
case vscode.CompletionItemKind.Variable:
return typescript.ScriptElementKind.variableElement;
case vscode.CompletionItemKind.Class:
return typescript.ScriptElementKind.classElement;
case vscode.CompletionItemKind.Interface:
return typescript.ScriptElementKind.interfaceElement;
case vscode.CompletionItemKind.Module:
return typescript.ScriptElementKind.moduleElement;
case vscode.CompletionItemKind.Property:
return typescript.ScriptElementKind.memberVariableElement;
case vscode.CompletionItemKind.Unit:
case vscode.CompletionItemKind.Value:
return typescript.ScriptElementKind.constElement;
case vscode.CompletionItemKind.Enum:
return typescript.ScriptElementKind.enumElement;
case vscode.CompletionItemKind.Keyword:
return typescript.ScriptElementKind.keyword;
case vscode.CompletionItemKind.Color:
return typescript.ScriptElementKind.constElement;
case vscode.CompletionItemKind.Reference:
return typescript.ScriptElementKind.alias;
case vscode.CompletionItemKind.File:
return typescript.ScriptElementKind.moduleElement;
case vscode.CompletionItemKind.Snippet:
case vscode.CompletionItemKind.Text:
default:
return typescript.ScriptElementKind.unknown;
}
}
function toDisplayParts(text) {
if (!text) {
return [];
}
return [{
kind: 'text',
text: typeof text === 'string' ? text : text.value,
}];
}
function arePositionsEqual(left, right) {
return left.line === right.line && left.character === right.character;
}
function toTsSpan(context, range) {
const editStart = context.toOffset(range.start);
const editEnd = context.toOffset(range.end);
return {
start: editStart,
length: editEnd - editStart,
};
}
function toTsTextChange(context, vsedit) {
return {
span: toTsSpan(context, vsedit.range),
newText: vsedit.newText,
};
}
//# sourceMappingURL=html-template-language-service.js.map