diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 125c3297863c2..c6c34c7434e0a 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5864,6 +5864,7 @@ namespace ts { /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ readonly importModuleSpecifierEnding?: "minimal" | "index" | "js"; readonly allowTextChangesInNewFiles?: boolean; + readonly providePrefixAndSuffixTextForRename?: boolean; } /** Represents a bigint literal value without requiring bigint support */ diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 1276de7de44a7..2b04b4bd8cda7 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -1170,7 +1170,7 @@ Actual: ${stringify(fullActual)}`); } public verifyRenameLocations(startRanges: ArrayOrSingle, options: FourSlashInterface.RenameLocationsOptions) { - const { findInStrings = false, findInComments = false, ranges = this.getRanges() } = ts.isArray(options) ? { findInStrings: false, findInComments: false, ranges: options } : options; + const { findInStrings = false, findInComments = false, ranges = this.getRanges(), providePrefixAndSuffixTextForRename = true } = ts.isArray(options) ? { findInStrings: false, findInComments: false, ranges: options, providePrefixAndSuffixTextForRename: true } : options; for (const startRange of toArray(startRanges)) { this.goToRangeStart(startRange); @@ -1182,7 +1182,7 @@ Actual: ${stringify(fullActual)}`); } const references = this.languageService.findRenameLocations( - this.activeFile.fileName, this.currentCaretPosition, findInStrings, findInComments); + this.activeFile.fileName, this.currentCaretPosition, findInStrings, findInComments, providePrefixAndSuffixTextForRename); const sort = (locations: ReadonlyArray | undefined) => locations && ts.sort(locations, (r1, r2) => ts.compareStringsCaseSensitive(r1.fileName, r2.fileName) || r1.textSpan.start - r2.textSpan.start); @@ -5087,6 +5087,7 @@ namespace FourSlashInterface { readonly findInStrings?: boolean; readonly findInComments?: boolean; readonly ranges: ReadonlyArray; + readonly providePrefixAndSuffixTextForRename?: boolean; }; export type RenameLocationOptions = FourSlash.Range | { readonly range: FourSlash.Range, readonly prefixText?: string, readonly suffixText?: string }; } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index d233ddf458522..a78ef88e5b74b 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -472,8 +472,8 @@ namespace Harness.LanguageService { getRenameInfo(fileName: string, position: number, options?: ts.RenameInfoOptions): ts.RenameInfo { return unwrapJSONCallResult(this.shim.getRenameInfo(fileName, position, options)); } - findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): ts.RenameLocation[] { - return unwrapJSONCallResult(this.shim.findRenameLocations(fileName, position, findInStrings, findInComments)); + findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ts.RenameLocation[] { + return unwrapJSONCallResult(this.shim.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename)); } getDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] { return unwrapJSONCallResult(this.shim.getDefinitionAtPosition(fileName, position)); diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 8b9df926d8252..11930de4d8cc1 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -2905,6 +2905,7 @@ namespace ts.server.protocol { readonly importModuleSpecifierPreference?: "relative" | "non-relative"; readonly allowTextChangesInNewFiles?: boolean; readonly lazyConfiguredProjectsFromExternalProject?: boolean; + readonly providePrefixAndSuffixTextForRename?: boolean; readonly allowRenameOfImportPath?: boolean; } diff --git a/src/server/session.ts b/src/server/session.ts index 8c300b121e1d4..2257c6740f498 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -314,7 +314,8 @@ namespace ts.server { defaultProject: Project, initialLocation: DocumentPosition, findInStrings: boolean, - findInComments: boolean + findInComments: boolean, + hostPreferences: UserPreferences ): ReadonlyArray { const outputs: RenameLocation[] = []; @@ -323,7 +324,7 @@ namespace ts.server { defaultProject, initialLocation, ({ project, location }, tryAddToTodo) => { - for (const output of project.getLanguageService().findRenameLocations(location.fileName, location.pos, findInStrings, findInComments) || emptyArray) { + for (const output of project.getLanguageService().findRenameLocations(location.fileName, location.pos, findInStrings, findInComments, hostPreferences.providePrefixAndSuffixTextForRename) || emptyArray) { if (!contains(outputs, output, documentSpansEqual) && !tryAddToTodo(project, documentSpanLocation(output))) { outputs.push(output); } @@ -1232,7 +1233,8 @@ namespace ts.server { this.getDefaultProject(args), { fileName: args.file, pos: position }, !!args.findInStrings, - !!args.findInComments + !!args.findInComments, + this.getHostPreferences() ); if (!simplifiedResult) return locations; diff --git a/src/services/findAllReferences.ts b/src/services/findAllReferences.ts index f290826690e01..bac1811f4290e 100644 --- a/src/services/findAllReferences.ts +++ b/src/services/findAllReferences.ts @@ -39,6 +39,11 @@ namespace ts.FindAllReferences { readonly isForRename?: boolean; /** True if we are searching for implementations. We will have a different method of adding references if so. */ readonly implementations?: boolean; + /** + * True to opt in for enhanced renaming of shorthand properties and import/export specifiers. + * Default is false for backwards compatibility. + */ + readonly providePrefixAndSuffixTextForRename?: boolean; } export function findReferencedSymbols(program: Program, cancellationToken: CancellationToken, sourceFiles: ReadonlyArray, sourceFile: SourceFile, position: number): ReferencedSymbol[] | undefined { @@ -157,8 +162,8 @@ namespace ts.FindAllReferences { return { displayParts, kind: symbolKind }; } - export function toRenameLocation(entry: Entry, originalNode: Node, checker: TypeChecker): RenameLocation { - return { ...entryToDocumentSpan(entry), ...getPrefixAndSuffixText(entry, originalNode, checker) }; + export function toRenameLocation(entry: Entry, originalNode: Node, checker: TypeChecker, providePrefixAndSuffixText: boolean): RenameLocation { + return { ...entryToDocumentSpan(entry), ...(providePrefixAndSuffixText && getPrefixAndSuffixText(entry, originalNode, checker)) }; } export function toReferenceEntry(entry: Entry): ReferenceEntry { @@ -484,7 +489,7 @@ namespace ts.FindAllReferences.Core { /** Core find-all-references algorithm for a normal symbol. */ function getReferencedSymbolsForSymbol(originalSymbol: Symbol, node: Node | undefined, sourceFiles: ReadonlyArray, sourceFilesSet: ReadonlyMap, checker: TypeChecker, cancellationToken: CancellationToken, options: Options): SymbolAndEntries[] { - const symbol = node && skipPastExportOrImportSpecifierOrUnion(originalSymbol, node, checker, !!options.isForRename) || originalSymbol; + const symbol = node && skipPastExportOrImportSpecifierOrUnion(originalSymbol, node, checker, /*useLocalSymbolForExportSpecifier*/ !isForRenameWithPrefixAndSuffixText(options)) || originalSymbol; // Compute the meaning from the location and the symbol it references const searchMeaning = node ? getIntersectingMeaningFromDeclarations(node, symbol) : SemanticMeaning.All; @@ -492,7 +497,7 @@ namespace ts.FindAllReferences.Core { const result: SymbolAndEntries[] = []; const state = new State(sourceFiles, sourceFilesSet, node ? getSpecialSearchKind(node) : SpecialSearchKind.None, checker, cancellationToken, searchMeaning, options, result); - const exportSpecifier = !options.isForRename ? undefined : find(symbol.declarations, isExportSpecifier); + const exportSpecifier = !isForRenameWithPrefixAndSuffixText(options) ? undefined : find(symbol.declarations, isExportSpecifier); if (exportSpecifier) { // When renaming at an export specifier, rename the export and not the thing being exported. getReferencesAtExportSpecifier(exportSpecifier.name, symbol, exportSpecifier, state.createSearch(node, originalSymbol, /*comingFrom*/ undefined), state, /*addReferencesHere*/ true, /*alwaysGetReferences*/ true); @@ -502,7 +507,7 @@ namespace ts.FindAllReferences.Core { searchForImportsOfExport(node, symbol, { exportingModuleSymbol: Debug.assertDefined(symbol.parent, "Expected export symbol to have a parent"), exportKind: ExportKind.Default }, state); } else { - const search = state.createSearch(node, symbol, /*comingFrom*/ undefined, { allSearchSymbols: node ? populateSearchSymbolSet(symbol, node, checker, !!options.isForRename, !!options.implementations) : [symbol] }); + const search = state.createSearch(node, symbol, /*comingFrom*/ undefined, { allSearchSymbols: node ? populateSearchSymbolSet(symbol, node, checker, !!options.isForRename, !!options.providePrefixAndSuffixTextForRename, !!options.implementations) : [symbol] }); // Try to get the smallest valid scope that we can limit our search to; // otherwise we'll need to search globally (i.e. include each file). @@ -538,9 +543,9 @@ namespace ts.FindAllReferences.Core { } /** Handle a few special cases relating to export/import specifiers. */ - function skipPastExportOrImportSpecifierOrUnion(symbol: Symbol, node: Node, checker: TypeChecker, isForRename: boolean): Symbol | undefined { + function skipPastExportOrImportSpecifierOrUnion(symbol: Symbol, node: Node, checker: TypeChecker, useLocalSymbolForExportSpecifier: boolean): Symbol | undefined { const { parent } = node; - if (isExportSpecifier(parent) && !isForRename) { + if (isExportSpecifier(parent) && useLocalSymbolForExportSpecifier) { return getLocalSymbolForExportSpecifier(node as Identifier, symbol, parent, checker); } // 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 { addReferencesHere: boolean, alwaysGetReferences?: boolean, ): void { + Debug.assert(!alwaysGetReferences || !!state.options.providePrefixAndSuffixTextForRename, "If alwaysGetReferences is true, then prefix/suffix text must be enabled"); + const { parent, propertyName, name } = exportSpecifier; const exportDeclaration = parent.parent; const localSymbol = getLocalSymbolForExportSpecifier(referenceLocation, referenceSymbol, exportSpecifier, state.checker); @@ -1102,7 +1109,7 @@ namespace ts.FindAllReferences.Core { } // For `export { foo as bar }`, rename `foo`, but not `bar`. - if (!state.options.isForRename || alwaysGetReferences) { + if (!isForRenameWithPrefixAndSuffixText(state.options) || alwaysGetReferences) { const exportKind = referenceLocation.originalKeywordKind === SyntaxKind.DefaultKeyword ? ExportKind.Default : ExportKind.Named; const exportSymbol = Debug.assertDefined(exportSpecifier.symbol); const exportInfo = Debug.assertDefined(getExportInfo(exportSymbol, exportKind, state.checker)); @@ -1110,7 +1117,7 @@ namespace ts.FindAllReferences.Core { } // At `export { x } from "foo"`, also search for the imported symbol `"foo".x`. - if (search.comingFrom !== ImportExport.Export && exportDeclaration.moduleSpecifier && !propertyName && !state.options.isForRename) { + if (search.comingFrom !== ImportExport.Export && exportDeclaration.moduleSpecifier && !propertyName && !isForRenameWithPrefixAndSuffixText(state.options)) { const imported = state.checker.getExportSpecifierLocalTargetSymbol(exportSpecifier); if (imported) searchForImportedSymbol(imported, state); } @@ -1145,7 +1152,7 @@ namespace ts.FindAllReferences.Core { const { symbol } = importOrExport; if (importOrExport.kind === ImportExport.Import) { - if (!state.options.isForRename) { + if (!(isForRenameWithPrefixAndSuffixText(state.options))) { searchForImportedSymbol(symbol, state); } } @@ -1514,16 +1521,16 @@ namespace ts.FindAllReferences.Core { // For certain symbol kinds, we need to include other symbols in the search set. // This is not needed when searching for re-exports. - function populateSearchSymbolSet(symbol: Symbol, location: Node, checker: TypeChecker, isForRename: boolean, implementations: boolean): Symbol[] { + function populateSearchSymbolSet(symbol: Symbol, location: Node, checker: TypeChecker, isForRename: boolean, providePrefixAndSuffixText: boolean, implementations: boolean): Symbol[] { const result: Symbol[] = []; - forEachRelatedSymbol(symbol, location, checker, isForRename, + forEachRelatedSymbol(symbol, location, checker, isForRename, !(isForRename && providePrefixAndSuffixText), (sym, root, base) => { result.push(base || root || sym); }, /*allowBaseTypes*/ () => !implementations); return result; } function forEachRelatedSymbol( - symbol: Symbol, location: Node, checker: TypeChecker, isForRenamePopulateSearchSymbolSet: boolean, + symbol: Symbol, location: Node, checker: TypeChecker, isForRenamePopulateSearchSymbolSet: boolean, onlyIncludeBindingElementAtReferenceLocation: boolean, cbSymbol: (symbol: Symbol, rootSymbol?: Symbol, baseSymbol?: Symbol, kind?: NodeEntryKind) => T | undefined, allowBaseTypes: (rootSymbol: Symbol) => boolean, ): T | undefined { @@ -1577,9 +1584,25 @@ namespace ts.FindAllReferences.Core { } // symbolAtLocation for a binding element is the local symbol. See if the search symbol is the property. - // Don't do this when populating search set for a rename -- just rename the local. + // Don't do this when populating search set for a rename when prefix and suffix text will be provided -- just rename the local. if (!isForRenamePopulateSearchSymbolSet) { - const bindingElementPropertySymbol = isObjectBindingElementWithoutPropertyName(location.parent) ? getPropertySymbolFromBindingElement(checker, location.parent) : undefined; + let bindingElementPropertySymbol: Symbol | undefined; + if (onlyIncludeBindingElementAtReferenceLocation) { + bindingElementPropertySymbol = isObjectBindingElementWithoutPropertyName(location.parent) ? getPropertySymbolFromBindingElement(checker, location.parent) : undefined; + } + else { + bindingElementPropertySymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol, checker); + } + return bindingElementPropertySymbol && fromRoot(bindingElementPropertySymbol, EntryKind.SearchedPropertyFoundLocal); + } + + Debug.assert(isForRenamePopulateSearchSymbolSet); + // due to the above assert and the arguments at the uses of this function, + // (onlyIncludeBindingElementAtReferenceLocation <=> !providePrefixAndSuffixTextForRename) holds + const includeOriginalSymbolOfBindingElement = onlyIncludeBindingElementAtReferenceLocation; + + if (includeOriginalSymbolOfBindingElement) { + const bindingElementPropertySymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol, checker); return bindingElementPropertySymbol && fromRoot(bindingElementPropertySymbol, EntryKind.SearchedPropertyFoundLocal); } @@ -1597,6 +1620,13 @@ namespace ts.FindAllReferences.Core { ? getPropertySymbolsFromBaseTypes(rootSymbol.parent, rootSymbol.name, checker, base => cbSymbol(sym, rootSymbol, base, kind)) : undefined)); } + + function getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol: Symbol, checker: TypeChecker): Symbol | undefined { + const bindingElement = getDeclarationOfKind(symbol, SyntaxKind.BindingElement); + if (bindingElement && isObjectBindingElementWithoutPropertyName(bindingElement)) { + return getPropertySymbolFromBindingElement(checker, bindingElement); + } + } } interface RelatedSymbol { @@ -1606,6 +1636,7 @@ namespace ts.FindAllReferences.Core { function getRelatedSymbol(search: Search, referenceSymbol: Symbol, referenceLocation: Node, state: State): RelatedSymbol | undefined { const { checker } = state; return forEachRelatedSymbol(referenceSymbol, referenceLocation, checker, /*isForRenamePopulateSearchSymbolSet*/ false, + /*onlyIncludeBindingElementAtReferenceLocation*/ !state.options.isForRename || !!state.options.providePrefixAndSuffixTextForRename, (sym, rootSymbol, baseSymbol, kind): RelatedSymbol | undefined => search.includes(baseSymbol || rootSymbol || sym) // For a base type, use the symbol for the derived type. For a synthetic (e.g. union) property, use the union symbol. ? { symbol: rootSymbol && !(getCheckFlags(sym) & CheckFlags.Synthetic) ? rootSymbol : sym, kind } @@ -1696,4 +1727,8 @@ namespace ts.FindAllReferences.Core { t.symbol && t.symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface) ? t.symbol : undefined); return res.length === 0 ? undefined : res; } + + function isForRenameWithPrefixAndSuffixText(options: Options) { + return options.isForRename && options.providePrefixAndSuffixTextForRename; + } } diff --git a/src/services/services.ts b/src/services/services.ts index 6bf6566da36ac..6a63497d7e665 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1549,7 +1549,7 @@ namespace ts { return DocumentHighlights.getDocumentHighlights(program, cancellationToken, sourceFile, position, sourceFilesToSearch); } - function findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] | undefined { + function findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): RenameLocation[] | undefined { synchronizeHostData(); const sourceFile = getValidSourceFile(fileName); const node = getTouchingPropertyName(sourceFile, position); @@ -1559,7 +1559,8 @@ namespace ts { ({ fileName: sourceFile.fileName, textSpan: createTextSpanFromNode(node.tagName, sourceFile) })); } else { - return getReferencesWorker(node, position, { findInStrings, findInComments, isForRename: true }, FindAllReferences.toRenameLocation); + return getReferencesWorker(node, position, { findInStrings, findInComments, providePrefixAndSuffixTextForRename, isForRename: true }, + (entry, originalNode, checker) => FindAllReferences.toRenameLocation(entry, originalNode, checker, providePrefixAndSuffixTextForRename || false)); } } diff --git a/src/services/shims.ts b/src/services/shims.ts index 33ea4333e6f01..208cf3dbc3c01 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -170,7 +170,7 @@ namespace ts { * Returns a JSON-encoded value of the type: * { fileName: string, textSpan: { start: number, length: number } }[] */ - findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): string; + findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): string; /** * Returns a JSON-encoded value of the type: @@ -838,10 +838,10 @@ namespace ts { ); } - public findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): string { + public findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): string { return this.forwardJSONCall( - `findRenameLocations('${fileName}', ${position}, ${findInStrings}, ${findInComments})`, - () => this.languageService.findRenameLocations(fileName, position, findInStrings, findInComments) + `findRenameLocations('${fileName}', ${position}, ${findInStrings}, ${findInComments}, ${providePrefixAndSuffixTextForRename})`, + () => this.languageService.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename) ); } diff --git a/src/services/types.ts b/src/services/types.ts index b45a816d6e007..3502e3e06712f 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -295,7 +295,7 @@ namespace ts { getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo; - findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): ReadonlyArray | undefined; + findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ReadonlyArray | undefined; getDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined; diff --git a/src/testRunner/unittests/tsserver/rename.ts b/src/testRunner/unittests/tsserver/rename.ts index 4e95e79e31f31..571db235a6e5a 100644 --- a/src/testRunner/unittests/tsserver/rename.ts +++ b/src/testRunner/unittests/tsserver/rename.ts @@ -32,13 +32,39 @@ namespace ts.projectSystem { }); }); - it("works with prefixText and suffixText", () => { + it("works with prefixText and suffixText when enabled", () => { const aTs: File = { path: "/a.ts", content: "const x = 0; const o = { x };" }; - const session = createSession(createServerHost([aTs])); + const host = createServerHost([aTs]); + const session = createSession(host); openFilesForSession([aTs], session); - const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(aTs, "x")); - assert.deepEqual(response, { + // rename with prefixText and suffixText disabled + const response1 = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(aTs, "x")); + assert.deepEqual(response1, { + info: { + canRename: true, + fileToRename: undefined, + displayName: "x", + fullDisplayName: "x", + kind: ScriptElementKind.constElement, + kindModifiers: ScriptElementKindModifier.none, + triggerSpan: protocolTextSpanFromSubstring(aTs.content, "x"), + }, + locs: [ + { + file: aTs.path, + locs: [ + protocolRenameSpanFromSubstring(aTs.content, "x"), + protocolRenameSpanFromSubstring(aTs.content, "x", { index: 1 }), + ], + }, + ], + }); + + // rename with prefixText and suffixText enabled + session.getProjectService().setHostConfiguration({ preferences: { providePrefixAndSuffixTextForRename: true } }); + const response2 = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(aTs, "x")); + assert.deepEqual(response2, { info: { canRename: true, fileToRename: undefined, diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 30effc9e91a3d..75ddd2765ab29 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3013,6 +3013,7 @@ declare namespace ts { /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ readonly importModuleSpecifierEnding?: "minimal" | "index" | "js"; readonly allowTextChangesInNewFiles?: boolean; + readonly providePrefixAndSuffixTextForRename?: boolean; } /** Represents a bigint literal value without requiring bigint support */ interface PseudoBigInt { @@ -4707,7 +4708,7 @@ declare namespace ts { getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan | undefined; getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo; - findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): ReadonlyArray | undefined; + findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ReadonlyArray | undefined; getDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined; getTypeDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; @@ -7926,6 +7927,7 @@ declare namespace ts.server.protocol { readonly importModuleSpecifierPreference?: "relative" | "non-relative"; readonly allowTextChangesInNewFiles?: boolean; readonly lazyConfiguredProjectsFromExternalProject?: boolean; + readonly providePrefixAndSuffixTextForRename?: boolean; readonly allowRenameOfImportPath?: boolean; } interface CompilerOptions { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index cd6d45a16478a..3dadfa5965a8f 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3013,6 +3013,7 @@ declare namespace ts { /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ readonly importModuleSpecifierEnding?: "minimal" | "index" | "js"; readonly allowTextChangesInNewFiles?: boolean; + readonly providePrefixAndSuffixTextForRename?: boolean; } /** Represents a bigint literal value without requiring bigint support */ interface PseudoBigInt { @@ -4707,7 +4708,7 @@ declare namespace ts { getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan | undefined; getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo; - findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): ReadonlyArray | undefined; + findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ReadonlyArray | undefined; getDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined; getTypeDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; diff --git a/tests/cases/fourslash/findAllRefsPrefixSuffixPreference.ts b/tests/cases/fourslash/findAllRefsPrefixSuffixPreference.ts new file mode 100644 index 0000000000000..3745eff4e5d84 --- /dev/null +++ b/tests/cases/fourslash/findAllRefsPrefixSuffixPreference.ts @@ -0,0 +1,56 @@ +/// + +// @Filename: /file1.ts +////declare function log(s: string | number): void; +////const [|{| "isWriteAccess": true, "isDefinition": true |}q|] = 1; +////export { [|{| "isWriteAccess": true, "isDefinition": true |}q|] }; +////const x = { +//// [|{| "isWriteAccess": true, "isDefinition": true |}z|]: 'value' +////} +////const { [|{| "isWriteAccess": true, "isDefinition": true |}z|] } = x; +////log([|z|]); + +// @Filename: /file2.ts +////declare function log(s: string | number): void; +////import { [|{| "isWriteAccess": true, "isDefinition": true |}q|] } from "./file1"; +////log([|q|] + 1); + +verify.noErrors(); + +const [q0, q1, z0, z1, z2, q2, q3] = test.ranges(); +const qFile1Ranges = [q0, q1]; +const qFile2Ranges = [q2, q3]; +const qFile1ReferenceGroup: FourSlashInterface.ReferenceGroup = { + definition: "const q: 1", + ranges: qFile1Ranges +}; +const qFile2ReferenceGroup: FourSlashInterface.ReferenceGroup = { + definition: "(alias) const q: 1\nimport q", + ranges: qFile2Ranges +}; +verify.referenceGroups([q0, q1], [qFile1ReferenceGroup, qFile2ReferenceGroup]); +verify.referenceGroups([q2, q3], [qFile2ReferenceGroup, qFile1ReferenceGroup]); + +verify.renameLocations(q0, { ranges: [q0, { range: q1, suffixText: " as q" }], providePrefixAndSuffixTextForRename: true }); +verify.renameLocations(q1, { ranges: [{ range: q1, prefixText: "q as " }, q2, q3], providePrefixAndSuffixTextForRename: true }); +verify.renameLocations([q2, q3], { ranges: [{ range: q2, prefixText: "q as " }, q3], providePrefixAndSuffixTextForRename: true }); + +verify.renameLocations([q0, q1, q2, q3], { ranges: [q0, q1, q2, q3], providePrefixAndSuffixTextForRename: false }); + +const zReferenceGroup1: FourSlashInterface.ReferenceGroup = { + definition: "(property) z: string", + ranges: [z0] +}; +const zReferenceGroup2: FourSlashInterface.ReferenceGroup = { + definition: "const z: string", + ranges: [z1, z2] +}; + +verify.referenceGroups([z0], [{ ...zReferenceGroup1, ranges: [z0, z1] }]); +verify.referenceGroups([z1], [zReferenceGroup1, zReferenceGroup2]); +verify.referenceGroups([z2], [zReferenceGroup2]); + +verify.renameLocations([z0], { ranges: [z0, { range: z1, suffixText: ": z" }], providePrefixAndSuffixTextForRename: true }); +verify.renameLocations([z1, z2], { ranges: [{ range: z1, prefixText: "z: " }, z2], providePrefixAndSuffixTextForRename: true }); + +verify.renameLocations([z0, z1, z2], { ranges: [z0, z1, z2], providePrefixAndSuffixTextForRename: false }); \ No newline at end of file diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index ed71f8893b8b5..db4f7be009b0e 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -633,7 +633,8 @@ declare namespace FourSlashInterface { readonly findInStrings?: boolean; readonly findInComments?: boolean; readonly ranges: ReadonlyArray; - } + readonly providePrefixAndSuffixTextForRename?: boolean; + }; type RenameLocationOptions = Range | { readonly range: Range, readonly prefixText?: string, readonly suffixText?: string }; } declare function verifyOperationIsCancelled(f: any): void;