Skip to content

Commit f1ae8f9

Browse files
author
Simon Holthausen
committed
(feat) ComponentEvents interface
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
1 parent 925a0b2 commit f1ae8f9

File tree

9 files changed

+225
-21
lines changed

9 files changed

+225
-21
lines changed

packages/language-server/src/plugins/html/HTMLPlugin.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,24 @@ export class HTMLPlugin implements HoverProvider, CompletionsProvider {
6363
this.lang.setCompletionParticipants([
6464
getEmmetCompletionParticipants(document, position, 'html', {}, emmetResults),
6565
]);
66-
const results = this.lang.doComplete(document, position, html);
66+
const results = this.isInComponentTag(html, document, position)
67+
? // Only allow emmet inside component element tags.
68+
// Other attributes/events would be false positives.
69+
CompletionList.create([])
70+
: this.lang.doComplete(document, position, html);
6771
return CompletionList.create(
6872
[...results.items, ...this.getLangCompletions(results.items), ...emmetResults.items],
6973
// Emmet completions change on every keystroke, so they are never complete
7074
emmetResults.items.length > 0,
7175
);
7276
}
7377

78+
private isInComponentTag(html: HTMLDocument, document: Document, position: Position) {
79+
const offset = document.offsetAt(position);
80+
const node = html.findNodeAt(offset);
81+
return !!node.tag && node.tag[0] === node.tag[0].toUpperCase();
82+
}
83+
7484
private getLangCompletions(completions: CompletionItem[]): CompletionItem[] {
7585
const styleScriptTemplateCompletions = completions.filter((completion) =>
7686
['template', 'style', 'script'].includes(completion.label),

packages/language-server/src/plugins/typescript/DocumentSnapshot.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { RawSourceMap, SourceMapConsumer } from 'source-map';
2-
import svelte2tsx, { IExportedNames } from 'svelte2tsx';
2+
import svelte2tsx, { IExportedNames, ComponentEvents } from 'svelte2tsx';
33
import ts from 'typescript';
44
import { Position, Range } from 'vscode-languageserver';
55
import {
@@ -86,6 +86,7 @@ export namespace DocumentSnapshot {
8686
tsxMap,
8787
text,
8888
exportedNames,
89+
componentEvents,
8990
parserError,
9091
nrPrependedLines,
9192
scriptKind,
@@ -98,6 +99,7 @@ export namespace DocumentSnapshot {
9899
text,
99100
nrPrependedLines,
100101
exportedNames,
102+
componentEvents,
101103
tsxMap,
102104
);
103105
}
@@ -127,6 +129,7 @@ function preprocessSvelteFile(document: Document, options: SvelteSnapshotOptions
127129
let nrPrependedLines = 0;
128130
let text = document.getText();
129131
let exportedNames: IExportedNames = { has: () => false };
132+
let componentEvents: ComponentEvents | undefined = undefined;
130133

131134
const scriptKind = [
132135
getScriptKindFromAttributes(document.scriptInfo?.attributes ?? {}),
@@ -144,6 +147,7 @@ function preprocessSvelteFile(document: Document, options: SvelteSnapshotOptions
144147
text = tsx.code;
145148
tsxMap = tsx.map;
146149
exportedNames = tsx.exportedNames;
150+
componentEvents = tsx.events;
147151
if (tsxMap) {
148152
tsxMap.sources = [document.uri];
149153

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

174-
return { tsxMap, text, exportedNames, parserError, nrPrependedLines, scriptKind };
178+
return {
179+
tsxMap,
180+
text,
181+
exportedNames,
182+
componentEvents,
183+
parserError,
184+
nrPrependedLines,
185+
scriptKind,
186+
};
175187
}
176188

177189
/**
@@ -189,6 +201,7 @@ export class SvelteDocumentSnapshot implements DocumentSnapshot {
189201
private readonly text: string,
190202
private readonly nrPrependedLines: number,
191203
private readonly exportedNames: IExportedNames,
204+
private readonly componentEvents?: ComponentEvents,
192205
private readonly tsxMap?: RawSourceMap,
193206
) {}
194207

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

232+
getEvents() {
233+
return this.componentEvents?.getAll() || [];
234+
}
235+
236+
getEvent(name: string) {
237+
return this.componentEvents?.get(name);
238+
}
239+
219240
async getFragment() {
220241
if (!this.fragment) {
221242
const uri = pathToUrl(this.filePath);

packages/language-server/src/plugins/typescript/features/CompletionProvider.ts

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ import {
1818
} from '../../../lib/documents';
1919
import { isNotNullOrUndefined, pathToUrl } from '../../../utils';
2020
import { AppCompletionItem, AppCompletionList, CompletionsProvider } from '../../interfaces';
21-
import { SvelteSnapshotFragment } from '../DocumentSnapshot';
21+
import { SvelteSnapshotFragment, SvelteDocumentSnapshot } from '../DocumentSnapshot';
2222
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
2323
import {
2424
convertRange,
2525
getCommitCharactersForScriptElement,
2626
scriptElementKindToCompletionItemKind,
2727
} from '../utils';
28+
import { getLanguageService } from 'vscode-html-languageservice';
2829

2930
export interface CompletionEntryWithIdentifer extends ts.CompletionEntry, TextDocumentIdentifier {
3031
position: Position;
@@ -71,10 +72,11 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
7172
? triggerCharacter
7273
: undefined;
7374
const isCustomTriggerCharacter = triggerKind === CompletionTriggerKind.TriggerCharacter;
75+
const isInvoked = triggerKind === CompletionTriggerKind.Invoked;
7476

7577
// ignore any custom trigger character specified in server capabilities
7678
// and is not allow by ts
77-
if (isCustomTriggerCharacter && !validTriggerCharacter) {
79+
if (isCustomTriggerCharacter && !validTriggerCharacter && !isInvoked) {
7880
return null;
7981
}
8082

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

8688
const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
87-
const completions = lang.getCompletionsAtPosition(filePath, offset, {
88-
includeCompletionsForModuleExports: true,
89-
triggerCharacter: validTriggerCharacter,
90-
});
91-
92-
if (!completions) {
89+
const completions =
90+
lang.getCompletionsAtPosition(filePath, offset, {
91+
includeCompletionsForModuleExports: true,
92+
triggerCharacter: validTriggerCharacter,
93+
})?.entries || [];
94+
const eventCompletions = this.getEventCompletions(lang, document, tsDoc, position);
95+
96+
if (completions.length === 0 && eventCompletions.length === 0) {
9397
return tsDoc.parserError ? CompletionList.create([], true) : null;
9498
}
9599

96-
const completionItems = completions.entries
100+
const completionItems = completions
97101
.map((comp) =>
98102
this.toCompletionItem(fragment, comp, pathToUrl(tsDoc.filePath), position),
99103
)
100104
.filter(isNotNullOrUndefined)
101-
.map((comp) => mapCompletionItemToOriginal(fragment, comp));
105+
.map((comp) => mapCompletionItemToOriginal(fragment, comp))
106+
.concat(eventCompletions);
102107

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

111+
private getEventCompletions(
112+
lang: ts.LanguageService,
113+
doc: Document,
114+
tsDoc: SvelteDocumentSnapshot,
115+
originalPosition: Position,
116+
): AppCompletionItem<CompletionEntryWithIdentifer>[] {
117+
if (tsDoc.parserError) {
118+
return [];
119+
}
120+
121+
const node = getLanguageService()
122+
// TODO performance: this is done already in Document and HTMLPlugin. Consolidate somehow.
123+
.parseHTMLDocument(doc)
124+
.findNodeAt(doc.offsetAt(originalPosition));
125+
const def = lang.getDefinitionAtPosition(tsDoc.filePath, node.start + 1)?.[0];
126+
if (!def) {
127+
return [];
128+
}
129+
130+
const snapshot = this.lsAndTsDocResovler.getSnapshot(def.fileName);
131+
if (!(snapshot instanceof SvelteDocumentSnapshot)) {
132+
return [];
133+
}
134+
135+
return snapshot.getEvents().map((event) => ({
136+
label: 'on:' + event.name,
137+
sortText: '-1',
138+
detail: event.name + ': ' + event.type,
139+
documentation: event.doc && { kind: MarkupKind.Markdown, value: event.doc },
140+
}));
141+
}
142+
106143
private toCompletionItem(
107144
fragment: SvelteSnapshotFragment,
108145
comp: ts.CompletionEntry,

packages/svelte2tsx/index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@ export interface SvelteCompiledToTsx {
22
code: string;
33
map: import("magic-string").SourceMap;
44
exportedNames: IExportedNames;
5+
events: ComponentEvents;
56
}
67

78
export interface IExportedNames {
89
has(name: string): boolean;
910
}
1011

12+
export interface ComponentEvents {
13+
getAll(): { name: string; type: string; doc?: string }[];
14+
get(name: string): { type: string; doc?: string } | undefined;
15+
}
16+
1117
export default function svelte2tsx(
1218
svelte: string,
1319
options?: {

packages/svelte2tsx/src/interfaces.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import MagicString from 'magic-string';
22
import { Node } from 'estree-walker';
33
import { ExportedNames } from './nodes/ExportedNames';
4+
import { ComponentEvents } from './nodes/ComponentEvents';
45

56
export interface InstanceScriptProcessResult {
67
exportedNames: ExportedNames;
8+
events: ComponentEvents;
79
uses$$props: boolean;
810
uses$$restProps: boolean;
911
getters: Set<string>;
@@ -14,6 +16,6 @@ export interface CreateRenderFunctionPara extends InstanceScriptProcessResult {
1416
scriptTag: Node;
1517
scriptDestination: number;
1618
slots: Map<string, Map<string, string>>;
17-
events: Map<string, string | string[]>;
19+
events: ComponentEvents;
1820
isTsFile: boolean;
1921
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import ts from 'typescript';
2+
import { eventMapToString } from './event-handler';
3+
4+
export abstract class ComponentEvents {
5+
protected events = new Map<string, { type: string; doc?: string }>();
6+
7+
getAll(): { name: string; type?: string; doc?: string }[] {
8+
const entries: { name: string; type: string; doc?: string }[] = [];
9+
10+
const iterableEntries = this.events.entries();
11+
for (const entry of iterableEntries) {
12+
entries.push({ name: entry[0], ...entry[1] });
13+
}
14+
15+
return entries;
16+
}
17+
18+
get(name: string): { type: string; doc?: string } | undefined {
19+
return this.events.get(name);
20+
}
21+
22+
abstract toDefString(): string;
23+
}
24+
25+
export class ComponentEventsFromInterface extends ComponentEvents {
26+
constructor(node: ts.InterfaceDeclaration) {
27+
super();
28+
this.events = this.extractEvents(node);
29+
}
30+
31+
toDefString() {
32+
return '{} as unknown as ComponentEvents';
33+
}
34+
35+
private extractEvents(node: ts.InterfaceDeclaration) {
36+
const map = new Map<string, { type: string; doc?: string }>();
37+
38+
node.members.filter(ts.isPropertySignature).forEach((member) => {
39+
map.set(member.name.getText(), {
40+
type: member.type?.getText() || 'Event',
41+
doc: this.getDoc(node, member),
42+
});
43+
});
44+
45+
return map;
46+
}
47+
48+
private getDoc(node: ts.InterfaceDeclaration, member: ts.PropertySignature) {
49+
let doc = undefined;
50+
const comment = ts.getLeadingCommentRanges(
51+
node.getText(),
52+
member.getFullStart() - node.getStart(),
53+
);
54+
55+
if (comment) {
56+
doc = node
57+
.getText()
58+
.substring(comment[0].pos, comment[0].end)
59+
// Remove /** */
60+
.replace(/\s*\/\*\*/, '')
61+
.replace(/\s*\*\//, '')
62+
.replace(/\s*\*/g, '');
63+
}
64+
65+
return doc;
66+
}
67+
}
68+
69+
export class ComponentEventsFromEventsMap extends ComponentEvents {
70+
constructor(private eventsMap: Map<string, string | string[]>) {
71+
super();
72+
this.events = this.extractEvents(eventsMap);
73+
}
74+
75+
toDefString() {
76+
return eventMapToString(this.eventsMap);
77+
}
78+
79+
private extractEvents(eventsMap: Map<string, string | string[]>) {
80+
const map = new Map();
81+
for (const name of eventsMap.keys()) {
82+
map.set(name, { type: 'Event' });
83+
}
84+
return map;
85+
}
86+
}

0 commit comments

Comments
 (0)