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) ComponentEvents interface #459

Merged
merged 19 commits into from
Aug 20, 2020
Merged
Show file tree
Hide file tree
Changes from 16 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
20 changes: 20 additions & 0 deletions docs/preprocessors/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,25 @@ You will need to tell svelte-vscode to restart the svelte language server in ord

Hit `ctrl-shift-p` or `cmd-shift-p` on mac, type `svelte restart`, and select `Svelte: Restart Language Server`. Any errors you were seeing should now go away and you're now all set up!

## Typing component events

When you are using TypeScript, you can type which events your component has by defining a reserved `interface` (_NOT_ `type`) called `ComponentEvents`:

```html
<script lang="ts">
interface ComponentEvents {
click: MouseEvent;
hello: CustomEvent<boolean>;
}

// ...
</script>
```

Doing this will give you autocompletion for these events as well as type safety when using the component in other components.

> In case you ask why this cannot be infered: Due to Svelte's dynamic nature, component events could be fired not only from a dispatcher created directly in the component, but from a dispatcher which is created as part of a mixin. This is almost impossible to infer, so we need you to tell us which events are possible.

## Troubleshooting / FAQ

### I cannot use TS inside my script even when `lang="ts"` is present
Expand Down Expand Up @@ -81,6 +100,7 @@ At the moment, you cannot. Only `script`/`style` tags are preprocessed/transpile
### Why is VSCode not finding absolute paths for type imports?

You may need to set `baseUrl` in `tsconfig.json` at the project root to include (restart the language server to see this take effect):

```
"compilerOptions": {
"baseUrl": "."
Expand Down
9 changes: 6 additions & 3 deletions packages/language-server/src/lib/documents/Document.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { urlToPath } from '../../utils';
import { WritableDocument } from './DocumentBase';
import { extractScriptTags, extractStyleTag, TagInformation } from './utils';
import { extractScriptTags, extractStyleTag, TagInformation, parseHtml } from './utils';
import { SvelteConfig, loadConfig } from './configLoader';
import { HTMLDocument } from 'vscode-html-languageservice';

/**
* Represents a text document contains a svelte component.
Expand All @@ -12,6 +13,7 @@ export class Document extends WritableDocument {
moduleScriptInfo: TagInformation | null = null;
styleInfo: TagInformation | null = null;
config!: SvelteConfig;
html!: HTMLDocument;

constructor(public url: string, public content: string) {
super();
Expand All @@ -22,10 +24,11 @@ export class Document extends WritableDocument {
if (!this.config || this.config.loadConfigError) {
this.config = loadConfig(this.getFilePath() || '');
}
const scriptTags = extractScriptTags(this.content);
this.html = parseHtml(this.content);
const scriptTags = extractScriptTags(this.content, this.html);
this.scriptInfo = this.addDefaultLanguage(scriptTags?.script || null, 'script');
this.moduleScriptInfo = this.addDefaultLanguage(scriptTags?.moduleScript || null, 'script');
this.styleInfo = this.addDefaultLanguage(extractStyleTag(this.content), 'style');
this.styleInfo = this.addDefaultLanguage(extractStyleTag(this.content, this.html), 'style');
}

/**
Expand Down
36 changes: 29 additions & 7 deletions packages/language-server/src/lib/documents/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { clamp, isInRange, regexLastIndexOf } from '../../utils';
import { Position, Range } from 'vscode-languageserver';
import { Node, getLanguageService } from 'vscode-html-languageservice';
import { Node, getLanguageService, HTMLDocument } from 'vscode-html-languageservice';
import * as path from 'path';

export interface TagInformation {
Expand Down Expand Up @@ -39,7 +39,11 @@ function parseAttributes(
}

const parser = getLanguageService();
function parseHtml(text: string) {

/**
* Parses text as HTML
*/
export function parseHtml(text: string): HTMLDocument {
// We can safely only set getText because only this is used for parsing
return parser.parseHTMLDocument(<any>{ getText: () => text });
}
Expand Down Expand Up @@ -77,9 +81,9 @@ function blankIfBlocks(text: string): string {
* @param source text content to extract tag from
* @param tag the tag to extract
*/
function extractTags(text: string, tag: 'script' | 'style'): TagInformation[] {
function extractTags(text: string, tag: 'script' | 'style', html?: HTMLDocument): TagInformation[] {
text = blankIfBlocks(text);
const rootNodes = parseHtml(text).roots;
const rootNodes = html?.roots || parseHtml(text).roots;
const matchedNodes = rootNodes
.filter((node) => node.tag === tag)
.filter((tag) => {
Expand Down Expand Up @@ -155,8 +159,9 @@ function extractTags(text: string, tag: 'script' | 'style'): TagInformation[] {

export function extractScriptTags(
source: string,
html?: HTMLDocument,
): { script?: TagInformation; moduleScript?: TagInformation } | null {
const scripts = extractTags(source, 'script');
const scripts = extractTags(source, 'script', html);
if (!scripts.length) {
return null;
}
Expand All @@ -166,8 +171,8 @@ export function extractScriptTags(
return { script, moduleScript };
}

export function extractStyleTag(source: string): TagInformation | null {
const styles = extractTags(source, 'style');
export function extractStyleTag(source: string, html?: HTMLDocument): TagInformation | null {
const styles = extractTags(source, 'style', html);
if (!styles.length) {
return null;
}
Expand Down Expand Up @@ -291,3 +296,20 @@ export function updateRelativeImport(oldPath: string, newPath: string, relativeI
}
return newImportPath;
}

/**
* Returns the node if offset is inside a component's starttag
*/
export function getNodeIfIsInComponentStartTag(
html: HTMLDocument,
offset: number,
): Node | undefined {
const node = html.findNodeAt(offset);
if (
!!node.tag &&
node.tag[0] === node.tag[0].toUpperCase() &&
(!node.startTagEnd || offset < node.startTagEnd)
) {
return node;
}
}
20 changes: 16 additions & 4 deletions packages/language-server/src/plugins/html/HTMLPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import {
SymbolInformation,
CompletionItem,
} from 'vscode-languageserver';
import { DocumentManager, Document, isInTag } from '../../lib/documents';
import {
DocumentManager,
Document,
isInTag,
getNodeIfIsInComponentStartTag,
} from '../../lib/documents';
import { LSConfigManager, LSHTMLConfig } from '../../ls-config';
import { svelteHtmlDataProvider } from './dataProvider';
import { HoverProvider, CompletionsProvider } from '../interfaces';
Expand All @@ -20,8 +25,7 @@ export class HTMLPlugin implements HoverProvider, CompletionsProvider {
constructor(docManager: DocumentManager, configManager: LSConfigManager) {
this.configManager = configManager;
docManager.on('documentChange', (document) => {
const html = this.lang.parseHTMLDocument(document);
this.documents.set(document, html);
this.documents.set(document, document.html);
});
}

Expand Down Expand Up @@ -63,14 +67,22 @@ 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) {
return !!getNodeIfIsInComponentStartTag(html, document.offsetAt(position));
}

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,10 @@ export class SvelteDocumentSnapshot implements DocumentSnapshot {
return this.exportedNames.has(name);
}

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

async getFragment() {
if (!this.fragment) {
const uri = pathToUrl(this.filePath);
Expand Down
Loading