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) implement generics attr on script tags #2020

Merged
merged 6 commits into from May 25, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/language-server/src/plugins/html/dataProvider.ts
Expand Up @@ -363,6 +363,13 @@ const addAttributes: Record<string, IAttributeData[]> = {
{
name: 'bind:open'
}
],
script: [
{
name: 'generics',
description:
'Generics used within the components. Only available when using TypeScript.'
}
]
};

Expand Down
Expand Up @@ -70,7 +70,8 @@ export class SemanticTokensProviderImpl implements SemanticTokensProvider {
textDocument,
tsDoc,
generatedOffset,
generatedLength
generatedLength,
encodedClassification
);
if (!originalPosition) {
continue;
Expand Down Expand Up @@ -106,14 +107,14 @@ export class SemanticTokensProviderImpl implements SemanticTokensProvider {
document: Document,
snapshot: SvelteDocumentSnapshot,
generatedOffset: number,
generatedLength: number
generatedLength: number,
token: number
): [line: number, character: number, length: number, start: number] | undefined {
const text = snapshot.getFullText();
if (
isInGeneratedCode(
snapshot.getFullText(),
generatedOffset,
generatedOffset + generatedLength
)
isInGeneratedCode(text, generatedOffset, generatedOffset + generatedLength) ||
(token === 2817 /* top level function */ &&
text.substring(generatedOffset, generatedOffset + generatedLength) === 'render')
) {
return;
}
Expand Down
30 changes: 29 additions & 1 deletion packages/svelte-vscode/syntaxes/svelte.tmLanguage.src.yaml
Expand Up @@ -452,6 +452,32 @@ repository:
end: (?<=[^\s=])(?!\s*=)|(?=/?>)
patterns: [include: '#attributes-value']

# Matches the generics attribute on script tags
attributes-generics:
begin: (generics)(=)(["'])
beginCaptures:
1: { name: entity.other.attribute-name.svelte }
2: { name: punctuation.separator.key-value.svelte }
3: { name: punctuation.definition.string.begin.svelte }
end: (\3)
endCaptures:
1: { name: punctuation.definition.string.end.svelte }
contentName: meta.embedded.expression.svelte source.ts
patterns: [ include: '#type-parameters' ]

# Copied over from https://github.com/microsoft/TypeScript-TmLanguage/blob/master/TypeScript.YAML-tmLanguage#L2308
# and removed the start/end matches which have the < and > included, which are not present in our case
type-parameters:
name: meta.type.parameters.ts
patterns:
- include: 'source.ts#comment'
- name: storage.modifier.ts
match: '(?<![_$[:alnum:]])(?:(?<=\.\.\.)|(?<!\.))(extends|in|out|const)(?![_$[:alnum:]])(?:(?=\.\.\.)|(?!\.))'
- include: 'source.ts#type'
- include: 'source.ts#punctuation-comma'
- name: keyword.operator.assignment.ts
match: (=)(?!>)

# ------
# TAGS

Expand Down Expand Up @@ -497,7 +523,9 @@ repository:
end: (?=/>)|>
endCaptures: { 0: { name: punctuation.definition.tag.end.svelte } }
name: meta.tag.start.svelte
patterns: [ include: '#attributes' ]
patterns:
- include: '#attributes-generics'
- include: '#attributes'

# Matches the beginning (`<name`) section of a tag start node.
tags-start-node:
Expand Down
@@ -0,0 +1,5 @@
<script lang="ts" generics="
T extends 'a' |'b' |'c'
">
export let t: T;
</script>
@@ -0,0 +1,28 @@
><script lang="ts" generics="
#^ source.svelte meta.script.svelte meta.tag.start.svelte punctuation.definition.tag.begin.svelte
# ^^^^^^ source.svelte meta.script.svelte meta.tag.start.svelte entity.name.tag.svelte
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte
# ^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte entity.other.attribute-name.svelte
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte punctuation.separator.key-value.svelte
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte string.quoted.svelte punctuation.definition.string.begin.svelte
# ^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte string.quoted.svelte
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte string.quoted.svelte punctuation.definition.string.end.svelte
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte
# ^^^^^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte entity.other.attribute-name.svelte
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte punctuation.separator.key-value.svelte
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte punctuation.definition.string.begin.svelte
> T extends 'a' |'b' |'c'
#^^^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.embedded.expression.svelte source.ts
# ^^^^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.embedded.expression.svelte source.ts storage.modifier.ts
# ^^^^^^^^^^^^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.embedded.expression.svelte source.ts
> ">
#^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.embedded.expression.svelte source.ts
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte punctuation.definition.string.end.svelte
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte punctuation.definition.tag.end.svelte
> export let t: T;
#^^^^^^^^^^^^^^^^^^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.embedded.block.svelte source.ts
></script>
#^^ source.svelte meta.script.svelte meta.tag.end.svelte punctuation.definition.tag.begin.svelte
# ^^^^^^ source.svelte meta.script.svelte meta.tag.end.svelte entity.name.tag.svelte
# ^ source.svelte meta.script.svelte meta.tag.end.svelte punctuation.definition.tag.end.svelte
>
@@ -0,0 +1,3 @@
<script generics="T" lang="ts">
export let t: T;
</script>
@@ -0,0 +1,23 @@
><script generics="T" lang="ts">
#^ source.svelte meta.script.svelte meta.tag.start.svelte punctuation.definition.tag.begin.svelte
# ^^^^^^ source.svelte meta.script.svelte meta.tag.start.svelte entity.name.tag.svelte
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte
# ^^^^^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte entity.other.attribute-name.svelte
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte punctuation.separator.key-value.svelte
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte punctuation.definition.string.begin.svelte
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.embedded.expression.svelte source.ts
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte punctuation.definition.string.end.svelte
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte
# ^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte entity.other.attribute-name.svelte
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte punctuation.separator.key-value.svelte
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte string.quoted.svelte punctuation.definition.string.begin.svelte
# ^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte string.quoted.svelte
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte string.quoted.svelte punctuation.definition.string.end.svelte
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte punctuation.definition.tag.end.svelte
> export let t: T;
#^^^^^^^^^^^^^^^^^^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.embedded.block.svelte source.ts
></script>
#^^ source.svelte meta.script.svelte meta.tag.end.svelte punctuation.definition.tag.begin.svelte
# ^^^^^^ source.svelte meta.script.svelte meta.tag.end.svelte entity.name.tag.svelte
# ^ source.svelte meta.script.svelte meta.tag.end.svelte punctuation.definition.tag.end.svelte
>
41 changes: 25 additions & 16 deletions packages/svelte2tsx/src/svelte2tsx/createRenderFunction.ts
Expand Up @@ -63,28 +63,37 @@ export function createRenderFunction({
//I couldn't get magicstring to let me put the script before the <> we prepend during conversion of the template to jsx, so we just close it instead
const scriptTagEnd = htmlx.lastIndexOf('>', scriptTag.content.start) + 1;
str.overwrite(scriptTag.start, scriptTag.start + 1, ';');
str.overwrite(
scriptTag.start + 1,
scriptTagEnd,
`function render${generics.toDefinitionString(true)}() {${propsDecl}\n`
);
if (generics.genericsAttr) {
let start = generics.genericsAttr.value[0].start;
let end = generics.genericsAttr.value[0].end;
if (htmlx.charAt(start) === '"' || htmlx.charAt(start) === "'") {
start++;
end--;
}
str.overwrite(scriptTag.start + 1, start - 1, `function render`);
str.overwrite(start - 1, start, `<`); // if the generics are unused, only this char is colored opaque
if (end < scriptTagEnd) {
str.overwrite(end, scriptTagEnd, `>() {${propsDecl}\n`);
} else {
str.prependRight(end, `>() {${propsDecl}\n`);
}
} else {
str.overwrite(
scriptTag.start + 1,
scriptTagEnd,
`function render${generics.toDefinitionString(true)}() {${propsDecl}\n`
);
}

const scriptEndTagStart = htmlx.lastIndexOf('<', scriptTag.end - 1);
// wrap template with callback
str.overwrite(
scriptEndTagStart,
scriptTag.end,
`${slotsDeclaration};\nasync () => {`,

{
contentOnly: true
}
);
str.overwrite(scriptEndTagStart, scriptTag.end, `${slotsDeclaration};\nasync () => {`, {
contentOnly: true
});
} else {
str.prependRight(
scriptDestination,
`;function render${generics.toDefinitionString(true)}() {` +
`${propsDecl}${slotsDeclaration}\nasync () => {`
`;function render() {` + `${propsDecl}${slotsDeclaration}\nasync () => {`
);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/svelte2tsx/src/svelte2tsx/index.ts
Expand Up @@ -355,7 +355,7 @@ export function svelte2tsx(
const implicitStoreValues = new ImplicitStoreValues(resolvedStores, renderFunctionStart);
//move the instance script and process the content
let exportedNames = new ExportedNames(str, 0, basename);
let generics = new Generics(str, 0);
let generics = new Generics(str, 0, { attributes: [] } as any);
let uses$$SlotsInterface = false;
if (scriptTag) {
//ensure it is between the module script and the rest of the template (the variables need to be declared before the jsx template)
Expand Down
18 changes: 17 additions & 1 deletion packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts
@@ -1,17 +1,33 @@
import MagicString from 'magic-string';
import ts from 'typescript';
import { Node } from 'estree-walker';
import { surroundWithIgnoreComments } from '../../utils/ignore';
import { throwError } from '../utils/error';

export class Generics {
private definitions: string[] = [];
private typeReferences: string[] = [];
private references: string[] = [];
genericsAttr: Node | undefined;

constructor(private str: MagicString, private astOffset: number) {}
constructor(private str: MagicString, private astOffset: number, script: Node) {
this.genericsAttr = script.attributes.find((attr) => attr.name === 'generics');
const generics = this.genericsAttr?.value[0]?.raw as string | undefined;
if (generics) {
this.definitions = generics.split(',').map((g) => g.trim());
this.references = this.definitions.map((def) => def.split(/\s/)[0]);
} else {
this.genericsAttr = undefined;
}
}

addIfIsGeneric(node: ts.Node) {
if (ts.isTypeAliasDeclaration(node) && this.is$$GenericType(node.type)) {
if (this.genericsAttr) {
throw new Error(
'Invalid $$Generic declaration: $$Generic definitions are not allowed when the generics attribute is present on the script tag'
);
}
if (node.type.typeArguments?.length > 1) {
throw new Error('Invalid $$Generic declaration: Only one type argument allowed');
}
Expand Down
Expand Up @@ -53,7 +53,7 @@ export function processInstanceScriptContent(
);
const astOffset = script.content.start;
const exportedNames = new ExportedNames(str, astOffset, basename);
const generics = new Generics(str, astOffset);
const generics = new Generics(str, astOffset, script);
const interfacesAndTypes = new InterfacesAndTypes();

const implicitTopLevelNames = new ImplicitTopLevelNames(str, astOffset);
Expand Down
11 changes: 10 additions & 1 deletion packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts
Expand Up @@ -25,7 +25,16 @@ export function processModuleScriptTag(
);
const astOffset = script.content.start;

const generics = new Generics(str, astOffset);
const generics = new Generics(str, astOffset, script);
if (generics.genericsAttr) {
const start = htmlx.indexOf('generics', script.start);
throwError(
start,
start + 8,
'The generics attribute is only allowed on the instance script',
str.original
);
}

const walk = (node: ts.Node) => {
resolveImplicitStoreValue(node, implicitStoreValues, str, astOffset);
Expand Down
47 changes: 23 additions & 24 deletions packages/svelte2tsx/src/utils/htmlxparser.ts
@@ -1,33 +1,32 @@
import { parse } from 'svelte/compiler';
import { Node } from 'estree-walker';

function parseAttributeValue(value: string): string {
return /^['"]/.test(value) ? value.slice(1, -1) : value;
}

function parseAttributes(str: string, start: number) {
const attrs: Node[] = [];
str.split(/\s+/)
.filter(Boolean)
.forEach((attr) => {
const attrStart = start + str.indexOf(attr);
const [name, value] = attr.split('=');
attrs[name] = value ? parseAttributeValue(value) : name;
attrs.push({
type: 'Attribute',
name,
value: !value || [
{
type: 'Text',
start: attrStart + attr.indexOf('=') + 1,
end: attrStart + attr.length,
raw: parseAttributeValue(value)
}
],
start: attrStart,
end: attrStart + attr.length
});
const pattern = /([\w-$]+\b)(?:=(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;

let match: RegExpMatchArray;
while ((match = pattern.exec(str)) !== null) {
const attr = match[0];
const name = match[1];
const value = match[2] || match[3] || match[4];
const attrStart = start + str.indexOf(attr);
attrs[name] = value ?? name;
attrs.push({
type: 'Attribute',
name,
value: !value || [
{
type: 'Text',
start: attrStart + attr.indexOf('=') + 1,
end: attrStart + attr.length,
raw: value
}
],
start: attrStart,
end: attrStart + attr.length
});
}

return attrs;
}
Expand Down
@@ -0,0 +1,39 @@
///<reference types="svelte" />
;
import { createEventDispatcher } from 'svelte';
function render<A, B extends keyof A, C extends boolean>() {



let a: A/*Ωignore_startΩ*/;a = __sveltets_2_any(a);/*Ωignore_endΩ*/;
let b: B/*Ωignore_startΩ*/;b = __sveltets_2_any(b);/*Ωignore_endΩ*/;
let c: C/*Ωignore_startΩ*/;c = __sveltets_2_any(c);/*Ωignore_endΩ*/;

const dispatch = createEventDispatcher<{a: A}>();

function getA() {
return a;
}

/*Ωignore_startΩ*/;const __sveltets_createSlot = __sveltets_2_createCreateSlot();/*Ωignore_endΩ*/;
async () => {

{ __sveltets_createSlot("default", { c,});}};
return { props: {a: a , b: b , c: c , getA: getA} as {a: A, b: B, c: C, getA?: typeof getA}, slots: {'default': {c:c}}, events: {...__sveltets_2_toEventTypings<{a: A}>()} }}
class __sveltets_Render<A,B extends keyof A,C extends boolean> {
props() {
return render<A,B,C>().props;
}
events() {
return __sveltets_2_with_any_event(render<A,B,C>()).events;
}
slots() {
return render<A,B,C>().slots;
}
}


import { SvelteComponentTyped as __SvelteComponentTyped__ } from "svelte"
export default class Input__SvelteComponent_<A,B extends keyof A,C extends boolean> extends __SvelteComponentTyped__<ReturnType<__sveltets_Render<A,B,C>['props']>, ReturnType<__sveltets_Render<A,B,C>['events']>, ReturnType<__sveltets_Render<A,B,C>['slots']>> {
get getA() { return __sveltets_2_nonNullable(this.$$prop_def.getA) }
}
@@ -0,0 +1,15 @@
<script lang="ts" generics="A, B extends keyof A, C extends boolean">
import { createEventDispatcher } from 'svelte';

export let a: A;
export let b: B;
export let c: C;

const dispatch = createEventDispatcher<{a: A}>();

export function getA() {
return a;
}
</script>

<slot {c} />