diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 527f1737cece0..37f12224c75fa 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -6143,6 +6143,34 @@ "category": "Message", "code": 95148 }, + "Return type must be inferred from a function": { + "category": "Message", + "code": 95149 + }, + "Could not determine function return type": { + "category": "Message", + "code": 95150 + }, + "Could not convert to arrow function": { + "category": "Message", + "code": 95151 + }, + "Could not convert to named function": { + "category": "Message", + "code": 95152 + }, + "Could not convert to anonymous function": { + "category": "Message", + "code": 95153 + }, + "Can only convert string concatenation": { + "category": "Message", + "code": 95154 + }, + "Selection is not a valid statement or statements": { + "category": "Message", + "code": 95155 + }, "No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": { "category": "Error", diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 551ac9aef1889..6355cb15acaba 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3420,6 +3420,12 @@ namespace FourSlash { } } + public verifyRefactorKindsAvailable(kind: string, expected: string[], preferences = ts.emptyOptions) { + const refactors = this.getApplicableRefactorsAtSelection("invoked", kind, preferences); + const availableKinds = ts.flatMap(refactors, refactor => refactor.actions).map(action => action.kind); + assert.deepEqual(availableKinds.sort(), expected.sort(), `Expected kinds to be equal`); + } + public verifyRefactorsAvailable(names: readonly string[]): void { assert.deepEqual(unique(this.getApplicableRefactorsAtSelection(), r => r.name), names); } @@ -3833,14 +3839,14 @@ namespace FourSlash { test(renameKeys(newFileContents, key => pathUpdater(key) || key), "with file moved"); } - private getApplicableRefactorsAtSelection(triggerReason: ts.RefactorTriggerReason = "implicit") { - return this.getApplicableRefactorsWorker(this.getSelection(), this.activeFile.fileName, ts.emptyOptions, triggerReason); + private getApplicableRefactorsAtSelection(triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string, preferences = ts.emptyOptions) { + return this.getApplicableRefactorsWorker(this.getSelection(), this.activeFile.fileName, preferences, triggerReason, kind); } - private getApplicableRefactors(rangeOrMarker: Range | Marker, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason = "implicit"): readonly ts.ApplicableRefactorInfo[] { - return this.getApplicableRefactorsWorker("position" in rangeOrMarker ? rangeOrMarker.position : rangeOrMarker, rangeOrMarker.fileName, preferences, triggerReason); // eslint-disable-line no-in-operator + private getApplicableRefactors(rangeOrMarker: Range | Marker, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string): readonly ts.ApplicableRefactorInfo[] { + return this.getApplicableRefactorsWorker("position" in rangeOrMarker ? rangeOrMarker.position : rangeOrMarker, rangeOrMarker.fileName, preferences, triggerReason, kind); // eslint-disable-line no-in-operator } - private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason): readonly ts.ApplicableRefactorInfo[] { - return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences, triggerReason) || ts.emptyArray; + private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason, kind?: string): readonly ts.ApplicableRefactorInfo[] { + return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences, triggerReason, kind) || ts.emptyArray; } public configurePlugin(pluginName: string, configuration: any): void { diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index 6fd2d834039d2..2e64b2921e8b6 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -215,6 +215,10 @@ namespace FourSlashInterface { this.state.verifyRefactorAvailable(this.negative, triggerReason, name, actionName); } + public refactorKindAvailable(kind: string, expected: string[], preferences = ts.emptyOptions) { + this.state.verifyRefactorKindsAvailable(kind, expected, preferences); + } + public toggleLineComment(newFileContent: string) { this.state.toggleLineComment(newFileContent); } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index ad8926470a5b0..9b406411dfd2d 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -566,7 +566,8 @@ namespace ts.server.protocol { arguments: GetApplicableRefactorsRequestArgs; } export type GetApplicableRefactorsRequestArgs = FileLocationOrRangeRequestArgs & { - triggerReason?: RefactorTriggerReason + triggerReason?: RefactorTriggerReason; + kind?: string; }; export type RefactorTriggerReason = "implicit" | "invoked"; @@ -626,6 +627,11 @@ namespace ts.server.protocol { * the current context. */ notApplicableReason?: string; + + /** + * The hierarchical dotted name of the refactor action. + */ + kind?: string; } export interface GetEditsForRefactorRequest extends Request { diff --git a/src/server/session.ts b/src/server/session.ts index 267b1c44ce3f3..a3fcdc7787e7a 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2129,7 +2129,7 @@ namespace ts.server { private getApplicableRefactors(args: protocol.GetApplicableRefactorsRequestArgs): protocol.ApplicableRefactorInfo[] { const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; - return project.getLanguageService().getApplicableRefactors(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file), args.triggerReason); + return project.getLanguageService().getApplicableRefactors(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file), args.triggerReason, args.kind); } private getEditsForRefactor(args: protocol.GetEditsForRefactorRequestArgs, simplifiedResult: boolean): RefactorEditInfo | protocol.RefactorEditInfo { diff --git a/src/services/codefixes/generateAccessors.ts b/src/services/codefixes/generateAccessors.ts index f2b397cca3ec5..99bd53e29343f 100644 --- a/src/services/codefixes/generateAccessors.ts +++ b/src/services/codefixes/generateAccessors.ts @@ -4,7 +4,8 @@ namespace ts.codefix { type AcceptedNameType = Identifier | StringLiteral; type ContainerDeclaration = ClassLikeDeclaration | ObjectLiteralExpression; - interface Info { + type Info = AccessorInfo | refactor.RefactorErrorInfo; + interface AccessorInfo { readonly container: ContainerDeclaration; readonly isStatic: boolean; readonly isReadonly: boolean; @@ -16,20 +17,12 @@ namespace ts.codefix { readonly renameAccessor: boolean; } - type InfoOrError = { - info: Info, - error?: never - } | { - info?: never, - error: string - }; - export function generateAccessorFromProperty(file: SourceFile, program: Program, start: number, end: number, context: textChanges.TextChangesContext, _actionName: string): FileTextChanges[] | undefined { const fieldInfo = getAccessorConvertiblePropertyAtPosition(file, program, start, end); - if (!fieldInfo || !fieldInfo.info) return undefined; + if (!fieldInfo || refactor.isRefactorErrorInfo(fieldInfo)) return undefined; const changeTracker = textChanges.ChangeTracker.fromContext(context); - const { isStatic, isReadonly, fieldName, accessorName, originalName, type, container, declaration } = fieldInfo.info; + const { isStatic, isReadonly, fieldName, accessorName, originalName, type, container, declaration } = fieldInfo; suppressLeadingAndTrailingTrivia(fieldName); suppressLeadingAndTrailingTrivia(accessorName); @@ -112,7 +105,7 @@ namespace ts.codefix { return modifierFlags; } - export function getAccessorConvertiblePropertyAtPosition(file: SourceFile, program: Program, start: number, end: number, considerEmptySpans = true): InfoOrError | undefined { + export function getAccessorConvertiblePropertyAtPosition(file: SourceFile, program: Program, start: number, end: number, considerEmptySpans = true): Info | undefined { const node = getTokenAtPosition(file, start); const cursorRequest = start === end && considerEmptySpans; const declaration = findAncestor(node.parent, isAcceptedDeclaration); @@ -142,17 +135,15 @@ namespace ts.codefix { const fieldName = createPropertyName(startWithUnderscore ? name : getUniqueName(`_${name}`, file), declaration.name); const accessorName = createPropertyName(startWithUnderscore ? getUniqueName(name.substring(1), file) : name, declaration.name); return { - info: { - isStatic: hasStaticModifier(declaration), - isReadonly: hasEffectiveReadonlyModifier(declaration), - type: getDeclarationType(declaration, program), - container: declaration.kind === SyntaxKind.Parameter ? declaration.parent.parent : declaration.parent, - originalName: (declaration.name).text, - declaration, - fieldName, - accessorName, - renameAccessor: startWithUnderscore - } + isStatic: hasStaticModifier(declaration), + isReadonly: hasEffectiveReadonlyModifier(declaration), + type: getDeclarationType(declaration, program), + container: declaration.kind === SyntaxKind.Parameter ? declaration.parent.parent : declaration.parent, + originalName: (declaration.name).text, + declaration, + fieldName, + accessorName, + renameAccessor: startWithUnderscore }; } diff --git a/src/services/refactorProvider.ts b/src/services/refactorProvider.ts index 42bda231be6f2..ccc6f81d2924d 100644 --- a/src/services/refactorProvider.ts +++ b/src/services/refactorProvider.ts @@ -11,7 +11,9 @@ namespace ts.refactor { export function getApplicableRefactors(context: RefactorContext): ApplicableRefactorInfo[] { return arrayFrom(flatMapIterator(refactors.values(), refactor => - context.cancellationToken && context.cancellationToken.isCancellationRequested() ? undefined : refactor.getAvailableActions(context))); + context.cancellationToken && context.cancellationToken.isCancellationRequested() || + !refactor.kinds?.some(kind => refactorKindBeginsWith(kind, context.kind)) ? undefined : + refactor.getAvailableActions(context))); } export function getEditsForRefactor(context: RefactorContext, refactorName: string, actionName: string): RefactorEditInfo | undefined { diff --git a/src/services/refactors/addOrRemoveBracesToArrowFunction.ts b/src/services/refactors/addOrRemoveBracesToArrowFunction.ts index a2a14152da738..3cd8e7dfb346b 100644 --- a/src/services/refactors/addOrRemoveBracesToArrowFunction.ts +++ b/src/services/refactors/addOrRemoveBracesToArrowFunction.ts @@ -2,45 +2,40 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction { const refactorName = "Add or remove braces in an arrow function"; const refactorDescription = Diagnostics.Add_or_remove_braces_in_an_arrow_function.message; - const addBracesActionName = "Add braces to arrow function"; - const removeBracesActionName = "Remove braces from arrow function"; - const addBracesActionDescription = Diagnostics.Add_braces_to_arrow_function.message; - const removeBracesActionDescription = Diagnostics.Remove_braces_from_arrow_function.message; - registerRefactor(refactorName, { getEditsForAction, getAvailableActions }); - interface Info { + const addBracesAction = { + name: "Add braces to arrow function", + description: Diagnostics.Add_braces_to_arrow_function.message, + kind: "refactor.rewrite.arrow.braces.add", + }; + const removeBracesAction = { + name: "Remove braces from arrow function", + description: Diagnostics.Remove_braces_from_arrow_function.message, + kind: "refactor.rewrite.arrow.braces.remove" + }; + registerRefactor(refactorName, { + kinds: [removeBracesAction.kind], + getEditsForAction, + getAvailableActions }); + + interface FunctionBracesInfo { func: ArrowFunction; expression: Expression | undefined; returnStatement?: ReturnStatement; addBraces: boolean; } - type InfoOrError = { - info: Info, - error?: never - } | { - info?: never, - error: string - }; - function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { const { file, startPosition, triggerReason } = context; const info = getConvertibleArrowFunctionAtPosition(file, startPosition, triggerReason === "invoked"); if (!info) return emptyArray; - if (info.error === undefined) { + if (!isRefactorErrorInfo(info)) { return [{ name: refactorName, description: refactorDescription, actions: [ - info.info.addBraces ? - { - name: addBracesActionName, - description: addBracesActionDescription - } : { - name: removeBracesActionName, - description: removeBracesActionDescription - } + info.addBraces ? addBracesAction : removeBracesAction ] }]; } @@ -49,15 +44,10 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction { return [{ name: refactorName, description: refactorDescription, - actions: [{ - name: addBracesActionName, - description: addBracesActionDescription, - notApplicableReason: info.error - }, { - name: removeBracesActionName, - description: removeBracesActionDescription, - notApplicableReason: info.error - }] + actions: [ + { ...addBracesAction, notApplicableReason: info.error }, + { ...removeBracesAction, notApplicableReason: info.error }, + ] }]; } @@ -67,19 +57,19 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction { function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { const { file, startPosition } = context; const info = getConvertibleArrowFunctionAtPosition(file, startPosition); - if (!info || !info.info) return undefined; + Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info"); - const { expression, returnStatement, func } = info.info; + const { expression, returnStatement, func } = info; let body: ConciseBody; - if (actionName === addBracesActionName) { + if (actionName === addBracesAction.name) { const returnStatement = factory.createReturnStatement(expression); body = factory.createBlock([returnStatement], /* multiLine */ true); suppressLeadingAndTrailingTrivia(body); copyLeadingComments(expression!, returnStatement, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ true); } - else if (actionName === removeBracesActionName && returnStatement) { + else if (actionName === removeBracesAction.name && returnStatement) { const actualExpression = expression || factory.createVoidZero(); body = needsParentheses(actualExpression) ? factory.createParenthesizedExpression(actualExpression) : actualExpression; suppressLeadingAndTrailingTrivia(body); @@ -98,7 +88,7 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction { return { renameFilename: undefined, renameLocation: undefined, edits }; } - function getConvertibleArrowFunctionAtPosition(file: SourceFile, startPosition: number, considerFunctionBodies = true): InfoOrError | undefined { + function getConvertibleArrowFunctionAtPosition(file: SourceFile, startPosition: number, considerFunctionBodies = true, kind?: string): FunctionBracesInfo | RefactorErrorInfo | undefined { const node = getTokenAtPosition(file, startPosition); const func = getContainingFunction(node); @@ -118,26 +108,13 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction { return undefined; } - if (isExpression(func.body)) { - return { - info: { - func, - addBraces: true, - expression: func.body - } - }; + if (refactorKindBeginsWith(addBracesAction.kind, kind) && isExpression(func.body)) { + return { func, addBraces: true, expression: func.body }; } - else if (func.body.statements.length === 1) { + else if (refactorKindBeginsWith(removeBracesAction.kind, kind) && isBlock(func.body) && func.body.statements.length === 1) { const firstStatement = first(func.body.statements); if (isReturnStatement(firstStatement)) { - return { - info: { - func, - addBraces: false, - expression: firstStatement.expression, - returnStatement: firstStatement - } - }; + return { func, addBraces: false, expression: firstStatement.expression, returnStatement: firstStatement }; } } return undefined; diff --git a/src/services/refactors/convertArrowFunctionOrFunctionExpression.ts b/src/services/refactors/convertArrowFunctionOrFunctionExpression.ts index c9ef923534f7d..dc5948c89874a 100644 --- a/src/services/refactors/convertArrowFunctionOrFunctionExpression.ts +++ b/src/services/refactors/convertArrowFunctionOrFunctionExpression.ts @@ -3,15 +3,30 @@ namespace ts.refactor.convertArrowFunctionOrFunctionExpression { const refactorName = "Convert arrow function or function expression"; const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_arrow_function_or_function_expression); - const toAnonymousFunctionActionName = "Convert to anonymous function"; - const toNamedFunctionActionName = "Convert to named function"; - const toArrowFunctionActionName = "Convert to arrow function"; - - const toAnonymousFunctionActionDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_anonymous_function); - const toNamedFunctionActionDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_named_function); - const toArrowFunctionActionDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_arrow_function); - - registerRefactor(refactorName, { getEditsForAction, getAvailableActions }); + const toAnonymousFunctionAction = { + name: "Convert to anonymous function", + description: getLocaleSpecificMessage(Diagnostics.Convert_to_anonymous_function), + kind: "refactor.rewrite.function.anonymous", + }; + const toNamedFunctionAction = { + name: "Convert to named function", + description: getLocaleSpecificMessage(Diagnostics.Convert_to_named_function), + kind: "refactor.rewrite.function.named", + }; + const toArrowFunctionAction = { + name: "Convert to arrow function", + description: getLocaleSpecificMessage(Diagnostics.Convert_to_arrow_function), + kind: "refactor.rewrite.function.arrow", + }; + registerRefactor(refactorName, { + kinds: [ + toAnonymousFunctionAction.kind, + toNamedFunctionAction.kind, + toArrowFunctionAction.kind + ], + getEditsForAction, + getAvailableActions + }); interface FunctionInfo { readonly selectedVariableDeclaration: boolean; @@ -26,38 +41,50 @@ namespace ts.refactor.convertArrowFunctionOrFunctionExpression { } function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { - const { file, startPosition, program } = context; + const { file, startPosition, program, kind } = context; const info = getFunctionInfo(file, startPosition, program); if (!info) return emptyArray; const { selectedVariableDeclaration, func } = info; const possibleActions: RefactorActionInfo[] = []; - - if (selectedVariableDeclaration || (isArrowFunction(func) && isVariableDeclaration(func.parent))) { - possibleActions.push({ - name: toNamedFunctionActionName, - description: toNamedFunctionActionDescription - }); + const errors: RefactorActionInfo[] = []; + if (refactorKindBeginsWith(toNamedFunctionAction.kind, kind)) { + const error = selectedVariableDeclaration || (isArrowFunction(func) && isVariableDeclaration(func.parent)) ? + undefined : getLocaleSpecificMessage(Diagnostics.Could_not_convert_to_named_function); + if (error) { + errors.push({ ...toNamedFunctionAction, notApplicableReason: error }); + } + else { + possibleActions.push(toNamedFunctionAction); + } } - if (!selectedVariableDeclaration && isArrowFunction(func)) { - possibleActions.push({ - name: toAnonymousFunctionActionName, - description: toAnonymousFunctionActionDescription - }); + if (refactorKindBeginsWith(toAnonymousFunctionAction.kind, kind)) { + const error = !selectedVariableDeclaration && isArrowFunction(func) ? + undefined: getLocaleSpecificMessage(Diagnostics.Could_not_convert_to_anonymous_function); + if (error) { + errors.push({ ...toAnonymousFunctionAction, notApplicableReason: error }); + } + else { + possibleActions.push(toAnonymousFunctionAction); + } } - if (isFunctionExpression(func)) { - possibleActions.push({ - name: toArrowFunctionActionName, - description: toArrowFunctionActionDescription - }); + if (refactorKindBeginsWith(toArrowFunctionAction.kind, kind)) { + const error = isFunctionExpression(func) ? undefined : getLocaleSpecificMessage(Diagnostics.Could_not_convert_to_arrow_function); + if (error) { + errors.push({ ...toArrowFunctionAction, notApplicableReason: error }); + } + else { + possibleActions.push(toArrowFunctionAction); + } } return [{ name: refactorName, description: refactorDescription, - actions: possibleActions + actions: possibleActions.length === 0 && context.preferences.provideRefactorNotApplicableReason ? + errors : possibleActions }]; } @@ -70,18 +97,18 @@ namespace ts.refactor.convertArrowFunctionOrFunctionExpression { const edits: FileTextChanges[] = []; switch (actionName) { - case toAnonymousFunctionActionName: + case toAnonymousFunctionAction.name: edits.push(...getEditInfoForConvertToAnonymousFunction(context, func)); break; - case toNamedFunctionActionName: + case toNamedFunctionAction.name: const variableInfo = getVariableInfo(func); if (!variableInfo) return undefined; edits.push(...getEditInfoForConvertToNamedFunction(context, func, variableInfo)); break; - case toArrowFunctionActionName: + case toArrowFunctionAction.name: if (!isFunctionExpression(func)) return undefined; edits.push(...getEditInfoForConvertToArrowFunction(context, func)); break; diff --git a/src/services/refactors/convertExport.ts b/src/services/refactors/convertExport.ts index 6912979261a87..43096a4d08134 100644 --- a/src/services/refactors/convertExport.ts +++ b/src/services/refactors/convertExport.ts @@ -1,54 +1,62 @@ /* @internal */ namespace ts.refactor { const refactorName = "Convert export"; - const actionNameDefaultToNamed = "Convert default export to named export"; - const actionNameNamedToDefault = "Convert named export to default export"; + + const defaultToNamedAction = { + name: "Convert default export to named export", + description: Diagnostics.Convert_default_export_to_named_export.message, + kind: "refactor.rewrite.export.named" + }; + const namedToDefaultAction = { + name: "Convert named export to default export", + description: Diagnostics.Convert_named_export_to_default_export.message, + kind: "refactor.rewrite.export.default" + }; registerRefactor(refactorName, { + kinds: [ + defaultToNamedAction.kind, + namedToDefaultAction.kind + ], getAvailableActions(context): readonly ApplicableRefactorInfo[] { const info = getInfo(context, context.triggerReason === "invoked"); if (!info) return emptyArray; - if (info.error === undefined) { - const description = info.info.wasDefault ? Diagnostics.Convert_default_export_to_named_export.message : Diagnostics.Convert_named_export_to_default_export.message; - const actionName = info.info.wasDefault ? actionNameDefaultToNamed : actionNameNamedToDefault; - return [{ name: refactorName, description, actions: [{ name: actionName, description }] }]; + if (!isRefactorErrorInfo(info)) { + const action = info.wasDefault ? defaultToNamedAction : namedToDefaultAction; + return [{ name: refactorName, description: action.description, actions: [action] }]; } if (context.preferences.provideRefactorNotApplicableReason) { return [ - { name: refactorName, description: Diagnostics.Convert_default_export_to_named_export.message, actions: [{ name: actionNameDefaultToNamed, description: Diagnostics.Convert_default_export_to_named_export.message, notApplicableReason: info.error }] }, - { name: refactorName, description: Diagnostics.Convert_named_export_to_default_export.message, actions: [{ name: actionNameNamedToDefault, description: Diagnostics.Convert_named_export_to_default_export.message, notApplicableReason: info.error }] }, + { name: refactorName, description: Diagnostics.Convert_default_export_to_named_export.message, actions: [ + { ...defaultToNamedAction, notApplicableReason: info.error }, + { ...namedToDefaultAction, notApplicableReason: info.error }, + ]} ]; } return emptyArray; }, getEditsForAction(context, actionName): RefactorEditInfo { - Debug.assert(actionName === actionNameDefaultToNamed || actionName === actionNameNamedToDefault, "Unexpected action name"); - const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, Debug.checkDefined(getInfo(context)?.info, "context must have info"), t, context.cancellationToken)); + Debug.assert(actionName === defaultToNamedAction.name || actionName === namedToDefaultAction.name, "Unexpected action name"); + const info = getInfo(context); + Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info"); + const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, info, t, context.cancellationToken)); return { edits, renameFilename: undefined, renameLocation: undefined }; }, }); // If a VariableStatement, will have exactly one VariableDeclaration, with an Identifier for a name. type ExportToConvert = FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | NamespaceDeclaration | TypeAliasDeclaration | VariableStatement; - interface Info { + interface ExportInfo { readonly exportNode: ExportToConvert; readonly exportName: Identifier; // This is exportNode.name except for VariableStatement_s. readonly wasDefault: boolean; readonly exportingModuleSymbol: Symbol; - } - - type InfoOrError = { - info: Info, - error?: never - } | { - info?: never, - error: string }; - function getInfo(context: RefactorContext, considerPartialSpans = true): InfoOrError | undefined { + function getInfo(context: RefactorContext, considerPartialSpans = true): ExportInfo | RefactorErrorInfo | undefined { const { file } = context; const span = getRefactorContextSpan(context); const token = getTokenAtPosition(file, span.start); @@ -74,7 +82,7 @@ namespace ts.refactor { case SyntaxKind.TypeAliasDeclaration: case SyntaxKind.ModuleDeclaration: { const node = exportNode as FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | TypeAliasDeclaration | NamespaceDeclaration; - return node.name && isIdentifier(node.name) ? { info: { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol } } : undefined; + return node.name && isIdentifier(node.name) ? { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol } : undefined; } case SyntaxKind.VariableStatement: { const vs = exportNode as VariableStatement; @@ -85,19 +93,19 @@ namespace ts.refactor { const decl = first(vs.declarationList.declarations); if (!decl.initializer) return undefined; Debug.assert(!wasDefault, "Can't have a default flag here"); - return isIdentifier(decl.name) ? { info: { exportNode: vs, exportName: decl.name, wasDefault, exportingModuleSymbol } } : undefined; + return isIdentifier(decl.name) ? { exportNode: vs, exportName: decl.name, wasDefault, exportingModuleSymbol } : undefined; } default: return undefined; } } - function doChange(exportingSourceFile: SourceFile, program: Program, info: Info, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void { + function doChange(exportingSourceFile: SourceFile, program: Program, info: ExportInfo, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void { changeExport(exportingSourceFile, info, changes, program.getTypeChecker()); changeImports(program, info, changes, cancellationToken); } - function changeExport(exportingSourceFile: SourceFile, { wasDefault, exportNode, exportName }: Info, changes: textChanges.ChangeTracker, checker: TypeChecker): void { + function changeExport(exportingSourceFile: SourceFile, { wasDefault, exportNode, exportName }: ExportInfo, changes: textChanges.ChangeTracker, checker: TypeChecker): void { if (wasDefault) { changes.delete(exportingSourceFile, Debug.checkDefined(findModifier(exportNode, SyntaxKind.DefaultKeyword), "Should find a default keyword in modifier list")); } @@ -131,7 +139,7 @@ namespace ts.refactor { } } - function changeImports(program: Program, { wasDefault, exportName, exportingModuleSymbol }: Info, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void { + function changeImports(program: Program, { wasDefault, exportName, exportingModuleSymbol }: ExportInfo, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void { const checker = program.getTypeChecker(); const exportSymbol = Debug.checkDefined(checker.getSymbolAtLocation(exportName), "Export name should resolve to a symbol"); FindAllReferences.Core.eachExportReference(program.getSourceFiles(), checker, cancellationToken, exportSymbol, exportingModuleSymbol, exportName.text, wasDefault, ref => { diff --git a/src/services/refactors/convertImport.ts b/src/services/refactors/convertImport.ts index 5dec7fcf7368f..e84cb4ef668c7 100644 --- a/src/services/refactors/convertImport.ts +++ b/src/services/refactors/convertImport.ts @@ -1,46 +1,55 @@ /* @internal */ namespace ts.refactor { const refactorName = "Convert import"; - const actionNameNamespaceToNamed = "Convert namespace import to named imports"; - const actionNameNamedToNamespace = "Convert named imports to namespace import"; - - type NamedImportBindingsOrError = { - info: NamedImportBindings, - error?: never - } | { - info?: never, - error: string + + const namespaceToNamedAction = { + name: "Convert namespace import to named imports", + description: Diagnostics.Convert_namespace_import_to_named_imports.message, + kind: "refactor.rewrite.import.named", + }; + const namedToNamespaceAction = { + name: "Convert named imports to namespace import", + description: Diagnostics.Convert_named_imports_to_namespace_import.message, + kind: "refactor.rewrite.import.namespace", }; registerRefactor(refactorName, { + kinds: [ + namespaceToNamedAction.kind, + namedToNamespaceAction.kind + ], getAvailableActions(context): readonly ApplicableRefactorInfo[] { - const i = getImportToConvert(context, context.triggerReason === "invoked"); - if (!i) return emptyArray; + const info = getImportToConvert(context, context.triggerReason === "invoked"); + if (!info) return emptyArray; - if (i.error === undefined) { - const description = i.info.kind === SyntaxKind.NamespaceImport ? Diagnostics.Convert_namespace_import_to_named_imports.message : Diagnostics.Convert_named_imports_to_namespace_import.message; - const actionName = i.info.kind === SyntaxKind.NamespaceImport ? actionNameNamespaceToNamed : actionNameNamedToNamespace; - return [{ name: refactorName, description, actions: [{ name: actionName, description }] }]; + if (!isRefactorErrorInfo(info)) { + const namespaceImport = info.kind === SyntaxKind.NamespaceImport; + const action = namespaceImport ? namespaceToNamedAction : namedToNamespaceAction; + return [{ name: refactorName, description: action.description, actions: [action] }]; } if (context.preferences.provideRefactorNotApplicableReason) { return [ - { name: refactorName, description: Diagnostics.Convert_namespace_import_to_named_imports.message, actions: [{ name: actionNameNamespaceToNamed, description: Diagnostics.Convert_namespace_import_to_named_imports.message, notApplicableReason: i.error }] }, - { name: refactorName, description: Diagnostics.Convert_named_imports_to_namespace_import.message, actions: [{ name: actionNameNamedToNamespace, description: Diagnostics.Convert_named_imports_to_namespace_import.message, notApplicableReason: i.error }] } + { name: refactorName, description: namespaceToNamedAction.description, + actions: [{ ...namespaceToNamedAction, notApplicableReason: info.error }] }, + { name: refactorName, description: namedToNamespaceAction.description, + actions: [{ ...namedToNamespaceAction, notApplicableReason: info.error }] } ]; } return emptyArray; }, getEditsForAction(context, actionName): RefactorEditInfo { - Debug.assert(actionName === actionNameNamespaceToNamed || actionName === actionNameNamedToNamespace, "Unexpected action name"); - const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, Debug.checkDefined(getImportToConvert(context)?.info, "Context must provide an import to convert"))); + Debug.assert(actionName === namespaceToNamedAction.name || actionName === namedToNamespaceAction.name, "Unexpected action name"); + const info = getImportToConvert(context); + Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info"); + const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, info)); return { edits, renameFilename: undefined, renameLocation: undefined }; } }); // Can convert imports of the form `import * as m from "m";` or `import d, { x, y } from "m";`. - function getImportToConvert(context: RefactorContext, considerPartialSpans = true): NamedImportBindingsOrError | undefined { + function getImportToConvert(context: RefactorContext, considerPartialSpans = true): NamedImportBindings | RefactorErrorInfo | undefined { const { file } = context; const span = getRefactorContextSpan(context); const token = getTokenAtPosition(file, span.start); @@ -57,7 +66,7 @@ namespace ts.refactor { return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_namespace_import_or_named_imports) }; } - return { info: importClause.namedBindings }; + return importClause.namedBindings; } function doChange(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, toConvert: NamedImportBindings): void { diff --git a/src/services/refactors/convertOverloadListToSingleSignature.ts b/src/services/refactors/convertOverloadListToSingleSignature.ts index fbcb6fece0cb4..cd0e4d9cd8ef4 100644 --- a/src/services/refactors/convertOverloadListToSingleSignature.ts +++ b/src/services/refactors/convertOverloadListToSingleSignature.ts @@ -2,8 +2,17 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction { const refactorName = "Convert overload list to single signature"; const refactorDescription = Diagnostics.Convert_overload_list_to_single_signature.message; - registerRefactor(refactorName, { getEditsForAction, getAvailableActions }); + const functionOverloadAction = { + name: refactorName, + description: refactorDescription, + kind: "refactor.rewrite.function.overloadList", + }; + registerRefactor(refactorName, { + kinds: [functionOverloadAction.kind], + getEditsForAction, + getAvailableActions + }); function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { const { file, startPosition, program } = context; @@ -13,10 +22,7 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction { return [{ name: refactorName, description: refactorDescription, - actions: [{ - name: refactorName, - description: refactorDescription - }] + actions: [functionOverloadAction] }]; } diff --git a/src/services/refactors/convertParamsToDestructuredObject.ts b/src/services/refactors/convertParamsToDestructuredObject.ts index ec2277b5da8a7..feef5606b113c 100644 --- a/src/services/refactors/convertParamsToDestructuredObject.ts +++ b/src/services/refactors/convertParamsToDestructuredObject.ts @@ -2,8 +2,18 @@ namespace ts.refactor.convertParamsToDestructuredObject { const refactorName = "Convert parameters to destructured object"; const minimumParameterLength = 2; - registerRefactor(refactorName, { getEditsForAction, getAvailableActions }); - + const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_parameters_to_destructured_object); + + const toDestructuredAction = { + name: refactorName, + description: refactorDescription, + kind: "refactor.rewrite.parameters.toDestructured" + }; + registerRefactor(refactorName, { + kinds: [toDestructuredAction.kind], + getEditsForAction, + getAvailableActions + }); function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { const { file, startPosition } = context; @@ -12,14 +22,10 @@ namespace ts.refactor.convertParamsToDestructuredObject { const functionDeclaration = getFunctionDeclarationAtPosition(file, startPosition, context.program.getTypeChecker()); if (!functionDeclaration) return emptyArray; - const description = getLocaleSpecificMessage(Diagnostics.Convert_parameters_to_destructured_object); return [{ name: refactorName, - description, - actions: [{ - name: refactorName, - description - }] + description: refactorDescription, + actions: [toDestructuredAction] }]; } diff --git a/src/services/refactors/convertStringOrTemplateLiteral.ts b/src/services/refactors/convertStringOrTemplateLiteral.ts index 7641987153d63..4238f96e02c27 100644 --- a/src/services/refactors/convertStringOrTemplateLiteral.ts +++ b/src/services/refactors/convertStringOrTemplateLiteral.ts @@ -3,7 +3,16 @@ namespace ts.refactor.convertStringOrTemplateLiteral { const refactorName = "Convert to template string"; const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_template_string); - registerRefactor(refactorName, { getEditsForAction, getAvailableActions }); + const convertStringAction = { + name: refactorName, + description: refactorDescription, + kind: "refactor.rewrite.string" + }; + registerRefactor(refactorName, { + kinds: [convertStringAction.kind], + getEditsForAction, + getAvailableActions + }); function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { const { file, startPosition } = context; @@ -12,7 +21,13 @@ namespace ts.refactor.convertStringOrTemplateLiteral { const refactorInfo: ApplicableRefactorInfo = { name: refactorName, description: refactorDescription, actions: [] }; if (isBinaryExpression(maybeBinary) && isStringConcatenationValid(maybeBinary)) { - refactorInfo.actions.push({ name: refactorName, description: refactorDescription }); + refactorInfo.actions.push(convertStringAction); + return [refactorInfo]; + } + else if (context.preferences.provideRefactorNotApplicableReason) { + refactorInfo.actions.push({ ...convertStringAction, + notApplicableReason: getLocaleSpecificMessage(Diagnostics.Can_only_convert_string_concatenation) + }); return [refactorInfo]; } return emptyArray; diff --git a/src/services/refactors/convertToOptionalChainExpression.ts b/src/services/refactors/convertToOptionalChainExpression.ts index 52365fc0ba597..8bfb222c8bf0f 100644 --- a/src/services/refactors/convertToOptionalChainExpression.ts +++ b/src/services/refactors/convertToOptionalChainExpression.ts @@ -3,20 +3,26 @@ namespace ts.refactor.convertToOptionalChainExpression { const refactorName = "Convert to optional chain expression"; const convertToOptionalChainExpressionMessage = getLocaleSpecificMessage(Diagnostics.Convert_to_optional_chain_expression); - registerRefactor(refactorName, { getAvailableActions, getEditsForAction }); + const toOptionalChainAction = { + name: refactorName, + description: convertToOptionalChainExpressionMessage, + kind: "refactor.rewrite.expression.optionalChain", + }; + registerRefactor(refactorName, { + kinds: [toOptionalChainAction.kind], + getAvailableActions, + getEditsForAction + }); function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { const info = getInfo(context, context.triggerReason === "invoked"); if (!info) return emptyArray; - if (!info.error) { + if (!isRefactorErrorInfo(info)) { return [{ name: refactorName, description: convertToOptionalChainExpressionMessage, - actions: [{ - name: refactorName, - description: convertToOptionalChainExpressionMessage - }] + actions: [toOptionalChainAction], }]; } @@ -24,11 +30,7 @@ namespace ts.refactor.convertToOptionalChainExpression { return [{ name: refactorName, description: convertToOptionalChainExpressionMessage, - actions: [{ - name: refactorName, - description: convertToOptionalChainExpressionMessage, - notApplicableReason: info.error - }] + actions: [{ ...toOptionalChainAction, notApplicableReason: info.error }], }]; } return emptyArray; @@ -36,24 +38,16 @@ namespace ts.refactor.convertToOptionalChainExpression { function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { const info = getInfo(context); - if (!info || !info.info) return undefined; + Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info"); const edits = textChanges.ChangeTracker.with(context, t => - doChange(context.file, context.program.getTypeChecker(), t, Debug.checkDefined(info.info, "context must have info"), actionName) + doChange(context.file, context.program.getTypeChecker(), t, info, actionName) ); return { edits, renameFilename: undefined, renameLocation: undefined }; } - type InfoOrError = { - info: Info, - error?: never; - } | { - info?: never, - error: string; - }; - type Occurrence = PropertyAccessExpression | ElementAccessExpression | Identifier; - interface Info { + interface OptionalChainInfo { finalExpression: PropertyAccessExpression | ElementAccessExpression | CallExpression, occurrences: Occurrence[], expression: ValidExpression, @@ -83,7 +77,7 @@ namespace ts.refactor.convertToOptionalChainExpression { return isValidExpression(node) || isValidStatement(node); } - function getInfo(context: RefactorContext, considerEmptySpans = true): InfoOrError | undefined { + function getInfo(context: RefactorContext, considerEmptySpans = true): OptionalChainInfo | RefactorErrorInfo | undefined { const { file, program } = context; const span = getRefactorContextSpan(context); @@ -103,7 +97,7 @@ namespace ts.refactor.convertToOptionalChainExpression { return isConditionalExpression(expression) ? getConditionalInfo(expression, checker) : getBinaryInfo(expression); } - function getConditionalInfo(expression: ConditionalExpression, checker: TypeChecker): InfoOrError | undefined { + function getConditionalInfo(expression: ConditionalExpression, checker: TypeChecker): OptionalChainInfo | RefactorErrorInfo | undefined { const condition = expression.condition; const finalExpression = getFinalExpressionInChain(expression.whenTrue); @@ -113,16 +107,16 @@ namespace ts.refactor.convertToOptionalChainExpression { if ((isPropertyAccessExpression(condition) || isIdentifier(condition)) && getMatchingStart(condition, finalExpression.expression)) { - return { info: { finalExpression, occurrences: [condition], expression } }; + return { finalExpression, occurrences: [condition], expression }; } else if (isBinaryExpression(condition)) { const occurrences = getOccurrencesInExpression(finalExpression.expression, condition); - return occurrences ? { info: { finalExpression, occurrences, expression } } : + return occurrences ? { finalExpression, occurrences, expression } : { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_matching_access_expressions) }; } } - function getBinaryInfo(expression: BinaryExpression): InfoOrError | undefined { + function getBinaryInfo(expression: BinaryExpression): OptionalChainInfo | RefactorErrorInfo | undefined { if (expression.operatorToken.kind !== SyntaxKind.AmpersandAmpersandToken) { return { error: getLocaleSpecificMessage(Diagnostics.Can_only_convert_logical_AND_access_chains) }; }; @@ -131,7 +125,7 @@ namespace ts.refactor.convertToOptionalChainExpression { if (!finalExpression) return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_convertible_access_expression) }; const occurrences = getOccurrencesInExpression(finalExpression.expression, expression.left); - return occurrences ? { info: { finalExpression, occurrences, expression } } : + return occurrences ? { finalExpression, occurrences, expression } : { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_matching_access_expressions) }; } @@ -288,7 +282,7 @@ namespace ts.refactor.convertToOptionalChainExpression { return toConvert; } - function doChange(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, info: Info, _actionName: string): void { + function doChange(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, info: OptionalChainInfo, _actionName: string): void { const { finalExpression, occurrences, expression } = info; const firstOccurrence = occurrences[occurrences.length - 1]; const convertedChain = convertOccurrences(checker, finalExpression, occurrences); diff --git a/src/services/refactors/extractSymbol.ts b/src/services/refactors/extractSymbol.ts index 73c8ca01e4cdc..6478ad24d5ba5 100644 --- a/src/services/refactors/extractSymbol.ts +++ b/src/services/refactors/extractSymbol.ts @@ -1,39 +1,56 @@ /* @internal */ namespace ts.refactor.extractSymbol { const refactorName = "Extract Symbol"; - registerRefactor(refactorName, { getAvailableActions, getEditsForAction }); + + const extractConstantAction = { + name: "Extract Constant", + description: getLocaleSpecificMessage(Diagnostics.Extract_constant), + kind: "refactor.extract.constant", + }; + const extractFunctionAction = { + name: "Extract Function", + description: getLocaleSpecificMessage(Diagnostics.Extract_function), + kind: "refactor.extract.function", + }; + registerRefactor(refactorName, { + kinds: [ + extractConstantAction.kind, + extractFunctionAction.kind + ], + getAvailableActions, + getEditsForAction + }); /** * Compute the associated code actions * Exported for tests. */ export function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { + const requestedRefactor = context.kind; const rangeToExtract = getRangeToExtract(context.file, getRefactorContextSpan(context), context.triggerReason === "invoked"); - const targetRange = rangeToExtract.targetRange; + if (targetRange === undefined) { if (!rangeToExtract.errors || rangeToExtract.errors.length === 0 || !context.preferences.provideRefactorNotApplicableReason) { return emptyArray; } - return [{ - name: refactorName, - description: getLocaleSpecificMessage(Diagnostics.Extract_function), - actions: [{ - description: getLocaleSpecificMessage(Diagnostics.Extract_function), - name: "function_extract_error", - notApplicableReason: getStringError(rangeToExtract.errors) - }] - }, - { - name: refactorName, - description: getLocaleSpecificMessage(Diagnostics.Extract_constant), - actions: [{ - description: getLocaleSpecificMessage(Diagnostics.Extract_constant), - name: "constant_extract_error", - notApplicableReason: getStringError(rangeToExtract.errors) - }] - }]; + const errors = []; + if (refactorKindBeginsWith(extractFunctionAction.kind, requestedRefactor)) { + errors.push({ + name: refactorName, + description: extractFunctionAction.description, + actions: [{ ...extractFunctionAction, notApplicableReason: getStringError(rangeToExtract.errors) }] + }); + } + if (refactorKindBeginsWith(extractConstantAction.kind, requestedRefactor)) { + errors.push({ + name: refactorName, + description: extractConstantAction.description, + actions: [{ ...extractConstantAction, notApplicableReason: getStringError(rangeToExtract.errors) }] + }); + } + return errors; } const extractions = getPossibleExtractions(targetRange, context); @@ -53,47 +70,55 @@ namespace ts.refactor.extractSymbol { let i = 0; for (const { functionExtraction, constantExtraction } of extractions) { const description = functionExtraction.description; - if (functionExtraction.errors.length === 0) { - // Don't issue refactorings with duplicated names. - // Scopes come back in "innermost first" order, so extractions will - // preferentially go into nearer scopes - if (!usedFunctionNames.has(description)) { - usedFunctionNames.set(description, true); - functionActions.push({ + if(refactorKindBeginsWith(extractFunctionAction.kind, requestedRefactor)){ + if (functionExtraction.errors.length === 0) { + // Don't issue refactorings with duplicated names. + // Scopes come back in "innermost first" order, so extractions will + // preferentially go into nearer scopes + if (!usedFunctionNames.has(description)) { + usedFunctionNames.set(description, true); + functionActions.push({ + description, + name: `function_scope_${i}`, + kind: extractFunctionAction.kind + }); + } + } + else if (!innermostErrorFunctionAction) { + innermostErrorFunctionAction = { description, - name: `function_scope_${i}` - }); + name: `function_scope_${i}`, + notApplicableReason: getStringError(functionExtraction.errors), + kind: extractFunctionAction.kind + }; } } - else if (!innermostErrorFunctionAction) { - innermostErrorFunctionAction = { - description, - name: `function_scope_${i}`, - notApplicableReason: getStringError(functionExtraction.errors) - }; - } // Skip these since we don't have a way to report errors yet - if (constantExtraction.errors.length === 0) { - // Don't issue refactorings with duplicated names. - // Scopes come back in "innermost first" order, so extractions will - // preferentially go into nearer scopes - const description = constantExtraction.description; - if (!usedConstantNames.has(description)) { - usedConstantNames.set(description, true); - constantActions.push({ + if(refactorKindBeginsWith(extractConstantAction.kind, requestedRefactor)) { + if (constantExtraction.errors.length === 0) { + // Don't issue refactorings with duplicated names. + // Scopes come back in "innermost first" order, so extractions will + // preferentially go into nearer scopes + const description = constantExtraction.description; + if (!usedConstantNames.has(description)) { + usedConstantNames.set(description, true); + constantActions.push({ + description, + name: `constant_scope_${i}`, + kind: extractConstantAction.kind + }); + } + } + else if (!innermostErrorConstantAction) { + innermostErrorConstantAction = { description, - name: `constant_scope_${i}` - }); + name: `constant_scope_${i}`, + notApplicableReason: getStringError(constantExtraction.errors), + kind: extractConstantAction.kind + }; } } - else if (!innermostErrorConstantAction) { - innermostErrorConstantAction = { - description, - name: `constant_scope_${i}`, - notApplicableReason: getStringError(constantExtraction.errors) - }; - } // *do* increment i anyway because we'll look for the i-th scope // later when actually doing the refactoring if the user requests it @@ -106,7 +131,7 @@ namespace ts.refactor.extractSymbol { infos.push({ name: refactorName, description: getLocaleSpecificMessage(Diagnostics.Extract_function), - actions: functionActions + actions: functionActions, }); } else if (context.preferences.provideRefactorNotApplicableReason && innermostErrorFunctionAction) { diff --git a/src/services/refactors/extractType.ts b/src/services/refactors/extractType.ts index 7f528ae245d4f..ab85bb198cc62 100644 --- a/src/services/refactors/extractType.ts +++ b/src/services/refactors/extractType.ts @@ -1,25 +1,39 @@ /* @internal */ namespace ts.refactor { const refactorName = "Extract type"; - const extractToTypeAlias = "Extract to type alias"; - const extractToInterface = "Extract to interface"; - const extractToTypeDef = "Extract to typedef"; + + const extractToTypeAliasAction = { + name: "Extract to type alias", + description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias), + kind: "refactor.extract.type", + }; + const extractToInterfaceAction = { + name: "Extract to interface", + description: getLocaleSpecificMessage(Diagnostics.Extract_to_interface), + kind: "refactor.extract.interface", + }; + const extractToTypeDefAction = { + name: "Extract to typedef", + description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef), + kind: "refactor.extract.typedef" + }; + registerRefactor(refactorName, { + kinds: [ + extractToTypeAliasAction.kind, + extractToInterfaceAction.kind, + extractToTypeDefAction.kind + ], getAvailableActions(context): readonly ApplicableRefactorInfo[] { const info = getRangeToExtract(context, context.triggerReason === "invoked"); if (!info) return emptyArray; - if (info.error === undefined) { + if (!isRefactorErrorInfo(info)) { return [{ name: refactorName, description: getLocaleSpecificMessage(Diagnostics.Extract_type), - actions: info.info.isJS ? [{ - name: extractToTypeDef, description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef) - }] : append([{ - name: extractToTypeAlias, description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias) - }], info.info.typeElements && { - name: extractToInterface, description: getLocaleSpecificMessage(Diagnostics.Extract_to_interface) - }) + actions: info.isJS ? + [extractToTypeDefAction] : append([extractToTypeAliasAction], info.typeElements && extractToInterfaceAction) }]; } @@ -28,9 +42,9 @@ namespace ts.refactor { name: refactorName, description: getLocaleSpecificMessage(Diagnostics.Extract_type), actions: [ - { name: extractToTypeDef, description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef), notApplicableReason: info.error }, - { name: extractToTypeAlias, description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias), notApplicableReason: info.error }, - { name: extractToInterface, description: getLocaleSpecificMessage(Diagnostics.Extract_to_interface), notApplicableReason: info.error }, + { ...extractToTypeDefAction, notApplicableReason: info.error }, + { ...extractToTypeAliasAction, notApplicableReason: info.error }, + { ...extractToInterfaceAction, notApplicableReason: info.error }, ] }]; } @@ -39,18 +53,19 @@ namespace ts.refactor { }, getEditsForAction(context, actionName): RefactorEditInfo { const { file, } = context; - const info = Debug.checkDefined(getRangeToExtract(context)?.info, "Expected to find a range to extract"); + const info = getRangeToExtract(context); + Debug.assert(info && !isRefactorErrorInfo(info), "Expected to find a range to extract"); const name = getUniqueName("NewType", file); const edits = textChanges.ChangeTracker.with(context, changes => { switch (actionName) { - case extractToTypeAlias: + case extractToTypeAliasAction.name: Debug.assert(!info.isJS, "Invalid actionName/JS combo"); return doTypeAliasChange(changes, file, name, info); - case extractToTypeDef: + case extractToTypeDefAction.name: Debug.assert(info.isJS, "Invalid actionName/JS combo"); return doTypedefChange(changes, file, name, info); - case extractToInterface: + case extractToInterfaceAction.name: Debug.assert(!info.isJS && !!info.typeElements, "Invalid actionName/JS combo"); return doInterfaceChange(changes, file, name, info as InterfaceInfo); default: @@ -72,16 +87,9 @@ namespace ts.refactor { isJS: boolean; selection: TypeNode; firstStatement: Statement; typeParameters: readonly TypeParameterDeclaration[]; typeElements: readonly TypeElement[]; } - type Info = TypeAliasInfo | InterfaceInfo; - type InfoOrError = { - info: Info, - error?: never - } | { - info?: never, - error: string - }; + type ExtractInfo = TypeAliasInfo | InterfaceInfo; - function getRangeToExtract(context: RefactorContext, considerEmptySpans = true): InfoOrError | undefined { + function getRangeToExtract(context: RefactorContext, considerEmptySpans = true): ExtractInfo | RefactorErrorInfo | undefined { const { file, startPosition } = context; const isJS = isSourceFileJS(file); const current = getTokenAtPosition(file, startPosition); @@ -98,7 +106,7 @@ namespace ts.refactor { if (!typeParameters) return { error: getLocaleSpecificMessage(Diagnostics.No_type_could_be_extracted_from_this_type_node) }; const typeElements = flattenTypeLiteralNodeReference(checker, selection); - return { info: { isJS, selection, firstStatement, typeParameters, typeElements } }; + return { isJS, selection, firstStatement, typeParameters, typeElements }; } function flattenTypeLiteralNodeReference(checker: TypeChecker, node: TypeNode | undefined): readonly TypeElement[] | undefined { @@ -209,7 +217,7 @@ namespace ts.refactor { changes.replaceNode(file, selection, factory.createTypeReferenceNode(name, typeParameters.map(id => factory.createTypeReferenceNode(id.name, /* typeArguments */ undefined))), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.ExcludeWhitespace }); } - function doTypedefChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: Info) { + function doTypedefChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: ExtractInfo) { const { firstStatement, selection, typeParameters } = info; const node = factory.createJSDocTypedefTag( diff --git a/src/services/refactors/generateGetAccessorAndSetAccessor.ts b/src/services/refactors/generateGetAccessorAndSetAccessor.ts index 160f4f1b5ae73..76de5a7f4eaff 100644 --- a/src/services/refactors/generateGetAccessorAndSetAccessor.ts +++ b/src/services/refactors/generateGetAccessorAndSetAccessor.ts @@ -2,18 +2,25 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor { const actionName = "Generate 'get' and 'set' accessors"; const actionDescription = Diagnostics.Generate_get_and_set_accessors.message; + + const generateGetSetAction = { + name: actionName, + description: actionDescription, + kind: "refactor.rewrite.property.generateAccessors", + }; registerRefactor(actionName, { + kinds: [generateGetSetAction.kind], getEditsForAction(context, actionName) { if (!context.endPosition) return undefined; const info = codefix.getAccessorConvertiblePropertyAtPosition(context.file, context.program, context.startPosition, context.endPosition); - if (!info || !info.info) return undefined; + Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info"); const edits = codefix.generateAccessorFromProperty(context.file, context.program, context.startPosition, context.endPosition, context, actionName); if (!edits) return undefined; const renameFilename = context.file.fileName; - const nameNeedRename = info.info.renameAccessor ? info.info.accessorName : info.info.fieldName; + const nameNeedRename = info.renameAccessor ? info.accessorName : info.fieldName; const renameLocationOffset = isIdentifier(nameNeedRename) ? 0 : -1; - const renameLocation = renameLocationOffset + getRenameLocation(edits, renameFilename, nameNeedRename.text, /*preferLastLocation*/ isParameter(info.info.declaration)); + const renameLocation = renameLocationOffset + getRenameLocation(edits, renameFilename, nameNeedRename.text, /*preferLastLocation*/ isParameter(info.declaration)); return { renameFilename, renameLocation, edits }; }, @@ -22,16 +29,11 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor { const info = codefix.getAccessorConvertiblePropertyAtPosition(context.file, context.program, context.startPosition, context.endPosition, context.triggerReason === "invoked"); if (!info) return emptyArray; - if (!info.error) { + if (!isRefactorErrorInfo(info)) { return [{ name: actionName, description: actionDescription, - actions: [ - { - name: actionName, - description: actionDescription - } - ] + actions: [generateGetSetAction], }]; } @@ -39,11 +41,7 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor { return [{ name: actionName, description: actionDescription, - actions: [{ - name: actionName, - description: actionDescription, - notApplicableReason: info.error - }] + actions: [{ ...generateGetSetAction, notApplicableReason: info.error }], }]; } diff --git a/src/services/refactors/helpers.ts b/src/services/refactors/helpers.ts new file mode 100644 index 0000000000000..59d9c085f2fc9 --- /dev/null +++ b/src/services/refactors/helpers.ts @@ -0,0 +1,25 @@ +/* @internal */ +namespace ts.refactor { + /** + * Returned by refactor funtions when some error message needs to be surfaced to users. + */ + export interface RefactorErrorInfo { + error: string; + }; + + /** + * Checks if some refactor info has refactor error info. + */ + export function isRefactorErrorInfo(info: unknown): info is RefactorErrorInfo { + return (info as RefactorErrorInfo).error !== undefined; + } + + /** + * Checks if string "known" begins with string "requested". + * Used to match requested kinds with a known kind. + */ + export function refactorKindBeginsWith(known: string, requested: string | undefined): boolean { + if(!requested) return true; + return known.substr(0, requested.length) === requested; + } +} \ No newline at end of file diff --git a/src/services/refactors/inferFunctionReturnType.ts b/src/services/refactors/inferFunctionReturnType.ts index 3f46c0edb4ccc..4fad099cad029 100644 --- a/src/services/refactors/inferFunctionReturnType.ts +++ b/src/services/refactors/inferFunctionReturnType.ts @@ -2,11 +2,21 @@ namespace ts.refactor.inferFunctionReturnType { const refactorName = "Infer function return type"; const refactorDescription = Diagnostics.Infer_function_return_type.message; - registerRefactor(refactorName, { getEditsForAction, getAvailableActions }); + + const inferReturnTypeAction = { + name: refactorName, + description: refactorDescription, + kind: "refactor.rewrite.function.returnType" + }; + registerRefactor(refactorName, { + kinds: [inferReturnTypeAction.kind], + getEditsForAction, + getAvailableActions + }); function getEditsForAction(context: RefactorContext): RefactorEditInfo | undefined { const info = getInfo(context); - if (info) { + if (info && !isRefactorErrorInfo(info)) { const edits = textChanges.ChangeTracker.with(context, t => t.tryInsertTypeAnnotation(context.file, info.declaration, info.returnTypeNode)); return { renameFilename: undefined, renameLocation: undefined, edits }; @@ -16,14 +26,19 @@ namespace ts.refactor.inferFunctionReturnType { function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { const info = getInfo(context); - if (info) { + if (!info) return emptyArray; + if (!isRefactorErrorInfo(info)) { return [{ name: refactorName, description: refactorDescription, - actions: [{ - name: refactorName, - description: refactorDescription - }] + actions: [inferReturnTypeAction] + }]; + } + if (context.preferences.provideRefactorNotApplicableReason) { + return [{ + name: refactorName, + description: refactorDescription, + actions: [{ ...inferReturnTypeAction, notApplicableReason: info.error }] }]; } return emptyArray; @@ -35,21 +50,25 @@ namespace ts.refactor.inferFunctionReturnType { | ArrowFunction | MethodDeclaration; - interface Info { + interface FunctionInfo { declaration: ConvertibleDeclaration; returnTypeNode: TypeNode; } - function getInfo(context: RefactorContext): Info | undefined { - if (isInJSFile(context.file)) return; + function getInfo(context: RefactorContext): FunctionInfo | RefactorErrorInfo | undefined { + if (isInJSFile(context.file) || !refactorKindBeginsWith(inferReturnTypeAction.kind, context.kind)) return; const token = getTokenAtPosition(context.file, context.startPosition); const declaration = findAncestor(token, isConvertibleDeclaration); - if (!declaration || !declaration.body || declaration.type) return; + if (!declaration || !declaration.body || declaration.type) { + return { error: getLocaleSpecificMessage(Diagnostics.Return_type_must_be_inferred_from_a_function) }; + } const typeChecker = context.program.getTypeChecker(); const returnType = tryGetReturnType(typeChecker, declaration); - if (!returnType) return; + if (!returnType) { + return { error: getLocaleSpecificMessage(Diagnostics.Could_not_determine_function_return_type) }; + }; const returnTypeNode = typeChecker.typeToTypeNode(returnType, declaration, NodeBuilderFlags.NoTruncation); if (returnTypeNode) { diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index bf5609620e6e1..be93b513244e1 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -1,11 +1,26 @@ /* @internal */ namespace ts.refactor { const refactorName = "Move to a new file"; + const description = getLocaleSpecificMessage(Diagnostics.Move_to_a_new_file); + + const moveToNewFileAction = { + name: refactorName, + description, + kind: "refactor.move.newFile", + }; registerRefactor(refactorName, { + kinds: [moveToNewFileAction.kind], getAvailableActions(context): readonly ApplicableRefactorInfo[] { - if (!context.preferences.allowTextChangesInNewFiles || getStatementsToMove(context) === undefined) return emptyArray; - const description = getLocaleSpecificMessage(Diagnostics.Move_to_a_new_file); - return [{ name: refactorName, description, actions: [{ name: refactorName, description }] }]; + const statements = getStatementsToMove(context); + if (context.preferences.allowTextChangesInNewFiles && statements) { + return [{ name: refactorName, description, actions: [moveToNewFileAction] }]; + } + if (context.preferences.provideRefactorNotApplicableReason) { + return [{ name: refactorName, description, actions: + [{ ...moveToNewFileAction, notApplicableReason: getLocaleSpecificMessage(Diagnostics.Selection_is_not_a_valid_statement_or_statements) }] + }]; + } + return emptyArray; }, getEditsForAction(context, actionName): RefactorEditInfo { Debug.assert(actionName === refactorName, "Wrong refactor invoked"); diff --git a/src/services/services.ts b/src/services/services.ts index fa0e3770d9049..7eedf0a2b85cf 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2451,7 +2451,7 @@ namespace ts { return Rename.getRenameInfo(program, getValidSourceFile(fileName), position, options); } - function getRefactorContext(file: SourceFile, positionOrRange: number | TextRange, preferences: UserPreferences, formatOptions?: FormatCodeSettings, triggerReason?: RefactorTriggerReason): RefactorContext { + function getRefactorContext(file: SourceFile, positionOrRange: number | TextRange, preferences: UserPreferences, formatOptions?: FormatCodeSettings, triggerReason?: RefactorTriggerReason, kind?: string): RefactorContext { const [startPosition, endPosition] = typeof positionOrRange === "number" ? [positionOrRange, undefined] : [positionOrRange.pos, positionOrRange.end]; return { file, @@ -2463,6 +2463,7 @@ namespace ts { cancellationToken, preferences, triggerReason, + kind }; } @@ -2470,10 +2471,10 @@ namespace ts { return SmartSelectionRange.getSmartSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName)); } - function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions, triggerReason: RefactorTriggerReason): ApplicableRefactorInfo[] { + function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions, triggerReason: RefactorTriggerReason, kind: string): ApplicableRefactorInfo[] { synchronizeHostData(); const file = getValidSourceFile(fileName); - return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences, emptyOptions, triggerReason)); + return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences, emptyOptions, triggerReason, kind)); } function getEditsForRefactor( diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 079b8e6ed4f73..0140929340340 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -116,6 +116,7 @@ "refactors/extractSymbol.ts", "refactors/extractType.ts", "refactors/generateGetAccessorAndSetAccessor.ts", + "refactors/helpers.ts", "refactors/moveToNewFile.ts", "refactors/addOrRemoveBracesToArrowFunction.ts", "refactors/convertParamsToDestructuredObject.ts", diff --git a/src/services/types.ts b/src/services/types.ts index 35815ade6f53f..dd97197a92416 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -520,7 +520,7 @@ namespace ts { /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; - getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason): ApplicableRefactorInfo[]; + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; @@ -792,6 +792,11 @@ namespace ts { * the current context. */ notApplicableReason?: string; + + /** + * The hierarchical dotted name of the refactor action. + */ + kind?: string; } /** @@ -1464,6 +1469,10 @@ namespace ts { /** @internal */ export interface Refactor { + /** List of action kinds a refactor can provide. + * Used to skip unnecessary calculation when specific refactors are requested. */ + kinds?: string[]; + /** Compute the associated code actions */ getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined; @@ -1480,5 +1489,6 @@ namespace ts { cancellationToken?: CancellationToken; preferences: UserPreferences; triggerReason?: RefactorTriggerReason; + kind?: string; } } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 984c51fa8542a..f2795baa971fd 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -5570,7 +5570,7 @@ 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): ApplicableRefactorInfo[]; + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; @@ -5796,6 +5796,10 @@ declare namespace ts { * the current context. */ notApplicableReason?: string; + /** + * The hierarchical dotted name of the refactor action. + */ + kind?: string; } /** * A set of edits to make in response to a refactor action, plus an optional @@ -6926,6 +6930,7 @@ declare namespace ts.server.protocol { } type GetApplicableRefactorsRequestArgs = FileLocationOrRangeRequestArgs & { triggerReason?: RefactorTriggerReason; + kind?: string; }; type RefactorTriggerReason = "implicit" | "invoked"; /** @@ -6978,6 +6983,10 @@ declare namespace ts.server.protocol { * the current context. */ notApplicableReason?: string; + /** + * The hierarchical dotted name of the refactor action. + */ + kind?: string; } interface GetEditsForRefactorRequest extends Request { command: CommandTypes.GetEditsForRefactor; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 35116f0ddb732..0f9c37e24b4ce 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -5570,7 +5570,7 @@ 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): ApplicableRefactorInfo[]; + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; @@ -5796,6 +5796,10 @@ declare namespace ts { * the current context. */ notApplicableReason?: string; + /** + * The hierarchical dotted name of the refactor action. + */ + kind?: string; } /** * A set of edits to make in response to a refactor action, plus an optional diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index d2ea70036a4b5..7228fee24b118 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -244,6 +244,7 @@ declare namespace FourSlashInterface { refactorAvailable(name: string, actionName?: string): void; refactorAvailableForTriggerReason(triggerReason: RefactorTriggerReason, name: string, action?: string): void; + refactorKindAvailable(refactorKind: string, expected: string[], preferences?: {}): void; } class verify extends verifyNegatable { assertHasRanges(ranges: Range[]): void; diff --git a/tests/cases/fourslash/refactorKind_extract.ts b/tests/cases/fourslash/refactorKind_extract.ts new file mode 100644 index 0000000000000..2162cb8dd0ea9 --- /dev/null +++ b/tests/cases/fourslash/refactorKind_extract.ts @@ -0,0 +1,16 @@ +/// + +//// const foo: /*a*/string/*b*/ = /*c*/1/*d*/; + +goTo.select("a", "b"); +verify.refactorKindAvailable("refactor.extract", +[ + "refactor.extract.type" +]); + +goTo.select("c", "d"); +verify.refactorKindAvailable("refactor.extract", +[ + "refactor.extract.constant", + "refactor.extract.function" +]); diff --git a/tests/cases/fourslash/refactorKind_generateGetAndSetAccessor.ts b/tests/cases/fourslash/refactorKind_generateGetAndSetAccessor.ts new file mode 100644 index 0000000000000..52799947c9ca2 --- /dev/null +++ b/tests/cases/fourslash/refactorKind_generateGetAndSetAccessor.ts @@ -0,0 +1,11 @@ +/// + +//// class A { +//// /*a*/public a: string;/*b*/ +//// } + +goTo.select("a", "b"); +verify.refactorKindAvailable("refactor.rewrite.property", +[ + "refactor.rewrite.property.generateAccessors" +]); diff --git a/tests/cases/fourslash/refactorKind_moveToNewFile.ts b/tests/cases/fourslash/refactorKind_moveToNewFile.ts new file mode 100644 index 0000000000000..d9b280f565222 --- /dev/null +++ b/tests/cases/fourslash/refactorKind_moveToNewFile.ts @@ -0,0 +1,12 @@ +/// + +//// /*a*/const moveMe = 1;/*b*/ + +goTo.select("a", "b"); +verify.refactorKindAvailable("refactor.move", +[ + "refactor.move.newFile" +], +{ + allowTextChangesInNewFiles: true +}); diff --git a/tests/cases/fourslash/refactorKind_rewriteExport.ts b/tests/cases/fourslash/refactorKind_rewriteExport.ts new file mode 100644 index 0000000000000..90c484209a0c5 --- /dev/null +++ b/tests/cases/fourslash/refactorKind_rewriteExport.ts @@ -0,0 +1,9 @@ +/// + +//// /*a*/export function f() {}/*b*/ + +goTo.select("a", "b"); +verify.refactorKindAvailable("refactor.rewrite.export", +[ + "refactor.rewrite.export.default" +]); diff --git a/tests/cases/fourslash/refactorKind_rewriteFunction.ts b/tests/cases/fourslash/refactorKind_rewriteFunction.ts new file mode 100644 index 0000000000000..56fe42cd9a449 --- /dev/null +++ b/tests/cases/fourslash/refactorKind_rewriteFunction.ts @@ -0,0 +1,12 @@ +/// + +//// const arrow = () /*a*/=>/*b*/ 1; + +goTo.select("a", "b"); +verify.refactorKindAvailable("refactor.rewrite", +[ + "refactor.rewrite.arrow.braces.add", + "refactor.rewrite.function.named", + "refactor.rewrite.function.anonymous", + "refactor.rewrite.function.returnType" +]); diff --git a/tests/cases/fourslash/refactorKind_rewriteFunctionOverloadList.ts b/tests/cases/fourslash/refactorKind_rewriteFunctionOverloadList.ts new file mode 100644 index 0000000000000..068e9830d9da0 --- /dev/null +++ b/tests/cases/fourslash/refactorKind_rewriteFunctionOverloadList.ts @@ -0,0 +1,10 @@ +/// + +//// /*a*/declare function foo(): void; +//// declare function foo(a: string): void;/*b*/ + +goTo.select("a", "b"); +verify.refactorKindAvailable("refactor.rewrite", +[ + "refactor.rewrite.function.overloadList" +]); diff --git a/tests/cases/fourslash/refactorKind_rewriteImport.ts b/tests/cases/fourslash/refactorKind_rewriteImport.ts new file mode 100644 index 0000000000000..7c78d1c3949bb --- /dev/null +++ b/tests/cases/fourslash/refactorKind_rewriteImport.ts @@ -0,0 +1,9 @@ +/// + +//// /*a*/import * as m from "m";/*b*/ + +goTo.select("a", "b"); +verify.refactorKindAvailable("refactor.rewrite", +[ + "refactor.rewrite.import.named" +]); diff --git a/tests/cases/fourslash/refactorKind_rewriteOptionalChain.ts b/tests/cases/fourslash/refactorKind_rewriteOptionalChain.ts new file mode 100644 index 0000000000000..5661056447be8 --- /dev/null +++ b/tests/cases/fourslash/refactorKind_rewriteOptionalChain.ts @@ -0,0 +1,10 @@ +/// + +//// /*a*/foo && foo.bar/*b*/ + +goTo.select("a", "b"); +verify.refactorKindAvailable("refactor.rewrite", +[ + "refactor.rewrite.expression.optionalChain" +]); + diff --git a/tests/cases/fourslash/refactorKind_rewriteParametersToDestructured.ts b/tests/cases/fourslash/refactorKind_rewriteParametersToDestructured.ts new file mode 100644 index 0000000000000..86597ce7b2e8b --- /dev/null +++ b/tests/cases/fourslash/refactorKind_rewriteParametersToDestructured.ts @@ -0,0 +1,9 @@ +/// + +//// function(/*a*/a: number, b: number/*b*/): number {} + +goTo.select("a", "b"); +verify.refactorKindAvailable("refactor.rewrite", +[ + "refactor.rewrite.parameters.toDestructured" +]); diff --git a/tests/cases/fourslash/refactorKind_rewriteString.ts b/tests/cases/fourslash/refactorKind_rewriteString.ts new file mode 100644 index 0000000000000..ed9222b0aa1fc --- /dev/null +++ b/tests/cases/fourslash/refactorKind_rewriteString.ts @@ -0,0 +1,9 @@ +/// + +//// const foo = /*a*/"a" + bar/*b*/; + +goTo.select("a", "b"); +verify.refactorKindAvailable("refactor.rewrite", +[ + "refactor.rewrite.string" +]);