Repository: sveltejs/language-tools
Pull request #1238 (2021-11-12 02:52:39)

(fix) slot let rename
Currently, renaming on slot let would result in invalid code because the svelte2tsx transform it into a destructured variable declaration

```svelte
<Component let:a>
</Component>
```

become

```svelte
<Component let:a: newName>
</Component>
```

This PR uses the logic in the shorthand props and transform it into 
```svelte
<Component let:a={newName}>
</Component>
```

Also, fix a sourcemap issue with multiple slot-let. Currently it would the generated code would source map back with extra whitespace. When renaming it now, the whitespace would be present in the new name input.  After rename, the white space might be removed.



Patch:
diff --git a/packages/language-server/src/plugins/typescript/SnapshotManager.ts b/packages/language-server/src/plugins/typescript/SnapshotManager.ts
index 8b95b2a5..d7b19c0c 100644
--- a/packages/language-server/src/plugins/typescript/SnapshotManager.ts
+++ b/packages/language-server/src/plugins/typescript/SnapshotManager.ts
@@ -48,6 +48,7 @@ export class GlobalSnapshotsManager {
                 return;
             }
             previousSnapshot.update(changes);
+            this.emitter.emit('change', fileName, previousSnapshot);
             return previousSnapshot;
         } else {
             const newSnapshot = DocumentSnapshot.fromNonSvelteFilePath(fileName);
diff --git a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts
index c22caf23..896b7947 100644
--- a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts
+++ b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts
@@ -70,7 +70,7 @@ export class RenameProviderImpl implements RenameProvider {
             }
         > = await this.mapAndFilterRenameLocations(renameLocations, docs);
 
-        convertedRenameLocations = this.checkShortHandBindingLocation(
+        convertedRenameLocations = this.checkShortHandBindingOrSlotLetLocation(
             lang,
             convertedRenameLocations,
             docs
@@ -245,7 +245,7 @@ export class RenameProviderImpl implements RenameProvider {
         const replacementsForProp =
             lang.findRenameLocations(updatePropLocation.fileName, idx, false, false) || [];
 
-        return this.checkShortHandBindingLocation(
+        return this.checkShortHandBindingOrSlotLetLocation(
             lang,
             await this.mapAndFilterRenameLocations(replacementsForProp, fragments),
             fragments
@@ -382,7 +382,7 @@ export class RenameProviderImpl implements RenameProvider {
         return this.lsAndTsDocResolver.getSnapshot(filePath);
     }
 
-    private checkShortHandBindingLocation(
+    private checkShortHandBindingOrSlotLetLocation(
         lang: ts.LanguageService,
         renameLocations: Array<ts.RenameLocation & { range: Range }>,
         fragments: SnapshotFragmentMap
@@ -408,40 +408,29 @@ export class RenameProviderImpl implements RenameProvider {
 
             const { originalText } = fragment;
 
-            const possibleJsxAttribute = findContainingNode(
-                sourceFile,
-                location.textSpan,
-                ts.isJsxAttribute
-            );
-            if (!possibleJsxAttribute) {
-                return location;
-            }
-
-            const attributeName = possibleJsxAttribute.name.getText();
-            const { initializer } = possibleJsxAttribute;
+            const renamingInfo =
+                this.getShorthandPropInfo(sourceFile, location) ??
+                this.getSlotLetInfo(sourceFile, location);
 
-            // not props={props}
-            if (
-                !initializer ||
-                !ts.isJsxExpression(initializer) ||
-                attributeName !== initializer.expression?.getText()
-            ) {
+            if (!renamingInfo) {
                 return location;
             }
 
+            const [renamingNode, identifierName] = renamingInfo;
+
             const originalStart = offsetAt(location.range.start, originalText);
 
             const isShortHandBinding =
                 originalText.substr(originalStart - bind.length, bind.length) === bind;
 
-            const directiveName = (isShortHandBinding ? bind : '') + attributeName;
+            const directiveName = (isShortHandBinding ? bind : '') + identifierName;
             const prefixText = directiveName + '={';
 
             const newRange = mapRangeToOriginal(
                 fragment,
                 convertRange(fragment, {
-                    start: possibleJsxAttribute.getStart(),
-                    length: possibleJsxAttribute.getWidth()
+                    start: renamingNode.getStart(),
+                    length: renamingNode.getWidth()
                 })
             );
 
@@ -463,6 +452,62 @@ export class RenameProviderImpl implements RenameProvider {
             };
         });
     }
+
+    private getShorthandPropInfo(
+        sourceFile: ts.SourceFile,
+        location: ts.RenameLocation
+    ): [ts.Node, string] | null {
+        const possibleJsxAttribute = findContainingNode(
+            sourceFile,
+            location.textSpan,
+            ts.isJsxAttribute
+        );
+        if (!possibleJsxAttribute) {
+            return null;
+        }
+
+        const attributeName = possibleJsxAttribute.name.getText();
+        const { initializer } = possibleJsxAttribute;
+
+        // not props={props}
+        if (
+            !initializer ||
+            !ts.isJsxExpression(initializer) ||
+            attributeName !== initializer.expression?.getText()
+        ) {
+            return null;
+        }
+
+        return [possibleJsxAttribute, attributeName];
+    }
+
+    private getSlotLetInfo(
+        sourceFile: ts.SourceFile,
+        location: ts.RenameLocation
+    ): [ts.Node, string] | null {
+        const possibleSlotLet = findContainingNode(
+            sourceFile,
+            location.textSpan,
+            ts.isVariableDeclaration
+        );
+        if (!possibleSlotLet || !ts.isObjectBindingPattern(possibleSlotLet.name)) {
+            return null;
+        }
+
+        const bindingElement = findContainingNode(
+            possibleSlotLet.name,
+            location.textSpan,
+            ts.isBindingElement
+        );
+
+        if (!bindingElement || bindingElement.propertyName) {
+            return null;
+        }
+
+        const identifierName = bindingElement.name.getText();
+
+        return [bindingElement, identifierName];
+    }
 }
 
 function unique<T>(array: T[]): T[] {
diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts
index dfa2f0ea..008ae99f 100644
--- a/packages/language-server/src/plugins/typescript/service.ts
+++ b/packages/language-server/src/plugins/typescript/service.ts
@@ -127,6 +127,7 @@ async function createLanguageService(
     ].map((f) => ts.sys.resolvePath(resolve(svelteTsPath, f)));
 
     let languageServiceReducedMode = false;
+    let projectVersion = 0;
 
     const host: ts.LanguageServiceHost = {
         getCompilationSettings: () => compilerOptions,
@@ -148,13 +149,19 @@ async function createLanguageService(
         readDirectory: svelteModuleLoader.readDirectory,
         getDirectories: ts.sys.getDirectories,
         useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
-        getScriptKind: (fileName: string) => getSnapshot(fileName).scriptKind
+        getScriptKind: (fileName: string) => getSnapshot(fileName).scriptKind,
+        getProjectVersion: () => projectVersion.toString()
     };
+
     let languageService = ts.createLanguageService(host);
     const transformationConfig = {
         transformOnTemplateError: docContext.transformOnTemplateError
     };
 
+    docContext.globalSnapshotsManager.onChange(() => {
+        projectVersion++;
+    });
+
     reduceLanguageServiceCapabilityIfFileSizeTooBig();
 
     return {
@@ -239,6 +246,7 @@ async function createLanguageService(
     }
 
     function updateProjectFiles(): void {
+        projectVersion++;
         const projectFileCountBefore = snapshotManager.getProjectFileNames().length;
         snapshotManager.updateProjectFiles();
         const projectFileCountAfter = snapshotManager.getProjectFileNames().length;
diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts
index 1be21930..725ab98e 100644
--- a/packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts
+++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts
@@ -53,11 +53,11 @@ export function handleSlot(
         str.move(attr.start, attr.end, slotDefInsertionPoint);
 
         //remove let:
+        str.remove(attr.start, attr.start + 'let:'.length);
         if (hasMoved) {
-            str.overwrite(attr.start, attr.start + 'let:'.length, ', ');
-        } else {
-            str.remove(attr.start, attr.start + 'let:'.length);
+            str.appendRight(attr.start + 'let:'.length, ', ');
         }
+
         templateScope.inits.add(attr.expression?.name || attr.name);
         hasMoved = true;
         if (attr.expression) {


Test patch:
diff --git a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts
index 4f84e9dd..a2dc1072 100644
--- a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts
+++ b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts
@@ -1023,6 +1023,32 @@ describe('DiagnosticsProvider', () => {
         assert.deepStrictEqual(diagnostics3.length, 1);
     }).timeout(5000);
 
+    it('notices update of imported module', async () => {
+        const { plugin, document, lsAndTsDocResolver } = setup(
+            'diagnostics-imported-js-update.svelte'
+        );
+
+        const newFilePath = normalizePath(path.join(testDir, 'empty-export.ts')) || '';
+        await lsAndTsDocResolver.getSnapshot(newFilePath);
+
+        const diagnostics1 = await plugin.getDiagnostics(document);
+        assert.deepStrictEqual(
+            diagnostics1[0]?.message,
+            "Module '\"./empty-export\"' has no exported member 'foo'."
+        );
+
+        await lsAndTsDocResolver.updateExistingTsOrJsFile(newFilePath, [
+            {
+                range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
+                text: 'export function foo() {}'
+            }
+        ]);
+
+        const diagnostics2 = await plugin.getDiagnostics(document);
+        assert.deepStrictEqual(diagnostics2.length, 0);
+        await lsAndTsDocResolver.deleteSnapshot(newFilePath);
+    }).timeout(5000);
+
     it('notices file changes in all services that reference that file', async () => {
         const { plugin, document, docManager, lsAndTsDocResolver } = setup(
             'different-ts-service.svelte'
diff --git a/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts b/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts
index bb7f67f4..40bc7777 100644
--- a/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts
+++ b/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts
@@ -39,6 +39,7 @@ describe('RenameProvider', () => {
         const renameDocSlotEventsImporter = await openDoc('rename-slot-events-importer.svelte');
         const renameDocPropWithSlotEvents = await openDoc('rename-prop-with-slot-events.svelte');
         const renameDocShorthand = await openDoc('rename-shorthand.svelte');
+        const renameSlotLet = await openDoc('rename-slot-let.svelte');
 
         return {
             provider,
@@ -52,6 +53,7 @@ describe('RenameProvider', () => {
             renameDocSlotEventsImporter,
             renameDocPropWithSlotEvents,
             renameDocShorthand,
+            renameSlotLet,
             docManager
         };
 
@@ -599,7 +601,7 @@ describe('RenameProvider', () => {
         });
     });
 
-    it('should can rename shorthand props without breaking value-passing', async () => {
+    it('can rename shorthand props without breaking value-passing', async () => {
         const { provider, renameDocShorthand } = await setup();
 
         const result = await provider.rename(renameDocShorthand, Position.create(3, 16), 'newName');
@@ -676,4 +678,43 @@ describe('RenameProvider', () => {
             }
         });
     });
+
+    it('can rename slot let to an alias', async () => {
+        const { provider, renameSlotLet } = await setup();
+
+        const result = await provider.rename(renameSlotLet, Position.create(4, 7), 'newName');
+
+        assert.deepStrictEqual(result, {
+            changes: {
+                [getUri('rename-slot-let.svelte')]: [
+                    {
+                        newText: 'aSlot={newName}',
+                        range: {
+                            end: {
+                                character: 12,
+                                line: 4
+                            },
+                            start: {
+                                character: 7,
+                                line: 4
+                            }
+                        }
+                    },
+                    {
+                        newText: 'newName',
+                        range: {
+                            end: {
+                                character: 26,
+                                line: 4
+                            },
+                            start: {
+                                character: 21,
+                                line: 4
+                            }
+                        }
+                    }
+                ]
+            }
+        });
+    });
 });
diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-imported-js-update.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-imported-js-update.svelte
new file mode 100644
index 00000000..7882f3b5
--- /dev/null
+++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-imported-js-update.svelte
@@ -0,0 +1,5 @@
+<script lang="ts">
+    import { foo } from './empty-export'
+
+    foo;
+</script>
\ No newline at end of file
diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/empty-export.ts b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/empty-export.ts
new file mode 100644
index 00000000..8cec2e9c
--- /dev/null
+++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/empty-export.ts
@@ -0,0 +1,1 @@
+export {};
\ No newline at end of file
diff --git a/packages/language-server/test/plugins/typescript/testfiles/rename/rename-slot-let.svelte b/packages/language-server/test/plugins/typescript/testfiles/rename/rename-slot-let.svelte
new file mode 100644
index 00000000..ec06a8cd
--- /dev/null
+++ b/packages/language-server/test/plugins/typescript/testfiles/rename/rename-slot-let.svelte
@@ -0,0 +1,5 @@
+<script>
+    import A from './rename-prop-with-slot-events.svelte';
+</script>
+  
+<A let:aSlot let:hi>{aSlot}</A>
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/sourcemaps/samples/slot-let/input.svelte b/packages/svelte2tsx/test/sourcemaps/samples/slot-let/input.svelte
new file mode 100644
index 00000000..6351ed48
--- /dev/null
+++ b/packages/svelte2tsx/test/sourcemaps/samples/slot-let/input.svelte
@@ -0,0 +1,3 @@
+<Component let:hi let:hi2={hi_2} let:hi3>
+    {hi}{hi_2}{hi3}
+</Component>
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/sourcemaps/samples/slot-let/mappings.jsx b/packages/svelte2tsx/test/sourcemaps/samples/slot-let/mappings.jsx
new file mode 100644
index 00000000..d195b06d
--- /dev/null
+++ b/packages/svelte2tsx/test/sourcemaps/samples/slot-let/mappings.jsx
@@ -0,0 +1,23 @@
+///<reference types="svelte" />
+<></>;function render() {                                                                                                                             {/**
+------------------------------------------------------------------------------------------------------------------------------------------------------ */}
+<><Component   >{() => { let {hi, hi2:hi_2, hi3} = /*Ωignore_startΩ*/new Component({target: __sveltets_1_any(''), props: {}})/*Ωignore_endΩ*/.$$slot_def['default'];<>{/**
+=#                                                                                                                                                                         Originless mappings
+<><Component•••>{()•=>•{•let•{hi,•hi2:hi_2,•hi3}•=•/*Ωignore_startΩ*/new•Component({target:•__sveltets_1_any(''),•props:•{}})/*Ωignore_endΩ*/.$$slot_def['default'];<>↲    [generated] line 3
+  <Component•••>              hi  hi2=hi_2  hi3                                                                                                                       ↲    
+               #==============                                                                                                                                             Order-breaking mappings
+<Component•    hi•    hi2= hi_2 •    hi3>↲                                                                                                                                 
+<Component•let:hi•let:hi2={hi_2}•let:hi3>↲                                                                                                                                 [original] line 1 
+------------------------------------------------------------------------------------------------------------------------------------------------------ */}
+    {hi}{hi_2}{hi3}                                                                                                                                   {/**
+------------------------------------------------------------------------------------------------------------------------------------------------------ */}
+</>}}</Component></>                                                                                                                                  {/**
+====#                    Originless mappings                                                                                                          
+</>}}</Component></>↲    [generated] line 5                                                                                                           
+     </Component>                                                                                                                                     
+</Component>             [original] line 3                                                                                                            
+------------------------------------------------------------------------------------------------------------------------------------------------------ */}
+return { props: {}, slots: {}, getters: {}, events: {} }}
+
+export default class Input__SvelteComponent_ extends __sveltets_1_createSvelte2TsxComponent(__sveltets_1_partial(__sveltets_1_with_any_event(render()))) {
+}
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/sourcemaps/samples/slot-let/test.jsx b/packages/svelte2tsx/test/sourcemaps/samples/slot-let/test.jsx
new file mode 100644
index 00000000..127f61dc
--- /dev/null
+++ b/packages/svelte2tsx/test/sourcemaps/samples/slot-let/test.jsx
@@ -0,0 +1,8 @@
+/** tested-ranges: [[15,2,"hi"],[22,9,"hi2:hi_2"],[37,3,"hi3"]] */                                                                                    {/**
+------------------------------------------------------------------------------------------------------------------------------------------------------ */}
+<><Component   >{() => { let {hi, hi2:hi_2, hi3} = /*Ωignore_startΩ*/new Component({target: __sveltets_1_any(''), props: {}})/*Ωignore_endΩ*/.$$slot_def['default'];<>{/**
+                              1=  2=======  3==                                                                                                                         [generated] line 3 */}
+<Component let:hi let:hi2={hi_2} let:hi3>                                                                                                             {/**
+               1=     2========      3==                                                                                                                                [original] line 1
+------------------------------------------------------------------------------------------------------------------------------------------------------ */}
+/** origin-hash: 1ljhdpn */
\ No newline at end of file