Skip to content

Commit a554ff9

Browse files
authored
(fix) better $types completions (#1609)
Add them as a distinct item to the list of all possible completions instead of manipulating an existing one
1 parent 15dcfd1 commit a554ff9

File tree

2 files changed

+95
-69
lines changed

2 files changed

+95
-69
lines changed

packages/language-server/src/plugins/typescript/features/CompletionProvider.ts

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { findContainingNode, getComponentAtPosition, isPartOfImportStatement } f
4444

4545
export interface CompletionEntryWithIdentifier extends ts.CompletionEntry, TextDocumentIdentifier {
4646
position: Position;
47+
__is_sveltekit$typeImport?: boolean;
4748
}
4849

4950
type validTriggerCharacter = '.' | '"' | "'" | '`' | '/' | '@' | '<' | '#';
@@ -257,6 +258,42 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
257258
.map((comp) => this.fixTextEditRange(wordRangeStartPosition, comp))
258259
.concat(eventAndSlotLetCompletions);
259260

261+
// Add ./$types imports for SvelteKit since TypeScript is bad at it
262+
if (basename(filePath).startsWith('+')) {
263+
const $typeImports = new Map<string, CompletionItem>();
264+
for (const c of completionItems) {
265+
if (c.data.source?.includes('.svelte-kit/types')) {
266+
$typeImports.set(c.label, c);
267+
}
268+
}
269+
for (const $typeImport of $typeImports.values()) {
270+
// resolve path from filePath to svelte-kit/types
271+
// src/routes/foo/+page.svelte -> .svelte-kit/types/foo/$types.d.ts
272+
const routesFolder = document.config?.kit?.files?.routes || 'src/routes';
273+
const relativeFilePath = filePath.split(routesFolder)[1]?.slice(1);
274+
if (relativeFilePath) {
275+
completionItems.push({
276+
...$typeImport,
277+
// Ensure it's sorted above the other imports
278+
sortText: !isNaN(Number($typeImport.sortText))
279+
? String(Number($typeImport.sortText) - 1)
280+
: $typeImport.sortText,
281+
data: {
282+
...$typeImport.data,
283+
__is_sveltekit$typeImport: true,
284+
source:
285+
$typeImport.data.source.split('.svelte-kit/types')[0] +
286+
// note the missing .d.ts at the end - TS wants it that way for some reason
287+
`.svelte-kit/types/${routesFolder}/${dirname(
288+
relativeFilePath
289+
)}/$types`,
290+
data: undefined
291+
}
292+
});
293+
}
294+
}
295+
}
296+
260297
const completionList = CompletionList.create(completionItems, !!tsDoc.parserError);
261298
this.lastCompletion = { key: document.getFilePath() || '', position, completionList };
262299

@@ -516,7 +553,7 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
516553
completionItem: AppCompletionItem<CompletionEntryWithIdentifier>,
517554
cancellationToken?: CancellationToken
518555
): Promise<AppCompletionItem<CompletionEntryWithIdentifier>> {
519-
let { data: comp } = completionItem;
556+
const { data: comp } = completionItem;
520557
const { tsDoc, lang, userPreferences } = await this.lsAndTsDocResolver.getLSAndTSDoc(
521558
document
522559
);
@@ -527,44 +564,21 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
527564
return completionItem;
528565
}
529566

567+
const is$typeImport = !!comp.__is_sveltekit$typeImport;
568+
530569
const errorPreventingUserPreferences = comp.source?.endsWith('.svelte')
531570
? this.fixUserPreferencesForSvelteComponentImport(userPreferences)
532571
: userPreferences;
533572

534-
let is$typeImport = false;
535-
const originalComp = { ...comp };
536-
if (basename(filePath).startsWith('+') && comp.source?.includes('.svelte-kit/types')) {
537-
// resolve path from filePath to svelte-kit/types
538-
// src/routes/foo/+page.svelte -> .svelte-kit/types/foo/$types.d.ts
539-
const routesFolder = document.config?.kit?.files?.routes || 'src/routes';
540-
const relativeFilePath = filePath.split(routesFolder)[1]?.slice(1);
541-
if (relativeFilePath) {
542-
is$typeImport = true;
543-
comp.source =
544-
comp.source.split('.svelte-kit/types')[0] +
545-
// note the missing .d.ts at the end - TS wants it that way for some reason
546-
`.svelte-kit/types/${routesFolder}/${dirname(relativeFilePath)}/$types`;
547-
comp.data = undefined;
548-
}
549-
}
550-
551-
const getDetail = () =>
552-
lang.getCompletionEntryDetails(
553-
filePath,
554-
tsDoc.offsetAt(tsDoc.getGeneratedPosition(comp!.position)),
555-
comp!.name,
556-
{},
557-
comp!.source,
558-
errorPreventingUserPreferences,
559-
comp!.data
560-
);
561-
let detail = getDetail();
562-
if (!detail && is$typeImport) {
563-
// try again
564-
is$typeImport = false;
565-
comp = originalComp;
566-
detail = getDetail();
567-
}
573+
const detail = lang.getCompletionEntryDetails(
574+
filePath,
575+
tsDoc.offsetAt(tsDoc.getGeneratedPosition(comp!.position)),
576+
comp!.name,
577+
{},
578+
comp!.source,
579+
errorPreventingUserPreferences,
580+
comp!.data
581+
);
568582

569583
if (detail) {
570584
const { detail: itemDetail, documentation: itemDocumentation } =

packages/typescript-plugin/src/language-service/completions.ts

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,50 @@ export function decorateCompletions(ls: ts.LanguageService, logger: Logger): voi
1212
if (!completions) {
1313
return completions;
1414
}
15+
16+
// Add ./$types imports for SvelteKit since TypeScript is bad at it
17+
if (basename(fileName).startsWith('+')) {
18+
const $typeImports = new Map<string, ts.CompletionEntry>();
19+
for (const c of completions.entries) {
20+
if (c.source?.includes('.svelte-kit/types') && c.data) {
21+
$typeImports.set(c.name, c);
22+
}
23+
}
24+
for (const $typeImport of $typeImports.values()) {
25+
// resolve path from FileName to svelte-kit/types
26+
// src/routes/foo/+page.svelte -> .svelte-kit/types/foo/$types.d.ts
27+
const routesFolder = 'src/routes'; // TODO somehow get access to kit.files.routes in here
28+
const relativeFileName = fileName.split(routesFolder)[1]?.slice(1);
29+
30+
if (relativeFileName) {
31+
const modifiedSource =
32+
$typeImport.source!.split('.svelte-kit/types')[0] +
33+
// note the missing .d.ts at the end - TS wants it that way for some reason
34+
`.svelte-kit/types/${routesFolder}/${dirname(relativeFileName)}/$types`;
35+
completions.entries.push({
36+
...$typeImport,
37+
// Ensure it's sorted above the other imports
38+
sortText: !isNaN(Number($typeImport.sortText))
39+
? String(Number($typeImport.sortText) - 1)
40+
: $typeImport.sortText,
41+
source: modifiedSource,
42+
data: {
43+
...$typeImport.data,
44+
fileName: $typeImport.data!.fileName?.replace(
45+
$typeImport.source!,
46+
modifiedSource
47+
),
48+
moduleSpecifier: $typeImport.data!.moduleSpecifier?.replace(
49+
$typeImport.source!,
50+
modifiedSource
51+
),
52+
__is_sveltekit$typeImport: true
53+
} as any
54+
});
55+
}
56+
}
57+
}
58+
1559
return {
1660
...completions,
1761
entries: completions.entries.map((entry) => {
@@ -39,28 +83,9 @@ export function decorateCompletions(ls: ts.LanguageService, logger: Logger): voi
3983
preferences,
4084
data
4185
) => {
42-
let is$typeImport = false;
43-
const originalSource = source;
44-
const originalData = data ? { ...data } : undefined;
45-
if (basename(fileName).startsWith('+') && source?.includes('.svelte-kit/types')) {
46-
// resolve path from FileName to svelte-kit/types
47-
// src/routes/foo/+page.svelte -> .svelte-kit/types/foo/$types.d.ts
48-
const routesFolder = 'src/routes'; // TODO somehow get access to kit.files.routes in here
49-
const relativeFileName = fileName.split(routesFolder)[1]?.slice(1);
50-
if (relativeFileName) {
51-
is$typeImport = true;
52-
source =
53-
source.split('.svelte-kit/types')[0] +
54-
// note the missing .d.ts at the end - TS wants it that way for some reason
55-
`.svelte-kit/types/${routesFolder}/${dirname(relativeFileName)}/$types`;
56-
if (data) {
57-
data.fileName = data.fileName?.replace(originalSource!, source);
58-
data.moduleSpecifier = data.moduleSpecifier?.replace(originalSource!, source);
59-
}
60-
}
61-
}
86+
const is$typeImport = (data as any).__is_sveltekit$typeImport;
6287

63-
let details = getCompletionEntryDetails(
88+
const details = getCompletionEntryDetails(
6489
fileName,
6590
position,
6691
entryName,
@@ -69,19 +94,6 @@ export function decorateCompletions(ls: ts.LanguageService, logger: Logger): voi
6994
preferences,
7095
data
7196
);
72-
if (!details && is$typeImport) {
73-
// Try again
74-
is$typeImport = false;
75-
details = getCompletionEntryDetails(
76-
fileName,
77-
position,
78-
entryName,
79-
formatOptions,
80-
originalSource,
81-
preferences,
82-
originalData
83-
);
84-
}
8597

8698
if (details) {
8799
if (is$typeImport) {

0 commit comments

Comments
 (0)