diff --git a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts index edeaf4cef..afccd1b89 100644 --- a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts @@ -103,7 +103,8 @@ export class RenameProviderImpl implements RenameProvider { position, convertedRenameLocations, docs, - lang + lang, + newName ); const additionalRenamesForPropRenameOutsideComponentWithProp = // This is an either-or-situation, don't do both @@ -262,7 +263,8 @@ export class RenameProviderImpl implements RenameProvider { position: Position, convertedRenameLocations: TsRenameLocation[], snapshots: SnapshotMap, - lang: ts.LanguageService + lang: ts.LanguageService, + newName: string ) { // First find out if it's really the "rename prop inside component with that prop" case // Use original document for that because only there the `export` is present. @@ -292,8 +294,13 @@ export class RenameProviderImpl implements RenameProvider { // It would not work for `return props: {bla}` because then typescript would do a rename of `{bla: renamed}`, // so other locations would not be affected. const replacementsForProp = ( - lang.findRenameLocations(updatePropLocation.fileName, idxOfOldPropName, false, false) || - [] + lang.findRenameLocations( + updatePropLocation.fileName, + idxOfOldPropName, + false, + false, + true + ) || [] ).filter( (rename) => // filter out all renames inside the component except the prop rename, @@ -301,7 +308,80 @@ export class RenameProviderImpl implements RenameProvider { rename.fileName !== updatePropLocation.fileName || this.isInSvelte2TsxPropLine(tsDoc, rename) ); - return await this.mapAndFilterRenameLocations(replacementsForProp, snapshots); + + const renameLocations = await this.mapAndFilterRenameLocations( + replacementsForProp, + snapshots + ); + const bind = 'bind:'; + + // Adjust shorthands + return renameLocations.map((location) => { + if (updatePropLocation.fileName === location.fileName) { + return location; + } + + const sourceFile = lang.getProgram()?.getSourceFile(location.fileName); + + if ( + !sourceFile || + location.fileName !== sourceFile.fileName || + location.range.start.line < 0 || + location.range.end.line < 0 + ) { + return location; + } + + const snapshot = snapshots.get(location.fileName); + if (!(snapshot instanceof SvelteDocumentSnapshot)) { + return location; + } + + const { parent } = snapshot; + + let rangeStart = parent.offsetAt(location.range.start); + let suffixText = location.suffixText?.trimStart(); + + // suffix is of the form `: oldVarName` -> hints at a shorthand + if (!suffixText?.startsWith(':') || !getNodeIfIsInStartTag(parent.html, rangeStart)) { + return location; + } + + const original = parent.getText({ + start: Position.create( + location.range.start.line, + location.range.start.character - bind.length + ), + end: location.range.end + }); + + if (original.startsWith(bind)) { + // bind:|foo| -> bind:|newName|={foo} + return { + ...location, + prefixText: '', + suffixText: `={${original.slice(bind.length)}}` + }; + } + + if (snapshot.getOriginalText().charAt(rangeStart - 1) === '{') { + // {|foo|} -> |{foo|} + rangeStart--; + return { + ...location, + range: { + start: parent.positionAt(rangeStart), + end: location.range.end + }, + // |{foo|} -> newName=|{foo|} + newName: parent.getText(location.range), + prefixText: `${newName}={`, + suffixText: '' + }; + } + + return location; + }); } /** diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index ec26770b8..74c419076 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -59,6 +59,7 @@ const pendingReloads = new FileSet(); export function __resetCache() { services.clear(); serviceSizeMap.clear(); + configFileForOpenFiles.clear(); } export interface LanguageServiceDocumentContext { 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 062f23886..75e93a8ee 100644 --- a/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts @@ -272,6 +272,87 @@ describe('RenameProvider', () => { }); }); + it('should do rename of prop without type of component A in component A that is used with shorthands in component B', async () => { + const { provider, renameDoc3 } = await setup(); + const result = await provider.rename(renameDoc3, Position.create(2, 20), 'newName'); + + console.log(JSON.stringify(result, null, 3)); + + assert.deepStrictEqual(result, { + changes: { + [getUri('rename3.svelte')]: [ + { + newText: 'newName', + range: { + start: { + line: 2, + character: 15 + }, + end: { + line: 2, + character: 21 + } + } + } + ], + [getUri('rename-shorthand.svelte')]: [ + { + newText: 'newName={props2}', + range: { + start: { + line: 6, + character: 12 + }, + end: { + line: 6, + character: 18 + } + } + }, + { + newText: 'newName={props2', + range: { + start: { + line: 7, + character: 7 + }, + end: { + line: 7, + character: 14 + } + } + }, + { + newText: 'newName', + range: { + start: { + line: 8, + character: 7 + }, + end: { + line: 8, + character: 13 + } + } + }, + { + newText: 'newName', + range: { + start: { + line: 9, + character: 7 + }, + end: { + line: 9, + character: 13 + } + } + } + ] + } + }); + }); + it('should do rename of svelte component', async () => { const { provider, renameDoc4 } = await setup(); const result = await provider.rename(renameDoc4, Position.create(1, 12), 'ChildNew');