diff --git a/news/1 Enhancements/10245.md b/news/1 Enhancements/10245.md new file mode 100644 index 000000000000..e59965b96e63 --- /dev/null +++ b/news/1 Enhancements/10245.md @@ -0,0 +1 @@ +Show quickfixes for launch.json diff --git a/src/client/extension.ts b/src/client/extension.ts index 03e6987da962..38f491259d21 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -95,7 +95,7 @@ import { IServiceContainer, IServiceManager } from './ioc/types'; import { getLanguageConfiguration } from './language/languageConfiguration'; import { LinterCommands } from './linters/linterCommands'; import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; -import { PythonCodeActionProvider } from './providers/codeActionsProvider'; +import { PythonCodeActionProvider } from './providers/codeActionProvider/pythonCodeActionProvider'; import { PythonFormattingEditProvider } from './providers/formatProvider'; import { ReplProvider } from './providers/replProvider'; import { registerTypes as providersRegisterTypes } from './providers/serviceRegistry'; diff --git a/src/client/providers/codeActionProvider/launchJsonCodeActionProvider.ts b/src/client/providers/codeActionProvider/launchJsonCodeActionProvider.ts new file mode 100644 index 000000000000..e3cb8f90ec60 --- /dev/null +++ b/src/client/providers/codeActionProvider/launchJsonCodeActionProvider.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CodeAction, + CodeActionContext, + CodeActionKind, + CodeActionProvider, + Diagnostic, + Range, + TextDocument, + WorkspaceEdit +} from 'vscode'; + +/** + * Provides code actions for launch.json + */ +export class LaunchJsonCodeActionProvider implements CodeActionProvider { + public provideCodeActions(document: TextDocument, _: Range, context: CodeActionContext): CodeAction[] { + return context.diagnostics + .filter(diagnostic => diagnostic.message === 'Incorrect type. Expected "string".') + .map(diagnostic => this.createFix(document, diagnostic)); + } + + private createFix(document: TextDocument, diagnostic: Diagnostic): CodeAction { + const finalText = `"${document.getText(diagnostic.range)}"`; + const fix = new CodeAction(`Convert to ${finalText}`, CodeActionKind.QuickFix); + fix.edit = new WorkspaceEdit(); + fix.edit.replace(document.uri, diagnostic.range, finalText); + return fix; + } +} diff --git a/src/client/providers/codeActionProvider/main.ts b/src/client/providers/codeActionProvider/main.ts new file mode 100644 index 000000000000..375c9986d949 --- /dev/null +++ b/src/client/providers/codeActionProvider/main.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as vscodeTypes from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IDisposableRegistry } from '../../common/types'; +import { LaunchJsonCodeActionProvider } from './launchJsonCodeActionProvider'; + +@injectable() +export class CodeActionProviderService implements IExtensionSingleActivationService { + constructor(@inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry) {} + public async activate(): Promise { + // tslint:disable-next-line:no-require-imports + const vscode = require('vscode') as typeof vscodeTypes; + const documentSelector: vscodeTypes.DocumentFilter = { + scheme: 'file', + language: 'jsonc', + pattern: '**/launch.json' + }; + this.disposableRegistry.push( + vscode.languages.registerCodeActionsProvider(documentSelector, new LaunchJsonCodeActionProvider(), { + providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] + }) + ); + } +} diff --git a/src/client/providers/codeActionsProvider.ts b/src/client/providers/codeActionProvider/pythonCodeActionProvider.ts similarity index 100% rename from src/client/providers/codeActionsProvider.ts rename to src/client/providers/codeActionProvider/pythonCodeActionProvider.ts diff --git a/src/client/providers/serviceRegistry.ts b/src/client/providers/serviceRegistry.ts index 7418e0175e51..66640455f08a 100644 --- a/src/client/providers/serviceRegistry.ts +++ b/src/client/providers/serviceRegistry.ts @@ -3,10 +3,16 @@ 'use strict'; +import { IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; +import { CodeActionProviderService } from './codeActionProvider/main'; import { SortImportsEditingProvider } from './importSortProvider'; import { ISortImportsEditingProvider } from './types'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(ISortImportsEditingProvider, SortImportsEditingProvider); + serviceManager.addSingleton( + IExtensionSingleActivationService, + CodeActionProviderService + ); } diff --git a/src/test/mocks/vsc/index.ts b/src/test/mocks/vsc/index.ts index 0f86994533fe..b309c0a55e58 100644 --- a/src/test/mocks/vsc/index.ts +++ b/src/test/mocks/vsc/index.ts @@ -76,6 +76,19 @@ export namespace vscMock { } } + export class CodeAction { + public title: string; + public edit?: vscode.WorkspaceEdit; + public diagnostics?: vscode.Diagnostic[]; + public command?: vscode.Command; + public kind?: CodeActionKind; + public isPreferred?: boolean; + constructor(_title: string, _kind?: CodeActionKind) { + this.title = _title; + this.kind = _kind; + } + } + export enum CompletionItemKind { Text = 0, Method = 1, diff --git a/src/test/providers/codeActionProvider/launchJsonCodeActionProvider.unit.test.ts b/src/test/providers/codeActionProvider/launchJsonCodeActionProvider.unit.test.ts new file mode 100644 index 000000000000..ceae05d4bbd0 --- /dev/null +++ b/src/test/providers/codeActionProvider/launchJsonCodeActionProvider.unit.test.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { CodeActionContext, CodeActionKind, Diagnostic, Range, TextDocument, Uri } from 'vscode'; +import { LaunchJsonCodeActionProvider } from '../../../client/providers/codeActionProvider/launchJsonCodeActionProvider'; + +suite('LaunchJson CodeAction Provider', () => { + const documentUri = Uri.parse('a'); + let document: TypeMoq.IMock; + let range: TypeMoq.IMock; + let context: TypeMoq.IMock; + let diagnostic: TypeMoq.IMock; + let codeActionsProvider: LaunchJsonCodeActionProvider; + + setup(() => { + codeActionsProvider = new LaunchJsonCodeActionProvider(); + document = TypeMoq.Mock.ofType(); + range = TypeMoq.Mock.ofType(); + context = TypeMoq.Mock.ofType(); + diagnostic = TypeMoq.Mock.ofType(); + document.setup(d => d.getText(TypeMoq.It.isAny())).returns(() => 'Diagnostic text'); + document.setup(d => d.uri).returns(() => documentUri); + context.setup(c => c.diagnostics).returns(() => [diagnostic.object]); + }); + + test('Ensure correct code action is returned if diagnostic message equals `Incorrect type. Expected "string".`', async () => { + diagnostic.setup(d => d.message).returns(() => 'Incorrect type. Expected "string".'); + diagnostic.setup(d => d.range).returns(() => new Range(2, 0, 7, 8)); + + const codeActions = codeActionsProvider.provideCodeActions(document.object, range.object, context.object); + + // Now ensure that the code action object is as expected + expect(codeActions).to.have.length(1); + expect(codeActions[0].kind).to.eq(CodeActionKind.QuickFix); + expect(codeActions[0].title).to.equal('Convert to "Diagnostic text"'); + + // Ensure the correct TextEdit is provided + const entries = codeActions[0].edit!.entries(); + // Edits the correct document is edited + assert.deepEqual(entries[0][0], documentUri); + const edit = entries[0][1][0]; + // Final text is as expected + expect(edit.newText).to.equal('"Diagnostic text"'); + // Text edit range is as expected + expect(edit.range.isEqual(new Range(2, 0, 7, 8))).to.equal(true, 'Text edit range not as expected'); + }); + + test('Ensure no code action is returned if diagnostic message does not equal `Incorrect type. Expected "string".`', async () => { + diagnostic.setup(d => d.message).returns(() => 'Random diagnostic message'); + + const codeActions = codeActionsProvider.provideCodeActions(document.object, range.object, context.object); + + expect(codeActions).to.have.length(0); + }); +}); diff --git a/src/test/providers/codeActionProvider/main.unit.test.ts b/src/test/providers/codeActionProvider/main.unit.test.ts new file mode 100644 index 000000000000..a29b78fcd4fb --- /dev/null +++ b/src/test/providers/codeActionProvider/main.unit.test.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable: match-default-export-name +import { assert, expect } from 'chai'; +import rewiremock from 'rewiremock'; +import * as typemoq from 'typemoq'; +import { CodeActionProvider, CodeActionProviderMetadata, DocumentSelector } from 'vscode'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { LaunchJsonCodeActionProvider } from '../../../client/providers/codeActionProvider/launchJsonCodeActionProvider'; +import { CodeActionProviderService } from '../../../client/providers/codeActionProvider/main'; + +suite('Code Action Provider service', async () => { + setup(() => { + rewiremock.disable(); + }); + test('Code actions are registered correctly', async () => { + let selector: DocumentSelector; + let provider: CodeActionProvider; + let metadata: CodeActionProviderMetadata; + const vscodeMock = { + languages: { + registerCodeActionsProvider: ( + _selector: DocumentSelector, + _provider: CodeActionProvider, + _metadata: CodeActionProviderMetadata + ) => { + selector = _selector; + provider = _provider; + metadata = _metadata; + } + }, + CodeActionKind: { + QuickFix: 'CodeAction' + } + }; + rewiremock.enable(); + rewiremock('vscode').with(vscodeMock); + const quickFixService = new CodeActionProviderService(typemoq.Mock.ofType().object); + + await quickFixService.activate(); + + // Ensure QuickFixLaunchJson is registered with correct arguments + assert.deepEqual(selector!, { + scheme: 'file', + language: 'jsonc', + pattern: '**/launch.json' + }); + assert.deepEqual(metadata!, { + // tslint:disable-next-line:no-any + providedCodeActionKinds: ['CodeAction' as any] + }); + expect(provider!).instanceOf(LaunchJsonCodeActionProvider); + }); +}); diff --git a/src/test/providers/codeActionsProvider.test.ts b/src/test/providers/codeActionProvider/pythonCodeActionsProvider.unit.test.ts similarity index 90% rename from src/test/providers/codeActionsProvider.test.ts rename to src/test/providers/codeActionProvider/pythonCodeActionsProvider.unit.test.ts index 8147063d649e..454dd2bc27ad 100644 --- a/src/test/providers/codeActionsProvider.test.ts +++ b/src/test/providers/codeActionProvider/pythonCodeActionsProvider.unit.test.ts @@ -6,9 +6,9 @@ import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; import { CancellationToken, CodeActionContext, CodeActionKind, Range, TextDocument } from 'vscode'; -import { PythonCodeActionProvider } from '../../client/providers/codeActionsProvider'; +import { PythonCodeActionProvider } from '../../../client/providers/codeActionProvider/pythonCodeActionProvider'; -suite('CodeAction Provider', () => { +suite('Python CodeAction Provider', () => { let codeActionsProvider: PythonCodeActionProvider; let document: TypeMoq.IMock; let range: TypeMoq.IMock; diff --git a/src/test/providers/serviceRegistry.unit.test.ts b/src/test/providers/serviceRegistry.unit.test.ts index d7e29720e3ff..fe4da7a03b12 100644 --- a/src/test/providers/serviceRegistry.unit.test.ts +++ b/src/test/providers/serviceRegistry.unit.test.ts @@ -4,8 +4,10 @@ 'use strict'; import { instance, mock, verify } from 'ts-mockito'; +import { IExtensionSingleActivationService } from '../../client/activation/types'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceManager } from '../../client/ioc/types'; +import { CodeActionProviderService } from '../../client/providers/codeActionProvider/main'; import { SortImportsEditingProvider } from '../../client/providers/importSortProvider'; import { registerTypes } from '../../client/providers/serviceRegistry'; import { ISortImportsEditingProvider } from '../../client/providers/types'; @@ -25,5 +27,11 @@ suite('Common Providers Service Registry', () => { SortImportsEditingProvider ) ).once(); + verify( + serviceManager.addSingleton( + IExtensionSingleActivationService, + CodeActionProviderService + ) + ).once(); }); }); diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index f3105a9bd1fd..0712ce3810c6 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -50,6 +50,7 @@ export function initialize() { } mockedVSCode.Disposable = vscodeMocks.vscMock.Disposable as any; +mockedVSCode.CodeAction = vscodeMocks.vscMock.CodeAction; mockedVSCode.EventEmitter = vscodeMocks.vscMock.EventEmitter; mockedVSCode.CancellationTokenSource = vscodeMocks.vscMock.CancellationTokenSource; mockedVSCode.CompletionItemKind = vscodeMocks.vscMock.CompletionItemKind;