Skip to content

Commit 5fc8f1d

Browse files
Add opt-in user preference for prefix and suffix text on renames (#29314)
* Add user preference to control renaming through exports * Only impact renaming * Update baselines * Use flag to control all prefix and suffix text and imports * [WIP] add tests * Only skip export import specifier with flag * [WIP] Update tests * Update test * Pick up preference from host and update test * Shorten flag name * Add missing utility function * Update comment * [WIP] rename flag and respond to cr * [WIP] Add flag for forRelatedSymbol * Use larger search symbol set for old-style rename * Respond to CR * Fix small error * Fix type mismatch * Update comment and remove unnecessary exprot * Respond to CR
1 parent 41a7bf4 commit 5fc8f1d

File tree

14 files changed

+163
-36
lines changed

14 files changed

+163
-36
lines changed

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5864,6 +5864,7 @@ namespace ts {
58645864
/** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */
58655865
readonly importModuleSpecifierEnding?: "minimal" | "index" | "js";
58665866
readonly allowTextChangesInNewFiles?: boolean;
5867+
readonly providePrefixAndSuffixTextForRename?: boolean;
58675868
}
58685869

58695870
/** Represents a bigint literal value without requiring bigint support */

src/harness/fourslash.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,7 +1170,7 @@ Actual: ${stringify(fullActual)}`);
11701170
}
11711171

11721172
public verifyRenameLocations(startRanges: ArrayOrSingle<Range>, options: FourSlashInterface.RenameLocationsOptions) {
1173-
const { findInStrings = false, findInComments = false, ranges = this.getRanges() } = ts.isArray(options) ? { findInStrings: false, findInComments: false, ranges: options } : options;
1173+
const { findInStrings = false, findInComments = false, ranges = this.getRanges(), providePrefixAndSuffixTextForRename = true } = ts.isArray(options) ? { findInStrings: false, findInComments: false, ranges: options, providePrefixAndSuffixTextForRename: true } : options;
11741174

11751175
for (const startRange of toArray(startRanges)) {
11761176
this.goToRangeStart(startRange);
@@ -1182,7 +1182,7 @@ Actual: ${stringify(fullActual)}`);
11821182
}
11831183

11841184
const references = this.languageService.findRenameLocations(
1185-
this.activeFile.fileName, this.currentCaretPosition, findInStrings, findInComments);
1185+
this.activeFile.fileName, this.currentCaretPosition, findInStrings, findInComments, providePrefixAndSuffixTextForRename);
11861186

11871187
const sort = (locations: ReadonlyArray<ts.RenameLocation> | undefined) =>
11881188
locations && ts.sort(locations, (r1, r2) => ts.compareStringsCaseSensitive(r1.fileName, r2.fileName) || r1.textSpan.start - r2.textSpan.start);
@@ -5087,6 +5087,7 @@ namespace FourSlashInterface {
50875087
readonly findInStrings?: boolean;
50885088
readonly findInComments?: boolean;
50895089
readonly ranges: ReadonlyArray<RenameLocationOptions>;
5090+
readonly providePrefixAndSuffixTextForRename?: boolean;
50905091
};
50915092
export type RenameLocationOptions = FourSlash.Range | { readonly range: FourSlash.Range, readonly prefixText?: string, readonly suffixText?: string };
50925093
}

src/harness/harnessLanguageService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,8 +472,8 @@ namespace Harness.LanguageService {
472472
getRenameInfo(fileName: string, position: number, options?: ts.RenameInfoOptions): ts.RenameInfo {
473473
return unwrapJSONCallResult(this.shim.getRenameInfo(fileName, position, options));
474474
}
475-
findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): ts.RenameLocation[] {
476-
return unwrapJSONCallResult(this.shim.findRenameLocations(fileName, position, findInStrings, findInComments));
475+
findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ts.RenameLocation[] {
476+
return unwrapJSONCallResult(this.shim.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename));
477477
}
478478
getDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] {
479479
return unwrapJSONCallResult(this.shim.getDefinitionAtPosition(fileName, position));

src/server/protocol.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2905,6 +2905,7 @@ namespace ts.server.protocol {
29052905
readonly importModuleSpecifierPreference?: "relative" | "non-relative";
29062906
readonly allowTextChangesInNewFiles?: boolean;
29072907
readonly lazyConfiguredProjectsFromExternalProject?: boolean;
2908+
readonly providePrefixAndSuffixTextForRename?: boolean;
29082909
readonly allowRenameOfImportPath?: boolean;
29092910
}
29102911

src/server/session.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,8 @@ namespace ts.server {
314314
defaultProject: Project,
315315
initialLocation: DocumentPosition,
316316
findInStrings: boolean,
317-
findInComments: boolean
317+
findInComments: boolean,
318+
hostPreferences: UserPreferences
318319
): ReadonlyArray<RenameLocation> {
319320
const outputs: RenameLocation[] = [];
320321

@@ -323,7 +324,7 @@ namespace ts.server {
323324
defaultProject,
324325
initialLocation,
325326
({ project, location }, tryAddToTodo) => {
326-
for (const output of project.getLanguageService().findRenameLocations(location.fileName, location.pos, findInStrings, findInComments) || emptyArray) {
327+
for (const output of project.getLanguageService().findRenameLocations(location.fileName, location.pos, findInStrings, findInComments, hostPreferences.providePrefixAndSuffixTextForRename) || emptyArray) {
327328
if (!contains(outputs, output, documentSpansEqual) && !tryAddToTodo(project, documentSpanLocation(output))) {
328329
outputs.push(output);
329330
}
@@ -1232,7 +1233,8 @@ namespace ts.server {
12321233
this.getDefaultProject(args),
12331234
{ fileName: args.file, pos: position },
12341235
!!args.findInStrings,
1235-
!!args.findInComments
1236+
!!args.findInComments,
1237+
this.getHostPreferences()
12361238
);
12371239
if (!simplifiedResult) return locations;
12381240

src/services/findAllReferences.ts

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ namespace ts.FindAllReferences {
3939
readonly isForRename?: boolean;
4040
/** True if we are searching for implementations. We will have a different method of adding references if so. */
4141
readonly implementations?: boolean;
42+
/**
43+
* True to opt in for enhanced renaming of shorthand properties and import/export specifiers.
44+
* Default is false for backwards compatibility.
45+
*/
46+
readonly providePrefixAndSuffixTextForRename?: boolean;
4247
}
4348

4449
export function findReferencedSymbols(program: Program, cancellationToken: CancellationToken, sourceFiles: ReadonlyArray<SourceFile>, sourceFile: SourceFile, position: number): ReferencedSymbol[] | undefined {
@@ -157,8 +162,8 @@ namespace ts.FindAllReferences {
157162
return { displayParts, kind: symbolKind };
158163
}
159164

160-
export function toRenameLocation(entry: Entry, originalNode: Node, checker: TypeChecker): RenameLocation {
161-
return { ...entryToDocumentSpan(entry), ...getPrefixAndSuffixText(entry, originalNode, checker) };
165+
export function toRenameLocation(entry: Entry, originalNode: Node, checker: TypeChecker, providePrefixAndSuffixText: boolean): RenameLocation {
166+
return { ...entryToDocumentSpan(entry), ...(providePrefixAndSuffixText && getPrefixAndSuffixText(entry, originalNode, checker)) };
162167
}
163168

164169
export function toReferenceEntry(entry: Entry): ReferenceEntry {
@@ -484,15 +489,15 @@ namespace ts.FindAllReferences.Core {
484489

485490
/** Core find-all-references algorithm for a normal symbol. */
486491
function getReferencedSymbolsForSymbol(originalSymbol: Symbol, node: Node | undefined, sourceFiles: ReadonlyArray<SourceFile>, sourceFilesSet: ReadonlyMap<true>, checker: TypeChecker, cancellationToken: CancellationToken, options: Options): SymbolAndEntries[] {
487-
const symbol = node && skipPastExportOrImportSpecifierOrUnion(originalSymbol, node, checker, !!options.isForRename) || originalSymbol;
492+
const symbol = node && skipPastExportOrImportSpecifierOrUnion(originalSymbol, node, checker, /*useLocalSymbolForExportSpecifier*/ !isForRenameWithPrefixAndSuffixText(options)) || originalSymbol;
488493

489494
// Compute the meaning from the location and the symbol it references
490495
const searchMeaning = node ? getIntersectingMeaningFromDeclarations(node, symbol) : SemanticMeaning.All;
491496

492497
const result: SymbolAndEntries[] = [];
493498
const state = new State(sourceFiles, sourceFilesSet, node ? getSpecialSearchKind(node) : SpecialSearchKind.None, checker, cancellationToken, searchMeaning, options, result);
494499

495-
const exportSpecifier = !options.isForRename ? undefined : find(symbol.declarations, isExportSpecifier);
500+
const exportSpecifier = !isForRenameWithPrefixAndSuffixText(options) ? undefined : find(symbol.declarations, isExportSpecifier);
496501
if (exportSpecifier) {
497502
// When renaming at an export specifier, rename the export and not the thing being exported.
498503
getReferencesAtExportSpecifier(exportSpecifier.name, symbol, exportSpecifier, state.createSearch(node, originalSymbol, /*comingFrom*/ undefined), state, /*addReferencesHere*/ true, /*alwaysGetReferences*/ true);
@@ -502,7 +507,7 @@ namespace ts.FindAllReferences.Core {
502507
searchForImportsOfExport(node, symbol, { exportingModuleSymbol: Debug.assertDefined(symbol.parent, "Expected export symbol to have a parent"), exportKind: ExportKind.Default }, state);
503508
}
504509
else {
505-
const search = state.createSearch(node, symbol, /*comingFrom*/ undefined, { allSearchSymbols: node ? populateSearchSymbolSet(symbol, node, checker, !!options.isForRename, !!options.implementations) : [symbol] });
510+
const search = state.createSearch(node, symbol, /*comingFrom*/ undefined, { allSearchSymbols: node ? populateSearchSymbolSet(symbol, node, checker, !!options.isForRename, !!options.providePrefixAndSuffixTextForRename, !!options.implementations) : [symbol] });
506511

507512
// Try to get the smallest valid scope that we can limit our search to;
508513
// otherwise we'll need to search globally (i.e. include each file).
@@ -538,9 +543,9 @@ namespace ts.FindAllReferences.Core {
538543
}
539544

540545
/** Handle a few special cases relating to export/import specifiers. */
541-
function skipPastExportOrImportSpecifierOrUnion(symbol: Symbol, node: Node, checker: TypeChecker, isForRename: boolean): Symbol | undefined {
546+
function skipPastExportOrImportSpecifierOrUnion(symbol: Symbol, node: Node, checker: TypeChecker, useLocalSymbolForExportSpecifier: boolean): Symbol | undefined {
542547
const { parent } = node;
543-
if (isExportSpecifier(parent) && !isForRename) {
548+
if (isExportSpecifier(parent) && useLocalSymbolForExportSpecifier) {
544549
return getLocalSymbolForExportSpecifier(node as Identifier, symbol, parent, checker);
545550
}
546551
// If the symbol is declared as part of a declaration like `{ type: "a" } | { type: "b" }`, use the property on the union type to get more references.
@@ -1071,6 +1076,8 @@ namespace ts.FindAllReferences.Core {
10711076
addReferencesHere: boolean,
10721077
alwaysGetReferences?: boolean,
10731078
): void {
1079+
Debug.assert(!alwaysGetReferences || !!state.options.providePrefixAndSuffixTextForRename, "If alwaysGetReferences is true, then prefix/suffix text must be enabled");
1080+
10741081
const { parent, propertyName, name } = exportSpecifier;
10751082
const exportDeclaration = parent.parent;
10761083
const localSymbol = getLocalSymbolForExportSpecifier(referenceLocation, referenceSymbol, exportSpecifier, state.checker);
@@ -1102,15 +1109,15 @@ namespace ts.FindAllReferences.Core {
11021109
}
11031110

11041111
// For `export { foo as bar }`, rename `foo`, but not `bar`.
1105-
if (!state.options.isForRename || alwaysGetReferences) {
1112+
if (!isForRenameWithPrefixAndSuffixText(state.options) || alwaysGetReferences) {
11061113
const exportKind = referenceLocation.originalKeywordKind === SyntaxKind.DefaultKeyword ? ExportKind.Default : ExportKind.Named;
11071114
const exportSymbol = Debug.assertDefined(exportSpecifier.symbol);
11081115
const exportInfo = Debug.assertDefined(getExportInfo(exportSymbol, exportKind, state.checker));
11091116
searchForImportsOfExport(referenceLocation, exportSymbol, exportInfo, state);
11101117
}
11111118

11121119
// At `export { x } from "foo"`, also search for the imported symbol `"foo".x`.
1113-
if (search.comingFrom !== ImportExport.Export && exportDeclaration.moduleSpecifier && !propertyName && !state.options.isForRename) {
1120+
if (search.comingFrom !== ImportExport.Export && exportDeclaration.moduleSpecifier && !propertyName && !isForRenameWithPrefixAndSuffixText(state.options)) {
11141121
const imported = state.checker.getExportSpecifierLocalTargetSymbol(exportSpecifier);
11151122
if (imported) searchForImportedSymbol(imported, state);
11161123
}
@@ -1145,7 +1152,7 @@ namespace ts.FindAllReferences.Core {
11451152
const { symbol } = importOrExport;
11461153

11471154
if (importOrExport.kind === ImportExport.Import) {
1148-
if (!state.options.isForRename) {
1155+
if (!(isForRenameWithPrefixAndSuffixText(state.options))) {
11491156
searchForImportedSymbol(symbol, state);
11501157
}
11511158
}
@@ -1514,16 +1521,16 @@ namespace ts.FindAllReferences.Core {
15141521

15151522
// For certain symbol kinds, we need to include other symbols in the search set.
15161523
// This is not needed when searching for re-exports.
1517-
function populateSearchSymbolSet(symbol: Symbol, location: Node, checker: TypeChecker, isForRename: boolean, implementations: boolean): Symbol[] {
1524+
function populateSearchSymbolSet(symbol: Symbol, location: Node, checker: TypeChecker, isForRename: boolean, providePrefixAndSuffixText: boolean, implementations: boolean): Symbol[] {
15181525
const result: Symbol[] = [];
1519-
forEachRelatedSymbol<void>(symbol, location, checker, isForRename,
1526+
forEachRelatedSymbol<void>(symbol, location, checker, isForRename, !(isForRename && providePrefixAndSuffixText),
15201527
(sym, root, base) => { result.push(base || root || sym); },
15211528
/*allowBaseTypes*/ () => !implementations);
15221529
return result;
15231530
}
15241531

15251532
function forEachRelatedSymbol<T>(
1526-
symbol: Symbol, location: Node, checker: TypeChecker, isForRenamePopulateSearchSymbolSet: boolean,
1533+
symbol: Symbol, location: Node, checker: TypeChecker, isForRenamePopulateSearchSymbolSet: boolean, onlyIncludeBindingElementAtReferenceLocation: boolean,
15271534
cbSymbol: (symbol: Symbol, rootSymbol?: Symbol, baseSymbol?: Symbol, kind?: NodeEntryKind) => T | undefined,
15281535
allowBaseTypes: (rootSymbol: Symbol) => boolean,
15291536
): T | undefined {
@@ -1577,9 +1584,25 @@ namespace ts.FindAllReferences.Core {
15771584
}
15781585

15791586
// symbolAtLocation for a binding element is the local symbol. See if the search symbol is the property.
1580-
// Don't do this when populating search set for a rename -- just rename the local.
1587+
// Don't do this when populating search set for a rename when prefix and suffix text will be provided -- just rename the local.
15811588
if (!isForRenamePopulateSearchSymbolSet) {
1582-
const bindingElementPropertySymbol = isObjectBindingElementWithoutPropertyName(location.parent) ? getPropertySymbolFromBindingElement(checker, location.parent) : undefined;
1589+
let bindingElementPropertySymbol: Symbol | undefined;
1590+
if (onlyIncludeBindingElementAtReferenceLocation) {
1591+
bindingElementPropertySymbol = isObjectBindingElementWithoutPropertyName(location.parent) ? getPropertySymbolFromBindingElement(checker, location.parent) : undefined;
1592+
}
1593+
else {
1594+
bindingElementPropertySymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol, checker);
1595+
}
1596+
return bindingElementPropertySymbol && fromRoot(bindingElementPropertySymbol, EntryKind.SearchedPropertyFoundLocal);
1597+
}
1598+
1599+
Debug.assert(isForRenamePopulateSearchSymbolSet);
1600+
// due to the above assert and the arguments at the uses of this function,
1601+
// (onlyIncludeBindingElementAtReferenceLocation <=> !providePrefixAndSuffixTextForRename) holds
1602+
const includeOriginalSymbolOfBindingElement = onlyIncludeBindingElementAtReferenceLocation;
1603+
1604+
if (includeOriginalSymbolOfBindingElement) {
1605+
const bindingElementPropertySymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol, checker);
15831606
return bindingElementPropertySymbol && fromRoot(bindingElementPropertySymbol, EntryKind.SearchedPropertyFoundLocal);
15841607
}
15851608

@@ -1597,6 +1620,13 @@ namespace ts.FindAllReferences.Core {
15971620
? getPropertySymbolsFromBaseTypes(rootSymbol.parent, rootSymbol.name, checker, base => cbSymbol(sym, rootSymbol, base, kind))
15981621
: undefined));
15991622
}
1623+
1624+
function getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol: Symbol, checker: TypeChecker): Symbol | undefined {
1625+
const bindingElement = getDeclarationOfKind<BindingElement>(symbol, SyntaxKind.BindingElement);
1626+
if (bindingElement && isObjectBindingElementWithoutPropertyName(bindingElement)) {
1627+
return getPropertySymbolFromBindingElement(checker, bindingElement);
1628+
}
1629+
}
16001630
}
16011631

16021632
interface RelatedSymbol {
@@ -1606,6 +1636,7 @@ namespace ts.FindAllReferences.Core {
16061636
function getRelatedSymbol(search: Search, referenceSymbol: Symbol, referenceLocation: Node, state: State): RelatedSymbol | undefined {
16071637
const { checker } = state;
16081638
return forEachRelatedSymbol(referenceSymbol, referenceLocation, checker, /*isForRenamePopulateSearchSymbolSet*/ false,
1639+
/*onlyIncludeBindingElementAtReferenceLocation*/ !state.options.isForRename || !!state.options.providePrefixAndSuffixTextForRename,
16091640
(sym, rootSymbol, baseSymbol, kind): RelatedSymbol | undefined => search.includes(baseSymbol || rootSymbol || sym)
16101641
// For a base type, use the symbol for the derived type. For a synthetic (e.g. union) property, use the union symbol.
16111642
? { symbol: rootSymbol && !(getCheckFlags(sym) & CheckFlags.Synthetic) ? rootSymbol : sym, kind }
@@ -1696,4 +1727,8 @@ namespace ts.FindAllReferences.Core {
16961727
t.symbol && t.symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface) ? t.symbol : undefined);
16971728
return res.length === 0 ? undefined : res;
16981729
}
1730+
1731+
function isForRenameWithPrefixAndSuffixText(options: Options) {
1732+
return options.isForRename && options.providePrefixAndSuffixTextForRename;
1733+
}
16991734
}

src/services/services.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,7 +1549,7 @@ namespace ts {
15491549
return DocumentHighlights.getDocumentHighlights(program, cancellationToken, sourceFile, position, sourceFilesToSearch);
15501550
}
15511551

1552-
function findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] | undefined {
1552+
function findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): RenameLocation[] | undefined {
15531553
synchronizeHostData();
15541554
const sourceFile = getValidSourceFile(fileName);
15551555
const node = getTouchingPropertyName(sourceFile, position);
@@ -1559,7 +1559,8 @@ namespace ts {
15591559
({ fileName: sourceFile.fileName, textSpan: createTextSpanFromNode(node.tagName, sourceFile) }));
15601560
}
15611561
else {
1562-
return getReferencesWorker(node, position, { findInStrings, findInComments, isForRename: true }, FindAllReferences.toRenameLocation);
1562+
return getReferencesWorker(node, position, { findInStrings, findInComments, providePrefixAndSuffixTextForRename, isForRename: true },
1563+
(entry, originalNode, checker) => FindAllReferences.toRenameLocation(entry, originalNode, checker, providePrefixAndSuffixTextForRename || false));
15631564
}
15641565
}
15651566

0 commit comments

Comments
 (0)