Skip to content

Commit

Permalink
fix: support more types of component type defintion in completion (#2407
Browse files Browse the repository at this point in the history
)

* fix: support more types of component type definition

* ts-ignore Component import

* format
  • Loading branch information
jasonlyu123 committed Jun 19, 2024
1 parent 0261112 commit 36981e7
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 [];
}
Expand All @@ -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 [];
}
Expand All @@ -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) {
Expand Down Expand Up @@ -87,32 +96,65 @@ 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);

if (!program || !sourceFile) {
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
);
}
}
39 changes: 6 additions & 33 deletions packages/language-server/src/plugins/typescript/features/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/// <reference lib="dom" />
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;

Expand Down Expand Up @@ -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<typeof ComponentDef3>[0]) => ComponentDef3;
ComponentDef7: {
new (options: ConstructorParameters<typeof ComponentDef3>[0]): ComponentDef3
new (options: ConstructorParameters<typeof ComponentDef3_ext>[0]): ComponentDef3_ext
}
}

export declare const ComponentDef5: Component<{ hi: string }>;

export { ComponentDef3 as ComponentDef6 };
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script lang="ts">
import ComponentPropsRune from './component-props-rune.svelte'
import { ComponentDef5 } from './ComponentDef'
</script>

<ComponentPropsRune />
<ComponentDef5 />
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<script lang="ts">
let { a }: { a: string } = $props();
</script>
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<script lang="ts">
import * as Components from './ComponentDef'
import { ComponentDef3 } from './ComponentDef'
import { ComponentDef3, ComponentDef6 } from './ComponentDef'
const Components2 = {
ComponentDef3
ComponentDef3, ComponentDef6
}
</script>

<Components.ComponentDef3 hi={''} ></Components.ComponentDef3>
<Components2.ComponentDef3 hi={''} ></Components2.ComponentDef3>
<Components2.ComponentDef3 hi={''} ></Components2.ComponentDef3>
<Components2.ComponentDef6 hi={''} ></Components2.ComponentDef6>
<Components.Namespace2.ComponentDef4 hi={''} ></Components.Namespace2.ComponentDef4>
<Components.Namespace2.ComponentDef7 hi4='' hi={''} ></Components.Namespace2.ComponentDef7>

0 comments on commit 36981e7

Please sign in to comment.