Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support more types of component type defintion in completion #2407

Merged
merged 4 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>