Skip to content

Commit

Permalink
(feat) ComponentEvents interface
Browse files Browse the repository at this point in the history
This adds the possibility to use a reserved interface name `ComponentEvents` and define all possible events within it.
Also adds autocompletion for these events.
Also disables autocompletions from HTMLPlugin on component tags.

sveltejs#424 sveltejs#304
  • Loading branch information
Simon Holthausen committed Aug 17, 2020
1 parent 925a0b2 commit f1ae8f9
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 21 deletions.
12 changes: 11 additions & 1 deletion packages/language-server/src/plugins/html/HTMLPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,24 @@ export class HTMLPlugin implements HoverProvider, CompletionsProvider {
this.lang.setCompletionParticipants([
getEmmetCompletionParticipants(document, position, 'html', {}, emmetResults),
]);
const results = this.lang.doComplete(document, position, html);
const results = this.isInComponentTag(html, document, position)
? // Only allow emmet inside component element tags.
// Other attributes/events would be false positives.
CompletionList.create([])
: this.lang.doComplete(document, position, html);
return CompletionList.create(
[...results.items, ...this.getLangCompletions(results.items), ...emmetResults.items],
// Emmet completions change on every keystroke, so they are never complete
emmetResults.items.length > 0,
);
}

private isInComponentTag(html: HTMLDocument, document: Document, position: Position) {
const offset = document.offsetAt(position);
const node = html.findNodeAt(offset);
return !!node.tag && node.tag[0] === node.tag[0].toUpperCase();
}

private getLangCompletions(completions: CompletionItem[]): CompletionItem[] {
const styleScriptTemplateCompletions = completions.filter((completion) =>
['template', 'style', 'script'].includes(completion.label),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RawSourceMap, SourceMapConsumer } from 'source-map';
import svelte2tsx, { IExportedNames } from 'svelte2tsx';
import svelte2tsx, { IExportedNames, ComponentEvents } from 'svelte2tsx';
import ts from 'typescript';
import { Position, Range } from 'vscode-languageserver';
import {
Expand Down Expand Up @@ -86,6 +86,7 @@ export namespace DocumentSnapshot {
tsxMap,
text,
exportedNames,
componentEvents,
parserError,
nrPrependedLines,
scriptKind,
Expand All @@ -98,6 +99,7 @@ export namespace DocumentSnapshot {
text,
nrPrependedLines,
exportedNames,
componentEvents,
tsxMap,
);
}
Expand Down Expand Up @@ -127,6 +129,7 @@ function preprocessSvelteFile(document: Document, options: SvelteSnapshotOptions
let nrPrependedLines = 0;
let text = document.getText();
let exportedNames: IExportedNames = { has: () => false };
let componentEvents: ComponentEvents | undefined = undefined;

const scriptKind = [
getScriptKindFromAttributes(document.scriptInfo?.attributes ?? {}),
Expand All @@ -144,6 +147,7 @@ function preprocessSvelteFile(document: Document, options: SvelteSnapshotOptions
text = tsx.code;
tsxMap = tsx.map;
exportedNames = tsx.exportedNames;
componentEvents = tsx.events;
if (tsxMap) {
tsxMap.sources = [document.uri];

Expand Down Expand Up @@ -171,7 +175,15 @@ function preprocessSvelteFile(document: Document, options: SvelteSnapshotOptions
text = document.scriptInfo ? document.scriptInfo.content : '';
}

return { tsxMap, text, exportedNames, parserError, nrPrependedLines, scriptKind };
return {
tsxMap,
text,
exportedNames,
componentEvents,
parserError,
nrPrependedLines,
scriptKind,
};
}

/**
Expand All @@ -189,6 +201,7 @@ export class SvelteDocumentSnapshot implements DocumentSnapshot {
private readonly text: string,
private readonly nrPrependedLines: number,
private readonly exportedNames: IExportedNames,
private readonly componentEvents?: ComponentEvents,
private readonly tsxMap?: RawSourceMap,
) {}

Expand Down Expand Up @@ -216,6 +229,14 @@ export class SvelteDocumentSnapshot implements DocumentSnapshot {
return this.exportedNames.has(name);
}

getEvents() {
return this.componentEvents?.getAll() || [];
}

getEvent(name: string) {
return this.componentEvents?.get(name);
}

async getFragment() {
if (!this.fragment) {
const uri = pathToUrl(this.filePath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ import {
} from '../../../lib/documents';
import { isNotNullOrUndefined, pathToUrl } from '../../../utils';
import { AppCompletionItem, AppCompletionList, CompletionsProvider } from '../../interfaces';
import { SvelteSnapshotFragment } from '../DocumentSnapshot';
import { SvelteSnapshotFragment, SvelteDocumentSnapshot } from '../DocumentSnapshot';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
import {
convertRange,
getCommitCharactersForScriptElement,
scriptElementKindToCompletionItemKind,
} from '../utils';
import { getLanguageService } from 'vscode-html-languageservice';

export interface CompletionEntryWithIdentifer extends ts.CompletionEntry, TextDocumentIdentifier {
position: Position;
Expand Down Expand Up @@ -71,10 +72,11 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
? triggerCharacter
: undefined;
const isCustomTriggerCharacter = triggerKind === CompletionTriggerKind.TriggerCharacter;
const isInvoked = triggerKind === CompletionTriggerKind.Invoked;

// ignore any custom trigger character specified in server capabilities
// and is not allow by ts
if (isCustomTriggerCharacter && !validTriggerCharacter) {
if (isCustomTriggerCharacter && !validTriggerCharacter && !isInvoked) {
return null;
}

Expand All @@ -84,25 +86,60 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
}

const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
const completions = lang.getCompletionsAtPosition(filePath, offset, {
includeCompletionsForModuleExports: true,
triggerCharacter: validTriggerCharacter,
});

if (!completions) {
const completions =
lang.getCompletionsAtPosition(filePath, offset, {
includeCompletionsForModuleExports: true,
triggerCharacter: validTriggerCharacter,
})?.entries || [];
const eventCompletions = this.getEventCompletions(lang, document, tsDoc, position);

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

const completionItems = completions.entries
const completionItems = completions
.map((comp) =>
this.toCompletionItem(fragment, comp, pathToUrl(tsDoc.filePath), position),
)
.filter(isNotNullOrUndefined)
.map((comp) => mapCompletionItemToOriginal(fragment, comp));
.map((comp) => mapCompletionItemToOriginal(fragment, comp))
.concat(eventCompletions);

return CompletionList.create(completionItems, !!tsDoc.parserError);
}

private getEventCompletions(
lang: ts.LanguageService,
doc: Document,
tsDoc: SvelteDocumentSnapshot,
originalPosition: Position,
): AppCompletionItem<CompletionEntryWithIdentifer>[] {
if (tsDoc.parserError) {
return [];
}

const node = getLanguageService()
// TODO performance: this is done already in Document and HTMLPlugin. Consolidate somehow.
.parseHTMLDocument(doc)
.findNodeAt(doc.offsetAt(originalPosition));
const def = lang.getDefinitionAtPosition(tsDoc.filePath, node.start + 1)?.[0];
if (!def) {
return [];
}

const snapshot = this.lsAndTsDocResovler.getSnapshot(def.fileName);
if (!(snapshot instanceof SvelteDocumentSnapshot)) {
return [];
}

return snapshot.getEvents().map((event) => ({
label: 'on:' + event.name,
sortText: '-1',
detail: event.name + ': ' + event.type,
documentation: event.doc && { kind: MarkupKind.Markdown, value: event.doc },
}));
}

private toCompletionItem(
fragment: SvelteSnapshotFragment,
comp: ts.CompletionEntry,
Expand Down
6 changes: 6 additions & 0 deletions packages/svelte2tsx/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ export interface SvelteCompiledToTsx {
code: string;
map: import("magic-string").SourceMap;
exportedNames: IExportedNames;
events: ComponentEvents;
}

export interface IExportedNames {
has(name: string): boolean;
}

export interface ComponentEvents {
getAll(): { name: string; type: string; doc?: string }[];
get(name: string): { type: string; doc?: string } | undefined;
}

export default function svelte2tsx(
svelte: string,
options?: {
Expand Down
4 changes: 3 additions & 1 deletion packages/svelte2tsx/src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import MagicString from 'magic-string';
import { Node } from 'estree-walker';
import { ExportedNames } from './nodes/ExportedNames';
import { ComponentEvents } from './nodes/ComponentEvents';

export interface InstanceScriptProcessResult {
exportedNames: ExportedNames;
events: ComponentEvents;
uses$$props: boolean;
uses$$restProps: boolean;
getters: Set<string>;
Expand All @@ -14,6 +16,6 @@ export interface CreateRenderFunctionPara extends InstanceScriptProcessResult {
scriptTag: Node;
scriptDestination: number;
slots: Map<string, Map<string, string>>;
events: Map<string, string | string[]>;
events: ComponentEvents;
isTsFile: boolean;
}
86 changes: 86 additions & 0 deletions packages/svelte2tsx/src/nodes/ComponentEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import ts from 'typescript';
import { eventMapToString } from './event-handler';

export abstract class ComponentEvents {
protected events = new Map<string, { type: string; doc?: string }>();

getAll(): { name: string; type?: string; doc?: string }[] {
const entries: { name: string; type: string; doc?: string }[] = [];

const iterableEntries = this.events.entries();
for (const entry of iterableEntries) {
entries.push({ name: entry[0], ...entry[1] });
}

return entries;
}

get(name: string): { type: string; doc?: string } | undefined {
return this.events.get(name);
}

abstract toDefString(): string;
}

export class ComponentEventsFromInterface extends ComponentEvents {
constructor(node: ts.InterfaceDeclaration) {
super();
this.events = this.extractEvents(node);
}

toDefString() {
return '{} as unknown as ComponentEvents';
}

private extractEvents(node: ts.InterfaceDeclaration) {
const map = new Map<string, { type: string; doc?: string }>();

node.members.filter(ts.isPropertySignature).forEach((member) => {
map.set(member.name.getText(), {
type: member.type?.getText() || 'Event',
doc: this.getDoc(node, member),
});
});

return map;
}

private getDoc(node: ts.InterfaceDeclaration, member: ts.PropertySignature) {
let doc = undefined;
const comment = ts.getLeadingCommentRanges(
node.getText(),
member.getFullStart() - node.getStart(),
);

if (comment) {
doc = node
.getText()
.substring(comment[0].pos, comment[0].end)
// Remove /** */
.replace(/\s*\/\*\*/, '')
.replace(/\s*\*\//, '')
.replace(/\s*\*/g, '');
}

return doc;
}
}

export class ComponentEventsFromEventsMap extends ComponentEvents {
constructor(private eventsMap: Map<string, string | string[]>) {
super();
this.events = this.extractEvents(eventsMap);
}

toDefString() {
return eventMapToString(this.eventsMap);
}

private extractEvents(eventsMap: Map<string, string | string[]>) {
const map = new Map();
for (const name of eventsMap.keys()) {
map.set(name, { type: 'Event' });
}
return map;
}
}
Loading

0 comments on commit f1ae8f9

Please sign in to comment.