diff --git a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts index 83ada1bae..c22caf23e 100644 --- a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts @@ -1,5 +1,5 @@ import { Position, WorkspaceEdit, Range } from 'vscode-languageserver'; -import { Document, mapRangeToOriginal, getLineAtPosition } from '../../../lib/documents'; +import { Document, mapRangeToOriginal, getLineAtPosition, offsetAt } from '../../../lib/documents'; import { filterAsync, isNotNullOrUndefined, pathToUrl } from '../../../utils'; import { RenameProvider } from '../../interfaces'; import { @@ -15,7 +15,8 @@ import { isComponentAtPosition, isAfterSvelte2TsxPropsReturn, isNoTextSpanInGeneratedCode, - SnapshotFragmentMap + SnapshotFragmentMap, + findContainingNode } from './utils'; export class RenameProviderImpl implements RenameProvider { @@ -68,7 +69,13 @@ export class RenameProviderImpl implements RenameProvider { range: Range; } > = await this.mapAndFilterRenameLocations(renameLocations, docs); - // eslint-disable-next-line max-len + + convertedRenameLocations = this.checkShortHandBindingLocation( + lang, + convertedRenameLocations, + docs + ); + const additionalRenameForPropRenameInsideComponentWithProp = await this.getAdditionLocationsForRenameOfPropInsideComponentWithProp( document, @@ -237,7 +244,12 @@ export class RenameProviderImpl implements RenameProvider { const idx = (match.index || 0) + match[0].lastIndexOf(match[1]); const replacementsForProp = lang.findRenameLocations(updatePropLocation.fileName, idx, false, false) || []; - return await this.mapAndFilterRenameLocations(replacementsForProp, fragments); + + return this.checkShortHandBindingLocation( + lang, + await this.mapAndFilterRenameLocations(replacementsForProp, fragments), + fragments + ); } // --------> svelte2tsx? @@ -369,6 +381,88 @@ export class RenameProviderImpl implements RenameProvider { private getSnapshot(filePath: string) { return this.lsAndTsDocResolver.getSnapshot(filePath); } + + private checkShortHandBindingLocation( + lang: ts.LanguageService, + renameLocations: Array, + fragments: SnapshotFragmentMap + ): Array { + const bind = 'bind:'; + + return renameLocations.map((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 fragment = fragments.getFragment(location.fileName); + if (!(fragment instanceof SvelteSnapshotFragment)) { + return location; + } + + const { originalText } = fragment; + + const possibleJsxAttribute = findContainingNode( + sourceFile, + location.textSpan, + ts.isJsxAttribute + ); + if (!possibleJsxAttribute) { + return location; + } + + const attributeName = possibleJsxAttribute.name.getText(); + const { initializer } = possibleJsxAttribute; + + // not props={props} + if ( + !initializer || + !ts.isJsxExpression(initializer) || + attributeName !== initializer.expression?.getText() + ) { + return location; + } + + const originalStart = offsetAt(location.range.start, originalText); + + const isShortHandBinding = + originalText.substr(originalStart - bind.length, bind.length) === bind; + + const directiveName = (isShortHandBinding ? bind : '') + attributeName; + const prefixText = directiveName + '={'; + + const newRange = mapRangeToOriginal( + fragment, + convertRange(fragment, { + start: possibleJsxAttribute.getStart(), + length: possibleJsxAttribute.getWidth() + }) + ); + + // somehow the mapping is one character before + if ( + isShortHandBinding || + originalText + .substring(offsetAt(newRange.start, originalText), originalStart) + .trimLeft() === '{' + ) { + newRange.start.character++; + } + + return { + ...location, + prefixText, + suffixText: '}', + range: newRange + }; + }); + } } 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 be73d5bfc..bb7f67f4b 100644 --- a/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts @@ -38,6 +38,8 @@ describe('RenameProvider', () => { const renameDocIgnoreGenerated = await openDoc('rename-ignore-generated.svelte'); 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'); + return { provider, renameDoc1, @@ -49,6 +51,7 @@ describe('RenameProvider', () => { renameDocIgnoreGenerated, renameDocSlotEventsImporter, renameDocPropWithSlotEvents, + renameDocShorthand, docManager }; @@ -595,4 +598,82 @@ describe('RenameProvider', () => { } }); }); + + it('should 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'); + + assert.deepStrictEqual(result, { + changes: { + [getUri('rename-shorthand.svelte')]: [ + { + newText: 'newName', + range: { + start: { + line: 3, + character: 15 + }, + end: { + line: 3, + character: 21 + } + } + }, + { + newText: 'bind:props2={newName}', + range: { + start: { + line: 6, + character: 7 + }, + end: { + line: 6, + character: 18 + } + } + }, + { + newText: 'props2={newName}', + range: { + start: { + line: 7, + character: 7 + }, + end: { + line: 7, + character: 15 + } + } + }, + { + newText: 'props2={newName}', + range: { + start: { + line: 8, + character: 7 + }, + end: { + line: 8, + character: 22 + } + } + }, + { + newText: 'newName', + range: { + start: { + line: 9, + character: 15 + }, + end: { + line: 9, + character: 21 + } + } + } + ] + } + }); + }); }); diff --git a/packages/language-server/test/plugins/typescript/testfiles/rename/rename-shorthand.svelte b/packages/language-server/test/plugins/typescript/testfiles/rename/rename-shorthand.svelte new file mode 100644 index 000000000..3298f436c --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/rename/rename-shorthand.svelte @@ -0,0 +1,10 @@ + + + + + + diff --git a/packages/language-server/test/plugins/typescript/testfiles/rename/rename3.svelte b/packages/language-server/test/plugins/typescript/testfiles/rename/rename3.svelte index 40dab70ae..c823c35ea 100644 --- a/packages/language-server/test/plugins/typescript/testfiles/rename/rename3.svelte +++ b/packages/language-server/test/plugins/typescript/testfiles/rename/rename3.svelte @@ -1,3 +1,4 @@