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

(feat) event completion/hover for components with a type definition #1057

Merged
merged 6 commits into from
Jun 20, 2021
Merged
Show file tree
Hide file tree
Changes from 4 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
@@ -0,0 +1,72 @@
import { ComponentEvents } from 'svelte2tsx';
import ts from 'typescript';
import { isNotNullOrUndefined } from '../../utils';
import { findContainingNode } from './features/utils';

type ComponentEventInfo = ReturnType<ComponentEvents['getAll']>;

export class JsOrTsComponentInfoProvider implements ComponentInfoProvider {
private constructor(
private readonly typeChecker: ts.TypeChecker,
private readonly classType: ts.Type
) {}

getEvents(): ComponentEventInfo {
const symbol = this.classType.getProperty('$$events_def');
if (!symbol) {
return [];
}

const declaration = symbol.valueDeclaration;
if (!declaration) {
return [];
}

const eventType = this.typeChecker.getTypeOfSymbolAtLocation(symbol, declaration);

return eventType
.getProperties()
.map((prop) => {
if (!prop.valueDeclaration) {
return;
}

return {
name: prop.name,
type: this.typeChecker.typeToString(
this.typeChecker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration)
),
doc: ts.displayPartsToString(prop.getDocumentationComment(this.typeChecker))
};
})
.filter(isNotNullOrUndefined);
}

static create(lang: ts.LanguageService, def: ts.DefinitionInfo): ComponentInfoProvider | null {
const program = lang.getProgram();
const sourceFile = program?.getSourceFile(def.fileName);

if (!program || !sourceFile) {
return null;
}

const defClass = findContainingNode(sourceFile, def.textSpan, ts.isClassDeclaration);

if (!defClass) {
return null;
}

const typeChecker = program.getTypeChecker();
const classType = typeChecker.getTypeAtLocation(defClass);

if (!classType) {
return null;
}

return new JsOrTsComponentInfoProvider(typeChecker, classType);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about making the instance of this a property on TsOrJsDocumentSnapshot? Then it could be cached and the cache would only be deleted on updates to the snapshot.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about the performance implications, but could this be something we could also use for the svelte2tsx-components? Then we could ditch our own implementation of creating the "events" property on the svelte2tsx return type.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cached in the snapshot seems like a good idea.

could this be something we could also use for the svelte2tsx-components

Yes. Tried with svelte2tsx-component. The biggest difference between this and the svelte2tsx approach is the fallback Event in the svelte2tsx.


export interface ComponentInfoProvider {
getEvents(): ComponentEventInfo;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
isInTag
} from '../../lib/documents';
import { pathToUrl } from '../../utils';
import { ComponentInfoProvider } from './ComponentInfoProvider';
import { ConsumerDocumentMapper } from './DocumentMapper';
import {
getScriptKindFromAttributes,
Expand Down Expand Up @@ -224,7 +225,7 @@ function preprocessSvelteFile(document: Document, options: SvelteSnapshotOptions
/**
* A svelte document snapshot suitable for the ts language service and the plugin.
*/
export class SvelteDocumentSnapshot implements DocumentSnapshot {
export class SvelteDocumentSnapshot implements DocumentSnapshot, ComponentInfoProvider {
private fragment?: SvelteSnapshotFragment;

version = this.parent.version;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,14 +210,14 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
tsDoc: SvelteDocumentSnapshot,
originalPosition: Position
): Promise<Array<AppCompletionItem<CompletionEntryWithIdentifer>>> {
const snapshot = await getComponentAtPosition(
const componentInfo = await getComponentAtPosition(
this.lsAndTsDocResolver,
lang,
doc,
tsDoc,
originalPosition
);
if (!snapshot) {
if (!componentInfo) {
return [];
}

Expand All @@ -227,7 +227,7 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
right: /[^\w$:]/
});

return snapshot.getEvents().map((event) => {
return componentInfo.getEvents().map((event) => {
const eventName = 'on:' + event.name;
return {
label: eventName,
Expand Down
39 changes: 33 additions & 6 deletions packages/language-server/src/plugins/typescript/features/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getNodeIfIsInComponentStartTag,
isInTag
} from '../../../lib/documents';
import { ComponentInfoProvider, JsOrTsComponentInfoProvider } from '../ComponentInfoProvider';
import { DocumentSnapshot, SnapshotFragment, SvelteDocumentSnapshot } from '../DocumentSnapshot';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';

Expand All @@ -14,12 +15,12 @@ import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
* return the snapshot of that component.
*/
export async function getComponentAtPosition(
lsAndTsDocResovler: LSAndTSDocResolver,
lsAndTsDocResolver: LSAndTSDocResolver,
lang: ts.LanguageService,
doc: Document,
tsDoc: SvelteDocumentSnapshot,
originalPosition: Position
): Promise<SvelteDocumentSnapshot | null> {
): Promise<ComponentInfoProvider | null> {
if (tsDoc.parserError) {
return null;
}
Expand Down Expand Up @@ -47,11 +48,13 @@ export async function getComponentAtPosition(
return null;
}

const snapshot = await lsAndTsDocResovler.getSnapshot(def.fileName);
if (!(snapshot instanceof SvelteDocumentSnapshot)) {
return null;
const snapshot = await lsAndTsDocResolver.getSnapshot(def.fileName);

if (snapshot instanceof SvelteDocumentSnapshot) {
return snapshot;
}
return snapshot;

return JsOrTsComponentInfoProvider.create(lang, def);
}

export function isComponentAtPosition(
Expand Down Expand Up @@ -130,3 +133,27 @@ export class SnapshotFragmentMap {
return (await this.retrieve(fileName)).fragment;
}
}

export function findContainingNode<T extends ts.Node>(
node: ts.Node,
textSpan: ts.TextSpan,
predicate: (node: ts.Node) => node is T
): T | undefined {
const children = node.getChildren();
const end = textSpan.start + textSpan.length;

for (const child of children) {
if (!(child.getStart() <= textSpan.start && child.getEnd() >= end)) {
continue;
}

if (predicate(child)) {
return child;
}

const foundInChildren = findContainingNode(child, textSpan, predicate);
if (foundInChildren) {
return foundInChildren;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ describe('CompletionProviderImpl', () => {
);
assert.ok(completions!.items.length > 0, 'Expected completions to have length');

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const eventCompletions = completions!.items.filter((item) => item.label.startsWith('on:'));

assert.deepStrictEqual(eventCompletions, <CompletionItem[]>[
Expand Down Expand Up @@ -238,6 +237,64 @@ describe('CompletionProviderImpl', () => {
]);
});

it('provides event completion for components with type definition', async () => {
const { completionProvider, document } = setup('component-events-completion-ts-def.svelte');

const completions = await completionProvider.getCompletions(
document,
Position.create(4, 17),
{
triggerKind: CompletionTriggerKind.Invoked
}
);

const eventCompletions = completions!.items.filter((item) => item.label.startsWith('on:'));

assert.deepStrictEqual(eventCompletions, <CompletionItem[]>[
{
detail: 'event1: CustomEvent<null>',
documentation: '',
label: 'on:event1',
sortText: '-1',
textEdit: {
newText: 'on:event1',
range: {
end: {
character: 17,
line: 4
},
start: {
character: 14,
line: 4
}
}
}
},
{
detail: 'event2: CustomEvent<string>',
documentation: {
kind: 'markdown',
value: 'documentation for event2'
},
label: 'on:event2',
sortText: '-1',
textEdit: {
newText: 'on:event2',
range: {
end: {
character: 17,
line: 4
},
start: {
character: 14,
line: 4
}
}
}
}
]);
});

it('does not provide completions inside style tag', async () => {
const { completionProvider, document } = setup('completionsstyle.svelte');

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// <reference lib="dom" />
import { SvelteComponentTyped } from 'svelte';

export class ComponentDef extends SvelteComponentTyped<
{},
{
event1: CustomEvent<null>;
/**
* documentation for event2
*/
event2: CustomEvent<string>;
},
{}
> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
import { ComponentDef } from './ComponentDef';
</script>

<ComponentDef on:></ComponentDef>