From 35d05793d92b2dec44f4a74945e9c9d571aa358b Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 29 Aug 2022 10:25:29 +0200 Subject: [PATCH 1/2] (fix) better $types completions Add them as a distinct item to the list of all possible completions instead of manipulating an existing one --- .../typescript/features/CompletionProvider.ts | 82 +++++++++++-------- .../src/language-service/completions.ts | 80 ++++++++++-------- 2 files changed, 94 insertions(+), 68 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts index ef0db3d9b..2b19cf7d1 100644 --- a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts @@ -44,6 +44,7 @@ import { findContainingNode, getComponentAtPosition, isPartOfImportStatement } f export interface CompletionEntryWithIdentifier extends ts.CompletionEntry, TextDocumentIdentifier { position: Position; + __is_sveltekit$typeImport?: boolean; } type validTriggerCharacter = '.' | '"' | "'" | '`' | '/' | '@' | '<' | '#'; @@ -257,6 +258,42 @@ export class CompletionsProviderImpl implements CompletionsProvider this.fixTextEditRange(wordRangeStartPosition, comp)) .concat(eventAndSlotLetCompletions); + // Add ./$types imports for SvelteKit since TypeScript is bad at it + if (basename(filePath).startsWith('+')) { + const $typeImports = new Map(); + for (const c of completionItems) { + if (c.data.source?.includes('.svelte-kit/types')) { + $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 relativeFilePath = filePath.split(routesFolder)[1]?.slice(1); + if (relativeFilePath) { + 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: + $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}/${dirname( + relativeFilePath + )}/$types`, + data: undefined + } + }); + } + } + } + const completionList = CompletionList.create(completionItems, !!tsDoc.parserError); this.lastCompletion = { key: document.getFilePath() || '', position, completionList }; @@ -527,44 +564,21 @@ export class CompletionsProviderImpl implements CompletionsProvider .svelte-kit/types/foo/$types.d.ts - const routesFolder = document.config?.kit?.files?.routes || 'src/routes'; - const relativeFilePath = filePath.split(routesFolder)[1]?.slice(1); - if (relativeFilePath) { - is$typeImport = true; - comp.source = - comp.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}/${dirname(relativeFilePath)}/$types`; - comp.data = undefined; - } - } - - const getDetail = () => - lang.getCompletionEntryDetails( - filePath, - tsDoc.offsetAt(tsDoc.getGeneratedPosition(comp!.position)), - comp!.name, - {}, - comp!.source, - errorPreventingUserPreferences, - comp!.data - ); - let detail = getDetail(); - if (!detail && is$typeImport) { - // try again - is$typeImport = false; - comp = originalComp; - detail = getDetail(); - } + const detail = lang.getCompletionEntryDetails( + filePath, + tsDoc.offsetAt(tsDoc.getGeneratedPosition(comp!.position)), + comp!.name, + {}, + comp!.source, + errorPreventingUserPreferences, + comp!.data + ); if (detail) { const { detail: itemDetail, documentation: itemDocumentation } = diff --git a/packages/typescript-plugin/src/language-service/completions.ts b/packages/typescript-plugin/src/language-service/completions.ts index 1aceb2f62..9f9b46d0e 100644 --- a/packages/typescript-plugin/src/language-service/completions.ts +++ b/packages/typescript-plugin/src/language-service/completions.ts @@ -12,6 +12,50 @@ export function decorateCompletions(ls: ts.LanguageService, logger: Logger): voi if (!completions) { return completions; } + + // Add ./$types imports for SvelteKit since TypeScript is bad at it + if (basename(fileName).startsWith('+')) { + const $typeImports = new Map(); + for (const c of completions.entries) { + if (c.source?.includes('.svelte-kit/types') && c.data) { + $typeImports.set(c.name, c); + } + } + for (const $typeImport of $typeImports.values()) { + // resolve path from FileName to svelte-kit/types + // src/routes/foo/+page.svelte -> .svelte-kit/types/foo/$types.d.ts + const routesFolder = 'src/routes'; // TODO somehow get access to kit.files.routes in here + const relativeFileName = fileName.split(routesFolder)[1]?.slice(1); + + if (relativeFileName) { + const modifiedSource = + $typeImport.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}/${dirname(relativeFileName)}/$types`; + completions.entries.push({ + ...$typeImport, + // Ensure it's sorted above the other imports + sortText: !isNaN(Number($typeImport.sortText)) + ? String(Number($typeImport.sortText) - 1) + : $typeImport.sortText, + source: modifiedSource, + data: { + ...$typeImport.data, + fileName: $typeImport.data!.fileName?.replace( + $typeImport.source!, + modifiedSource + ), + moduleSpecifier: $typeImport.data!.moduleSpecifier?.replace( + $typeImport.source!, + modifiedSource + ), + __is_sveltekit$typeImport: true + } as any + }); + } + } + } + return { ...completions, entries: completions.entries.map((entry) => { @@ -39,28 +83,9 @@ export function decorateCompletions(ls: ts.LanguageService, logger: Logger): voi preferences, data ) => { - let is$typeImport = false; - const originalSource = source; - const originalData = data ? { ...data } : undefined; - if (basename(fileName).startsWith('+') && source?.includes('.svelte-kit/types')) { - // resolve path from FileName to svelte-kit/types - // src/routes/foo/+page.svelte -> .svelte-kit/types/foo/$types.d.ts - const routesFolder = 'src/routes'; // TODO somehow get access to kit.files.routes in here - const relativeFileName = fileName.split(routesFolder)[1]?.slice(1); - if (relativeFileName) { - is$typeImport = true; - source = - 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}/${dirname(relativeFileName)}/$types`; - if (data) { - data.fileName = data.fileName?.replace(originalSource!, source); - data.moduleSpecifier = data.moduleSpecifier?.replace(originalSource!, source); - } - } - } + const is$typeImport = (data as any).__is_sveltekit$typeImport; - let details = getCompletionEntryDetails( + const details = getCompletionEntryDetails( fileName, position, entryName, @@ -69,19 +94,6 @@ export function decorateCompletions(ls: ts.LanguageService, logger: Logger): voi preferences, data ); - if (!details && is$typeImport) { - // Try again - is$typeImport = false; - details = getCompletionEntryDetails( - fileName, - position, - entryName, - formatOptions, - originalSource, - preferences, - originalData - ); - } if (details) { if (is$typeImport) { From 22b0a662d9d974bbfc87779fd84af537683bc612 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 29 Aug 2022 10:33:46 +0200 Subject: [PATCH 2/2] lint --- .../src/plugins/typescript/features/CompletionProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts index 2b19cf7d1..f7aed51eb 100644 --- a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts @@ -553,7 +553,7 @@ export class CompletionsProviderImpl implements CompletionsProvider, cancellationToken?: CancellationToken ): Promise> { - let { data: comp } = completionItem; + const { data: comp } = completionItem; const { tsDoc, lang, userPreferences } = await this.lsAndTsDocResolver.getLSAndTSDoc( document );