UNPKG

svelte-language-server

Version:
794 lines 41.6 kB
"use strict"; 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