Skip to content
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 @@ -3,28 +3,58 @@ import ts from 'typescript';
import { isNotNullOrUndefined } from '../../utils';
import { findContainingNode } from './features/utils';

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

export interface ComponentInfoProvider {
getEvents(): ComponentPartInfo;
getSlotLets(slot?: string): ComponentPartInfo;
}

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) {
getEvents(): ComponentPartInfo {
const eventType = this.getType('$$events_def');
if (!eventType) {
return [];
}

const declaration = symbol.valueDeclaration;
if (!declaration) {
return this.mapPropertiesOfType(eventType);
}

getSlotLets(slot = 'default'): ComponentPartInfo {
const slotType = this.getType('$$slot_def');
if (!slotType) {
return [];
}

const eventType = this.typeChecker.getTypeOfSymbolAtLocation(symbol, declaration);
const slotLets = slotType.getProperties().find((prop) => prop.name === slot);
if (!slotLets?.valueDeclaration) {
return [];
}

const slotLetsType = this.typeChecker.getTypeOfSymbolAtLocation(
slotLets,
slotLets.valueDeclaration
);

return this.mapPropertiesOfType(slotLetsType);
}

private getType(classProperty: string) {
const symbol = this.classType.getProperty(classProperty);
if (!symbol?.valueDeclaration) {
return null;
}

return this.typeChecker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration);
}

return eventType
private mapPropertiesOfType(type: ts.Type): ComponentPartInfo {
return type
.getProperties()
.map((prop) => {
if (!prop.valueDeclaration) {
Expand All @@ -42,6 +72,10 @@ export class JsOrTsComponentInfoProvider implements ComponentInfoProvider {
.filter(isNotNullOrUndefined);
}

/**
* 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 {
const program = lang.getProgram();
const sourceFile = program?.getSourceFile(def.fileName);
Expand All @@ -66,7 +100,3 @@ export class JsOrTsComponentInfoProvider implements ComponentInfoProvider {
return new JsOrTsComponentInfoProvider(typeChecker, classType);
}
}

export interface ComponentInfoProvider {
getEvents(): ComponentEventInfo;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
isInTag
} from '../../lib/documents';
import { pathToUrl } from '../../utils';
import { ComponentInfoProvider, JsOrTsComponentInfoProvider } from './ComponentInfoProvider';
import { ComponentInfoProvider } from './ComponentInfoProvider';
import { ConsumerDocumentMapper } from './DocumentMapper';
import {
getScriptKindFromAttributes,
Expand Down Expand Up @@ -278,6 +278,10 @@ export class SvelteDocumentSnapshot implements DocumentSnapshot, ComponentInfoPr
return this.componentEvents?.getAll() || [];
}

getSlotLets() {
return []; // TODO implement
}

async getFragment() {
if (!this.fragment) {
const uri = pathToUrl(this.filePath);
Expand Down Expand Up @@ -326,8 +330,6 @@ export class JSOrTSDocumentSnapshot
scriptKind = getScriptKindFromFileName(this.filePath);
scriptInfo = null;

private readonly componentInfos = new Map<ts.DefinitionInfo, ComponentInfoProvider | null>();

constructor(public version: number, public readonly filePath: string, private text: string) {
super(pathToUrl(filePath));
}
Expand Down Expand Up @@ -380,21 +382,6 @@ export class JSOrTSDocumentSnapshot

this.version++;
}

getComponentInfo(
lang: ts.LanguageService,
def: ts.DefinitionInfo
): ComponentInfoProvider | null {
// there might multiple component class in a js or ts file
if (this.componentInfos.has(def)) {
return this.componentInfos.get(def) ?? null;
}

const componentInfoProvider = JsOrTsComponentInfoProvider.create(lang, def);
this.componentInfos.set(def, componentInfoProvider);

return componentInfoProvider;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from '../../../lib/documents';
import { flatten, getRegExpMatches, isNotNullOrUndefined, pathToUrl } from '../../../utils';
import { AppCompletionItem, AppCompletionList, CompletionsProvider } from '../../interfaces';
import { ComponentPartInfo } from '../ComponentInfoProvider';
import { SvelteDocumentSnapshot, SvelteSnapshotFragment } from '../DocumentSnapshot';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
import { getMarkdownDocumentation } from '../previewer';
Expand Down Expand Up @@ -93,15 +94,15 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
: undefined;
const isCustomTriggerCharacter = triggerKind === CompletionTriggerKind.TriggerCharacter;
const isJsDocTriggerCharacter = triggerCharacter === '*';
const isEventTriggerCharacter = triggerCharacter === ':';
const isEventOrSlotLetTriggerCharacter = triggerCharacter === ':';

// ignore any custom trigger character specified in server capabilities
// and is not allow by ts
if (
isCustomTriggerCharacter &&
!validTriggerCharacter &&
!isJsDocTriggerCharacter &&
!isEventTriggerCharacter
!isEventOrSlotLetTriggerCharacter
) {
return null;
}
Expand Down Expand Up @@ -136,10 +137,15 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
return null;
}

const eventCompletions = await this.getEventCompletions(lang, document, tsDoc, position);
const eventAndSlotLetCompletions = await this.getEventAndSlotLetCompletions(
lang,
document,
tsDoc,
position
);

if (isEventTriggerCharacter) {
return CompletionList.create(eventCompletions, !!tsDoc.parserError);
if (isEventOrSlotLetTriggerCharacter) {
return CompletionList.create(eventAndSlotLetCompletions, !!tsDoc.parserError);
}

if (cancellationToken?.isCancellationRequested) {
Expand All @@ -152,7 +158,7 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
triggerCharacter: validTriggerCharacter
})?.entries || [];

if (completions.length === 0 && eventCompletions.length === 0) {
if (completions.length === 0 && eventAndSlotLetCompletions.length === 0) {
return tsDoc.parserError ? CompletionList.create([], true) : null;
}

Expand All @@ -170,7 +176,7 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
)
.filter(isNotNullOrUndefined)
.map((comp) => mapCompletionItemToOriginal(fragment, comp))
.concat(eventCompletions);
.concat(eventAndSlotLetCompletions);

const completionList = CompletionList.create(completionItems, !!tsDoc.parserError);
this.lastCompletion = { key: document.getFilePath() || '', position, completionList };
Expand Down Expand Up @@ -204,7 +210,7 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
return new Set(tidiedImports);
}

private async getEventCompletions(
private async getEventAndSlotLetCompletions(
lang: ts.LanguageService,
doc: Document,
tsDoc: SvelteDocumentSnapshot,
Expand All @@ -227,19 +233,25 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
right: /[^\w$:]/
});

return componentInfo.getEvents().map((event) => {
const eventName = 'on:' + event.name;
const events = componentInfo.getEvents().map((event) => mapToCompletionEntry(event, 'on:'));
const slotLets = componentInfo
.getSlotLets()
.map((slot) => mapToCompletionEntry(slot, 'let:'));
return [...events, ...slotLets];

function mapToCompletionEntry(info: ComponentPartInfo[0], prefix: string) {
const slotName = prefix + info.name;
return {
label: eventName,
label: slotName,
sortText: '-1',
detail: event.name + ': ' + event.type,
documentation: event.doc && { kind: MarkupKind.Markdown, value: event.doc },
detail: info.name + ': ' + info.type,
documentation: info.doc && { kind: MarkupKind.Markdown, value: info.doc },
textEdit:
start !== end
? TextEdit.replace(toRange(doc.getText(), start, end), eventName)
? TextEdit.replace(toRange(doc.getText(), start, end), slotName)
: undefined
};
});
}
}

private toCompletionItem(
Expand Down
15 changes: 3 additions & 12 deletions packages/language-server/src/plugins/typescript/features/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,8 @@ import {
getNodeIfIsInComponentStartTag,
isInTag
} from '../../../lib/documents';
import { ComponentInfoProvider } from '../ComponentInfoProvider';
import {
DocumentSnapshot,
JSOrTSDocumentSnapshot,
SnapshotFragment,
SvelteDocumentSnapshot
} from '../DocumentSnapshot';
import { ComponentInfoProvider, JsOrTsComponentInfoProvider } from '../ComponentInfoProvider';
import { DocumentSnapshot, SnapshotFragment, SvelteDocumentSnapshot } from '../DocumentSnapshot';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';

/**
Expand Down Expand Up @@ -59,11 +54,7 @@ export async function getComponentAtPosition(
return snapshot;
}

if (snapshot instanceof JSOrTSDocumentSnapshot) {
return snapshot.getComponentInfo(lang, def);
}

return null;
return JsOrTsComponentInfoProvider.create(lang, def);
}

export function isComponentAtPosition(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,66 @@ describe('CompletionProviderImpl', () => {
assert.strictEqual(item?.label, 'abc');
}
}).timeout(4000);

it('provides default slot-let 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(5, 18),
{
triggerKind: CompletionTriggerKind.Invoked
}
);

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

assert.deepStrictEqual(slotLetCompletions, <CompletionItem[]>[
{
detail: 'let1: boolean',
documentation: '',
label: 'let:let1',
sortText: '-1',
textEdit: {
newText: 'let:let1',
range: {
end: {
character: 18,
line: 5
},
start: {
character: 14,
line: 5
}
}
}
},
{
detail: 'let2: string',
documentation: {
kind: 'markdown',
value: 'documentation for let2'
},
label: 'let:let2',
sortText: '-1',
textEdit: {
newText: 'let:let2',
range: {
end: {
character: 18,
line: 5
},
start: {
character: 14,
line: 5
}
}
}
}
]);
});
});

function harmonizeNewLines(input?: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,13 @@ export class ComponentDef extends SvelteComponentTyped<
*/
event2: CustomEvent<string>;
},
{}
{
default: {
let1: boolean;
/**
* documentation for let2
*/
let2: string;
}
}
> {}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
import { ComponentDef } from './ComponentDef';
</script>

<ComponentDef on:></ComponentDef>
<ComponentDef on:></ComponentDef>
<ComponentDef let:></ComponentDef>