Skip to content

Commit

Permalink
(feat) implement generics attr on script tags (#2020)
Browse files Browse the repository at this point in the history
  • Loading branch information
dummdidumm committed May 25, 2023
1 parent 2f25562 commit 932b8d3
Show file tree
Hide file tree
Showing 17 changed files with 260 additions and 52 deletions.
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 @@ -464,6 +464,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 @@ -509,7 +535,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} />

0 comments on commit 932b8d3

Please sign in to comment.