Skip to content

Commit 36981e7

Browse files
authored
fix: support more types of component type defintion in completion (#2407)
* fix: support more types of component type definition * ts-ignore Component import * format
1 parent 0261112 commit 36981e7

File tree

7 files changed

+146
-55
lines changed

7 files changed

+146
-55
lines changed

packages/language-server/src/plugins/typescript/ComponentInfoProvider.ts

+60-18
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@ export interface ComponentInfoProvider {
1313
export class JsOrTsComponentInfoProvider implements ComponentInfoProvider {
1414
private constructor(
1515
private readonly typeChecker: ts.TypeChecker,
16-
private readonly classType: ts.Type
16+
private readonly classType: ts.Type,
17+
private readonly useSvelte5PlusPropsParameter: boolean = false
1718
) {}
1819

1920
getEvents(): ComponentPartInfo {
20-
const eventType = this.getType('$$events_def');
21+
const eventType = this.getType(
22+
this.useSvelte5PlusPropsParameter ? '$$events' : '$$events_def'
23+
);
2124
if (!eventType) {
2225
return [];
2326
}
@@ -26,7 +29,7 @@ export class JsOrTsComponentInfoProvider implements ComponentInfoProvider {
2629
}
2730

2831
getSlotLets(slot = 'default'): ComponentPartInfo {
29-
const slotType = this.getType('$$slot_def');
32+
const slotType = this.getType(this.useSvelte5PlusPropsParameter ? '$$slots' : '$$slot_def');
3033
if (!slotType) {
3134
return [];
3235
}
@@ -45,12 +48,18 @@ export class JsOrTsComponentInfoProvider implements ComponentInfoProvider {
4548
}
4649

4750
getProps() {
48-
const props = this.getType('$$prop_def');
49-
if (!props) {
50-
return [];
51+
if (!this.useSvelte5PlusPropsParameter) {
52+
const props = this.getType('$$prop_def');
53+
if (!props) {
54+
return [];
55+
}
56+
57+
return this.mapPropertiesOfType(props);
5158
}
5259

53-
return this.mapPropertiesOfType(props);
60+
return this.mapPropertiesOfType(this.classType).filter(
61+
(prop) => !prop.name.startsWith('$$')
62+
);
5463
}
5564

5665
private getType(classProperty: string) {
@@ -87,32 +96,65 @@ export class JsOrTsComponentInfoProvider implements ComponentInfoProvider {
8796
* The result of this shouldn't be cached as it could lead to memory leaks. The type checker
8897
* could become old and then multiple versions of it could exist.
8998
*/
90-
static create(lang: ts.LanguageService, def: ts.DefinitionInfo): ComponentInfoProvider | null {
99+
static create(
100+
lang: ts.LanguageService,
101+
def: ts.DefinitionInfo,
102+
isSvelte5Plus: boolean
103+
): ComponentInfoProvider | null {
91104
const program = lang.getProgram();
92105
const sourceFile = program?.getSourceFile(def.fileName);
93106

94107
if (!program || !sourceFile) {
95108
return null;
96109
}
97110

98-
const defClass = findContainingNode(
99-
sourceFile,
100-
def.textSpan,
101-
(node): node is ts.ClassDeclaration | ts.VariableDeclaration =>
102-
ts.isClassDeclaration(node) || ts.isTypeAliasDeclaration(node)
103-
);
111+
const defIdentifier = findContainingNode(sourceFile, def.textSpan, ts.isIdentifier);
104112

105-
if (!defClass) {
113+
if (!defIdentifier) {
106114
return null;
107115
}
108116

109117
const typeChecker = program.getTypeChecker();
110-
const classType = typeChecker.getTypeAtLocation(defClass);
111118

112-
if (!classType) {
119+
const componentSymbol = typeChecker.getSymbolAtLocation(defIdentifier);
120+
121+
if (!componentSymbol) {
113122
return null;
114123
}
115124

116-
return new JsOrTsComponentInfoProvider(typeChecker, classType);
125+
const type = typeChecker.getTypeOfSymbolAtLocation(componentSymbol, defIdentifier);
126+
127+
if (type.isClass()) {
128+
return new JsOrTsComponentInfoProvider(typeChecker, type);
129+
}
130+
131+
const constructorSignatures = type.getConstructSignatures();
132+
if (constructorSignatures.length === 1) {
133+
return new JsOrTsComponentInfoProvider(
134+
typeChecker,
135+
constructorSignatures[0].getReturnType()
136+
);
137+
}
138+
139+
if (!isSvelte5Plus) {
140+
return null;
141+
}
142+
143+
const signatures = type.getCallSignatures();
144+
if (signatures.length !== 1) {
145+
return null;
146+
}
147+
148+
const propsParameter = signatures[0].parameters[1];
149+
if (!propsParameter) {
150+
return null;
151+
}
152+
const propsParameterType = typeChecker.getTypeOfSymbol(propsParameter);
153+
154+
return new JsOrTsComponentInfoProvider(
155+
typeChecker,
156+
propsParameterType,
157+
/** useSvelte5PlusPropsParameter */ true
158+
);
117159
}
118160
}

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

+6-33
Original file line numberDiff line numberDiff line change
@@ -50,42 +50,15 @@ export function getComponentAtPosition(
5050
doc.positionAt(node.start + symbolPosWithinNode + 1)
5151
);
5252

53-
let defs = lang.getDefinitionAtPosition(tsDoc.filePath, tsDoc.offsetAt(generatedPosition));
54-
// Svelte 5 uses a const and a type alias instead of a class, and we want the latter.
55-
// We still gotta check for a class in Svelte 5 because of d.ts files generated for Svelte 4 containing classes.
56-
let def1 = defs?.[0];
57-
let def2 = tsDoc.isSvelte5Plus ? defs?.[1] : undefined;
58-
59-
while (
60-
def1 != null &&
61-
def1.kind !== ts.ScriptElementKind.classElement &&
62-
(def2 == null ||
63-
def2.kind !== ts.ScriptElementKind.constElement ||
64-
!def2.name.endsWith('__SvelteComponent_'))
65-
) {
66-
const newDefs = lang.getDefinitionAtPosition(tsDoc.filePath, def1.textSpan.start);
67-
const newDef = newDefs?.[0];
68-
if (newDef?.fileName === def1.fileName && newDef?.textSpan.start === def1.textSpan.start) {
69-
break;
70-
}
71-
defs = newDefs;
72-
def1 = newDef;
73-
def2 = tsDoc.isSvelte5Plus ? newDefs?.[1] : undefined;
74-
}
75-
76-
if (!def1 && !def2) {
53+
const def = lang.getDefinitionAtPosition(
54+
tsDoc.filePath,
55+
tsDoc.offsetAt(generatedPosition)
56+
)?.[0];
57+
if (!def) {
7758
return null;
7859
}
7960

80-
if (
81-
def2 != null &&
82-
def2.kind === ts.ScriptElementKind.constElement &&
83-
def2.name.endsWith('__SvelteComponent_')
84-
) {
85-
def1 = undefined;
86-
}
87-
88-
return JsOrTsComponentInfoProvider.create(lang, def1! || def2!);
61+
return JsOrTsComponentInfoProvider.create(lang, def, tsDoc.isSvelte5Plus);
8962
}
9063

9164
export function isComponentAtPosition(

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

+46-1
Original file line numberDiff line numberDiff line change
@@ -1715,7 +1715,13 @@ describe('CompletionProviderImpl', function () {
17151715
[Position.create(9, 26), 'namespace import after tag name'],
17161716
[Position.create(9, 35), 'namespace import before tag end'],
17171717
[Position.create(10, 27), 'object namespace after tag name'],
1718-
[Position.create(10, 36), 'object namespace before tag end']
1718+
[Position.create(10, 36), 'object namespace before tag end'],
1719+
[Position.create(11, 27), 'object namespace + reexport after tag name'],
1720+
[Position.create(11, 36), 'object namespace + reexport before tag end'],
1721+
[Position.create(12, 37), 'constructor signature after tag name'],
1722+
[Position.create(12, 46), 'constructor signature before tag end'],
1723+
[Position.create(12, 37), 'overloaded constructor signature after tag name'],
1724+
[Position.create(12, 46), 'overloaded constructor signature before tag end']
17191725
];
17201726

17211727
for (const [position, name] of namespacedComponentTestList) {
@@ -1734,4 +1740,43 @@ describe('CompletionProviderImpl', function () {
17341740
after(() => {
17351741
__resetCache();
17361742
});
1743+
1744+
// -------------------- put tests that only run in Svelte 5 below this line and everything else above --------------------
1745+
if (!isSvelte5Plus) return;
1746+
1747+
it(`provide props completions for rune-mode component`, async () => {
1748+
const { completionProvider, document } = setup('component-props-completion-rune.svelte');
1749+
1750+
const completions = await completionProvider.getCompletions(
1751+
document,
1752+
{
1753+
line: 5,
1754+
character: 20
1755+
},
1756+
{
1757+
triggerKind: CompletionTriggerKind.Invoked
1758+
}
1759+
);
1760+
1761+
const item = completions?.items.find((item) => item.label === 'a');
1762+
assert.ok(item);
1763+
});
1764+
1765+
it(`provide props completions for v5+ Component type`, async () => {
1766+
const { completionProvider, document } = setup('component-props-completion-rune.svelte');
1767+
1768+
const completions = await completionProvider.getCompletions(
1769+
document,
1770+
{
1771+
line: 6,
1772+
character: 15
1773+
},
1774+
{
1775+
triggerKind: CompletionTriggerKind.Invoked
1776+
}
1777+
);
1778+
1779+
const item = completions?.items.find((item) => item.label === 'hi');
1780+
assert.ok(item);
1781+
});
17371782
});

packages/language-server/test/plugins/typescript/testfiles/completions/ComponentDef.ts

+18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/// <reference lib="dom" />
22
import type { SvelteComponentTyped as tmp } from 'svelte';
3+
// @ts-ignore only exists in svelte 5+
4+
import { Component } from 'svelte';
35

46
const SvelteComponentTyped: typeof tmp = null as any;
57

@@ -37,3 +39,19 @@ export class ComponentDef2 extends SvelteComponentTyped<
3739
export class ComponentDef3 extends SvelteComponentTyped<
3840
{ hi: string, hi2: string }
3941
> {}
42+
43+
class ComponentDef3_ext extends SvelteComponentTyped<
44+
{ hi: string, hi2: string, hi4: string }
45+
> {}
46+
47+
export declare const Namespace2: {
48+
ComponentDef4: new (options: ConstructorParameters<typeof ComponentDef3>[0]) => ComponentDef3;
49+
ComponentDef7: {
50+
new (options: ConstructorParameters<typeof ComponentDef3>[0]): ComponentDef3
51+
new (options: ConstructorParameters<typeof ComponentDef3_ext>[0]): ComponentDef3_ext
52+
}
53+
}
54+
55+
export declare const ComponentDef5: Component<{ hi: string }>;
56+
57+
export { ComponentDef3 as ComponentDef6 };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script lang="ts">
2+
import ComponentPropsRune from './component-props-rune.svelte'
3+
import { ComponentDef5 } from './ComponentDef'
4+
</script>
5+
6+
<ComponentPropsRune />
7+
<ComponentDef5 />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<script lang="ts">
2+
let { a }: { a: string } = $props();
3+
</script>
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
<script lang="ts">
22
import * as Components from './ComponentDef'
3-
import { ComponentDef3 } from './ComponentDef'
3+
import { ComponentDef3, ComponentDef6 } from './ComponentDef'
44
55
const Components2 = {
6-
ComponentDef3
6+
ComponentDef3, ComponentDef6
77
}
88
</script>
99

1010
<Components.ComponentDef3 hi={''} ></Components.ComponentDef3>
11-
<Components2.ComponentDef3 hi={''} ></Components2.ComponentDef3>
11+
<Components2.ComponentDef3 hi={''} ></Components2.ComponentDef3>
12+
<Components2.ComponentDef6 hi={''} ></Components2.ComponentDef6>
13+
<Components.Namespace2.ComponentDef4 hi={''} ></Components.Namespace2.ComponentDef4>
14+
<Components.Namespace2.ComponentDef7 hi4='' hi={''} ></Components.Namespace2.ComponentDef7>

0 commit comments

Comments
 (0)