diff --git a/extensions/typescript-language-features/src/features/autoFix.ts b/extensions/typescript-language-features/src/features/autoFix.ts new file mode 100644 index 0000000000000..a10b31cb5931c --- /dev/null +++ b/extensions/typescript-language-features/src/features/autoFix.ts @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import * as Proto from '../protocol'; +import { ITypeScriptServiceClient } from '../typescriptService'; +import API from '../utils/api'; +import { VersionDependentRegistration } from '../utils/dependentRegistration'; +import * as typeConverters from '../utils/typeConverters'; +import { DiagnosticsManager } from './diagnostics'; +import FileConfigurationManager from './fileConfigurationManager'; + +const localize = nls.loadMessageBundle(); + +const autoFixableDiagnosticCodes = new Set([ + 2420, // Incorrectly implemented interface + 2552, // Cannot find name +]); + +class TypeScriptAutoFixProvider implements vscode.CodeActionProvider { + + public static readonly metadata: vscode.CodeActionProviderMetadata = { + providedCodeActionKinds: [vscode.CodeActionKind.SourceAutoFix] + }; + + constructor( + private readonly client: ITypeScriptServiceClient, + private readonly fileConfigurationManager: FileConfigurationManager, + private readonly diagnosticsManager: DiagnosticsManager, + ) { } + + public async provideCodeActions( + document: vscode.TextDocument, + _range: vscode.Range, + context: vscode.CodeActionContext, + token: vscode.CancellationToken + ): Promise { + if (!context.only || !context.only.contains(vscode.CodeActionKind.Source)) { + return undefined; + } + + const file = this.client.toOpenedFilePath(document); + if (!file) { + return undefined; + } + + const autoFixableDiagnostics = this.getAutoFixableDiagnostics(document); + if (!autoFixableDiagnostics.length) { + return undefined; + } + + const fixAllAction = await this.getFixAllCodeAction(document, file, autoFixableDiagnostics, token); + return fixAllAction ? [fixAllAction] : undefined; + } + + private getAutoFixableDiagnostics( + document: vscode.TextDocument + ): vscode.Diagnostic[] { + if (this.client.bufferSyncSupport.hasPendingDiagnostics(document.uri)) { + return []; + } + + return this.diagnosticsManager.getDiagnostics(document.uri) + .filter(x => autoFixableDiagnosticCodes.has(x.code as number)); + } + + private async getFixAllCodeAction( + document: vscode.TextDocument, + file: string, + diagnostics: ReadonlyArray, + token: vscode.CancellationToken, + ): Promise { + await this.fileConfigurationManager.ensureConfigurationForDocument(document, token); + + const autoFixResponse = await this.getAutoFixEdit(file, diagnostics, token); + if (!autoFixResponse) { + return undefined; + } + const { edit, fixedDiagnostics } = autoFixResponse; + const codeAction = new vscode.CodeAction( + localize('autoFix.label', 'Auto fix'), + vscode.CodeActionKind.SourceAutoFix); + codeAction.edit = edit; + codeAction.diagnostics = fixedDiagnostics; + + return codeAction; + } + + private async getAutoFixEdit( + file: string, + diagnostics: ReadonlyArray, + token: vscode.CancellationToken, + ): Promise<{ edit: vscode.WorkspaceEdit, fixedDiagnostics: vscode.Diagnostic[] } | undefined> { + const edit = new vscode.WorkspaceEdit(); + const fixedDiagnostics: vscode.Diagnostic[] = []; + for (const diagnostic of diagnostics) { + const args: Proto.CodeFixRequestArgs = { + ...typeConverters.Range.toFileRangeRequestArgs(file, diagnostic.range), + errorCodes: [+(diagnostic.code!)] + }; + const response = await this.client.execute('getCodeFixes', args, token); + if (response.type !== 'response' || !response.body || response.body.length > 1) { + return undefined; + } + + const fix = response.body[0]; + if (new Set(['fixClassIncorrectlyImplementsInterface', 'spelling']).has(fix.fixName)) { + typeConverters.WorkspaceEdit.withFileCodeEdits(edit, this.client, fix.changes); + fixedDiagnostics.push(diagnostic); + } + } + + if (!fixedDiagnostics.length) { + return undefined; + } + + return { edit, fixedDiagnostics }; + } +} + +export function register( + selector: vscode.DocumentSelector, + client: ITypeScriptServiceClient, + fileConfigurationManager: FileConfigurationManager, + diagnosticsManager: DiagnosticsManager) { + return new VersionDependentRegistration(client, API.v213, () => + vscode.languages.registerCodeActionsProvider(selector, + new TypeScriptAutoFixProvider(client, fileConfigurationManager, diagnosticsManager), + TypeScriptAutoFixProvider.metadata)); +} diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index dc25b87ee7347..beaa7b3b9f1e9 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -68,6 +68,7 @@ export default class LanguageProvider extends Disposable { this._register((await import('./features/jsDocCompletions')).register(selector, this.client)); this._register((await import('./features/organizeImports')).register(selector, this.client, this.commandManager, this.fileConfigurationManager, this.telemetryReporter)); this._register((await import('./features/quickFix')).register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.client.diagnosticsManager, this.telemetryReporter)); + this._register((await import('./features/autoFix')).register(selector, this.client, this.fileConfigurationManager, this.client.diagnosticsManager)); this._register((await import('./features/refactor')).register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.telemetryReporter)); this._register((await import('./features/references')).register(selector, this.client)); this._register((await import('./features/referencesCodeLens')).register(selector, this.description.id, this.client, cachedResponse)); diff --git a/extensions/typescript-language-features/src/typescriptService.ts b/extensions/typescript-language-features/src/typescriptService.ts index eb48a67b99a68..fecd18e10d697 100644 --- a/extensions/typescript-language-features/src/typescriptService.ts +++ b/extensions/typescript-language-features/src/typescriptService.ts @@ -37,7 +37,7 @@ export interface TypeScriptRequestTypes { 'format': [Proto.FormatRequestArgs, Proto.FormatResponse]; 'formatonkey': [Proto.FormatOnKeyRequestArgs, Proto.FormatResponse]; 'getApplicableRefactors': [Proto.GetApplicableRefactorsRequestArgs, Proto.GetApplicableRefactorsResponse]; - 'getCodeFixes': [Proto.CodeFixRequestArgs, Proto.GetCodeFixesResponse]; + 'getCodeFixes': [Proto.CodeFixRequestArgs, Proto.CodeFixResponse]; 'getCombinedCodeFix': [Proto.GetCombinedCodeFixRequestArgs, Proto.GetCombinedCodeFixResponse]; 'getEditsForFileRename': [Proto.GetEditsForFileRenameRequestArgs, Proto.GetEditsForFileRenameResponse]; 'getEditsForRefactor': [Proto.GetEditsForRefactorRequestArgs, Proto.GetEditsForRefactorResponse]; diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index f663112cf4763..63b2ee793a83f 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -838,6 +838,10 @@ const editorConfiguration: IConfigurationNode = { 'source.organizeImports': { 'type': 'boolean', 'description': nls.localize('codeActionsOnSave.organizeImports', "Controls whether organize imports action should be run on file save.") + }, + 'source.autoFix': { + 'type': 'boolean', + 'description': nls.localize('codeActionsOnSave.autoFix', "Controls whether auto fix action should be run on file save.") } }, 'additionalProperties': { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 812a9e33d2556..4fce9d49e5057 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1113,7 +1113,7 @@ declare module 'vscode' { //#region CodeAction.isPreferred - mjbvz export interface CodeAction { /** - * If the action is a prefered action or fix to take. + * If the action is a preferred action or fix to take. * * A quick fix should be marked preferred if it properly addresses the underlying error. * A refactoring should be marked preferred if it is the most reasonable choice of actions to take. @@ -1123,4 +1123,12 @@ declare module 'vscode' { //#endregion + //#region Autofix - mjbvz + export namespace CodeActionKind { + /** + * Base kind for an auto fix source action: `source.autoFix`. + */ + export const SourceAutoFix: CodeActionKind; + } + //#endregion } diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 90c05fd6e1183..3662ff559ee44 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -1022,6 +1022,7 @@ export class CodeActionKind { public static readonly RefactorRewrite = CodeActionKind.Refactor.append('rewrite'); public static readonly Source = CodeActionKind.Empty.append('source'); public static readonly SourceOrganizeImports = CodeActionKind.Source.append('organizeImports'); + public static readonly SourceAutoFix = CodeActionKind.Source.append('autoFix'); constructor( public readonly value: string