diff --git a/src/harness/client.ts b/src/harness/client.ts index a74e616072302..230ba2354a959 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -1,4 +1,6 @@ import { + ApplicableInteractiveRefactorInfo, + ApplicableNonInteractiveRefactorInfo, ApplicableRefactorInfo, CallHierarchyIncomingCall, CallHierarchyItem, @@ -52,6 +54,7 @@ import { Program, QuickInfo, RefactorEditInfo, + RefactorTriggerReason, ReferencedSymbol, ReferenceEntry, RenameInfo, @@ -785,10 +788,13 @@ export class SessionClient implements LanguageService { return { file, line, offset, endLine, endOffset }; } - getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] { + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableNonInteractiveRefactorInfo[]; + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason: RefactorTriggerReason | undefined, kind: string | undefined, includeInteractive: true): ApplicableInteractiveRefactorInfo[]; + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string, includeInteractive?: boolean): ApplicableRefactorInfo[]; + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, _preferences: UserPreferences | undefined, _triggerReason?: RefactorTriggerReason, _kind?: string, includeInteractive?: boolean): ApplicableRefactorInfo[] { const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName); - const request = this.processRequest(protocol.CommandTypes.GetApplicableRefactors, args); + const request = this.processRequest(protocol.CommandTypes.GetApplicableRefactors, { ...args, includeInteractive }); const response = this.processResponse(request); return response.body!; // TODO: GH#18217 } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index a8c854325a266..edab4b4461cd6 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -613,6 +613,9 @@ class LanguageServiceShimProxy implements ts.LanguageService { getEditsForRefactor(): ts.RefactorEditInfo { throw new Error("Not supported on the shim."); } + getApplicableRefactors(): ts.ApplicableNonInteractiveRefactorInfo[]; + getApplicableRefactors(): ts.ApplicableInteractiveRefactorInfo[]; + getApplicableRefactors(): ts.ApplicableRefactorInfo[]; getApplicableRefactors(): ts.ApplicableRefactorInfo[] { throw new Error("Not supported on the shim."); } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 819c4797084fc..24fb1603cdcfe 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -4,11 +4,14 @@ import type { EndOfLineState, FileExtensionInfo, HighlightSpanKind, + InteractiveRefactorName, MapLike, + NonInteractiveRefactorName, OutliningSpanKind, OutputFile, PluginImport, ProjectReference, + RefactorName, RenameLocation, ScriptElementKind, ScriptKind, @@ -586,6 +589,7 @@ export interface GetApplicableRefactorsRequest extends Request { export type GetApplicableRefactorsRequestArgs = FileLocationOrRangeRequestArgs & { triggerReason?: RefactorTriggerReason; kind?: string; + includeInteractive?: boolean; }; export type RefactorTriggerReason = "implicit" | "invoked"; @@ -594,18 +598,18 @@ export type RefactorTriggerReason = "implicit" | "invoked"; * Response is a list of available refactorings. * Each refactoring exposes one or more "Actions"; a user selects one action to invoke a refactoring */ -export interface GetApplicableRefactorsResponse extends Response { - body?: ApplicableRefactorInfo[]; +export interface GetApplicableRefactorsResponse extends Response { + body?: TRefactorInfo[]; } /** * A set of one or more available refactoring actions, grouped under a parent refactoring. */ -export interface ApplicableRefactorInfo { +export interface BaseApplicableRefactorInfo { /** * The programmatic name of the refactoring */ - name: string; + name: RefactorName; /** * A description of this refactoring category to show to the user. * If the refactoring gets inlined (see below), this text will not be visible. @@ -623,6 +627,17 @@ export interface ApplicableRefactorInfo { actions: RefactorActionInfo[]; } +export interface ApplicableNonInteractiveRefactorInfo extends BaseApplicableRefactorInfo { + name: NonInteractiveRefactorName; +} + +export interface ApplicableInteractiveRefactorInfo extends BaseApplicableRefactorInfo { + name: InteractiveRefactorName; +} + +export type ApplicableRefactorInfo = ApplicableNonInteractiveRefactorInfo | ApplicableInteractiveRefactorInfo; + + /** * Represents a single refactoring action - for example, the "Extract Method..." refactor might * offer several actions, each corresponding to a surround class or closure to extract into. diff --git a/src/services/refactorProvider.ts b/src/services/refactorProvider.ts index 611c846c21c99..d04357c683349 100644 --- a/src/services/refactorProvider.ts +++ b/src/services/refactorProvider.ts @@ -2,35 +2,44 @@ import { ApplicableRefactorInfo, arrayFrom, flatMapIterator, + InteractiveRefactor, + InteractiveRefactorName, + NonInteractiveRefactor, + NonInteractiveRefactorName, Refactor, RefactorContext, RefactorEditInfo, + RefactorName, } from "./_namespaces/ts"; import { refactorKindBeginsWith } from "./_namespaces/ts.refactor"; // A map with the refactor code as key, the refactor itself as value // e.g. nonSuggestableRefactors[refactorCode] -> the refactor you want -const refactors = new Map(); +const refactors = new Map(); /** * @param name An unique code associated with each refactor. Does not have to be human-readable. * * @internal */ -export function registerRefactor(name: string, refactor: Refactor) { +export function registerRefactor(name: Name, refactor: InteractiveRefactor): void; +/** @internal */ +export function registerRefactor(name: NonInteractiveRefactorName, refactor: NonInteractiveRefactor): void; +/** @internal */ +export function registerRefactor(name: RefactorName, refactor: Refactor) { refactors.set(name, refactor); } /** @internal */ -export function getApplicableRefactors(context: RefactorContext): ApplicableRefactorInfo[] { +export function getApplicableRefactors(context: RefactorContext, includeInteractive?: boolean): ApplicableRefactorInfo[] { return arrayFrom(flatMapIterator(refactors.values(), refactor => context.cancellationToken && context.cancellationToken.isCancellationRequested() || - !refactor.kinds?.some(kind => refactorKindBeginsWith(kind, context.kind)) ? undefined : + (!refactor.kinds?.some(kind => refactorKindBeginsWith(kind, context.kind)) || refactor.isInteractive && !includeInteractive) ? undefined : refactor.getAvailableActions(context))); } /** @internal */ -export function getEditsForRefactor(context: RefactorContext, refactorName: string, actionName: string): RefactorEditInfo | undefined { +export function getEditsForRefactor(context: RefactorContext, refactorName: RefactorName, actionName: string, ...args: unknown[]): RefactorEditInfo | undefined { const refactor = refactors.get(refactorName); - return refactor && refactor.getEditsForAction(context, actionName); + return refactor && refactor.getEditsForAction(context, actionName, ...args); } diff --git a/src/services/refactors/addOrRemoveBracesToArrowFunction.ts b/src/services/refactors/addOrRemoveBracesToArrowFunction.ts index 930a1b42d443f..e592704bda29b 100644 --- a/src/services/refactors/addOrRemoveBracesToArrowFunction.ts +++ b/src/services/refactors/addOrRemoveBracesToArrowFunction.ts @@ -19,6 +19,7 @@ import { isExpression, isReturnStatement, needsParentheses, + NonInteractiveRefactorName, rangeContainsRange, RefactorContext, RefactorEditInfo, @@ -34,7 +35,7 @@ import { registerRefactor, } from "../_namespaces/ts.refactor"; -const refactorName = "Add or remove braces in an arrow function"; +const refactorName = NonInteractiveRefactorName.AddOrRemoveBracesToArrowFunction; const refactorDescription = Diagnostics.Add_or_remove_braces_in_an_arrow_function.message; const addBracesAction = { diff --git a/src/services/refactors/convertArrowFunctionOrFunctionExpression.ts b/src/services/refactors/convertArrowFunctionOrFunctionExpression.ts index 414ef006bc926..c4ca5de7f4f1e 100644 --- a/src/services/refactors/convertArrowFunctionOrFunctionExpression.ts +++ b/src/services/refactors/convertArrowFunctionOrFunctionExpression.ts @@ -35,6 +35,7 @@ import { length, ModifierFlags, Node, + NonInteractiveRefactorName, Program, rangeContainsRange, RefactorActionInfo, @@ -58,7 +59,7 @@ import { registerRefactor, } from "../_namespaces/ts.refactor"; -const refactorName = "Convert arrow function or function expression"; +const refactorName = NonInteractiveRefactorName.ConvertArrowFunctionOrFunctionExpression; const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_arrow_function_or_function_expression); const toAnonymousFunctionAction = { diff --git a/src/services/refactors/convertExport.ts b/src/services/refactors/convertExport.ts index 3760cd8b31321..f01a9f9d3093d 100644 --- a/src/services/refactors/convertExport.ts +++ b/src/services/refactors/convertExport.ts @@ -37,6 +37,7 @@ import { NamespaceDeclaration, Node, NodeFlags, + NonInteractiveRefactorName, Program, PropertyAccessExpression, QuotePreference, @@ -57,7 +58,7 @@ import { registerRefactor, } from "../_namespaces/ts.refactor"; -const refactorName = "Convert export"; +const refactorName = NonInteractiveRefactorName.ConvertExport; const defaultToNamedAction = { name: "Convert default export to named export", diff --git a/src/services/refactors/convertImport.ts b/src/services/refactors/convertImport.ts index da3a66107def6..5d81ddda9e5b0 100644 --- a/src/services/refactors/convertImport.ts +++ b/src/services/refactors/convertImport.ts @@ -30,6 +30,7 @@ import { isStringLiteral, NamedImports, NamespaceImport, + NonInteractiveRefactorName, Program, PropertyAccessExpression, QualifiedName, @@ -50,7 +51,7 @@ import { registerRefactor, } from "../_namespaces/ts.refactor"; -const refactorName = "Convert import"; +const refactorName = NonInteractiveRefactorName.ConvertImport; const actions = { [ImportKind.Named]: { diff --git a/src/services/refactors/convertOverloadListToSingleSignature.ts b/src/services/refactors/convertOverloadListToSingleSignature.ts index c0ad2970a6138..6faf0a310569e 100644 --- a/src/services/refactors/convertOverloadListToSingleSignature.ts +++ b/src/services/refactors/convertOverloadListToSingleSignature.ts @@ -25,6 +25,7 @@ import { NamedTupleMember, Node, NodeArray, + NonInteractiveRefactorName, ParameterDeclaration, Program, rangeContainsPosition, @@ -41,7 +42,7 @@ import { } from "../_namespaces/ts"; import { registerRefactor } from "../_namespaces/ts.refactor"; -const refactorName = "Convert overload list to single signature"; +const refactorName = NonInteractiveRefactorName.ConvertOverloadListToSingleSignature; const refactorDescription = Diagnostics.Convert_overload_list_to_single_signature.message; const functionOverloadAction = { diff --git a/src/services/refactors/convertParamsToDestructuredObject.ts b/src/services/refactors/convertParamsToDestructuredObject.ts index a86e190d2d8cd..217baf29fb80c 100644 --- a/src/services/refactors/convertParamsToDestructuredObject.ts +++ b/src/services/refactors/convertParamsToDestructuredObject.ts @@ -80,6 +80,7 @@ import { NewExpression, Node, NodeArray, + NonInteractiveRefactorName, ObjectLiteralElementLike, ObjectLiteralExpression, ParameterDeclaration, @@ -106,7 +107,7 @@ import { } from "../_namespaces/ts"; import { registerRefactor } from "../_namespaces/ts.refactor"; -const refactorName = "Convert parameters to destructured object"; +const refactorName = NonInteractiveRefactorName.ConvertParamsToDestructuredObject; const minimumParameterLength = 1; const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_parameters_to_destructured_object); diff --git a/src/services/refactors/convertStringOrTemplateLiteral.ts b/src/services/refactors/convertStringOrTemplateLiteral.ts index 53ac3c4bb5628..3b24487ae3a57 100644 --- a/src/services/refactors/convertStringOrTemplateLiteral.ts +++ b/src/services/refactors/convertStringOrTemplateLiteral.ts @@ -24,6 +24,7 @@ import { isTemplateMiddle, map, Node, + NonInteractiveRefactorName, ParenthesizedExpression, RefactorContext, RefactorEditInfo, @@ -38,7 +39,7 @@ import { } from "../_namespaces/ts"; import { registerRefactor } from "../_namespaces/ts.refactor"; -const refactorName = "Convert to template string"; +const refactorName = NonInteractiveRefactorName.ConvertStringOrTemplateLiteral; const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_template_string); const convertStringAction = { diff --git a/src/services/refactors/convertToOptionalChainExpression.ts b/src/services/refactors/convertToOptionalChainExpression.ts index a86777e97dd63..40d4e05f151cd 100644 --- a/src/services/refactors/convertToOptionalChainExpression.ts +++ b/src/services/refactors/convertToOptionalChainExpression.ts @@ -29,6 +29,7 @@ import { isStringOrNumericLiteralLike, isVariableStatement, Node, + NonInteractiveRefactorName, PropertyAccessExpression, RefactorContext, RefactorEditInfo, @@ -47,7 +48,7 @@ import { registerRefactor, } from "../_namespaces/ts.refactor"; -const refactorName = "Convert to optional chain expression"; +const refactorName = NonInteractiveRefactorName.ConvertToOptionalChainExpression; const convertToOptionalChainExpressionMessage = getLocaleSpecificMessage(Diagnostics.Convert_to_optional_chain_expression); const toOptionalChainAction = { diff --git a/src/services/refactors/extractSymbol.ts b/src/services/refactors/extractSymbol.ts index 9efcce3006624..5e3eccf0316c1 100644 --- a/src/services/refactors/extractSymbol.ts +++ b/src/services/refactors/extractSymbol.ts @@ -120,6 +120,7 @@ import { Node, NodeBuilderFlags, NodeFlags, + NonInteractiveRefactorName, nullTransformationContext, ObjectLiteralElementLike, ParameterDeclaration, @@ -165,7 +166,7 @@ import { registerRefactor, } from "../_namespaces/ts.refactor"; -const refactorName = "Extract Symbol"; +const refactorName = NonInteractiveRefactorName.ExtractSymbol; const extractConstantAction = { name: "Extract Constant", diff --git a/src/services/refactors/extractType.ts b/src/services/refactors/extractType.ts index 02e5f10ba22d9..587b1149fc1a5 100644 --- a/src/services/refactors/extractType.ts +++ b/src/services/refactors/extractType.ts @@ -48,6 +48,7 @@ import { JSDocTemplateTag, Node, nodeOverlapsWithStartEnd, + NonInteractiveRefactorName, pushIfUnique, rangeContainsStartEnd, RefactorContext, @@ -70,7 +71,7 @@ import { registerRefactor, } from "../_namespaces/ts.refactor"; -const refactorName = "Extract type"; +const refactorName = NonInteractiveRefactorName.ExtractType; const extractToTypeAliasAction = { name: "Extract to type alias", diff --git a/src/services/refactors/generateGetAccessorAndSetAccessor.ts b/src/services/refactors/generateGetAccessorAndSetAccessor.ts index 2982cacd8af7c..f85eb48d54bec 100644 --- a/src/services/refactors/generateGetAccessorAndSetAccessor.ts +++ b/src/services/refactors/generateGetAccessorAndSetAccessor.ts @@ -7,6 +7,7 @@ import { getRenameLocation, isIdentifier, isParameter, + NonInteractiveRefactorName, RefactorContext, } from "../_namespaces/ts"; import { @@ -14,7 +15,7 @@ import { registerRefactor, } from "../_namespaces/ts.refactor"; -const actionName = "Generate 'get' and 'set' accessors"; +const actionName = NonInteractiveRefactorName.GenerateGetAccessorAndSetAccessor; const actionDescription = Diagnostics.Generate_get_and_set_accessors.message; const generateGetSetAction = { diff --git a/src/services/refactors/inferFunctionReturnType.ts b/src/services/refactors/inferFunctionReturnType.ts index ccce34a8a14ac..7c05531ca6b84 100644 --- a/src/services/refactors/inferFunctionReturnType.ts +++ b/src/services/refactors/inferFunctionReturnType.ts @@ -18,6 +18,7 @@ import { MethodDeclaration, Node, NodeBuilderFlags, + NonInteractiveRefactorName, RefactorContext, RefactorEditInfo, SourceFile, @@ -34,7 +35,7 @@ import { registerRefactor, } from "../_namespaces/ts.refactor"; -const refactorName = "Infer function return type"; +const refactorName = NonInteractiveRefactorName.InferFunctionReturnType; const refactorDescription = Diagnostics.Infer_function_return_type.message; const inferReturnTypeAction = { diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index 2fdff08a85282..f18a643f1330a 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -95,6 +95,7 @@ import { Node, NodeFlags, nodeSeenTracker, + NonInteractiveRefactorName, normalizePath, ObjectBindingElementWithoutPropertyName, Program, @@ -131,7 +132,7 @@ import { } from "../_namespaces/ts"; import { registerRefactor } from "../_namespaces/ts.refactor"; -const refactorName = "Move to a new file"; +const refactorName = NonInteractiveRefactorName.MoveToNewFile; const description = getLocaleSpecificMessage(Diagnostics.Move_to_a_new_file); const moveToNewFileAction = { diff --git a/src/services/services.ts b/src/services/services.ts index ceb05c7420949..7bd2c6e472c72 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1,5 +1,6 @@ import { __String, + ApplicableNonInteractiveRefactorInfo, ApplicableRefactorInfo, ApplyCodeActionCommandResult, AssignmentDeclarationKind, @@ -243,6 +244,7 @@ import { refactor, RefactorContext, RefactorEditInfo, + RefactorName, RefactorTriggerReason, ReferencedSymbol, ReferenceEntry, @@ -2967,23 +2969,26 @@ export function createLanguageService( return SmartSelectionRange.getSmartSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName)); } - function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions, triggerReason: RefactorTriggerReason, kind: string): ApplicableRefactorInfo[] { + function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableNonInteractiveRefactorInfo[]; + function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string, includeInteractive?: boolean): ApplicableRefactorInfo[]; + function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined = emptyOptions, triggerReason?: RefactorTriggerReason, kind?: string, includeInteractive?: boolean): ApplicableRefactorInfo[] { synchronizeHostData(); const file = getValidSourceFile(fileName); - return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences, emptyOptions, triggerReason, kind)); + return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences, emptyOptions, triggerReason, kind), includeInteractive); } function getEditsForRefactor( fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, - refactorName: string, + refactorName: RefactorName, actionName: string, preferences: UserPreferences = emptyOptions, + ...args: unknown[] ): RefactorEditInfo | undefined { synchronizeHostData(); const file = getValidSourceFile(fileName); - return refactor.getEditsForRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), refactorName, actionName); + return refactor.getEditsForRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), refactorName, actionName, ...args); } function toLineColumnOffset(fileName: string, position: number): LineAndCharacter { diff --git a/src/services/types.ts b/src/services/types.ts index 08d983b8691c9..f7245bbd80155 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -630,7 +630,31 @@ export interface LanguageService { /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; - getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string, includeInteractive?: false): ApplicableNonInteractiveRefactorInfo[]; + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string, includeInteractive?: boolean): ApplicableRefactorInfo[]; + + getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: Name, actionName: string, preferences: UserPreferences | undefined, ...args: RefactorArgs): RefactorEditInfo | undefined; + getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: NonInteractiveRefactorName, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; + /** + * @deprecated + * As of TypeScript 5.1, some refactors may require an additional argument. + * If the `isInteractive` property of the `ApplicableRefactorInfo` object is `true`, + * getting edits for the refactor will require a specific argument linked to its name: + * + * ```ts + * if (refactor.isInteractive) { + * if (refactor.name === ts.InteractiveRefactorName.MoveToFile) { + * const action = getAction(refactor); + * const edits = ls.getEditsForRefactor(fileName, formatOptions, positionOrRange, refactor.name, action.name, preferences, { + * targetFile: await askUserForTargetFileName() + * }); + * } + * } else { + * const edits = ls.getEditsForRefactor(...) + * } + * ``` + */ + // eslint-disable-next-line @typescript-eslint/unified-signatures getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(args: OrganizeImportsArgs, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; @@ -911,14 +935,40 @@ export interface InstallPackageAction { /** @internal */ readonly packageName: string; } +export const enum NonInteractiveRefactorName { + AddOrRemoveBracesToArrowFunction = "Add or remove braces in an arrow function", + ConvertArrowFunctionOrFunctionExpression = "Convert arrow function or function expression", + ConvertExport = "Convert export", + ConvertImport = "Convert import", + ConvertOverloadListToSingleSignature = "Convert overload list to single signature", + ConvertParamsToDestructuredObject = "Convert parameters to destructured object", + ConvertStringOrTemplateLiteral = "Convert to template string", + ConvertToOptionalChainExpression = "Convert to optional chain expression", + ExtractSymbol = "Extract Symbol", + ExtractType = "Extract type", + GenerateGetAccessorAndSetAccessor = "Generate 'get' and 'set' accessors", + InferFunctionReturnType = "Infer function return type", + MoveToNewFile = "Move to a new file", +} + +export const enum InteractiveRefactorName { + MoveToFile = "Move to file" +} + +export type RefactorName = NonInteractiveRefactorName | InteractiveRefactorName; + +export type RefactorArgs = { + [InteractiveRefactorName.MoveToFile]: (args: { targetFile: string }) => void; +}[T] extends (...args: infer A) => void ? A : never; + /** * A set of one or more available refactoring actions, grouped under a parent refactoring. */ -export interface ApplicableRefactorInfo { +export interface BaseApplicableRefactorInfo { /** * The programmatic name of the refactoring */ - name: string; + name: RefactorName; /** * A description of this refactoring category to show to the user. * If the refactoring gets inlined (see below), this text will not be visible. @@ -936,6 +986,16 @@ export interface ApplicableRefactorInfo { actions: RefactorActionInfo[]; } +export interface ApplicableNonInteractiveRefactorInfo extends BaseApplicableRefactorInfo { + name: NonInteractiveRefactorName; +} + +export interface ApplicableInteractiveRefactorInfo extends BaseApplicableRefactorInfo { + name: InteractiveRefactorName; +} + +export type ApplicableRefactorInfo = ApplicableNonInteractiveRefactorInfo | ApplicableInteractiveRefactorInfo; + /** * Represents a single refactoring action - for example, the "Extract Method..." refactor might * offer several actions, each corresponding to a surround class or closure to extract into. @@ -1752,18 +1812,37 @@ export interface CodeFixContext extends CodeFixContextBase { } /** @internal */ -export interface Refactor { +export interface RefactorBase { /** List of action kinds a refactor can provide. * Used to skip unnecessary calculation when specific refactors are requested. */ kinds?: string[]; + /** Compute (quickly) which actions are available here */ + getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[]; + + isInteractive?: boolean; +} + +/** @internal */ +export interface InteractiveRefactor extends RefactorBase { + /** Compute the associated code actions */ + getEditsForAction(context: RefactorContext, actionName: string, ...args: RefactorArgs): RefactorEditInfo | undefined; + + /** Indicates that `getEditsForAction` requires additional arguments */ + isInteractive: true; +} + +/** @internal */ +export interface NonInteractiveRefactor extends RefactorBase { /** Compute the associated code actions */ getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined; - /** Compute (quickly) which actions are available here */ - getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[]; + isInteractive?: undefined; } +/** @internal */ +export type Refactor = InteractiveRefactor | NonInteractiveRefactor; + /** @internal */ export interface RefactorContext extends textChanges.TextChangesContext { file: SourceFile; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 4bf70d659b1ff..1b5e38d004d54 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -501,23 +501,24 @@ declare namespace ts { type GetApplicableRefactorsRequestArgs = FileLocationOrRangeRequestArgs & { triggerReason?: RefactorTriggerReason; kind?: string; + includeInteractive?: boolean; }; type RefactorTriggerReason = "implicit" | "invoked"; /** * Response is a list of available refactorings. * Each refactoring exposes one or more "Actions"; a user selects one action to invoke a refactoring */ - interface GetApplicableRefactorsResponse extends Response { - body?: ApplicableRefactorInfo[]; + interface GetApplicableRefactorsResponse extends Response { + body?: TRefactorInfo[]; } /** * A set of one or more available refactoring actions, grouped under a parent refactoring. */ - interface ApplicableRefactorInfo { + interface BaseApplicableRefactorInfo { /** * The programmatic name of the refactoring */ - name: string; + name: RefactorName; /** * A description of this refactoring category to show to the user. * If the refactoring gets inlined (see below), this text will not be visible. @@ -533,6 +534,13 @@ declare namespace ts { inlineable?: boolean; actions: RefactorActionInfo[]; } + interface ApplicableNonInteractiveRefactorInfo extends BaseApplicableRefactorInfo { + name: NonInteractiveRefactorName; + } + interface ApplicableInteractiveRefactorInfo extends BaseApplicableRefactorInfo { + name: InteractiveRefactorName; + } + type ApplicableRefactorInfo = ApplicableNonInteractiveRefactorInfo | ApplicableInteractiveRefactorInfo; /** * Represents a single refactoring action - for example, the "Extract Method..." refactor might * offer several actions, each corresponding to a surround class or closure to extract into. @@ -10117,7 +10125,29 @@ declare namespace ts { applyCodeActionCommand(fileName: string, action: CodeActionCommand[]): Promise; /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; - getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string, includeInteractive?: false): ApplicableNonInteractiveRefactorInfo[]; + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string, includeInteractive?: boolean): ApplicableRefactorInfo[]; + getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: Name, actionName: string, preferences: UserPreferences | undefined, ...args: RefactorArgs): RefactorEditInfo | undefined; + getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: NonInteractiveRefactorName, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; + /** + * @deprecated + * As of TypeScript 5.1, some refactors may require an additional argument. + * If the `isInteractive` property of the `ApplicableRefactorInfo` object is `true`, + * getting edits for the refactor will require a specific argument linked to its name: + * + * ```ts + * if (refactor.isInteractive) { + * if (refactor.name === ts.InteractiveRefactorName.MoveToFile) { + * const action = getAction(refactor); + * const edits = ls.getEditsForRefactor(fileName, formatOptions, positionOrRange, refactor.name, action.name, preferences, { + * targetFile: await askUserForTargetFileName() + * }); + * } + * } else { + * const edits = ls.getEditsForRefactor(...) + * } + * ``` + */ getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(args: OrganizeImportsArgs, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; @@ -10342,14 +10372,38 @@ declare namespace ts { type CodeActionCommand = InstallPackageAction; interface InstallPackageAction { } + enum NonInteractiveRefactorName { + AddOrRemoveBracesToArrowFunction = "Add or remove braces in an arrow function", + ConvertArrowFunctionOrFunctionExpression = "Convert arrow function or function expression", + ConvertExport = "Convert export", + ConvertImport = "Convert import", + ConvertOverloadListToSingleSignature = "Convert overload list to single signature", + ConvertParamsToDestructuredObject = "Convert parameters to destructured object", + ConvertStringOrTemplateLiteral = "Convert to template string", + ConvertToOptionalChainExpression = "Convert to optional chain expression", + ExtractSymbol = "Extract Symbol", + ExtractType = "Extract type", + GenerateGetAccessorAndSetAccessor = "Generate 'get' and 'set' accessors", + InferFunctionReturnType = "Infer function return type", + MoveToNewFile = "Move to a new file" + } + enum InteractiveRefactorName { + MoveToFile = "Move to file" + } + type RefactorName = NonInteractiveRefactorName | InteractiveRefactorName; + type RefactorArgs = { + [InteractiveRefactorName.MoveToFile]: (args: { + targetFile: string; + }) => void; + }[T] extends (...args: infer A) => void ? A : never; /** * A set of one or more available refactoring actions, grouped under a parent refactoring. */ - interface ApplicableRefactorInfo { + interface BaseApplicableRefactorInfo { /** * The programmatic name of the refactoring */ - name: string; + name: RefactorName; /** * A description of this refactoring category to show to the user. * If the refactoring gets inlined (see below), this text will not be visible. @@ -10365,6 +10419,13 @@ declare namespace ts { inlineable?: boolean; actions: RefactorActionInfo[]; } + interface ApplicableNonInteractiveRefactorInfo extends BaseApplicableRefactorInfo { + name: NonInteractiveRefactorName; + } + interface ApplicableInteractiveRefactorInfo extends BaseApplicableRefactorInfo { + name: InteractiveRefactorName; + } + type ApplicableRefactorInfo = ApplicableNonInteractiveRefactorInfo | ApplicableInteractiveRefactorInfo; /** * Represents a single refactoring action - for example, the "Extract Method..." refactor might * offer several actions, each corresponding to a surround class or closure to extract into. diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 6acc0c5bc906e..65b744cffaeec 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -6186,7 +6186,29 @@ declare namespace ts { applyCodeActionCommand(fileName: string, action: CodeActionCommand[]): Promise; /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; - getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string, includeInteractive?: false): ApplicableNonInteractiveRefactorInfo[]; + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string, includeInteractive?: boolean): ApplicableRefactorInfo[]; + getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: Name, actionName: string, preferences: UserPreferences | undefined, ...args: RefactorArgs): RefactorEditInfo | undefined; + getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: NonInteractiveRefactorName, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; + /** + * @deprecated + * As of TypeScript 5.1, some refactors may require an additional argument. + * If the `isInteractive` property of the `ApplicableRefactorInfo` object is `true`, + * getting edits for the refactor will require a specific argument linked to its name: + * + * ```ts + * if (refactor.isInteractive) { + * if (refactor.name === ts.InteractiveRefactorName.MoveToFile) { + * const action = getAction(refactor); + * const edits = ls.getEditsForRefactor(fileName, formatOptions, positionOrRange, refactor.name, action.name, preferences, { + * targetFile: await askUserForTargetFileName() + * }); + * } + * } else { + * const edits = ls.getEditsForRefactor(...) + * } + * ``` + */ getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(args: OrganizeImportsArgs, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; @@ -6411,14 +6433,38 @@ declare namespace ts { type CodeActionCommand = InstallPackageAction; interface InstallPackageAction { } + enum NonInteractiveRefactorName { + AddOrRemoveBracesToArrowFunction = "Add or remove braces in an arrow function", + ConvertArrowFunctionOrFunctionExpression = "Convert arrow function or function expression", + ConvertExport = "Convert export", + ConvertImport = "Convert import", + ConvertOverloadListToSingleSignature = "Convert overload list to single signature", + ConvertParamsToDestructuredObject = "Convert parameters to destructured object", + ConvertStringOrTemplateLiteral = "Convert to template string", + ConvertToOptionalChainExpression = "Convert to optional chain expression", + ExtractSymbol = "Extract Symbol", + ExtractType = "Extract type", + GenerateGetAccessorAndSetAccessor = "Generate 'get' and 'set' accessors", + InferFunctionReturnType = "Infer function return type", + MoveToNewFile = "Move to a new file" + } + enum InteractiveRefactorName { + MoveToFile = "Move to file" + } + type RefactorName = NonInteractiveRefactorName | InteractiveRefactorName; + type RefactorArgs = { + [InteractiveRefactorName.MoveToFile]: (args: { + targetFile: string; + }) => void; + }[T] extends (...args: infer A) => void ? A : never; /** * A set of one or more available refactoring actions, grouped under a parent refactoring. */ - interface ApplicableRefactorInfo { + interface BaseApplicableRefactorInfo { /** * The programmatic name of the refactoring */ - name: string; + name: RefactorName; /** * A description of this refactoring category to show to the user. * If the refactoring gets inlined (see below), this text will not be visible. @@ -6434,6 +6480,13 @@ declare namespace ts { inlineable?: boolean; actions: RefactorActionInfo[]; } + interface ApplicableNonInteractiveRefactorInfo extends BaseApplicableRefactorInfo { + name: NonInteractiveRefactorName; + } + interface ApplicableInteractiveRefactorInfo extends BaseApplicableRefactorInfo { + name: InteractiveRefactorName; + } + type ApplicableRefactorInfo = ApplicableNonInteractiveRefactorInfo | ApplicableInteractiveRefactorInfo; /** * Represents a single refactoring action - for example, the "Extract Method..." refactor might * offer several actions, each corresponding to a surround class or closure to extract into.