diff options
Diffstat (limited to 'tools/language-server/src/plugins/html/HTMLPlugin.ts')
-rw-r--r-- | tools/language-server/src/plugins/html/HTMLPlugin.ts | 126 |
1 files changed, 126 insertions, 0 deletions
diff --git a/tools/language-server/src/plugins/html/HTMLPlugin.ts b/tools/language-server/src/plugins/html/HTMLPlugin.ts new file mode 100644 index 000000000..7e0ab4861 --- /dev/null +++ b/tools/language-server/src/plugins/html/HTMLPlugin.ts @@ -0,0 +1,126 @@ +import { CompletionsProvider, FoldingRangeProvider } from '../interfaces'; +import { getEmmetCompletionParticipants, VSCodeEmmetConfig } from 'vscode-emmet-helper'; +import { getLanguageService, HTMLDocument, CompletionItem as HtmlCompletionItem, Node, FoldingRange } from 'vscode-html-languageservice'; +import { CompletionList, Position, CompletionItem, CompletionItemKind, TextEdit } from 'vscode-languageserver'; +import type { Document, DocumentManager } from '../../core/documents'; +import { isInsideExpression, isInsideFrontmatter } from '../../core/documents/utils'; +import type { ConfigManager } from '../../core/config'; + +export class HTMLPlugin implements CompletionsProvider, FoldingRangeProvider { + private lang = getLanguageService(); + private documents = new WeakMap<Document, HTMLDocument>(); + private styleScriptTemplate = new Set(['template', 'style', 'script']); + private configManager: ConfigManager; + + constructor(docManager: DocumentManager, configManager: ConfigManager) { + docManager.on('documentChange', (document) => { + this.documents.set(document, document.html); + }); + this.configManager = configManager; + } + + getCompletions(document: Document, position: Position): CompletionList | null { + const html = this.documents.get(document); + + if (!html) { + return null; + } + + if (this.isInsideFrontmatter(document, position) || this.isInsideExpression(html, document, position)) { + return null; + } + + const emmetResults: CompletionList = { + isIncomplete: true, + items: [], + }; + this.lang.setCompletionParticipants([getEmmetCompletionParticipants(document, position, 'html', this.configManager.getEmmetConfig(), emmetResults)]); + + const results = this.lang.doComplete(document, position, html); + const items = this.toCompletionItems(results.items); + + return CompletionList.create( + [...this.toCompletionItems(items), ...this.getLangCompletions(items), ...emmetResults.items], + // Emmet completions change on every keystroke, so they are never complete + emmetResults.items.length > 0 + ); + } + + getFoldingRanges(document: Document): FoldingRange[] | null { + const html = this.documents.get(document); + if (!html) { + return null; + } + + return this.lang.getFoldingRanges(document); + } + + doTagComplete(document: Document, position: Position): string | null { + const html = this.documents.get(document); + if (!html) { + return null; + } + + if (this.isInsideFrontmatter(document, position) || this.isInsideExpression(html, document, position)) { + return null; + } + + return this.lang.doTagComplete(document, position, html); + } + + /** + * The HTML language service uses newer types which clash + * without the stable ones. Transform to the stable types. + */ + private toCompletionItems(items: HtmlCompletionItem[]): CompletionItem[] { + return items.map((item) => { + if (!item.textEdit || TextEdit.is(item.textEdit)) { + return item as CompletionItem; + } + return { + ...item, + textEdit: TextEdit.replace(item.textEdit.replace, item.textEdit.newText), + }; + }); + } + + private getLangCompletions(completions: CompletionItem[]): CompletionItem[] { + const styleScriptTemplateCompletions = completions.filter((completion) => completion.kind === CompletionItemKind.Property && this.styleScriptTemplate.has(completion.label)); + const langCompletions: CompletionItem[] = []; + addLangCompletion('style', ['scss', 'sass']); + return langCompletions; + + /** Add language completions */ + function addLangCompletion(tag: string, languages: string[]) { + const existingCompletion = styleScriptTemplateCompletions.find((completion) => completion.label === tag); + if (!existingCompletion) { + return; + } + + languages.forEach((lang) => + langCompletions.push({ + ...existingCompletion, + label: `${tag} (lang="${lang}")`, + insertText: existingCompletion.insertText && `${existingCompletion.insertText} lang="${lang}"`, + textEdit: + existingCompletion.textEdit && TextEdit.is(existingCompletion.textEdit) + ? { + range: existingCompletion.textEdit.range, + newText: `${existingCompletion.textEdit.newText} lang="${lang}"`, + } + : undefined, + }) + ); + } + } + + private isInsideExpression(html: HTMLDocument, document: Document, position: Position) { + const offset = document.offsetAt(position); + const node = html.findNodeAt(offset); + return isInsideExpression(document.getText(), node.start, offset); + } + + private isInsideFrontmatter(document: Document, position: Position) { + return isInsideFrontmatter(document.getText(), document.offsetAt(position)); + } +} |