Skip to content

Show quickfixes for launch.json #10245

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/1 Enhancements/10245.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Show quickfixes for launch.json
2 changes: 1 addition & 1 deletion src/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
27 changes: 27 additions & 0 deletions src/client/providers/codeActionProvider/main.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
// 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]
})
);
}
}
6 changes: 6 additions & 0 deletions src/client/providers/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>(ISortImportsEditingProvider, SortImportsEditingProvider);
serviceManager.addSingleton<IExtensionSingleActivationService>(
IExtensionSingleActivationService,
CodeActionProviderService
);
}
13 changes: 13 additions & 0 deletions src/test/mocks/vsc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TextDocument>;
let range: TypeMoq.IMock<Range>;
let context: TypeMoq.IMock<CodeActionContext>;
let diagnostic: TypeMoq.IMock<Diagnostic>;
let codeActionsProvider: LaunchJsonCodeActionProvider;

setup(() => {
codeActionsProvider = new LaunchJsonCodeActionProvider();
document = TypeMoq.Mock.ofType<TextDocument>();
range = TypeMoq.Mock.ofType<Range>();
context = TypeMoq.Mock.ofType<CodeActionContext>();
diagnostic = TypeMoq.Mock.ofType<Diagnostic>();
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);
});
});
57 changes: 57 additions & 0 deletions src/test/providers/codeActionProvider/main.unit.test.ts
Original file line number Diff line number Diff line change
@@ -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<IDisposableRegistry>().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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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<TextDocument>;
let range: TypeMoq.IMock<Range>;
Expand Down
8 changes: 8 additions & 0 deletions src/test/providers/serviceRegistry.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,5 +27,11 @@ suite('Common Providers Service Registry', () => {
SortImportsEditingProvider
)
).once();
verify(
serviceManager.addSingleton<IExtensionSingleActivationService>(
IExtensionSingleActivationService,
CodeActionProviderService
)
).once();
});
});
1 change: 1 addition & 0 deletions src/test/vscode-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down