svelte-language-server
Version:
A language server for Svelte
352 lines • 15.2 kB
JavaScript
;
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()));
docManager.on('documentChange', (document) => {
this.documents.set(document, document.html);
});
}
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)) {
return doEmmetCompleteInner() ?? 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:')) {
item.textEdit = {
...item.textEdit,
newText: item.textEdit.newText.replace(attributeValuePlaceHolder, `$2=${startQuote}$1${endQuote}`)
};
// In Svelte 5, people should use `onclick` instead of `on:click`
if (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