Skip to content

Commit 26fa882

Browse files
committed
New functionality to register an action to a diagnostic
1 parent 4dc2d17 commit 26fa882

File tree

6 files changed

+183
-8
lines changed

6 files changed

+183
-8
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/* --------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
* ------------------------------------------------------------------------------------------ */
5+
6+
import * as vscode from 'vscode';
7+
import * as assert from 'assert';
8+
import { getDocUri, activate, runOnActivate } from './helper';
9+
import { toRange } from './util';
10+
11+
suite('Should get code actions', () => {
12+
test('actions.module.missingOptionExplicitCodeAction', async () => {
13+
const docUri = getDocUri('EmptyModule.bas');
14+
const edits = new vscode.WorkspaceEdit();
15+
edits.set(docUri, [
16+
vscode.TextEdit.insert(new vscode.Position(1, 1), "\nOption Explicit")
17+
]);
18+
await testCodeActions(docUri, toRange(2, 1, 2, 1), [
19+
{
20+
title: "Insert Option Explicit",
21+
kind: vscode.CodeActionKind.QuickFix,
22+
edit: edits
23+
}
24+
]);
25+
});
26+
});
27+
28+
async function testCodeActions(docUri: vscode.Uri, activationRange: vscode.Range, expectedResults: vscode.CodeAction[]) {
29+
await activate(docUri);
30+
31+
// Use this method first to ensure the extension is activated.
32+
const actualResults = await runOnActivate(
33+
// Action to run.
34+
() => vscode.commands.executeCommand<vscode.CodeAction[]>(
35+
'vscode.executeCodeActionProvider',
36+
docUri,
37+
activationRange
38+
),
39+
// Test that shows it ran.
40+
(result) => result.length > 0
41+
);
42+
43+
assert.equal(actualResults.length, expectedResults.length, `Expected ${expectedResults.length}, got ${actualResults.length}`);
44+
45+
expectedResults.forEach((expected, i) => {
46+
const actual = actualResults[i];
47+
assert.equal(actual.title, expected.title, `Title: expected ${expected.title}, got ${actual.title}`);
48+
assert.equal(actual.edit.has(docUri), true, "Missing actual edits");
49+
assert.equal(expected.edit.has(docUri), true, "Missing expected edits");
50+
51+
const actEdits = actual.edit.get(docUri);
52+
const expEdits = expected.edit.get(docUri);
53+
54+
assert.equal(actEdits.length, expEdits.length, `Count edits for ${actual.title}`);
55+
expEdits.forEach((expEdit, i) => {
56+
const actEdit = actEdits[i];
57+
assert.equal(actEdit.newText, expEdit.newText, `Edit text: expected ${expEdit.newText}, got ${actEdit.newText}`);
58+
assert.deepEqual(actEdit.range, expEdit.range, `Edit range: expected ${JSON.stringify(expEdit.range)}, got ${JSON.stringify(actEdit.range)}`);
59+
});
60+
});
61+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { CodeAction, Command, Diagnostic } from "vscode-languageserver";
2+
import { BaseDiagnostic } from "./diagnostics";
3+
import { Services } from "../injection/services";
4+
5+
/**
6+
* This class is not designed to be referenced or instantiated directly.
7+
* Please use Services to get its singleton instance.
8+
*/
9+
export class CodeActionsRegistry {
10+
actionFactory: Map<string | number, (diagnostic: Diagnostic, uri: string) => Command | CodeAction | undefined> = new Map();
11+
private logger = Services.logger;
12+
13+
getDiagnosticAction(diagnostic: Diagnostic, uri: string): Command | CodeAction | undefined {
14+
if (diagnostic.code && this.actionFactory.has(diagnostic.code)) {
15+
this.logger.debug(`Getting code action for ${diagnostic.code}`, 1);
16+
return this.actionFactory.get(diagnostic.code)!(diagnostic, uri);
17+
}
18+
}
19+
20+
/**
21+
* Allows a diagnostic to lazily register its action factory when it is
22+
* instantiated. This means code actions for diagnostics are managed on
23+
* the diagnostic itself without any additional wiring up.
24+
*/
25+
registerDiagnosticAction(diagnostic: BaseDiagnostic): void {
26+
// A diagnostic must have a code and a factory and must not already be registered.
27+
if (diagnostic.code && diagnostic.actionFactory && !this.actionFactory.has(diagnostic.code)) {
28+
this.logger.debug(`Registering code action for ${diagnostic.code}`, 1);
29+
this.actionFactory.set(diagnostic.code, diagnostic.actionFactory);
30+
}
31+
}
32+
}
33+
34+
// Example params that are related to a diagnostic.
35+
const onCodeActionParams = {
36+
"textDocument":{
37+
"uri":"file:///c%3A/Repos/vba-LanguageServer/sample/b.bas"
38+
},
39+
"range":{"start":{"line":4,"character":1},"end":{"line":4,"character":1}},
40+
"context":{
41+
"diagnostics":[{
42+
"range":{"start":{"line":4,"character":1},"end":{"line":4,"character":1}},
43+
"message":"Option Explicit is missing from module header.",
44+
"data":{"uri":"file:///c%3A/Repos/vba-LanguageServer/sample/b.bas"},
45+
"code":"W001",
46+
"severity":2
47+
}],
48+
"only":["quickfix"],
49+
"triggerKind":1
50+
}
51+
};

server/src/capabilities/diagnostics.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Core
2-
import { CodeDescription, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, Position, Range } from 'vscode-languageserver';
2+
import { CodeAction, CodeActionKind, CodeDescription, Command, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, Position, Range } from 'vscode-languageserver';
3+
import { Services } from '../injection/services';
34

45

56
export type DiagnosticConstructor<T extends BaseDiagnostic> =
@@ -9,12 +10,13 @@ export type DiagnosticConstructor<T extends BaseDiagnostic> =
910
export abstract class BaseDiagnostic implements Diagnostic {
1011
range: Range;
1112
message: string;
12-
severity?: DiagnosticSeverity | undefined;
13-
code?: string | number | undefined;
14-
codeDescription?: CodeDescription | undefined;
15-
source?: string | undefined;
16-
tags?: DiagnosticTag[] | undefined;
17-
relatedInformation?: DiagnosticRelatedInformation[] | undefined;
13+
severity?: DiagnosticSeverity;
14+
code?: string | number;
15+
actionFactory?: (diagnostic: Diagnostic, uri: string) => CodeAction | Command;
16+
codeDescription?: CodeDescription;
17+
source?: string;
18+
tags?: DiagnosticTag[];
19+
relatedInformation?: DiagnosticRelatedInformation[];
1820
data?: unknown;
1921

2022
constructor(range: Range)
@@ -151,6 +153,21 @@ export class MissingOptionExplicitDiagnostic extends BaseDiagnostic {
151153
severity = DiagnosticSeverity.Warning;
152154
constructor(range: Range) {
153155
super(range);
156+
157+
// Set up the properties that will enable the action.
158+
this.code = 'W001';
159+
this.actionFactory = (diagnostic: Diagnostic, uri: string) =>
160+
CodeAction.create(
161+
"Insert Option Explicit",
162+
{ changes: { [uri]: [{
163+
range: diagnostic.range,
164+
newText: "\nOption Explicit"
165+
}]}},
166+
CodeActionKind.QuickFix
167+
);
168+
169+
// Register the action factory to enable onActionRequest.
170+
Services.codeActionsRegistry.registerDiagnosticAction(this);
154171
}
155172
}
156173

server/src/injection/services.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { Logger, IWorkspace, ILanguageServer } from './interface';
33
import { LspLogger } from '../utils/logger';
44
import { _Connection, createConnection, ProposedFeatures } from 'vscode-languageserver/node';
55
import { ScopeItemCapability } from '../capabilities/capabilities';
6+
import { CodeActionsRegistry } from '../capabilities/codeActions';
67

78

89
export class Services {
910
static registerServices(): void {
1011
container.registerSingleton("ILogger", LspLogger);
1112
container.registerInstance("_Connection", createConnection(ProposedFeatures.all));
13+
container.registerSingleton("CodeActions", CodeActionsRegistry);
1214
}
1315

1416
static registerProjectScope(scope: ScopeItemCapability): void {
@@ -35,6 +37,10 @@ export class Services {
3537
return container.resolve("ILanguageServer");
3638
}
3739

40+
static get codeActionsRegistry(): CodeActionsRegistry {
41+
return container.resolve("CodeActions");
42+
}
43+
3844
static get projectScope(): ScopeItemCapability {
3945
return container.resolve("ProjectScope");
4046
}

server/src/project/workspace.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { TextDocument } from 'vscode-languageserver-textdocument';
33
import {
44
CancellationToken,
55
CancellationTokenSource,
6+
CodeAction,
7+
CodeActionParams,
8+
Command,
69
CompletionItem,
710
CompletionParams,
811
DidChangeConfigurationNotification,
@@ -277,6 +280,7 @@ class WorkspaceEvents {
277280
connection.onHover(params => this.onHover(params));
278281
connection.onDocumentFormatting(async (params, token) => await this.onDocumentFormatting(params, token));
279282
connection.onDidCloseTextDocument(params => { Services.logger.debug('[event] onDidCloseTextDocument'); Services.logger.debug(JSON.stringify(params), 1); });
283+
connection.onCodeAction((params, token) => this.onCodeActionRequest(params, token));
280284

281285
if (hasWorkspaceConfigurationCapability(Services.server)) {
282286
connection.onFoldingRanges(async (params, token) => await cancellableOnFoldingRanges(params, token));
@@ -393,6 +397,42 @@ class WorkspaceEvents {
393397

394398
}
395399

400+
private async onCodeActionRequest(params: CodeActionParams, token: CancellationToken): Promise<(Command | CodeAction)[] | null | undefined> {
401+
const logger = Services.logger;
402+
logger.debug(`[event] onCodeAction: ${JSON.stringify(params)}`);
403+
404+
// For now, if we have no diagnostics then don't return any actions.
405+
if (params.context.diagnostics.length === 0) {
406+
return [];
407+
}
408+
409+
if (token.isCancellationRequested) {
410+
logger.debug(`[cbs] onCodeAction`);
411+
return;
412+
}
413+
414+
try {
415+
// We don't actually need the document but await to ensure it's parsed.
416+
await this.getParsedProjectDocument(params.textDocument.uri, 0, token);
417+
const uri = params.textDocument.uri;
418+
const result: (Command | CodeAction)[] = [];
419+
const codeActionRegistry = Services.codeActionsRegistry;
420+
params.context.diagnostics.forEach(d => {
421+
const action = codeActionRegistry.getDiagnosticAction(d, uri);
422+
if (action) {
423+
result.push(action);
424+
}
425+
});
426+
return result;
427+
} catch (e) {
428+
// If cancelled or something went wrong, just return.
429+
if (e instanceof Error) {
430+
logger.stack(e);
431+
}
432+
return;
433+
}
434+
}
435+
396436
/** Documents event handlers */
397437

398438
/**

server/src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export class LanguageServerConfiguration {
8282
// },
8383

8484
// Implement soon.
85-
codeActionProvider: false,
85+
codeActionProvider: true,
8686
completionProvider: undefined,
8787
hoverProvider: false,
8888

0 commit comments

Comments
 (0)