From 36981e70aaee7baf960d87e64c1f08983fd80b7e Mon Sep 17 00:00:00 2001
From: "Lyu, Wei-Da" <36730922+jasonlyu123@users.noreply.github.com>
Date: Wed, 19 Jun 2024 23:44:04 +0800
Subject: [PATCH] fix: support more types of component type defintion in
completion (#2407)
* fix: support more types of component type definition
* ts-ignore Component import
* format
---
.../typescript/ComponentInfoProvider.ts | 78 ++++++++++++++-----
.../src/plugins/typescript/features/utils.ts | 39 ++--------
.../features/CompletionProvider.test.ts | 47 ++++++++++-
.../testfiles/completions/ComponentDef.ts | 18 +++++
.../component-props-completion-rune.svelte | 7 ++
.../completions/component-props-rune.svelte | 3 +
.../testfiles/completions/namespaced.svelte | 9 ++-
7 files changed, 146 insertions(+), 55 deletions(-)
create mode 100644 packages/language-server/test/plugins/typescript/testfiles/completions/component-props-completion-rune.svelte
create mode 100644 packages/language-server/test/plugins/typescript/testfiles/completions/component-props-rune.svelte
diff --git a/packages/language-server/src/plugins/typescript/ComponentInfoProvider.ts b/packages/language-server/src/plugins/typescript/ComponentInfoProvider.ts
index 733a85506..64b94d50b 100644
--- a/packages/language-server/src/plugins/typescript/ComponentInfoProvider.ts
+++ b/packages/language-server/src/plugins/typescript/ComponentInfoProvider.ts
@@ -13,11 +13,14 @@ export interface ComponentInfoProvider {
export class JsOrTsComponentInfoProvider implements ComponentInfoProvider {
private constructor(
private readonly typeChecker: ts.TypeChecker,
- private readonly classType: ts.Type
+ private readonly classType: ts.Type,
+ private readonly useSvelte5PlusPropsParameter: boolean = false
) {}
getEvents(): ComponentPartInfo {
- const eventType = this.getType('$$events_def');
+ const eventType = this.getType(
+ this.useSvelte5PlusPropsParameter ? '$$events' : '$$events_def'
+ );
if (!eventType) {
return [];
}
@@ -26,7 +29,7 @@ export class JsOrTsComponentInfoProvider implements ComponentInfoProvider {
}
getSlotLets(slot = 'default'): ComponentPartInfo {
- const slotType = this.getType('$$slot_def');
+ const slotType = this.getType(this.useSvelte5PlusPropsParameter ? '$$slots' : '$$slot_def');
if (!slotType) {
return [];
}
@@ -45,12 +48,18 @@ export class JsOrTsComponentInfoProvider implements ComponentInfoProvider {
}
getProps() {
- const props = this.getType('$$prop_def');
- if (!props) {
- return [];
+ if (!this.useSvelte5PlusPropsParameter) {
+ const props = this.getType('$$prop_def');
+ if (!props) {
+ return [];
+ }
+
+ return this.mapPropertiesOfType(props);
}
- return this.mapPropertiesOfType(props);
+ return this.mapPropertiesOfType(this.classType).filter(
+ (prop) => !prop.name.startsWith('$$')
+ );
}
private getType(classProperty: string) {
@@ -87,7 +96,11 @@ export class JsOrTsComponentInfoProvider implements ComponentInfoProvider {
* The result of this shouldn't be cached as it could lead to memory leaks. The type checker
* could become old and then multiple versions of it could exist.
*/
- static create(lang: ts.LanguageService, def: ts.DefinitionInfo): ComponentInfoProvider | null {
+ static create(
+ lang: ts.LanguageService,
+ def: ts.DefinitionInfo,
+ isSvelte5Plus: boolean
+ ): ComponentInfoProvider | null {
const program = lang.getProgram();
const sourceFile = program?.getSourceFile(def.fileName);
@@ -95,24 +108,53 @@ export class JsOrTsComponentInfoProvider implements ComponentInfoProvider {
return null;
}
- const defClass = findContainingNode(
- sourceFile,
- def.textSpan,
- (node): node is ts.ClassDeclaration | ts.VariableDeclaration =>
- ts.isClassDeclaration(node) || ts.isTypeAliasDeclaration(node)
- );
+ const defIdentifier = findContainingNode(sourceFile, def.textSpan, ts.isIdentifier);
- if (!defClass) {
+ if (!defIdentifier) {
return null;
}
const typeChecker = program.getTypeChecker();
- const classType = typeChecker.getTypeAtLocation(defClass);
- if (!classType) {
+ const componentSymbol = typeChecker.getSymbolAtLocation(defIdentifier);
+
+ if (!componentSymbol) {
return null;
}
- return new JsOrTsComponentInfoProvider(typeChecker, classType);
+ const type = typeChecker.getTypeOfSymbolAtLocation(componentSymbol, defIdentifier);
+
+ if (type.isClass()) {
+ return new JsOrTsComponentInfoProvider(typeChecker, type);
+ }
+
+ const constructorSignatures = type.getConstructSignatures();
+ if (constructorSignatures.length === 1) {
+ return new JsOrTsComponentInfoProvider(
+ typeChecker,
+ constructorSignatures[0].getReturnType()
+ );
+ }
+
+ if (!isSvelte5Plus) {
+ return null;
+ }
+
+ const signatures = type.getCallSignatures();
+ if (signatures.length !== 1) {
+ return null;
+ }
+
+ const propsParameter = signatures[0].parameters[1];
+ if (!propsParameter) {
+ return null;
+ }
+ const propsParameterType = typeChecker.getTypeOfSymbol(propsParameter);
+
+ return new JsOrTsComponentInfoProvider(
+ typeChecker,
+ propsParameterType,
+ /** useSvelte5PlusPropsParameter */ true
+ );
}
}
diff --git a/packages/language-server/src/plugins/typescript/features/utils.ts b/packages/language-server/src/plugins/typescript/features/utils.ts
index 696964d8a..02adde610 100644
--- a/packages/language-server/src/plugins/typescript/features/utils.ts
+++ b/packages/language-server/src/plugins/typescript/features/utils.ts
@@ -50,42 +50,15 @@ export function getComponentAtPosition(
doc.positionAt(node.start + symbolPosWithinNode + 1)
);
- let defs = lang.getDefinitionAtPosition(tsDoc.filePath, tsDoc.offsetAt(generatedPosition));
- // Svelte 5 uses a const and a type alias instead of a class, and we want the latter.
- // We still gotta check for a class in Svelte 5 because of d.ts files generated for Svelte 4 containing classes.
- let def1 = defs?.[0];
- let def2 = tsDoc.isSvelte5Plus ? defs?.[1] : undefined;
-
- while (
- def1 != null &&
- def1.kind !== ts.ScriptElementKind.classElement &&
- (def2 == null ||
- def2.kind !== ts.ScriptElementKind.constElement ||
- !def2.name.endsWith('__SvelteComponent_'))
- ) {
- const newDefs = lang.getDefinitionAtPosition(tsDoc.filePath, def1.textSpan.start);
- const newDef = newDefs?.[0];
- if (newDef?.fileName === def1.fileName && newDef?.textSpan.start === def1.textSpan.start) {
- break;
- }
- defs = newDefs;
- def1 = newDef;
- def2 = tsDoc.isSvelte5Plus ? newDefs?.[1] : undefined;
- }
-
- if (!def1 && !def2) {
+ const def = lang.getDefinitionAtPosition(
+ tsDoc.filePath,
+ tsDoc.offsetAt(generatedPosition)
+ )?.[0];
+ if (!def) {
return null;
}
- if (
- def2 != null &&
- def2.kind === ts.ScriptElementKind.constElement &&
- def2.name.endsWith('__SvelteComponent_')
- ) {
- def1 = undefined;
- }
-
- return JsOrTsComponentInfoProvider.create(lang, def1! || def2!);
+ return JsOrTsComponentInfoProvider.create(lang, def, tsDoc.isSvelte5Plus);
}
export function isComponentAtPosition(
diff --git a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts
index 682a0ec8d..78ca8adaa 100644
--- a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts
+++ b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts
@@ -1715,7 +1715,13 @@ describe('CompletionProviderImpl', function () {
[Position.create(9, 26), 'namespace import after tag name'],
[Position.create(9, 35), 'namespace import before tag end'],
[Position.create(10, 27), 'object namespace after tag name'],
- [Position.create(10, 36), 'object namespace before tag end']
+ [Position.create(10, 36), 'object namespace before tag end'],
+ [Position.create(11, 27), 'object namespace + reexport after tag name'],
+ [Position.create(11, 36), 'object namespace + reexport before tag end'],
+ [Position.create(12, 37), 'constructor signature after tag name'],
+ [Position.create(12, 46), 'constructor signature before tag end'],
+ [Position.create(12, 37), 'overloaded constructor signature after tag name'],
+ [Position.create(12, 46), 'overloaded constructor signature before tag end']
];
for (const [position, name] of namespacedComponentTestList) {
@@ -1734,4 +1740,43 @@ describe('CompletionProviderImpl', function () {
after(() => {
__resetCache();
});
+
+ // -------------------- put tests that only run in Svelte 5 below this line and everything else above --------------------
+ if (!isSvelte5Plus) return;
+
+ it(`provide props completions for rune-mode component`, async () => {
+ const { completionProvider, document } = setup('component-props-completion-rune.svelte');
+
+ const completions = await completionProvider.getCompletions(
+ document,
+ {
+ line: 5,
+ character: 20
+ },
+ {
+ triggerKind: CompletionTriggerKind.Invoked
+ }
+ );
+
+ const item = completions?.items.find((item) => item.label === 'a');
+ assert.ok(item);
+ });
+
+ it(`provide props completions for v5+ Component type`, async () => {
+ const { completionProvider, document } = setup('component-props-completion-rune.svelte');
+
+ const completions = await completionProvider.getCompletions(
+ document,
+ {
+ line: 6,
+ character: 15
+ },
+ {
+ triggerKind: CompletionTriggerKind.Invoked
+ }
+ );
+
+ const item = completions?.items.find((item) => item.label === 'hi');
+ assert.ok(item);
+ });
});
diff --git a/packages/language-server/test/plugins/typescript/testfiles/completions/ComponentDef.ts b/packages/language-server/test/plugins/typescript/testfiles/completions/ComponentDef.ts
index a49af65f4..f1aa4131c 100644
--- a/packages/language-server/test/plugins/typescript/testfiles/completions/ComponentDef.ts
+++ b/packages/language-server/test/plugins/typescript/testfiles/completions/ComponentDef.ts
@@ -1,5 +1,7 @@
///
import type { SvelteComponentTyped as tmp } from 'svelte';
+// @ts-ignore only exists in svelte 5+
+import { Component } from 'svelte';
const SvelteComponentTyped: typeof tmp = null as any;
@@ -37,3 +39,19 @@ export class ComponentDef2 extends SvelteComponentTyped<
export class ComponentDef3 extends SvelteComponentTyped<
{ hi: string, hi2: string }
> {}
+
+class ComponentDef3_ext extends SvelteComponentTyped<
+ { hi: string, hi2: string, hi4: string }
+> {}
+
+export declare const Namespace2: {
+ ComponentDef4: new (options: ConstructorParameters[0]) => ComponentDef3;
+ ComponentDef7: {
+ new (options: ConstructorParameters[0]): ComponentDef3
+ new (options: ConstructorParameters[0]): ComponentDef3_ext
+ }
+}
+
+export declare const ComponentDef5: Component<{ hi: string }>;
+
+export { ComponentDef3 as ComponentDef6 };
diff --git a/packages/language-server/test/plugins/typescript/testfiles/completions/component-props-completion-rune.svelte b/packages/language-server/test/plugins/typescript/testfiles/completions/component-props-completion-rune.svelte
new file mode 100644
index 000000000..68e0c48f2
--- /dev/null
+++ b/packages/language-server/test/plugins/typescript/testfiles/completions/component-props-completion-rune.svelte
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/packages/language-server/test/plugins/typescript/testfiles/completions/component-props-rune.svelte b/packages/language-server/test/plugins/typescript/testfiles/completions/component-props-rune.svelte
new file mode 100644
index 000000000..9f5240a72
--- /dev/null
+++ b/packages/language-server/test/plugins/typescript/testfiles/completions/component-props-rune.svelte
@@ -0,0 +1,3 @@
+
diff --git a/packages/language-server/test/plugins/typescript/testfiles/completions/namespaced.svelte b/packages/language-server/test/plugins/typescript/testfiles/completions/namespaced.svelte
index 3e735e9b9..b093844be 100644
--- a/packages/language-server/test/plugins/typescript/testfiles/completions/namespaced.svelte
+++ b/packages/language-server/test/plugins/typescript/testfiles/completions/namespaced.svelte
@@ -1,11 +1,14 @@
-
\ No newline at end of file
+
+
+
+