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 + + + +