Repository: sveltejs/language-tools
Pull request #2020 (2023-05-17 14:24:59)

(feat) implement generics attr on script tags
https://github.com/sveltejs/rfcs/pull/38#issuecomment-1486353966

jasonlyu123: Overall looks good. There are two minor issues I found. Do we need an equivalent `throwIfIsGeneric` for the generic attribute? You can define generic on the module script now, but it doesn't do anything. I also notice that semantic tokens sometimes highlight `script lang="ts" generic` as a function. The current check needs to be more. I think we can skip the highlight if the highlighted identifier is `render` and if the mapped source includes a script tag. 
dummdidumm: > I also notice that semantic tokens sometimes highlight script lang="ts" generic as a function.

Do you have a code snippet that reproduces this?

Patch:
diff --git a/packages/language-server/src/plugins/html/dataProvider.ts b/packages/language-server/src/plugins/html/dataProvider.ts
index 2acd57db..dbf38c66 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<string, IAttributeData[]> = {
         {
             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 edda6db8..9af4f423 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-check/README.md b/packages/svelte-check/README.md
index c1dacf67..85567d05 100644
--- a/packages/svelte-check/README.md
+++ b/packages/svelte-check/README.md
@@ -53,7 +53,7 @@ Usage:
 | Flag                                                            | Description                                                                                                                                                                                                                                                                                                                                                                                 |
 | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | `--workspace <path>`                                            | Path to your workspace. All subdirectories except node_modules and those listed in `--ignore` are checked                                                                                                                                                                                                                                                                                   |
-| `--output <human\|human-verbose\|machine>`                      |
+| `--output <human\|human-verbose\|machine\|machine-verbose>`     |
 | `--watch`                                                       | Will not exit after one pass but keep watching files for changes and rerun diagnostics                                                                                                                                                                                                                                                                                                      |
 | `--preserveWatchOutput`                                         | Do not clear the screen in watch mode                                                                                                                                                                                                                                                                                                                                                       |
 | `--tsconfig <path>`                                             | Pass a path to a tsconfig or jsconfig file. The path can be relative to the workspace path or absolute. Doing this means that only files matched by the files/include/exclude pattern of the config file are diagnosed. It also means that errors from TypeScript and JavaScript files are reported. If not given, will do an upwards traversal looking for the next jsconfig/tsconfig.json |
@@ -76,7 +76,7 @@ Usage:
 
 ### Machine-Readable Output
 
-Setting the `--output` to `machine` will format output in a way that is easier to read
+Setting the `--output` to `machine` or `machine-verbose` will format output in a way that is easier to read
 by machines, e.g. inside CI pipelines, for code quality checks, etc.
 
 Each row corresponds to a new record. Rows are made up of columns that are separated by a
@@ -92,9 +92,9 @@ The first row is of type `START` and contains the workspace folder (wrapped in q
 1590680325583 START "/home/user/language-tools/packages/language-server/test/plugins/typescript/testfiles"
 ```
 
-Any number of `ERROR` or `WARNING` records may follow. Their structure is identical and tells
-us the filename, the line and column numbers, and the error message. The filename is relative
-to the workspace directory. The filename and the message are both wrapped in quotes.
+Any number of `ERROR` or `WARNING` records may follow. Their structure is identical and depends on the output argoument.
+
+If the argument is `machine` it will tell us the filename, the starting line and column numbers, and the error message. The filename is relative to the workspace directory. The filename and the message are both wrapped in quotes.
 
 ###### Example:
 
@@ -103,6 +103,16 @@ to the workspace directory. The filename and the message are both wrapped in quo
 1590680326778 WARNING "imported-file.svelte" 0:37 "Component has unused export property 'prop'. If it is for external reference only, please consider using `export const prop`"
 ```
 
+If the argument is `machine-verbose` it will tell us the filename, the starting line and column numbers, the ending line and column numbers, the error message, the code of diagnostic, the human-friendly description of the code and the human-friendly source of the diagnostic (eg. svelte/typescript). The filename is relative to the workspace directory. Each diagnostic is represented as an [ndjson](https://en.wikipedia.org/wiki/JSON_streaming#Newline-Delimited_JSON) line prefixed by the timestamp of the log.
+
+###### Example:
+
+```
+1590680326283 {"type":"ERROR","fn":"codeaction.svelte","start":{"line":1,"character":16},"end":{"line":1,"character":23},"message":"Cannot find module 'blubb' or its corresponding type declarations.","code":2307,"source":"js"}
+1590680326778 {"type":"WARNING","filename":"imported-file.svelte","start":{"line":0,"character":37},"end":{"line":0,"character":51},"message":"Component has unused export property 'prop'. If it is for external reference only, please consider using `export
+const prop`","code":"unused-export-let","source":"svelte"}
+```
+
 The output concludes with a `COMPLETED` message that summarizes total numbers of files, errors and warnings that were encountered during the check.
 
 ###### Example:
diff --git a/packages/svelte-check/src/index.ts b/packages/svelte-check/src/index.ts
index b28de500..93958c35 100644
--- a/packages/svelte-check/src/index.ts
+++ b/packages/svelte-check/src/index.ts
@@ -173,7 +173,11 @@ function instantiateWriter(opts: SvelteCheckCliOptions): Writer {
             filter
         );
     } else {
-        return new MachineFriendlyWriter(process.stdout, filter);
+        return new MachineFriendlyWriter(
+            process.stdout,
+            opts.outputFormat === 'machine-verbose',
+            filter
+        );
     }
 }
 
diff --git a/packages/svelte-check/src/options.ts b/packages/svelte-check/src/options.ts
index 1b7709ce..cf8bd2de 100644
--- a/packages/svelte-check/src/options.ts
+++ b/packages/svelte-check/src/options.ts
@@ -25,7 +25,7 @@ export function parseOptions(cb: (opts: SvelteCheckCliOptions) => any) {
         )
         .option(
             '--output',
-            'What output format to use. Options are human, human-verbose, machine.',
+            'What output format to use. Options are human, human-verbose, machine, machine-verbose.',
             'human-verbose'
         )
         .option(
@@ -86,7 +86,7 @@ export function parseOptions(cb: (opts: SvelteCheckCliOptions) => any) {
     });
 }
 
-const outputFormats = ['human', 'human-verbose', 'machine'] as const;
+const outputFormats = ['human', 'human-verbose', 'machine', 'machine-verbose'] as const;
 type OutputFormat = (typeof outputFormats)[number];
 
 function getOutputFormat(opts: Record<string, any>): OutputFormat {
diff --git a/packages/svelte-check/src/writers.ts b/packages/svelte-check/src/writers.ts
index f7b9313f..c20a9bc9 100644
--- a/packages/svelte-check/src/writers.ts
+++ b/packages/svelte-check/src/writers.ts
@@ -138,7 +138,11 @@ export class HumanFriendlyWriter implements Writer {
 }
 
 export class MachineFriendlyWriter implements Writer {
-    constructor(private stream: Writable, private diagnosticFilter = DEFAULT_FILTER) {}
+    constructor(
+        private stream: Writable,
+        private isVerbose = false,
+        private diagnosticFilter = DEFAULT_FILTER
+    ) {}
 
     private log(msg: string) {
         this.stream.write(`${new Date().getTime()} ${msg}\n`);
@@ -150,7 +154,7 @@ export class MachineFriendlyWriter implements Writer {
 
     file(diagnostics: Diagnostic[], workspaceDir: string, filename: string, _text: string) {
         diagnostics.filter(this.diagnosticFilter).forEach((d) => {
-            const { message, severity, range } = d;
+            const { message, severity, range, code, codeDescription, source } = d;
             const type =
                 severity === DiagnosticSeverity.Error
                     ? 'ERROR'
@@ -159,10 +163,25 @@ export class MachineFriendlyWriter implements Writer {
                     : null;
 
             if (type) {
-                const { line, character } = range.start;
-                const fn = JSON.stringify(filename);
-                const msg = JSON.stringify(message);
-                this.log(`${type} ${fn} ${line + 1}:${character + 1} ${msg}`);
+                const { start, end } = range;
+                if (this.isVerbose) {
+                    this.log(
+                        JSON.stringify({
+                            type,
+                            filename,
+                            start,
+                            end,
+                            message,
+                            code,
+                            codeDescription,
+                            source
+                        })
+                    );
+                } else {
+                    const fn = JSON.stringify(filename);
+                    const msg = JSON.stringify(message);
+                    this.log(`${type} ${fn} ${start.line + 1}:${start.character + 1} ${msg}`);
+                }
             }
         });
     }
diff --git a/packages/svelte-vscode/syntaxes/svelte.tmLanguage.src.yaml b/packages/svelte-vscode/syntaxes/svelte.tmLanguage.src.yaml
index c145f357..b40ff7f0 100644
--- a/packages/svelte-vscode/syntaxes/svelte.tmLanguage.src.yaml
+++ b/packages/svelte-vscode/syntaxes/svelte.tmLanguage.src.yaml
@@ -178,6 +178,16 @@ repository:
     - { begin: '(?=\[)', end: '(?<=\])',
         name: meta.embedded.expression.svelte source.ts,
         patterns: [include: source.ts#array-binding-pattern] }
+  destructuring-const:
+    patterns:
+    # {...}
+    - { begin: '(?={)', end: '(?<=})',
+        name: meta.embedded.expression.svelte source.ts,
+        patterns: [include: source.ts#object-binding-pattern-const] }
+    # [...]
+    - { begin: '(?=\[)', end: '(?<=\])',
+        name: meta.embedded.expression.svelte source.ts,
+        patterns: [include: source.ts#array-binding-pattern-const] }
 
   # Plain old interpolation between `{...}` blocks.
   interpolation:
@@ -231,12 +241,14 @@ repository:
     - begin: (?<=const.*?)\G
       end: (?=})
       patterns:
+      - include: '#destructuring-const'
       # Variable.
       - begin: \G\s*([_$[:alpha:]][_$[:alnum:]]+)\s*
         end: (?=\=)
         beginCaptures: { 1: { name: variable.other.constant.svelte } }
       # Expression (starting with "=").
       - begin: (?=\=)
+        name: meta.embedded.expression.svelte source.ts
         end: (?=})
         patterns: [ include: source.ts ]
 
@@ -452,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
 
@@ -497,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:
diff --git a/packages/svelte2tsx/src/svelte2tsx/createRenderFunction.ts b/packages/svelte2tsx/src/svelte2tsx/createRenderFunction.ts
index 0ce69740..80b57173 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 15440537..2009d688 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 7410ae49..12832c66 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 be3c447e..32607758 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 5f295e02..fa5f89a8 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 be53ec15..9fa3db1b 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/typescript-plugin/src/index.ts b/packages/typescript-plugin/src/index.ts
index c9f591ab..3816fd6e 100644
--- a/packages/typescript-plugin/src/index.ts
+++ b/packages/typescript-plugin/src/index.ts
@@ -19,7 +19,7 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule {
             return info.languageService;
         }
 
-        if (isPatched(info.languageService)) {
+        if (isPatched(info.project)) {
             logger.log('Already patched. Checking tsconfig updates.');
 
             ProjectSvelteFilesManager.getInstance(
@@ -104,6 +104,7 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule {
                   info.project,
                   info.serverHost,
                   snapshotManager,
+                  logger,
                   parsedCommandLine,
                   configManager
               )
@@ -130,18 +131,14 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule {
             }
         });
 
-        return decorateLanguageServiceDispose(
-            decorateLanguageService(
-                info.languageService,
-                snapshotManager,
-                logger,
-                configManager,
-                info,
-                modules.typescript
-            ),
-            projectSvelteFilesManager ?? {
-                dispose() {}
-            }
+        return decorateLanguageService(
+            info.languageService,
+            snapshotManager,
+            logger,
+            configManager,
+            info,
+            modules.typescript,
+            () => projectSvelteFilesManager?.dispose()
         );
     }
 
@@ -191,20 +188,6 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule {
         configManager.updateConfigFromPluginConfig(config);
     }
 
-    function decorateLanguageServiceDispose(
-        languageService: ts.LanguageService,
-        disposable: { dispose(): void }
-    ) {
-        const dispose = languageService.dispose;
-
-        languageService.dispose = () => {
-            disposable.dispose();
-            dispose();
-        };
-
-        return languageService;
-    }
-
     /**
      * TypeScript doesn't load the external files in projects without a config file. So we load it by ourselves.
      * TypeScript also seems to expect files added to the root to be opened by the client in this situation.
diff --git a/packages/typescript-plugin/src/language-service/index.ts b/packages/typescript-plugin/src/language-service/index.ts
index 9ebc80d0..54ee4b4e 100644
--- a/packages/typescript-plugin/src/language-service/index.ts
+++ b/packages/typescript-plugin/src/language-service/index.ts
@@ -15,10 +15,10 @@ import { decorateRename } from './rename';
 import { decorateUpdateImports } from './update-imports';
 import { decorateLanguageServiceHost } from './host';
 
-const sveltePluginPatchSymbol = Symbol('sveltePluginPatchSymbol');
+const patchedProject = new Set<string>();
 
-export function isPatched(ls: ts.LanguageService) {
-    return (ls as any)[sveltePluginPatchSymbol] === true;
+export function isPatched(project: ts.server.Project) {
+    return patchedProject.has(project.getProjectName());
 }
 
 export function decorateLanguageService(
@@ -27,13 +27,16 @@ export function decorateLanguageService(
     logger: Logger,
     configManager: ConfigManager,
     info: ts.server.PluginCreateInfo,
-    typescript: typeof ts
+    typescript: typeof ts,
+    onDispose: () => void
 ) {
+    patchedProject.add(info.project.getProjectName());
+
     // Decorate using a proxy so we can dynamically enable/disable method
     // patches depending on the enabled state of our config
     const proxy = new Proxy(ls, createProxyHandler(configManager));
     decorateLanguageServiceHost(info.languageServiceHost);
-    decorateLanguageServiceInner(proxy, snapshotManager, logger, info, typescript);
+    decorateLanguageServiceInner(proxy, snapshotManager, logger, info, typescript, onDispose);
 
     return proxy;
 }
@@ -43,7 +46,8 @@ function decorateLanguageServiceInner(
     snapshotManager: SvelteSnapshotManager,
     logger: Logger,
     info: ts.server.PluginCreateInfo,
-    typescript: typeof ts
+    typescript: typeof ts,
+    onDispose: () => void
 ): ts.LanguageService {
     patchLineColumnOffset(ls, snapshotManager);
     decorateRename(ls, snapshotManager, logger);
@@ -56,6 +60,7 @@ function decorateLanguageServiceInner(
     decorateCallHierarchy(ls, snapshotManager, typescript);
     decorateHover(ls, info, typescript, logger);
     decorateInlayHints(ls, info, typescript, logger);
+    decorateDispose(ls, info.project, onDispose);
     return ls;
 }
 
@@ -64,12 +69,8 @@ function createProxyHandler(configManager: ConfigManager): ProxyHandler<ts.Langu
 
     return {
         get(target, p) {
-            // always return patch symbol whether the plugin is enabled or not
-            if (p === sveltePluginPatchSymbol) {
-                return true;
-            }
-
-            if (!configManager.getConfig().enable || p === 'dispose') {
+            // always check for decorated dispose
+            if (!configManager.getConfig().enable && p !== 'dispose') {
                 return target[p as keyof ts.LanguageService];
             }
 
@@ -102,3 +103,19 @@ function patchLineColumnOffset(ls: ts.LanguageService, snapshotManager: SvelteSn
         return toLineColumnOffset(fileName, position);
     };
 }
+
+function decorateDispose(
+    ls: ts.LanguageService,
+    project: ts.server.Project,
+    onDispose: () => void
+) {
+    const dispose = ls.dispose;
+
+    ls.dispose = () => {
+        patchedProject.delete(project.getProjectName());
+        onDispose();
+        dispose();
+    };
+
+    return ls;
+}
diff --git a/packages/typescript-plugin/src/project-svelte-files.ts b/packages/typescript-plugin/src/project-svelte-files.ts
index 9300007f..835b0554 100644
--- a/packages/typescript-plugin/src/project-svelte-files.ts
+++ b/packages/typescript-plugin/src/project-svelte-files.ts
@@ -2,6 +2,7 @@ import type ts from 'typescript/lib/tsserverlibrary';
 import { ConfigManager, Configuration } from './config-manager';
 import { SvelteSnapshotManager } from './svelte-snapshots';
 import { getConfigPathForProject, isSvelteFilePath } from './utils';
+import { Logger } from './logger';
 
 export interface TsFilesSpec {
     include?: readonly string[];
@@ -9,7 +10,7 @@ export interface TsFilesSpec {
 }
 
 export class ProjectSvelteFilesManager {
-    private files = new Set<string>();
+    private projectFileToOriginalCasing = new Map<string, string>();
     private directoryWatchers = new Set<ts.FileWatcher>();
 
     private static instances = new Map<string, ProjectSvelteFilesManager>();
@@ -23,6 +24,7 @@ export class ProjectSvelteFilesManager {
         private readonly project: ts.server.Project,
         private readonly serverHost: ts.server.ServerHost,
         private readonly snapshotManager: SvelteSnapshotManager,
+        private readonly logger: Logger,
         private parsedCommandLine: ts.ParsedCommandLine,
         configManager: ConfigManager
     ) {
@@ -51,7 +53,7 @@ export class ProjectSvelteFilesManager {
     }
 
     getFiles() {
-        return Array.from(this.files);
+        return Array.from(this.projectFileToOriginalCasing.values());
     }
 
     /**
@@ -94,23 +96,34 @@ export class ProjectSvelteFilesManager {
     }
 
     private updateProjectSvelteFiles() {
-        const fileNamesAfter = this.readProjectSvelteFilesFromFs();
-        const removedFiles = new Set(...this.files);
-        const newFiles = fileNamesAfter.filter((fileName) => {
-            const has = this.files.has(fileName);
-            if (has) {
-                removedFiles.delete(fileName);
+        const fileNamesAfter = this.readProjectSvelteFilesFromFs().map((file) => ({
+            originalCasing: file,
+            canonicalFileName: this.project.projectService.toCanonicalFileName(file)
+        }));
+
+        const removedFiles = new Set(this.projectFileToOriginalCasing.keys());
+        const newFiles: typeof fileNamesAfter = [];
+
+        for (const file of fileNamesAfter) {
+            const existingFile = this.projectFileToOriginalCasing.get(file.canonicalFileName);
+            if (!existingFile) {
+                newFiles.push(file);
+                continue;
             }
-            return !has;
-        });
+
+            removedFiles.delete(file.canonicalFileName);
+            if (existingFile !== file.originalCasing) {
+                this.projectFileToOriginalCasing.set(file.canonicalFileName, file.originalCasing);
+            }
+        }
 
         for (const newFile of newFiles) {
-            this.addFileToProject(newFile);
-            this.files.add(newFile);
+            this.addFileToProject(newFile.originalCasing);
+            this.projectFileToOriginalCasing.set(newFile.canonicalFileName, newFile.originalCasing);
         }
         for (const removedFile of removedFiles) {
             this.removeFileFromProject(removedFile, false);
-            this.files.delete(removedFile);
+            this.projectFileToOriginalCasing.delete(removedFile);
         }
     }
 
@@ -118,9 +131,16 @@ export class ProjectSvelteFilesManager {
         this.snapshotManager.create(newFile);
         const snapshot = this.project.projectService.getScriptInfo(newFile);
 
-        if (snapshot) {
-            this.project.addRoot(snapshot);
+        if (!snapshot) {
+            return;
         }
+
+        if (this.project.isRoot(snapshot)) {
+            this.logger.debug(`File ${newFile} is already in root`);
+            return;
+        }
+
+        this.project.addRoot(snapshot);
     }
 
     private readProjectSvelteFilesFromFs() {
@@ -162,8 +182,8 @@ export class ProjectSvelteFilesManager {
         this.directoryWatchers.forEach((watcher) => watcher.close());
         this.directoryWatchers.clear();
 
-        this.files.forEach((file) => this.removeFileFromProject(file));
-        this.files.clear();
+        this.projectFileToOriginalCasing.forEach((file) => this.removeFileFromProject(file));
+        this.projectFileToOriginalCasing.clear();
     }
 
     dispose() {


Test patch:
diff --git a/packages/svelte-vscode/test/grammar/dummy/ts.tmLanguage-dummy.json b/packages/svelte-vscode/test/grammar/dummy/ts.tmLanguage-dummy.json
index 4ed56205..47d6b470 100644
--- a/packages/svelte-vscode/test/grammar/dummy/ts.tmLanguage-dummy.json
+++ b/packages/svelte-vscode/test/grammar/dummy/ts.tmLanguage-dummy.json
@@ -1,4 +1,26 @@
 {
     "comment": "Dummy TS TextMate grammar for use in testing",
-    "scopeName": "source.ts"
+    "scopeName": "source.ts",
+    "repository": {
+        "object-binding-pattern": {
+            "comment": "Referenced in source.svelte#destructuring, needed to be defined here so that matching won't end at the } of destructuring",
+            "begin": "(?:(\\.\\.\\.)\\s*)?(\\{)",
+            "end": "\\}"
+        },
+        "object-binding-pattern-const": {
+            "comment": "Referenced in source.svelte#destructuring-const",
+            "begin": "(?:(\\.\\.\\.)\\s*)?(\\{)",
+            "end": "\\}"
+        },
+        "array-binding-pattern": {
+            "comment": "Referenced in source.svelte#destructuring",
+            "begin": "(?:(\\.\\.\\.)\\s*)?(\\[)",
+            "end": "\\]"
+        },
+        "array-binding-pattern-const": {
+            "comment": "Referenced in source.svelte#destructuring-const",
+            "begin": "(?:(\\.\\.\\.)\\s*)?(\\[)",
+            "end": "\\]"
+        }
+    }
 }
diff --git a/packages/svelte-vscode/test/grammar/samples/const/input.svelte b/packages/svelte-vscode/test/grammar/samples/const/input.svelte
index dbff6ea3..e960f24c 100644
--- a/packages/svelte-vscode/test/grammar/samples/const/input.svelte
+++ b/packages/svelte-vscode/test/grammar/samples/const/input.svelte
@@ -1,4 +1,6 @@
 {#each items as item}
     {@const _item = item}
     {item}
+    {@const { a } = abc}
+    {@const [ e ] = abc}
 {/each}
diff --git a/packages/svelte-vscode/test/grammar/samples/const/input.svelte.snap b/packages/svelte-vscode/test/grammar/samples/const/input.svelte.snap
index a4a22f96..07d36e0d 100644
--- a/packages/svelte-vscode/test/grammar/samples/const/input.svelte.snap
+++ b/packages/svelte-vscode/test/grammar/samples/const/input.svelte.snap
@@ -16,13 +16,37 @@
 #           ^ source.svelte meta.special.const.svelte
 #            ^^^^^ source.svelte meta.special.const.svelte variable.other.constant.svelte
 #                 ^ source.svelte meta.special.const.svelte
-#                  ^^^^^^ source.svelte meta.special.const.svelte
+#                  ^^^^^^ source.svelte meta.special.const.svelte meta.embedded.expression.svelte source.ts
 #                        ^ source.svelte meta.special.const.svelte punctuation.definition.block.end.svelte
 >    {item}
 #^^^^ source.svelte text.svelte
 #    ^ source.svelte punctuation.section.embedded.begin.svelte
 #     ^^^^ source.svelte meta.embedded.expression.svelte source.ts
 #         ^ source.svelte punctuation.section.embedded.end.svelte
+>    {@const { a } = abc}
+#^^^^ source.svelte text.svelte
+#    ^ source.svelte meta.special.const.svelte punctuation.definition.block.begin.svelte
+#     ^ source.svelte meta.special.const.svelte punctuation.definition.keyword.svelte
+#      ^^^^^ source.svelte meta.special.const.svelte storage.type.svelte
+#           ^ source.svelte meta.special.const.svelte
+#            ^ source.svelte meta.special.const.svelte meta.embedded.expression.svelte source.ts
+#             ^^^ source.svelte meta.special.const.svelte meta.embedded.expression.svelte source.ts
+#                ^ source.svelte meta.special.const.svelte meta.embedded.expression.svelte source.ts
+#                 ^ source.svelte meta.special.const.svelte
+#                  ^^^^^ source.svelte meta.special.const.svelte meta.embedded.expression.svelte source.ts
+#                       ^ source.svelte meta.special.const.svelte punctuation.definition.block.end.svelte
+>    {@const [ e ] = abc}
+#^^^^ source.svelte text.svelte
+#    ^ source.svelte meta.special.const.svelte punctuation.definition.block.begin.svelte
+#     ^ source.svelte meta.special.const.svelte punctuation.definition.keyword.svelte
+#      ^^^^^ source.svelte meta.special.const.svelte storage.type.svelte
+#           ^ source.svelte meta.special.const.svelte
+#            ^ source.svelte meta.special.const.svelte meta.embedded.expression.svelte source.ts
+#             ^^^ source.svelte meta.special.const.svelte meta.embedded.expression.svelte source.ts
+#                ^ source.svelte meta.special.const.svelte meta.embedded.expression.svelte source.ts
+#                 ^ source.svelte meta.special.const.svelte
+#                  ^^^^^ source.svelte meta.special.const.svelte meta.embedded.expression.svelte source.ts
+#                       ^ source.svelte meta.special.const.svelte punctuation.definition.block.end.svelte
 >{/each}
 #^ source.svelte meta.special.each.svelte meta.special.end.svelte punctuation.definition.block.begin.svelte
 # ^ source.svelte meta.special.each.svelte meta.special.end.svelte punctuation.definition.keyword.svelte
diff --git a/packages/svelte-vscode/test/grammar/samples/each-block/input.svelte.snap b/packages/svelte-vscode/test/grammar/samples/each-block/input.svelte.snap
index c8cca1ab..0888c820 100644
--- a/packages/svelte-vscode/test/grammar/samples/each-block/input.svelte.snap
+++ b/packages/svelte-vscode/test/grammar/samples/each-block/input.svelte.snap
@@ -46,11 +46,11 @@
 #       ^^^^^^^^^^ source.svelte meta.special.each.svelte meta.special.start.svelte meta.embedded.expression.svelte source.ts
 #                 ^ source.svelte meta.special.each.svelte meta.special.start.svelte
 #                  ^^ source.svelte meta.special.each.svelte meta.special.start.svelte keyword.control.as.svelte
-#                    ^^ source.svelte meta.special.each.svelte meta.special.start.svelte
-#                      ^^^ source.svelte meta.special.each.svelte meta.special.start.svelte meta.embedded.expression.svelte source.ts
-#                         ^ source.svelte meta.special.each.svelte meta.special.start.svelte punctuation.separator.svelte
-#                          ^^^^^^ source.svelte meta.special.each.svelte meta.special.start.svelte meta.embedded.expression.svelte source.ts
-#                                ^^ source.svelte meta.special.each.svelte meta.special.start.svelte
+#                    ^ source.svelte meta.special.each.svelte meta.special.start.svelte
+#                     ^ source.svelte meta.special.each.svelte meta.special.start.svelte meta.embedded.expression.svelte source.ts
+#                      ^^^^^^^^^^ source.svelte meta.special.each.svelte meta.special.start.svelte meta.embedded.expression.svelte source.ts
+#                                ^ source.svelte meta.special.each.svelte meta.special.start.svelte meta.embedded.expression.svelte source.ts
+#                                 ^ source.svelte meta.special.each.svelte meta.special.start.svelte
 #                                  ^ source.svelte meta.special.each.svelte meta.special.start.svelte meta.brace.round.svelte
 #                                   ^^^ source.svelte meta.special.each.svelte meta.special.start.svelte meta.embedded.expression.svelte source.ts
 #                                      ^ source.svelte meta.special.each.svelte meta.special.start.svelte meta.brace.round.svelte
diff --git a/packages/svelte-vscode/test/grammar/samples/handler/input.svelte b/packages/svelte-vscode/test/grammar/samples/handler/input.svelte
index 7a9232e2..281d386f 100644
--- a/packages/svelte-vscode/test/grammar/samples/handler/input.svelte
+++ b/packages/svelte-vscode/test/grammar/samples/handler/input.svelte
@@ -1,3 +1,3 @@
-<div on:click="{(e) => {console.log(e)}}"></div>
+<div on:click="{(e) => console.log(e)}"></div>
 
-<Component on:click={(e) => {console.log(e)}}></Component>
+<Component on:click={(e) => console.log(e)}></Component>
diff --git a/packages/svelte-vscode/test/grammar/samples/handler/input.svelte.snap b/packages/svelte-vscode/test/grammar/samples/handler/input.svelte.snap
index a1c1a42b..c2580ecb 100644
--- a/packages/svelte-vscode/test/grammar/samples/handler/input.svelte.snap
+++ b/packages/svelte-vscode/test/grammar/samples/handler/input.svelte.snap
@@ -1,4 +1,4 @@
-><div on:click="{(e) => {console.log(e)}}"></div>
+><div on:click="{(e) => console.log(e)}"></div>
 #^ source.svelte meta.scope.tag.div.svelte meta.tag.start.svelte punctuation.definition.tag.begin.svelte
 # ^^^ source.svelte meta.scope.tag.div.svelte meta.tag.start.svelte entity.name.tag.svelte
 #    ^ source.svelte meta.scope.tag.div.svelte meta.tag.start.svelte
@@ -8,16 +8,15 @@
 #             ^ source.svelte meta.scope.tag.div.svelte meta.tag.start.svelte meta.directive.on.svelte punctuation.separator.key-value.svelte
 #              ^ source.svelte meta.scope.tag.div.svelte meta.tag.start.svelte meta.directive.on.svelte string.quoted.svelte punctuation.definition.string.begin.svelte
 #               ^ source.svelte meta.scope.tag.div.svelte meta.tag.start.svelte meta.directive.on.svelte string.quoted.svelte punctuation.section.embedded.begin.svelte
-#                ^^^^^^^^^^^^^^^^^^^^^^ source.svelte meta.scope.tag.div.svelte meta.tag.start.svelte meta.directive.on.svelte string.quoted.svelte meta.embedded.expression.svelte source.ts
-#                                      ^ source.svelte meta.scope.tag.div.svelte meta.tag.start.svelte meta.directive.on.svelte string.quoted.svelte punctuation.section.embedded.end.svelte
-#                                       ^ source.svelte meta.scope.tag.div.svelte meta.tag.start.svelte meta.directive.on.svelte string.quoted.svelte
-#                                        ^ source.svelte meta.scope.tag.div.svelte meta.tag.start.svelte meta.directive.on.svelte string.quoted.svelte punctuation.definition.string.end.svelte
-#                                         ^ source.svelte meta.scope.tag.div.svelte meta.tag.start.svelte punctuation.definition.tag.end.svelte
-#                                          ^^ source.svelte meta.scope.tag.div.svelte meta.tag.end.svelte punctuation.definition.tag.begin.svelte
-#                                            ^^^ source.svelte meta.scope.tag.div.svelte meta.tag.end.svelte entity.name.tag.svelte
-#                                               ^ source.svelte meta.scope.tag.div.svelte meta.tag.end.svelte punctuation.definition.tag.end.svelte
+#                ^^^^^^^^^^^^^^^^^^^^^ source.svelte meta.scope.tag.div.svelte meta.tag.start.svelte meta.directive.on.svelte string.quoted.svelte meta.embedded.expression.svelte source.ts
+#                                     ^ source.svelte meta.scope.tag.div.svelte meta.tag.start.svelte meta.directive.on.svelte string.quoted.svelte punctuation.section.embedded.end.svelte
+#                                      ^ source.svelte meta.scope.tag.div.svelte meta.tag.start.svelte meta.directive.on.svelte string.quoted.svelte punctuation.definition.string.end.svelte
+#                                       ^ source.svelte meta.scope.tag.div.svelte meta.tag.start.svelte punctuation.definition.tag.end.svelte
+#                                        ^^ source.svelte meta.scope.tag.div.svelte meta.tag.end.svelte punctuation.definition.tag.begin.svelte
+#                                          ^^^ source.svelte meta.scope.tag.div.svelte meta.tag.end.svelte entity.name.tag.svelte
+#                                             ^ source.svelte meta.scope.tag.div.svelte meta.tag.end.svelte punctuation.definition.tag.end.svelte
 >
-><Component on:click={(e) => {console.log(e)}}></Component>
+><Component on:click={(e) => console.log(e)}></Component>
 #^ source.svelte meta.scope.tag.Component.svelte meta.tag.start.svelte punctuation.definition.tag.begin.svelte
 # ^^^^^^^^^ source.svelte meta.scope.tag.Component.svelte meta.tag.start.svelte support.class.component.svelte
 #          ^ source.svelte meta.scope.tag.Component.svelte meta.tag.start.svelte
@@ -26,11 +25,10 @@
 #              ^^^^^ source.svelte meta.scope.tag.Component.svelte meta.tag.start.svelte meta.directive.on.svelte entity.name.type.svelte
 #                   ^ source.svelte meta.scope.tag.Component.svelte meta.tag.start.svelte meta.directive.on.svelte punctuation.separator.key-value.svelte
 #                    ^ source.svelte meta.scope.tag.Component.svelte meta.tag.start.svelte meta.directive.on.svelte punctuation.section.embedded.begin.svelte
-#                     ^^^^^^^^^^^^^^^^^^^^^^ source.svelte meta.scope.tag.Component.svelte meta.tag.start.svelte meta.directive.on.svelte meta.embedded.expression.svelte source.ts
-#                                           ^ source.svelte meta.scope.tag.Component.svelte meta.tag.start.svelte meta.directive.on.svelte punctuation.section.embedded.end.svelte
-#                                            ^ source.svelte meta.scope.tag.Component.svelte meta.tag.start.svelte
-#                                             ^ source.svelte meta.scope.tag.Component.svelte meta.tag.start.svelte punctuation.definition.tag.end.svelte
-#                                              ^^ source.svelte meta.scope.tag.Component.svelte meta.tag.end.svelte punctuation.definition.tag.begin.svelte
-#                                                ^^^^^^^^^ source.svelte meta.scope.tag.Component.svelte meta.tag.end.svelte support.class.component.svelte
-#                                                         ^ source.svelte meta.scope.tag.Component.svelte meta.tag.end.svelte punctuation.definition.tag.end.svelte
+#                     ^^^^^^^^^^^^^^^^^^^^^ source.svelte meta.scope.tag.Component.svelte meta.tag.start.svelte meta.directive.on.svelte meta.embedded.expression.svelte source.ts
+#                                          ^ source.svelte meta.scope.tag.Component.svelte meta.tag.start.svelte meta.directive.on.svelte punctuation.section.embedded.end.svelte
+#                                           ^ source.svelte meta.scope.tag.Component.svelte meta.tag.start.svelte punctuation.definition.tag.end.svelte
+#                                            ^^ source.svelte meta.scope.tag.Component.svelte meta.tag.end.svelte punctuation.definition.tag.begin.svelte
+#                                              ^^^^^^^^^ source.svelte meta.scope.tag.Component.svelte meta.tag.end.svelte support.class.component.svelte
+#                                                       ^ source.svelte meta.scope.tag.Component.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-multiline/input.svelte b/packages/svelte-vscode/test/grammar/samples/script-generics-multiline/input.svelte
new file mode 100644
index 00000000..4a0b6b86
--- /dev/null
+++ b/packages/svelte-vscode/test/grammar/samples/script-generics-multiline/input.svelte
@@ -0,0 +1,5 @@
+<script lang="ts" generics="
+    T extends 'a' |'b' |'c'
+    ">
+    export let t: T;
+</script>
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 00000000..47be954d
--- /dev/null
+++ b/packages/svelte-vscode/test/grammar/samples/script-generics-multiline/input.svelte.snap
@@ -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
+>
\ 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 00000000..89a3991b
--- /dev/null
+++ b/packages/svelte-vscode/test/grammar/samples/script-generics/input.svelte
@@ -0,0 +1,3 @@
+<script generics="T" lang="ts">
+    export let t: T;
+</script>
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 00000000..fb3a5921
--- /dev/null
+++ b/packages/svelte-vscode/test/grammar/samples/script-generics/input.svelte.snap
@@ -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
+>
\ No newline at end of file
diff --git a/packages/svelte-vscode/test/grammar/test.js b/packages/svelte-vscode/test/grammar/test.js
index 03b9f03b..8f3aea42 100644
--- a/packages/svelte-vscode/test/grammar/test.js
+++ b/packages/svelte-vscode/test/grammar/test.js
@@ -40,7 +40,10 @@ async function snapShotTest() {
         'source.svelte',
         '-t',
         './test/grammar/samples/**/*.svelte',
-        ...allGrammars.reduce((previous, path) => [...previous, '-g', path], []),
+        ...allGrammars.reduce(
+            (previous, path) => [...previous, '-g', path],
+            /** @type {string[]} */ ([])
+        ),
         ...extraArgs
     ];
 
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 00000000..bf76800a
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-generics-attribute1/expectedv2.ts
@@ -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) }
+}
\ 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 00000000..d7c9a1dd
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-generics-attribute1/input.svelte
@@ -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} />
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 00000000..4f7e9aa0
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-generics-attribute2/expectedv2.ts
@@ -0,0 +1,23 @@
+///<reference types="svelte" />
+;function render<T>() {
+
+     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<T> {
+    props() {
+        return render<T>().props;
+    }
+    events() {
+        return __sveltets_2_with_any_event(render<T>()).events;
+    }
+    slots() {
+        return render<T>().slots;
+    }
+}
+
+
+import { SvelteComponentTyped as __SvelteComponentTyped__ } from "svelte" 
+export default class Input__SvelteComponent_<T> extends __SvelteComponentTyped__<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['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 00000000..9d3e4d45
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-generics-attribute2/input.svelte
@@ -0,0 +1,3 @@
+<script lang="ts" generics=T>
+    export let a: T;
+</script>