UNPKG

svelte-language-server

Version:
363 lines 15.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HTMLPlugin = void 0; const emmet_helper_1 = require("@vscode/emmet-helper"); const vscode_html_languageservice_1 = require("vscode-html-languageservice"); const vscode_languageserver_1 = require("vscode-languageserver"); const documents_1 = require("../../lib/documents"); const dataProvider_1 = require("./dataProvider"); const utils_1 = require("../../lib/documents/utils"); const utils_2 = require("../../utils"); const importPackage_1 = require("../../importPackage"); const path_1 = __importDefault(require("path")); const logger_1 = require("../../logger"); const indentFolding_1 = require("../../lib/foldingRange/indentFolding"); const wordHighlight_1 = require("../../lib/documentHighlight/wordHighlight"); // https://github.com/microsoft/vscode/blob/c6f507deeb99925e713271b1048f21dbaab4bd54/extensions/html/language-configuration.json#L34 const wordPattern = /(-?\d*\.\d\w*)|([^`~!@$^&*()=+[{\]}\|;:'",.<>\/\s]+)/g; const attributeValuePlaceHolder = '="$1"'; class HTMLPlugin { constructor(docManager, configManager) { this.configManager = configManager; this.__name = 'html'; this.lang = (0, vscode_html_languageservice_1.getLanguageService)({ customDataProviders: this.getCustomDataProviders(), useDefaultDataProvider: false, clientCapabilities: this.configManager.getClientCapabilities() }); this.documents = new WeakMap(); this.styleScriptTemplate = new Set(['template', 'style', 'script']); this.htmlTriggerCharacters = ['.', ':', '<', '"', '=', '/']; configManager.onChange(() => this.lang.setDataProviders(false, this.getCustomDataProviders())); const sync = (document) => { this.documents.set(document, document.html); }; docManager.on('documentChange', sync); docManager.on('documentOpen', sync); } doHover(document, position) { if (!this.featureEnabled('hover')) { return null; } const html = this.documents.get(document); if (!html) { return null; } const node = html.findNodeAt(document.offsetAt(position)); if (!node || (0, utils_2.possiblyComponent)(node)) { return null; } return this.lang.doHover(document, position, html); } async getCompletions(document, position, completionContext) { if (!this.featureEnabled('completions')) { return null; } const html = this.documents.get(document); if (!html) { return null; } if (this.isInsideMoustacheTag(html, document, position) || (0, documents_1.isInTag)(position, document.scriptInfo) || (0, documents_1.isInTag)(position, document.moduleScriptInfo)) { return null; } let emmetResults = { isIncomplete: false, items: [] }; let doEmmetCompleteInner = () => null; if (this.configManager.getConfig().html.completions.emmet && this.configManager.getEmmetConfig().showExpandedAbbreviation !== 'never') { doEmmetCompleteInner = () => (0, emmet_helper_1.doComplete)(document, position, 'html', this.configManager.getEmmetConfig()); this.lang.setCompletionParticipants([ { onHtmlContent: () => (emmetResults = doEmmetCompleteInner() || emmetResults) } ]); } if (completionContext?.triggerCharacter && !this.htmlTriggerCharacters.includes(completionContext?.triggerCharacter)) { const node = html.findNodeAt(document.offsetAt(position)); const offset = document.offsetAt(position); if (!node?.tag || (offset > (node.startTagEnd ?? node.end) && (node.endTagStart == null || offset <= node.endTagStart))) { return doEmmetCompleteInner() ?? null; } return null; } const results = this.isInComponentTag(html, document, position) ? // Only allow emmet inside component element tags. // Other attributes/events would be false positives. vscode_languageserver_1.CompletionList.create([]) : this.lang.doComplete(document, position, html); const items = this.toCompletionItems(results.items); const filePath = document.getFilePath(); const prettierConfig = filePath && items.some((item) => item.label.startsWith('on:') || item.label.startsWith('bind:')) ? this.configManager.getMergedPrettierConfig(await (0, importPackage_1.importPrettier)(filePath).resolveConfig(filePath, { editorconfig: true })) : null; const svelteStrictMode = prettierConfig?.svelteStrictMode; const startQuote = svelteStrictMode ? '"{' : '{'; const endQuote = svelteStrictMode ? '}"' : '}'; items.forEach((item) => { if (item.label.endsWith(':')) { item.kind = vscode_languageserver_1.CompletionItemKind.Keyword; if (item.textEdit) { item.textEdit.newText = item.textEdit.newText.replace(attributeValuePlaceHolder, ''); } } if (!item.textEdit) { return; } if (item.label.startsWith('on')) { const isLegacyDirective = item.label.startsWith('on:'); const modifierTabStop = isLegacyDirective ? '$2' : ''; item.textEdit = { ...item.textEdit, newText: item.textEdit.newText.replace(attributeValuePlaceHolder, `${modifierTabStop}=${startQuote}$1${endQuote}`) }; // In Svelte 5, people should use `onclick` instead of `on:click` if (isLegacyDirective && document.isSvelte5) { item.sortText = 'z' + (item.sortText ?? item.label); } } if (item.label.startsWith('bind:')) { item.textEdit = { ...item.textEdit, newText: item.textEdit.newText.replace(attributeValuePlaceHolder, `=${startQuote}$1${endQuote}`) }; } }); return vscode_languageserver_1.CompletionList.create([...items, ...this.getLangCompletions(items), ...emmetResults.items], // Emmet completions change on every keystroke, so they are never complete emmetResults.items.length > 0); } /** * The HTML language service uses newer types which clash * without the stable ones. Transform to the stable types. */ toCompletionItems(items) { return items.map((item) => { if (!item.textEdit || vscode_languageserver_1.TextEdit.is(item.textEdit)) { return item; } return { ...item, textEdit: vscode_languageserver_1.TextEdit.replace(item.textEdit.replace, item.textEdit.newText) }; }); } isInComponentTag(html, document, position) { return !!(0, documents_1.getNodeIfIsInComponentStartTag)(html, document, document.offsetAt(position)); } getLangCompletions(completions) { const styleScriptTemplateCompletions = completions.filter((completion) => completion.kind === vscode_languageserver_1.CompletionItemKind.Property && this.styleScriptTemplate.has(completion.label)); const langCompletions = []; addLangCompletion('script', ['ts']); addLangCompletion('style', ['less', 'scss']); addLangCompletion('template', ['pug']); return langCompletions; function addLangCompletion(tag, languages) { 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 && vscode_languageserver_1.TextEdit.is(existingCompletion.textEdit) ? { range: existingCompletion.textEdit.range, newText: `${existingCompletion.textEdit.newText} lang="${lang}"` } : undefined })); } } doTagComplete(document, position) { if (!this.featureEnabled('tagComplete')) { return null; } const html = this.documents.get(document); if (!html) { return null; } if (this.isInsideMoustacheTag(html, document, position)) { return null; } return this.lang.doTagComplete(document, position, html); } isInsideMoustacheTag(html, document, position) { const offset = document.offsetAt(position); const node = html.findNodeAt(offset); return (0, utils_1.isInsideMoustacheTag)(document.getText(), node.start, offset); } getDocumentSymbols(document) { if (!this.featureEnabled('documentSymbols')) { return []; } const html = this.documents.get(document); if (!html) { return []; } return this.lang.findDocumentSymbols(document, html); } rename(document, position, newName) { const html = this.documents.get(document); if (!html) { return null; } const node = html.findNodeAt(document.offsetAt(position)); if (!node || (0, utils_2.possiblyComponent)(node)) { return null; } return this.lang.doRename(document, position, newName, html); } prepareRename(document, position) { const html = this.documents.get(document); if (!html) { return null; } const offset = document.offsetAt(position); const node = html.findNodeAt(offset); if (!node || (0, utils_2.possiblyComponent)(node) || !node.tag || !this.isRenameAtTag(node, offset)) { return null; } const tagNameStart = node.start + '<'.length; return (0, utils_1.toRange)(document, tagNameStart, tagNameStart + node.tag.length); } getLinkedEditingRanges(document, position) { if (!this.featureEnabled('linkedEditing')) { return null; } const html = this.documents.get(document); if (!html) { return null; } const ranges = this.lang.findLinkedEditingRanges(document, position, html); if (!ranges) { return null; } // Note that `.` is excluded from the word pattern. This is intentional to support property access in Svelte component tags. return { ranges, wordPattern: '(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\^\\&\\*\\(\\)\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\\'\\"\\,\\<\\>\\/\\s]+)' }; } getFoldingRanges(document) { const result = this.lang.getFoldingRanges(document); const templateRange = document.templateInfo ? (0, indentFolding_1.indentBasedFoldingRangeForTag)(document, document.templateInfo) : []; const ARROW = '=>'; if (!document.getText().includes(ARROW)) { return result.concat(templateRange); } const byEnd = new Map(); for (const fold of result) { byEnd.set(fold.endLine, (byEnd.get(fold.endLine) ?? []).concat(fold)); } let startIndex = 0; while (startIndex < document.getTextLength()) { const index = document.getText().indexOf(ARROW, startIndex); startIndex = index + ARROW.length; if (index === -1) { break; } const position = document.positionAt(index); const isInStyleOrScript = (0, documents_1.isInTag)(position, document.styleInfo) || (0, documents_1.isInTag)(position, document.scriptInfo) || (0, documents_1.isInTag)(position, document.moduleScriptInfo); if (isInStyleOrScript) { continue; } const tag = document.html.findNodeAt(index); // our version of html document patched it so it's within the start tag // but not the folding range returned by the language service // which uses unpatched scanner if (!tag.startTagEnd || index > tag.startTagEnd) { continue; } const tagStartPosition = document.positionAt(tag.start); const range = byEnd .get(position.line) ?.find((r) => r.startLine === tagStartPosition.line); const newEndLine = document.positionAt(tag.end).line - 1; if (newEndLine <= tagStartPosition.line) { continue; } if (range) { range.endLine = newEndLine; } else { result.push({ startLine: tagStartPosition.line, endLine: newEndLine }); } } return result.concat(templateRange); } findDocumentHighlight(document, position) { const html = this.documents.get(document); if (!html) { return null; } const templateResult = (0, wordHighlight_1.wordHighlightForTag)(document, position, document.templateInfo, wordPattern); if (templateResult) { return templateResult; } const node = html.findNodeAt(document.offsetAt(position)); if ((0, utils_2.possiblyComponent)(node)) { return null; } const result = this.lang.findDocumentHighlights(document, position, html); if (!result.length) { return null; } return result; } /** * Returns true if rename happens at the tag name, not anywhere inbetween. */ isRenameAtTag(node, offset) { if (!node.tag) { return false; } const startTagNameEnd = node.start + `<${node.tag}`.length; const isAtStartTag = offset > node.start && offset <= startTagNameEnd; const isAtEndTag = node.endTagStart !== undefined && offset >= node.endTagStart && offset < node.end; return isAtStartTag || isAtEndTag; } getCustomDataProviders() { const providers = this.configManager .getHTMLConfig() ?.customData?.map((customDataPath) => { try { const jsonPath = path_1.default.resolve(customDataPath); return (0, vscode_html_languageservice_1.newHTMLDataProvider)(customDataPath, require(jsonPath)); } catch (error) { logger_1.Logger.error(error); } }) .filter(utils_2.isNotNullOrUndefined) ?? []; return [dataProvider_1.svelteHtmlDataProvider].concat(providers); } featureEnabled(feature) { return (this.configManager.enabled('html.enable') && this.configManager.enabled(`html.${feature}.enable`)); } } exports.HTMLPlugin = HTMLPlugin; //# sourceMappingURL=HTMLPlugin.js.map