svelte-language-server
Version:
A language server for Svelte
794 lines • 41.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CompletionsProviderImpl = void 0;
const path_1 = require("path");
const typescript_1 = __importDefault(require("typescript"));
const vscode_languageserver_1 = require("vscode-languageserver");
const documents_1 = require("../../../lib/documents");
const parseHtml_1 = require("../../../lib/documents/parseHtml");
const utils_1 = require("../../../utils");
const previewer_1 = require("../previewer");
const utils_2 = require("../utils");
const getJsDocTemplateCompletion_1 = require("./getJsDocTemplateCompletion");
const utils_3 = require("./utils");
const svelte_ast_utils_1 = require("../svelte-ast-utils");
class CompletionsProviderImpl {
constructor(lsAndTsDocResolver, configManager) {
this.lsAndTsDocResolver = lsAndTsDocResolver;
this.configManager = configManager;
/**
* The language service throws an error if the character is not a valid trigger character.
* Also, the completions are worse.
* Therefore, only use the characters the typescript compiler treats as valid.
*/
this.validTriggerCharacters = ['.', '"', "'", '`', '/', '@', '<', '#'];
this.commitCharacters = ['.', ',', ';', '('];
}
isValidTriggerCharacter(character) {
return this.validTriggerCharacters.includes(character);
}
async getCompletions(document, position, completionContext, cancellationToken) {
if ((0, documents_1.isInTag)(position, document.styleInfo)) {
return null;
}
const { lang: langForSyntheticOperations, tsDoc, userPreferences } = await this.lsAndTsDocResolver.getLsForSyntheticOperations(document);
const filePath = tsDoc.filePath;
if (!filePath) {
return null;
}
const triggerCharacter = completionContext?.triggerCharacter;
const triggerKind = completionContext?.triggerKind;
const validTriggerCharacter = this.isValidTriggerCharacter(triggerCharacter)
? triggerCharacter
: undefined;
const isCustomTriggerCharacter = triggerKind === vscode_languageserver_1.CompletionTriggerKind.TriggerCharacter;
const isJsDocTriggerCharacter = triggerCharacter === '*';
const isEventOrSlotLetTriggerCharacter = triggerCharacter === ':';
// ignore any custom trigger character specified in server capabilities
// and is not allow by ts
if (isCustomTriggerCharacter &&
!validTriggerCharacter &&
!isJsDocTriggerCharacter &&
!isEventOrSlotLetTriggerCharacter) {
return null;
}
if (this.canReuseLastCompletion(this.lastCompletion, triggerKind, triggerCharacter, document, position)) {
this.lastCompletion.position = position;
return this.lastCompletion.completionList;
}
else {
this.lastCompletion = undefined;
}
if (!tsDoc.isInGenerated(position)) {
return null;
}
const originalOffset = document.offsetAt(position);
let offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));
if (isJsDocTriggerCharacter) {
return (0, getJsDocTemplateCompletion_1.getJsDocTemplateCompletion)(tsDoc, langForSyntheticOperations, filePath, offset);
}
const svelteNode = tsDoc.svelteNodeAt(originalOffset);
if (
// Cursor is somewhere in regular HTML text
(svelteNode?.type === 'Text' &&
[
'Element',
'InlineComponent',
'Fragment',
'SlotTemplate',
'SnippetBlock',
'IfBlock',
'EachBlock',
'AwaitBlock'
].includes(svelteNode.parent?.type)) ||
// Cursor is at <div>|</div> in which case there's no TextNode inbetween
document.getText().substring(originalOffset - 1, originalOffset + 2) === '></') {
return null;
}
const { lang, lsContainer } = await this.lsAndTsDocResolver.getLSAndTSDoc(document);
if (cancellationToken?.isCancellationRequested) {
return null;
}
const inScript = (0, utils_2.isInScript)(position, tsDoc);
const wordInfo = this.getWordAtPosition(document, originalOffset);
if (!inScript &&
wordInfo.word[0] === '{' &&
(wordInfo.word[1] === '#' ||
wordInfo.word[1] === '@' ||
wordInfo.word[1] === ':' ||
wordInfo.word[1] === '/')) {
// Typing something like {/if}
return null;
}
// Special case: completion at `<Comp.` -> mapped one character too short -> adjust
if (!inScript &&
wordInfo.word === '' &&
document.getText()[originalOffset - 1] === '.' &&
tsDoc.getFullText()[offset] === '.') {
offset++;
}
const componentInfo = (0, utils_3.getComponentAtPosition)(lang, document, tsDoc, position);
const attributeContext = componentInfo && (0, parseHtml_1.getAttributeContextAtPosition)(document, position);
const eventAndSlotLetCompletions = this.getEventAndSlotLetCompletions(componentInfo, attributeContext, wordInfo.defaultTextEditRange);
if (isEventOrSlotLetTriggerCharacter) {
return vscode_languageserver_1.CompletionList.create(eventAndSlotLetCompletions, !!tsDoc.parserError);
}
const tagCompletions = componentInfo || eventAndSlotLetCompletions.length > 0
? []
: this.getCustomElementCompletions(lang, lsContainer, document, tsDoc, position);
const formatSettings = await this.configManager.getFormatCodeSettingsForFile(document, tsDoc.scriptKind);
if (cancellationToken?.isCancellationRequested) {
return null;
}
// one or two characters after start tag might be mapped to the component name
if (svelteNode?.type === 'InlineComponent' &&
'name' in svelteNode &&
typeof svelteNode.name === 'string') {
const name = svelteNode.name;
const nameEnd = svelteNode.start + 1 + name.length;
const isWhitespaceAfterStartTag = document.getText().slice(nameEnd, originalOffset).trim() === '' &&
this.mightBeAtStartTagWhitespace(document, originalOffset);
if (isWhitespaceAfterStartTag) {
// We can be sure only to get completions for directives and props here
// so don't bother with the expensive global completions
return this.getCompletionListForDirectiveOrProps(attributeContext, componentInfo, wordInfo.defaultTextEditRange, eventAndSlotLetCompletions, tsDoc);
}
}
const response = lang.getCompletionsAtPosition(filePath, offset, {
...userPreferences,
triggerCharacter: validTriggerCharacter
}, formatSettings);
const commitCharactersOptions = this.getCommitCharactersOptions(response, tsDoc, position);
let completions = response?.entries || [];
const customCompletions = eventAndSlotLetCompletions.concat(tagCompletions ?? []);
if (completions.length === 0 && customCompletions.length === 0) {
return tsDoc.parserError ? vscode_languageserver_1.CompletionList.create([], true) : null;
}
if (completions.length > 500 &&
svelteNode?.type === 'Element' &&
completions[0].kind !== typescript_1.default.ScriptElementKind.memberVariableElement) {
// False global completions inside element start tag
return null;
}
if (completions.length > 500 &&
svelteNode?.type === 'InlineComponent' &&
this.mightBeAtStartTagWhitespace(document, originalOffset)) {
// Very likely false global completions inside component start tag -> narrow
return this.getCompletionListForDirectiveOrProps(attributeContext, componentInfo, wordInfo.defaultTextEditRange, eventAndSlotLetCompletions, tsDoc);
}
// moved here due to perf reasons
const existingImports = this.getExistingImports(document);
const fileUrl = (0, utils_1.pathToUrl)(tsDoc.filePath);
const isCompletionInTag = (0, svelte_ast_utils_1.isInTag)(svelteNode, originalOffset);
const isHandlerCompletion = svelteNode?.type === 'EventHandler' && svelteNode.parent?.type === 'Element';
const preferComponents = wordInfo.word[0] === '<' || inScript;
const completionItems = customCompletions;
const isValidCompletion = createIsValidCompletion(document, position, !!tsDoc.parserError);
const addCompletion = (entry, asStore) => {
if (isValidCompletion(entry)) {
let completion = this.toCompletionItem(tsDoc, entry, fileUrl, position, isCompletionInTag, commitCharactersOptions, asStore, existingImports, preferComponents);
if (completion) {
completionItems.push(this.fixTextEditRange(wordInfo.range, (0, documents_1.mapCompletionItemToOriginal)(tsDoc, completion), isHandlerCompletion, completion.textEdit, tsDoc));
}
}
};
// If completion is about a store which is not imported yet, do another
// completion request at the beginning of the file to get all global
// import completions and then filter them down to likely matches.
if (wordInfo.word.charAt(0) === '$') {
const storeName = wordInfo.word.substring(1);
const text = '__sveltets_2_store_get(' + storeName;
if (!tsDoc.getFullText().includes(text)) {
const pos = (tsDoc.scriptInfo || tsDoc.moduleScriptInfo)?.endPos ?? {
line: 0,
character: 0
};
const virtualOffset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(pos));
const storeCompletions = lang.getCompletionsAtPosition(filePath, virtualOffset, {
...userPreferences,
triggerCharacter: validTriggerCharacter
}, formatSettings);
for (const entry of storeCompletions?.entries || []) {
if (entry.name.startsWith(storeName)) {
addCompletion(entry, true);
}
}
}
}
for (const entry of completions) {
addCompletion(entry, false);
}
// Add ./$types imports for SvelteKit since TypeScript is bad at it
if ((0, path_1.basename)(filePath).startsWith('+')) {
const $typeImports = new Map();
for (const c of completionItems) {
if ((0, utils_3.isKitTypePath)(c.data?.source)) {
$typeImports.set(c.label, c);
}
}
for (const $typeImport of $typeImports.values()) {
// resolve path from filePath to svelte-kit/types
// src/routes/foo/+page.svelte -> .svelte-kit/types/foo/$types.d.ts
const routesFolder = document.config?.kit?.files?.routes || 'src/routes';
const relativeFileName = filePath.split(routesFolder)[1]?.slice(1);
if (relativeFileName) {
const relativePath = (0, path_1.dirname)(relativeFileName) === '.' ? '' : `${(0, path_1.dirname)(relativeFileName)}/`;
const modifiedSource = $typeImport.data.source.split('.svelte-kit/types')[0] +
// note the missing .d.ts at the end - TS wants it that way for some reason
`.svelte-kit/types/${routesFolder}/${relativePath}$types`;
completionItems.push({
...$typeImport,
// Ensure it's sorted above the other imports
sortText: !isNaN(Number($typeImport.sortText))
? String(Number($typeImport.sortText) - 1)
: $typeImport.sortText,
data: {
...$typeImport.data,
__is_sveltekit$typeImport: true,
source: modifiedSource,
data: undefined
}
});
}
}
}
const completionList = vscode_languageserver_1.CompletionList.create(completionItems, !!tsDoc.parserError);
if (commitCharactersOptions.checkCommitCharacters &&
commitCharactersOptions.defaultCommitCharacters?.length) {
const clientSupportsItemsDefault = this.configManager
.getClientCapabilities()
?.textDocument?.completion?.completionList?.itemDefaults?.includes('commitCharacters');
if (clientSupportsItemsDefault) {
completionList.itemDefaults = {
commitCharacters: commitCharactersOptions.defaultCommitCharacters
};
}
else {
completionList.items.forEach((item) => {
item.commitCharacters ??= commitCharactersOptions.defaultCommitCharacters;
});
}
}
this.lastCompletion = { key: document.getFilePath() || '', position, completionList };
return completionList;
}
getWordAtPosition(document, offset) {
const wordRange = (0, documents_1.getWordRangeAt)(document.getText(), offset, {
left: /[^\s.]+$/,
right: /[^\w$:]/
});
const range = vscode_languageserver_1.Range.create(document.positionAt(wordRange.start), document.positionAt(wordRange.end));
return {
wordRange,
word: document.getText().slice(wordRange.start, wordRange.end),
range,
defaultTextEditRange: wordRange.start === wordRange.end ? undefined : range
};
}
mightBeAtStartTagWhitespace(document, originalOffset) {
return /\s[\s>/]/.test(document.getText().substring(originalOffset - 1, originalOffset + 1));
}
canReuseLastCompletion(lastCompletion, triggerKind, triggerCharacter, document, position) {
return (!!lastCompletion &&
lastCompletion.key === document.getFilePath() &&
lastCompletion.position.line === position.line &&
((Math.abs(lastCompletion.position.character - position.character) < 2 &&
(triggerKind === vscode_languageserver_1.CompletionTriggerKind.TriggerForIncompleteCompletions ||
// Special case: `.` is a trigger character, but inside import path completions
// it shouldn't trigger another completion because we can reuse the old one
(triggerCharacter === '.' &&
(0, utils_3.isPartOfImportStatement)(document.getText(), position)))) ||
// `let:` or `on:` -> up to 3 previous characters allowed
(Math.abs(lastCompletion.position.character - position.character) < 4 &&
triggerCharacter === ':' &&
!!(0, documents_1.getNodeIfIsInStartTag)(document.html, document.offsetAt(position)))));
}
getExistingImports(document) {
const rawImports = (0, utils_1.getRegExpMatches)(scriptImportRegex, document.getText()).map((match) => (match[1] ?? match[2]).split(','));
const tidiedImports = (0, utils_1.flatten)(rawImports).map((match) => match.trim());
return new Set(tidiedImports);
}
getEventAndSlotLetCompletions(componentInfo, attributeContext, defaultTextEditRange) {
if (componentInfo === null) {
return [];
}
if (attributeContext?.inValue) {
return [];
}
return [
...componentInfo
.getEvents()
.map((event) => this.componentInfoToCompletionEntry(event, 'on:', undefined, defaultTextEditRange)),
...componentInfo
.getSlotLets()
.map((slot) => this.componentInfoToCompletionEntry(slot, 'let:', undefined, defaultTextEditRange))
];
}
getCustomElementCompletions(lang, lsContainer, document, tsDoc, position) {
const offset = document.offsetAt(position);
const tag = (0, documents_1.getNodeIfIsInHTMLStartTag)(document.html, offset);
if (!tag) {
return;
}
const tagNameEnd = tag.start + 1 + (tag.tag?.length ?? 0);
if (offset > tagNameEnd) {
return;
}
const program = lang.getProgram();
const sourceFile = program?.getSourceFile(tsDoc.filePath);
const typeChecker = program?.getTypeChecker();
if (!typeChecker || !sourceFile) {
return;
}
const typingsNamespace = lsContainer.getTsConfigSvelteOptions().namespace;
const typingsNamespaceSymbol = this.findTypingsNamespaceSymbol(typingsNamespace, typeChecker, sourceFile);
if (!typingsNamespaceSymbol) {
return;
}
const elements = typeChecker
.getExportsOfModule(typingsNamespaceSymbol)
.find((symbol) => symbol.name === 'IntrinsicElements');
if (!elements || !(elements.flags & typescript_1.default.SymbolFlags.Interface)) {
return;
}
let tagNames = typeChecker
.getDeclaredTypeOfSymbol(elements)
.getProperties()
.map((p) => typescript_1.default.symbolName(p));
if (tagNames.length && tag.tag) {
tagNames = tagNames.filter((name) => name.startsWith(tag.tag ?? ''));
}
const replacementRange = (0, documents_1.toRange)(document, tag.start + 1, tagNameEnd);
return tagNames.map((name) => ({
label: name,
kind: vscode_languageserver_1.CompletionItemKind.Property,
textEdit: vscode_languageserver_1.TextEdit.replace(this.cloneRange(replacementRange), name),
commitCharacters: []
}));
}
findTypingsNamespaceSymbol(namespaceExpression, typeChecker, sourceFile) {
if (!namespaceExpression || typeof namespaceExpression !== 'string') {
return;
}
const [first, ...rest] = namespaceExpression.split('.');
let symbol = typeChecker
.getSymbolsInScope(sourceFile, typescript_1.default.SymbolFlags.Namespace)
.find((symbol) => symbol.name === first);
for (const part of rest) {
if (!symbol) {
return;
}
symbol = typeChecker.getExportsOfModule(symbol).find((symbol) => symbol.name === part);
}
return symbol;
}
componentInfoToCompletionEntry(info, prefix, kind, defaultTextEditRange) {
const name = prefix + info.name;
return {
label: name,
kind,
sortText: '-1',
detail: info.name + ': ' + info.type,
documentation: info.doc && { kind: vscode_languageserver_1.MarkupKind.Markdown, value: info.doc },
commitCharacters: [],
textEdit: defaultTextEditRange
? vscode_languageserver_1.TextEdit.replace(this.cloneRange(defaultTextEditRange), name)
: undefined
};
}
cloneRange(range) {
return vscode_languageserver_1.Range.create(vscode_languageserver_1.Position.create(range.start.line, range.start.character), vscode_languageserver_1.Position.create(range.end.line, range.end.character));
}
getCompletionListForDirectiveOrProps(attributeContext, componentInfo, defaultTextEditRange, eventAndSlotLetCompletions, tsDoc) {
const props = (!attributeContext?.inValue &&
componentInfo
?.getProps()
.map((entry) => this.componentInfoToCompletionEntry(entry, '', vscode_languageserver_1.CompletionItemKind.Field, defaultTextEditRange))) ||
[];
return vscode_languageserver_1.CompletionList.create([...eventAndSlotLetCompletions, ...props], !!tsDoc.parserError);
}
toCompletionItem(snapshot, comp, uri, position, isCompletionInTag, commitCharactersOptions, asStore, existingImports, preferComponents) {
const completionLabelAndInsert = this.getCompletionLabelAndInsert(snapshot, comp);
if (!completionLabelAndInsert) {
return null;
}
let { label, insertText, isSvelteComp, isRunesCompletion, replacementSpan } = completionLabelAndInsert;
// TS may suggest another Svelte component even if there already exists an import
// with the same name, because under the hood every Svelte component is postfixed
// with `__SvelteComponent`. In this case, filter out this completion by returning null.
if (isSvelteComp && existingImports.has(label)) {
return null;
}
// Remove wrong quotes, for example when using --css-props
if (isCompletionInTag &&
!insertText &&
label[0] === '"' &&
label[label.length - 1] === '"') {
label = label.slice(1, -1);
}
else if (asStore) {
// only modify label, so that the data property is untouched, which is important so the resolving still works
label = `$${label}`;
}
const textEdit = replacementSpan
? vscode_languageserver_1.TextEdit.replace((0, utils_2.convertRange)(snapshot, replacementSpan), insertText ?? label)
: undefined;
const labelDetails = comp.labelDetails ??
(comp.sourceDisplay
? {
description: typescript_1.default.displayPartsToString(comp.sourceDisplay)
}
: undefined);
return {
label,
insertText,
kind: (0, utils_2.scriptElementKindToCompletionItemKind)(comp.kind),
commitCharacters: this.getCommitCharacters(comp, commitCharactersOptions, isSvelteComp),
// Make sure svelte component and runes take precedence
sortText: preferComponents && (isRunesCompletion || isSvelteComp) ? '-1' : comp.sortText,
preselect: preferComponents && (isRunesCompletion || isSvelteComp) ? true : comp.isRecommended,
insertTextFormat: comp.isSnippet ? vscode_languageserver_1.InsertTextFormat.Snippet : undefined,
labelDetails,
textEdit,
// pass essential data for resolving completion
data: {
name: comp.name,
source: comp.source,
data: comp.data,
uri,
position
}
};
}
getCompletionLabelAndInsert(snapshot, comp) {
let { name, insertText, kindModifiers } = comp;
const isScriptElement = comp.kind === typescript_1.default.ScriptElementKind.scriptElement;
const hasModifier = Boolean(comp.kindModifiers);
const isRunesCompletion = name === '$props' || name === '$state' || name === '$derived' || name === '$effect';
const isSvelteComp = !isRunesCompletion && (0, utils_2.isGeneratedSvelteComponentName)(name);
if (isSvelteComp) {
name = (0, utils_2.changeSvelteComponentName)(name);
if (this.isExistingSvelteComponentImport(snapshot, name, comp.source)) {
return null;
}
}
if (isScriptElement && hasModifier) {
const label = kindModifiers && !name.endsWith(kindModifiers) ? name + kindModifiers : name;
return {
insertText: name,
label,
isSvelteComp,
isRunesCompletion
};
}
if (comp.replacementSpan) {
return {
label: name,
isSvelteComp,
isRunesCompletion,
insertText: insertText ? (0, utils_2.changeSvelteComponentName)(insertText) : undefined,
replacementSpan: comp.replacementSpan
};
}
return {
label: name,
insertText,
isSvelteComp,
isRunesCompletion
};
}
getCommitCharactersOptions(response, tsDoc, position) {
if ((!(0, utils_2.isInScript)(position, tsDoc) && tsDoc.parserError) || !response) {
return {
checkCommitCharacters: false
};
}
const isNewIdentifierLocation = response.isNewIdentifierLocation;
// TypeScript 5.7+ reused the same array for different completions
let defaultCommitCharacters = response.defaultCommitCharacters
? Array.from(response.defaultCommitCharacters)
: undefined;
if (!isNewIdentifierLocation) {
// This actually always exists although it's optional in the type, at least in ts 5.6,
// so our commit characters are mostly fallback for older ts versions
if (defaultCommitCharacters) {
// this is controlled by a vscode setting that isn't available in the ts server so it isn't added to the language service
defaultCommitCharacters?.push('(');
}
else {
defaultCommitCharacters = this.commitCharacters;
}
}
return {
checkCommitCharacters: true,
defaultCommitCharacters,
isNewIdentifierLocation
};
}
getCommitCharacters(entry, options, isSvelteComp) {
// Because Svelte components take precedence, we leave out commit characters to not auto complete
// in weird places (e.g. when you have foo.filter(a => a)) and get autocomplete for component A,
// then a commit character of `.` would auto import the component which is not what we want
if (isSvelteComp) {
return ['>'];
}
// https://github.com/microsoft/vscode/blob/d012408e88ffabd6456c367df4d343654da2eb10/extensions/typescript-language-features/src/languageFeatures/completions.ts#L504
if (!options.checkCommitCharacters) {
return undefined;
}
const commitCharacters = entry.commitCharacters;
// Ambient JS word based suggestions
const skipCommitCharacters = entry.kind === typescript_1.default.ScriptElementKind.warning ||
entry.kind === typescript_1.default.ScriptElementKind.string;
if (commitCharacters) {
if (!options.isNewIdentifierLocation && !skipCommitCharacters) {
return commitCharacters.concat('(');
}
return commitCharacters;
}
return skipCommitCharacters ? [] : undefined;
}
isExistingSvelteComponentImport(snapshot, name, source) {
const importStatement = new RegExp(`import ${name} from ["'\`][\\s\\S]+\\.svelte["'\`]`);
return !!source && !!snapshot.getFullText().match(importStatement);
}
fixTextEditRange(wordRange, completionItem, isHandlerCompletion, generatedTextEdit, tsDoc) {
if (isHandlerCompletion && completionItem.label.startsWith('on:')) {
completionItem.textEdit = vscode_languageserver_1.TextEdit.replace(this.cloneRange(wordRange), completionItem.label);
return completionItem;
}
const { textEdit } = completionItem;
if (!textEdit || !vscode_languageserver_1.TextEdit.is(textEdit)) {
return completionItem;
}
if (vscode_languageserver_1.TextEdit.is(generatedTextEdit)) {
(0, utils_3.checkRangeMappingWithGeneratedSemi)(textEdit.range, generatedTextEdit.range, tsDoc);
}
const { newText, range: { start } } = textEdit;
//If the textEdit is out of the word range of the triggered position
// vscode would refuse to show the completions
// split those edits into additionalTextEdit to fix it
if (start.line !== wordRange.start.line || start.character > wordRange.start.character) {
return completionItem;
}
textEdit.newText = newText.substring(wordRange.start.character - start.character);
textEdit.range.start = {
line: start.line,
character: wordRange.start.character
};
completionItem.additionalTextEdits = [
vscode_languageserver_1.TextEdit.replace({
start,
end: {
line: start.line,
character: wordRange.start.character
}
}, newText.substring(0, wordRange.start.character - start.character))
];
return completionItem;
}
/**
* TypeScript throws a debug assertion error if the importModuleSpecifierEnding config is
* 'js' and there's an unknown file extension - which is the case for `.svelte`. Therefore
* rewrite the importModuleSpecifierEnding for this case to silence the error.
*/
fixUserPreferencesForSvelteComponentImport(userPreferences) {
if (userPreferences.importModuleSpecifierEnding === 'js') {
return {
...userPreferences,
importModuleSpecifierEnding: 'index'
};
}
return userPreferences;
}
async resolveCompletion(document, completionItem, cancellationToken) {
const { data: comp } = completionItem;
const { tsDoc, lang, userPreferences } = await this.lsAndTsDocResolver.getLSAndTSDoc(document);
const filePath = tsDoc.filePath;
const formatCodeOptions = await this.configManager.getFormatCodeSettingsForFile(document, tsDoc.scriptKind);
if (!comp || !filePath || cancellationToken?.isCancellationRequested) {
return completionItem;
}
const is$typeImport = !!comp.__is_sveltekit$typeImport;
const errorPreventingUserPreferences = comp.source?.endsWith('.svelte')
? this.fixUserPreferencesForSvelteComponentImport(userPreferences)
: userPreferences;
const detail = lang.getCompletionEntryDetails(filePath, tsDoc.offsetAt(tsDoc.getGeneratedPosition(comp.position)), comp.name, formatCodeOptions, comp.source, errorPreventingUserPreferences, comp.data);
if (detail) {
const { detail: itemDetail, documentation: itemDocumentation } = this.getCompletionDocument(tsDoc, detail, is$typeImport);
// VSCode + tsserver won't have this pop-in effect
// because tsserver has internal APIs for caching
// TODO: consider if we should adopt the internal APIs
if (detail.sourceDisplay && !completionItem.labelDetails) {
completionItem.labelDetails = {
description: typescript_1.default.displayPartsToString(detail.sourceDisplay)
};
}
completionItem.detail = itemDetail;
completionItem.documentation = itemDocumentation;
}
const actions = detail?.codeActions;
const isImport = !!detail?.source;
if (actions) {
const edit = [];
const formatCodeBasis = (0, utils_3.getFormatCodeBasis)(formatCodeOptions);
for (const action of actions) {
for (const change of action.changes) {
edit.push(...this.codeActionChangesToTextEdit(document, tsDoc, change, isImport, comp.position, formatCodeBasis.newLine, is$typeImport));
}
}
completionItem.additionalTextEdits = (completionItem.additionalTextEdits ?? []).concat(edit);
}
return completionItem;
}
getCompletionDocument(tsDoc, compDetail, is$typeImport) {
const { sourceDisplay, documentation: tsDocumentation, displayParts, tags } = compDetail;
let parts = compDetail.codeActions?.map((codeAction) => codeAction.description) ?? [];
if (sourceDisplay && is$typeImport) {
const importPath = typescript_1.default.displayPartsToString(sourceDisplay);
// Take into account Node16 moduleResolution
parts = parts.map((detail) => detail.replace(importPath, `'./$types${importPath.endsWith('.js') ? '.js' : ''}'`));
}
let text = (0, utils_2.changeSvelteComponentName)(typescript_1.default.displayPartsToString(displayParts));
if (tsDoc.isSvelte5Plus && text.includes('(alias)')) {
// The info contains both the const and type export along with a bunch of gibberish we want to hide
if (text.includes('__SvelteComponent_')) {
// import - remove completely
text = '';
}
else if (text.includes('__sveltets_2_IsomorphicComponent')) {
// already imported - only keep the last part
text = text.substring(text.lastIndexOf('import'));
}
}
parts.push(text);
const markdownDoc = (0, previewer_1.getMarkdownDocumentation)(tsDocumentation, tags);
const documentation = markdownDoc
? { value: markdownDoc, kind: vscode_languageserver_1.MarkupKind.Markdown }
: undefined;
return {
documentation,
detail: parts.filter(Boolean).join('\n\n')
};
}
codeActionChangesToTextEdit(doc, snapshot, changes, isImport, originalTriggerPosition, newLine, is$typeImport) {
return changes.textChanges.map((change) => this.codeActionChangeToTextEdit(doc, snapshot, change, isImport, originalTriggerPosition, newLine, is$typeImport));
}
codeActionChangeToTextEdit(doc, snapshot, change, isImport, originalTriggerPosition, newLine, is$typeImport, isCombinedCodeAction) {
change.newText = isCombinedCodeAction
? (0, utils_1.modifyLines)(change.newText, (line) => this.fixImportNewText(line, (0, utils_2.isInScript)(originalTriggerPosition, doc), is$typeImport))
: this.fixImportNewText(change.newText, (0, utils_2.isInScript)(originalTriggerPosition, doc), is$typeImport);
const scriptTagInfo = snapshot.scriptInfo || snapshot.moduleScriptInfo;
// no script tag defined yet, add it.
if (!scriptTagInfo) {
if (isCombinedCodeAction) {
return vscode_languageserver_1.TextEdit.insert(vscode_languageserver_1.Position.create(0, 0), change.newText);
}
const config = this.configManager.getConfig();
// Remove the empty line after the script tag because getNewScriptStartTag will always add one
let newText = change.newText;
if (newText[0] === '\r') {
newText = newText.substring(1);
}
if (newText[0] === '\n') {
newText = newText.substring(1);
}
return vscode_languageserver_1.TextEdit.replace(beginOfDocumentRange, `${(0, utils_3.getNewScriptStartTag)(config, newLine)}${newText}</script>${newLine}`);
}
const { span } = change;
const virtualRange = (0, utils_2.convertRange)(snapshot, span);
let range;
const isNewImport = isImport && virtualRange.start.character === 0;
// Since new import always can't be mapped, we'll have special treatment here
// but only hack this when there is multiple line in script
if (isNewImport && virtualRange.start.line > 1) {
range = this.mapRangeForNewImport(snapshot, virtualRange);
}
else {
range = (0, documents_1.mapRangeToOriginal)(snapshot, virtualRange);
}
// If range is somehow not mapped in parent,
// the import is mapped wrong or is outside script tag,
// use script starting point instead.
// This happens among other things if the completion is the first import of the file.
if (range.start.line === -1 ||
(range.start.line === 0 && range.start.character <= 1 && span.length === 0) ||
!(0, utils_2.isInScript)(range.start, snapshot)) {
range = (0, utils_2.convertRange)(doc, {
start: (0, documents_1.isInTag)(originalTriggerPosition, doc.scriptInfo)
? snapshot.scriptInfo?.start || scriptTagInfo.start
: (0, documents_1.isInTag)(originalTriggerPosition, doc.moduleScriptInfo)
? snapshot.moduleScriptInfo?.start || scriptTagInfo.start
: scriptTagInfo.start,
length: span.length
});
}
// prevent newText from being placed like this: <script>import {} from ''
const editOffset = doc.offsetAt(range.start);
if ((editOffset === snapshot.scriptInfo?.start ||
editOffset === snapshot.moduleScriptInfo?.start) &&
!change.newText.startsWith('\r\n') &&
!change.newText.startsWith('\n')) {
change.newText = newLine + change.newText;
}
const after = doc.getText().slice(doc.offsetAt(range.end));
// typescript add empty line after import when the generated ts file
// doesn't have new line at the start of the file
if (after.startsWith('\r\n') || after.startsWith('\n')) {
change.newText = change.newText.trimEnd() + newLine;
}
return vscode_languageserver_1.TextEdit.replace(range, change.newText);
}
mapRangeForNewImport(snapshot, virtualRange) {
const sourceMappableRange = this.offsetLinesAndMovetoStartOfLine(virtualRange, -1);
const mappableRange = (0, documents_1.mapRangeToOriginal)(snapshot, sourceMappableRange);
return this.offsetLinesAndMovetoStartOfLine(mappableRange, 1);
}
offsetLinesAndMovetoStartOfLine({ start, end }, offsetLines) {
return vscode_languageserver_1.Range.create(vscode_languageserver_1.Position.create(start.line + offsetLines, 0), vscode_languageserver_1.Position.create(end.line + offsetLines, 0));
}
fixImportNewText(importText, actionTriggeredInScript, is$typeImport) {
if (is$typeImport && importText.trim().startsWith('import ')) {
// Take into account Node16 moduleResolution
return importText.replace(/(['"])(.+?)['"]/, (_match, quote, path) => `${quote}./$types${path.endsWith('.js') ? '.js' : ''}${quote}`);
}
const changedName = (0, utils_2.changeSvelteComponentName)(importText);
if (importText !== changedName || !actionTriggeredInScript) {
// For some reason, TS sometimes adds the `type` modifier. Remove it
// in case of Svelte component imports or if import triggered from markup.
return changedName.replace(' type ', ' ');
}
return importText;
}
}
exports.CompletionsProviderImpl = CompletionsProviderImpl;
const beginOfDocumentRange = vscode_languageserver_1.Range.create(vscode_languageserver_1.Position.create(0, 0), vscode_languageserver_1.Position.create(0, 0));
// `import {...} from '..'` or `import ... from '..'`
// Note: Does not take into account if import is within a comment.
const scriptImportRegex = /\bimport\s+{([^}]*?)}\s+?from\s+['"`].+?['"`]|\bimport\s+(\w+?)\s+from\s+['"`].+?['"`]/g;
// Type definitions from svelte-shims.d.ts that shouldn't appear in completion suggestions
// because they are meant to be used "behind the scenes"
const svelte2tsxTypes = new Set([
'Svelte2TsxComponent',
'Svelte2TsxComponentConstructorParameters',
'SvelteComponentConstructor',
'SvelteActionReturnType',
'SvelteTransitionConfig',
'SvelteTransitionReturnType',
'SvelteAnimationReturnType',
'SvelteWithOptionalProps',
'SvelteAllProps',
'SveltePropsAnyFallback',
'SvelteSlotsAnyFallback',
'SvelteRestProps',
'SvelteSlots',
'SvelteStore'
]);
const startsWithUppercase = /^[A-Z]/;
function createIsValidCompletion(document, position, hasParserError) {
// Make fallback completions for tags inside the template a bit better
const isAtStartTag = !(0, documents_1.isInTag)(position, document.scriptInfo) &&
/<\w*$/.test(document.getText(vscode_languageserver_1.Range.create(position.line, 0, position.line, position.character)));
const noWrongCompletionAtStartTag = isAtStartTag && hasParserError
? (value) => startsWithUppercase.test(value.name)
: () => true;
const isNoSvelte2tsxCompletion = (value) => {
if (value.kindModifiers === 'declare') {
return !value.name.startsWith('__sveltets_') && !svelte2tsxTypes.has(value.name);
}
return !value.name.startsWith('$$_');
};
const isCompletionInHTMLStartTag = !!(0, documents_1.getNodeIfIsInHTMLStartTag)(document.html, document.offsetAt(position));
if (!isCompletionInHTMLStartTag) {
return isNoSvelte2tsxCompletion;
}
// TODO with the new transformation this is ts.ScriptElementKind.memberVariableElement
// which is also true for all properties of any other object -> how reliably filter this out?
// ---> another /*ignore*/ pragma?
// ---> OR: make these lower priority if we find out they are inside a html start tag
return (value) => isNoSvelte2tsxCompletion(value) && noWrongCompletionAtStartTag(value);
}
//# sourceMappingURL=CompletionProvider.js.map