diff --git a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts index c22caf23e..896b7947f 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, 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(array: T[]): T[] { 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 bb7f67f4b..40bc77771 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/rename/rename-slot-let.svelte b/packages/language-server/test/plugins/typescript/testfiles/rename/rename-slot-let.svelte new file mode 100644 index 000000000..ec06a8cd6 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/rename/rename-slot-let.svelte @@ -0,0 +1,5 @@ + + +{aSlot} \ No newline at end of file diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts index 1be21930d..725ab98e8 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) { 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 000000000..6351ed48c --- /dev/null +++ b/packages/svelte2tsx/test/sourcemaps/samples/slot-let/input.svelte @@ -0,0 +1,3 @@ + + {hi}{hi_2}{hi3} + \ 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 000000000..d195b06d6 --- /dev/null +++ b/packages/svelte2tsx/test/sourcemaps/samples/slot-let/mappings.jsx @@ -0,0 +1,23 @@ +/// +<>;function render() { {/** +------------------------------------------------------------------------------------------------------------------------------------------------------ */} +<>{() => { let {hi, hi2:hi_2, hi3} = /*Ωignore_startΩ*/new Component({target: __sveltets_1_any(''), props: {}})/*Ωignore_endΩ*/.$$slot_def['default'];<>{/** +=# Originless mappings +<>{()•=>•{•let•{hi,•hi2:hi_2,•hi3}•=•/*Ωignore_startΩ*/new•Component({target:•__sveltets_1_any(''),•props:•{}})/*Ωignore_endΩ*/.$$slot_def['default'];<>↲ [generated] line 3 + hi hi2=hi_2 hi3 ↲ + #============== Order-breaking mappings +↲ +↲ [original] line 1 +------------------------------------------------------------------------------------------------------------------------------------------------------ */} + {hi}{hi_2}{hi3} {/** +------------------------------------------------------------------------------------------------------------------------------------------------------ */} +}} {/** +====# Originless mappings +}}↲ [generated] line 5 + + [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 000000000..127f61dc8 --- /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"]] */ {/** +------------------------------------------------------------------------------------------------------------------------------------------------------ */} +<>{() => { 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 */} + {/** + 1= 2======== 3== [original] line 1 +------------------------------------------------------------------------------------------------------------------------------------------------------ */} +/** origin-hash: 1ljhdpn */ \ No newline at end of file