Skip to content

Commit 4c4d921

Browse files
authored
(fix) binding/ shorthand props rename (#1087)
Preserve property names when renaming shorthands #969
1 parent 9949a3a commit 4c4d921

File tree

4 files changed

+190
-4
lines changed

4 files changed

+190
-4
lines changed

packages/language-server/src/plugins/typescript/features/RenameProvider.ts

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Position, WorkspaceEdit, Range } from 'vscode-languageserver';
2-
import { Document, mapRangeToOriginal, getLineAtPosition } from '../../../lib/documents';
2+
import { Document, mapRangeToOriginal, getLineAtPosition, offsetAt } from '../../../lib/documents';
33
import { filterAsync, isNotNullOrUndefined, pathToUrl } from '../../../utils';
44
import { RenameProvider } from '../../interfaces';
55
import {
@@ -15,7 +15,8 @@ import {
1515
isComponentAtPosition,
1616
isAfterSvelte2TsxPropsReturn,
1717
isNoTextSpanInGeneratedCode,
18-
SnapshotFragmentMap
18+
SnapshotFragmentMap,
19+
findContainingNode
1920
} from './utils';
2021

2122
export class RenameProviderImpl implements RenameProvider {
@@ -68,7 +69,13 @@ export class RenameProviderImpl implements RenameProvider {
6869
range: Range;
6970
}
7071
> = await this.mapAndFilterRenameLocations(renameLocations, docs);
71-
// eslint-disable-next-line max-len
72+
73+
convertedRenameLocations = this.checkShortHandBindingLocation(
74+
lang,
75+
convertedRenameLocations,
76+
docs
77+
);
78+
7279
const additionalRenameForPropRenameInsideComponentWithProp =
7380
await this.getAdditionLocationsForRenameOfPropInsideComponentWithProp(
7481
document,
@@ -237,7 +244,12 @@ export class RenameProviderImpl implements RenameProvider {
237244
const idx = (match.index || 0) + match[0].lastIndexOf(match[1]);
238245
const replacementsForProp =
239246
lang.findRenameLocations(updatePropLocation.fileName, idx, false, false) || [];
240-
return await this.mapAndFilterRenameLocations(replacementsForProp, fragments);
247+
248+
return this.checkShortHandBindingLocation(
249+
lang,
250+
await this.mapAndFilterRenameLocations(replacementsForProp, fragments),
251+
fragments
252+
);
241253
}
242254

243255
// --------> svelte2tsx?
@@ -369,6 +381,88 @@ export class RenameProviderImpl implements RenameProvider {
369381
private getSnapshot(filePath: string) {
370382
return this.lsAndTsDocResolver.getSnapshot(filePath);
371383
}
384+
385+
private checkShortHandBindingLocation(
386+
lang: ts.LanguageService,
387+
renameLocations: Array<ts.RenameLocation & { range: Range }>,
388+
fragments: SnapshotFragmentMap
389+
): Array<ts.RenameLocation & { range: Range }> {
390+
const bind = 'bind:';
391+
392+
return renameLocations.map((location) => {
393+
const sourceFile = lang.getProgram()?.getSourceFile(location.fileName);
394+
395+
if (
396+
!sourceFile ||
397+
location.fileName !== sourceFile.fileName ||
398+
location.range.start.line < 0 ||
399+
location.range.end.line < 0
400+
) {
401+
return location;
402+
}
403+
404+
const fragment = fragments.getFragment(location.fileName);
405+
if (!(fragment instanceof SvelteSnapshotFragment)) {
406+
return location;
407+
}
408+
409+
const { originalText } = fragment;
410+
411+
const possibleJsxAttribute = findContainingNode(
412+
sourceFile,
413+
location.textSpan,
414+
ts.isJsxAttribute
415+
);
416+
if (!possibleJsxAttribute) {
417+
return location;
418+
}
419+
420+
const attributeName = possibleJsxAttribute.name.getText();
421+
const { initializer } = possibleJsxAttribute;
422+
423+
// not props={props}
424+
if (
425+
!initializer ||
426+
!ts.isJsxExpression(initializer) ||
427+
attributeName !== initializer.expression?.getText()
428+
) {
429+
return location;
430+
}
431+
432+
const originalStart = offsetAt(location.range.start, originalText);
433+
434+
const isShortHandBinding =
435+
originalText.substr(originalStart - bind.length, bind.length) === bind;
436+
437+
const directiveName = (isShortHandBinding ? bind : '') + attributeName;
438+
const prefixText = directiveName + '={';
439+
440+
const newRange = mapRangeToOriginal(
441+
fragment,
442+
convertRange(fragment, {
443+
start: possibleJsxAttribute.getStart(),
444+
length: possibleJsxAttribute.getWidth()
445+
})
446+
);
447+
448+
// somehow the mapping is one character before
449+
if (
450+
isShortHandBinding ||
451+
originalText
452+
.substring(offsetAt(newRange.start, originalText), originalStart)
453+
.trimLeft() === '{'
454+
) {
455+
newRange.start.character++;
456+
}
457+
458+
return {
459+
...location,
460+
prefixText,
461+
suffixText: '}',
462+
range: newRange
463+
};
464+
});
465+
}
372466
}
373467

374468
function unique<T>(array: T[]): T[] {

packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ describe('RenameProvider', () => {
3838
const renameDocIgnoreGenerated = await openDoc('rename-ignore-generated.svelte');
3939
const renameDocSlotEventsImporter = await openDoc('rename-slot-events-importer.svelte');
4040
const renameDocPropWithSlotEvents = await openDoc('rename-prop-with-slot-events.svelte');
41+
const renameDocShorthand = await openDoc('rename-shorthand.svelte');
42+
4143
return {
4244
provider,
4345
renameDoc1,
@@ -49,6 +51,7 @@ describe('RenameProvider', () => {
4951
renameDocIgnoreGenerated,
5052
renameDocSlotEventsImporter,
5153
renameDocPropWithSlotEvents,
54+
renameDocShorthand,
5255
docManager
5356
};
5457

@@ -595,4 +598,82 @@ describe('RenameProvider', () => {
595598
}
596599
});
597600
});
601+
602+
it('should can rename shorthand props without breaking value-passing', async () => {
603+
const { provider, renameDocShorthand } = await setup();
604+
605+
const result = await provider.rename(renameDocShorthand, Position.create(3, 16), 'newName');
606+
607+
assert.deepStrictEqual(result, {
608+
changes: {
609+
[getUri('rename-shorthand.svelte')]: [
610+
{
611+
newText: 'newName',
612+
range: {
613+
start: {
614+
line: 3,
615+
character: 15
616+
},
617+
end: {
618+
line: 3,
619+
character: 21
620+
}
621+
}
622+
},
623+
{
624+
newText: 'bind:props2={newName}',
625+
range: {
626+
start: {
627+
line: 6,
628+
character: 7
629+
},
630+
end: {
631+
line: 6,
632+
character: 18
633+
}
634+
}
635+
},
636+
{
637+
newText: 'props2={newName}',
638+
range: {
639+
start: {
640+
line: 7,
641+
character: 7
642+
},
643+
end: {
644+
line: 7,
645+
character: 15
646+
}
647+
}
648+
},
649+
{
650+
newText: 'props2={newName}',
651+
range: {
652+
start: {
653+
line: 8,
654+
character: 7
655+
},
656+
end: {
657+
line: 8,
658+
character: 22
659+
}
660+
}
661+
},
662+
{
663+
newText: 'newName',
664+
range: {
665+
start: {
666+
line: 9,
667+
character: 15
668+
},
669+
end: {
670+
line: 9,
671+
character: 21
672+
}
673+
}
674+
}
675+
]
676+
}
677+
});
678+
});
598679
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
import Child from './rename3.svelte';
3+
4+
export let props2;
5+
</script>
6+
7+
<Child bind:props2 />
8+
<Child {props2} />
9+
<Child props2={props2} />
10+
<Child props2={props2?.toString()} />
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
<script>
22
export let exportedPropFromJs;
3+
export let props2;
34
</script>

0 commit comments

Comments
 (0)