diff --git a/packages/language-server/src/plugins/html/dataProvider.ts b/packages/language-server/src/plugins/html/dataProvider.ts index 2acd57dba..dbf38c667 100644 --- a/packages/language-server/src/plugins/html/dataProvider.ts +++ b/packages/language-server/src/plugins/html/dataProvider.ts @@ -363,6 +363,13 @@ const addAttributes: Record = { { name: 'bind:open' } + ], + script: [ + { + name: 'generics', + description: + 'Generics used within the components. Only available when using TypeScript.' + } ] }; diff --git a/packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts b/packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts index edda6db8e..9af4f4239 100644 --- a/packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts @@ -70,7 +70,8 @@ export class SemanticTokensProviderImpl implements SemanticTokensProvider { textDocument, tsDoc, generatedOffset, - generatedLength + generatedLength, + encodedClassification ); if (!originalPosition) { continue; @@ -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; } diff --git a/packages/svelte-vscode/syntaxes/svelte.tmLanguage.src.yaml b/packages/svelte-vscode/syntaxes/svelte.tmLanguage.src.yaml index f2a5951a2..b40ff7f06 100644 --- a/packages/svelte-vscode/syntaxes/svelte.tmLanguage.src.yaml +++ b/packages/svelte-vscode/syntaxes/svelte.tmLanguage.src.yaml @@ -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: '(?) + # ------ # TAGS @@ -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 (` + export let t: T; + diff --git a/packages/svelte-vscode/test/grammar/samples/script-generics-multiline/input.svelte.snap b/packages/svelte-vscode/test/grammar/samples/script-generics-multiline/input.svelte.snap new file mode 100644 index 000000000..47be954dc --- /dev/null +++ b/packages/svelte-vscode/test/grammar/samples/script-generics-multiline/input.svelte.snap @@ -0,0 +1,28 @@ +> +#^^ 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 +> \ No newline at end of file diff --git a/packages/svelte-vscode/test/grammar/samples/script-generics/input.svelte b/packages/svelte-vscode/test/grammar/samples/script-generics/input.svelte new file mode 100644 index 000000000..89a3991b0 --- /dev/null +++ b/packages/svelte-vscode/test/grammar/samples/script-generics/input.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte-vscode/test/grammar/samples/script-generics/input.svelte.snap b/packages/svelte-vscode/test/grammar/samples/script-generics/input.svelte.snap new file mode 100644 index 000000000..fb3a5921c --- /dev/null +++ b/packages/svelte-vscode/test/grammar/samples/script-generics/input.svelte.snap @@ -0,0 +1,23 @@ +> +#^^ 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 +> \ No newline at end of file diff --git a/packages/svelte2tsx/src/svelte2tsx/createRenderFunction.ts b/packages/svelte2tsx/src/svelte2tsx/createRenderFunction.ts index 0ce697409..80b571730 100644 --- a/packages/svelte2tsx/src/svelte2tsx/createRenderFunction.ts +++ b/packages/svelte2tsx/src/svelte2tsx/createRenderFunction.ts @@ -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 () => {` ); } diff --git a/packages/svelte2tsx/src/svelte2tsx/index.ts b/packages/svelte2tsx/src/svelte2tsx/index.ts index 15440537a..2009d6884 100644 --- a/packages/svelte2tsx/src/svelte2tsx/index.ts +++ b/packages/svelte2tsx/src/svelte2tsx/index.ts @@ -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) diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts index 7410ae498..12832c669 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts @@ -1,5 +1,6 @@ import MagicString from 'magic-string'; import ts from 'typescript'; +import { Node } from 'estree-walker'; import { surroundWithIgnoreComments } from '../../utils/ignore'; import { throwError } from '../utils/error'; @@ -7,11 +8,26 @@ 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'); } diff --git a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts index be3c447e4..326077585 100644 --- a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts +++ b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts @@ -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); diff --git a/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts b/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts index 5f295e024..fa5f89a89 100644 --- a/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts +++ b/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts @@ -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); diff --git a/packages/svelte2tsx/src/utils/htmlxparser.ts b/packages/svelte2tsx/src/utils/htmlxparser.ts index be53ec159..9fa3db1b4 100644 --- a/packages/svelte2tsx/src/utils/htmlxparser.ts +++ b/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; } diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-generics-attribute1/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-generics-attribute1/expectedv2.ts new file mode 100644 index 000000000..bf76800a4 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-generics-attribute1/expectedv2.ts @@ -0,0 +1,39 @@ +/// +; +import { createEventDispatcher } from 'svelte'; +function render() { + + + + 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 { + props() { + return render().props; + } + events() { + return __sveltets_2_with_any_event(render()).events; + } + slots() { + return render().slots; + } +} + + +import { SvelteComponentTyped as __SvelteComponentTyped__ } from "svelte" +export default class Input__SvelteComponent_ extends __SvelteComponentTyped__['props']>, ReturnType<__sveltets_Render['events']>, ReturnType<__sveltets_Render['slots']>> { + get getA() { return __sveltets_2_nonNullable(this.$$prop_def.getA) } +} \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-generics-attribute1/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-generics-attribute1/input.svelte new file mode 100644 index 000000000..d7c9a1ddb --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-generics-attribute1/input.svelte @@ -0,0 +1,15 @@ + + + diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-generics-attribute2/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-generics-attribute2/expectedv2.ts new file mode 100644 index 000000000..4f7e9aa0e --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-generics-attribute2/expectedv2.ts @@ -0,0 +1,23 @@ +/// +;function render() { + + let a: T/*Ωignore_startΩ*/;a = __sveltets_2_any(a);/*Ωignore_endΩ*/; +; +async () => {}; +return { props: {a: a} as {a: T}, slots: {}, events: {} }} +class __sveltets_Render { + props() { + return render().props; + } + events() { + return __sveltets_2_with_any_event(render()).events; + } + slots() { + return render().slots; + } +} + + +import { SvelteComponentTyped as __SvelteComponentTyped__ } from "svelte" +export default class Input__SvelteComponent_ extends __SvelteComponentTyped__['props']>, ReturnType<__sveltets_Render['events']>, ReturnType<__sveltets_Render['slots']>> { +} \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-generics-attribute2/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-generics-attribute2/input.svelte new file mode 100644 index 000000000..9d3e4d456 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-generics-attribute2/input.svelte @@ -0,0 +1,3 @@ +