Skip to content

Commit 2194de0

Browse files
render inspections as codelens/guttericons/diagnostics/rulerhighlights… (#1318)
* render inspections as codelens/guttericons/diagnostics/rulerhighlights... * allow user to configure inspection display types via settings. * resolve comments. * remove unused property
1 parent bf75ef0 commit 2194de0

File tree

12 files changed

+453
-10
lines changed

12 files changed

+453
-10
lines changed

package.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,32 @@
254254
"default": false,
255255
"description": "Whether to send back detailed information from error logs for diagnostic purpose.",
256256
"scope": "window"
257+
},
258+
"java.copilot.inspection.renderer": {
259+
"type": "object",
260+
"description": "How to display rewriting suggestions.",
261+
"properties": {
262+
"diagnostics": {
263+
"type": "boolean",
264+
"default": true,
265+
"description": "Hints (Diagnostics)"
266+
},
267+
"guttericons": {
268+
"type": "boolean",
269+
"default": false,
270+
"description": "Gutter Icons"
271+
},
272+
"codelenses": {
273+
"type": "boolean",
274+
"default": false,
275+
"description": "Code Lenses"
276+
},
277+
"rulerhighlights": {
278+
"type": "boolean",
279+
"default": true,
280+
"description": "Ruler Highlights (Scrollbar)"
281+
}
282+
}
257283
}
258284
}
259285
},

resources/gutter-blue.svg

Lines changed: 12 additions & 0 deletions
Loading

resources/gutter-red.svg

Lines changed: 12 additions & 0 deletions
Loading

src/copilot/inspect/commands.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,23 @@ import { instrumentOperationAsVsCodeCommand, sendInfo } from "vscode-extension-t
33
import InspectionCopilot from "./InspectionCopilot";
44
import { Inspection, InspectionProblem } from "./Inspection";
55
import { uncapitalize } from "../utils";
6+
import { InspectionRenderer } from "./render/InspectionRenderer";
67

78
export const COMMAND_INSPECT_CLASS = 'java.copilot.inspect.class';
89
export const COMMAND_INSPECT_RANGE = 'java.copilot.inspect.range';
910
export const COMMAND_FIX = 'java.copilot.fix.inspection';
1011

11-
export function registerCommands() {
12+
export function registerCommands(renderer: InspectionRenderer) {
1213
instrumentOperationAsVsCodeCommand(COMMAND_INSPECT_CLASS, async (document: TextDocument, clazz: DocumentSymbol) => {
1314
const copilot = new InspectionCopilot();
14-
void copilot.inspectClass(document, clazz);
15+
const inspections = await copilot.inspectClass(document, clazz);
16+
renderer.renderInspections(document, inspections);
1517
});
1618

1719
instrumentOperationAsVsCodeCommand(COMMAND_INSPECT_RANGE, async (document: TextDocument, range: Range | Selection) => {
1820
const copilot = new InspectionCopilot();
19-
void copilot.inspectRange(document, range);
21+
const inspections = await copilot.inspectRange(document, range);
22+
renderer.renderInspections(document, inspections);
2023
});
2124

2225
instrumentOperationAsVsCodeCommand(COMMAND_FIX, async (problem: InspectionProblem, solution: string, source: string) => {

src/copilot/inspect/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { CancellationToken, CodeAction, CodeActionContext, CodeActionKind, ExtensionContext, TextDocument, languages, window, workspace, Range, Selection } from "vscode";
22
import { COMMAND_INSPECT_RANGE, registerCommands } from "./commands";
33
import { InspectActionCodeLensProvider } from "./InspectActionCodeLensProvider";
4+
import { DefaultRenderer as DefaultInspectionRenderer } from "./render/DefaultRenderer";
5+
import { InspectionRenderer } from "./render/InspectionRenderer";
46

5-
export const DEPENDENT_EXTENSIONS = ['github.copilot-chat', 'redhat.java'];
6-
7-
export async function activateCopilotInspection(context: ExtensionContext): Promise<void> {
8-
9-
registerCommands();
10-
7+
export async function activateCopilotInspection(context: ExtensionContext): Promise<void> {
118
const inspectActionCodeLenses = new InspectActionCodeLensProvider().install(context);
9+
const inspectionRenderer: InspectionRenderer = new DefaultInspectionRenderer().install(context);
10+
// Commands
11+
registerCommands(inspectionRenderer);
1212

1313
context.subscriptions.push(
1414
workspace.onDidOpenTextDocument(doc => inspectActionCodeLenses.rerender(doc)), // Rerender class codelens when open a new document
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/* eslint-disable @typescript-eslint/ban-ts-comment */
2+
import { CodeLens, CodeLensProvider, Disposable, Event, EventEmitter, ExtensionContext, TextDocument, Uri, languages } from "vscode";
3+
import { Inspection } from "../Inspection";
4+
import { InspectionRenderer } from "./InspectionRenderer";
5+
import { logger } from "../../../copilot/utils";
6+
import { COMMAND_FIX } from "../commands";
7+
8+
export class CodeLensRenderer implements InspectionRenderer {
9+
private readonly codeLenses: Map<Uri, CodeLens[]> = new Map();
10+
private readonly provider = new InspectionCodeLensProvider(this.codeLenses);
11+
private disposableRegistry: Disposable | undefined;
12+
13+
public install(context: ExtensionContext): InspectionRenderer {
14+
if (this.disposableRegistry) return this;
15+
logger.debug(`[CodeLensRenderer] install`);
16+
this.disposableRegistry = languages.registerCodeLensProvider({ language: 'java' }, this.provider);
17+
context.subscriptions.push(this.disposableRegistry);
18+
return this;
19+
}
20+
21+
public uninstall(): void {
22+
if (!this.disposableRegistry) return;
23+
logger.debug(`[CodeLensRenderer] uninstall`);
24+
this.codeLenses.clear();
25+
this.disposableRegistry.dispose();
26+
this.provider.refresh();
27+
this.disposableRegistry = undefined;
28+
}
29+
30+
public clear(document?: TextDocument): void {
31+
if (document) {
32+
this.codeLenses?.set(document.uri, []);
33+
} else {
34+
this.codeLenses.clear();
35+
}
36+
this.provider.refresh();
37+
}
38+
39+
public renderInspections(document: TextDocument, inspections: Inspection[]): void {
40+
if (inspections.length < 1 || !this.codeLenses) {
41+
return;
42+
}
43+
const newCodeLenses: CodeLens[] = inspections.map(s => CodeLensRenderer.toCodeLens(s));
44+
const newCodeLensesMessages = newCodeLenses.map(c => c.command?.title.trim());
45+
const existingCodeLenses = this.codeLenses.get(document.uri) ?? [];
46+
const leftCodeLenses = existingCodeLenses.filter(c => !newCodeLensesMessages.includes(c.command?.title.trim()));
47+
newCodeLenses.push(...leftCodeLenses);
48+
this.codeLenses.set(document.uri, newCodeLenses);
49+
this.provider.refresh();
50+
}
51+
52+
private static toCodeLens(inspection: Inspection): CodeLens {
53+
const range = Inspection.getIndicatorRangeOfInspection(inspection.problem);
54+
const codeLens = new CodeLens(range, {
55+
title: inspection.solution,
56+
tooltip: inspection.problem.description,
57+
command: COMMAND_FIX,
58+
arguments: [inspection.problem, inspection.solution, 'codelenses']
59+
});
60+
return codeLens;
61+
}
62+
}
63+
64+
class InspectionCodeLensProvider implements CodeLensProvider {
65+
private readonly emitter: EventEmitter<void> = new EventEmitter<void>();
66+
public readonly onDidChangeCodeLenses: Event<void> = this.emitter.event;
67+
68+
constructor(private readonly codeLenses: Map<Uri, CodeLens[]>) { }
69+
70+
provideCodeLenses(document: TextDocument): CodeLens[] {
71+
return this.codeLenses.get(document.uri) ?? [];
72+
}
73+
74+
refresh(): void {
75+
this.emitter.fire();
76+
}
77+
}
78+
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/* eslint-disable @typescript-eslint/ban-ts-comment */
2+
import { ExtensionContext, TextDocument, WorkspaceConfiguration, workspace } from "vscode";
3+
import { CodeLensRenderer } from "./CodeLensRenderer";
4+
import { DiagnosticRenderer } from "./DiagnosticRenderer";
5+
import { GutterIconRenderer } from "./GutterIconRenderer";
6+
import { RulerHighlightRenderer } from "./RulerHighlightRenderer";
7+
import { Inspection } from "../Inspection";
8+
import { InspectionRenderer } from "./InspectionRenderer";
9+
import { sendInfo } from "vscode-extension-telemetry-wrapper";
10+
import { isCodeLensDisabled, logger } from "../../../copilot/utils";
11+
12+
export class DefaultRenderer implements InspectionRenderer {
13+
private readonly renderers: { [type: string]: InspectionRenderer } = {};
14+
private readonly installedRenderers: InspectionRenderer[] = [];
15+
16+
public constructor() {
17+
this.renderers['diagnostics'] = new DiagnosticRenderer();
18+
this.renderers['guttericons'] = new GutterIconRenderer();
19+
this.renderers['codelenses'] = new CodeLensRenderer();
20+
this.renderers['rulerhighlights'] = new RulerHighlightRenderer();
21+
}
22+
23+
public install(context: ExtensionContext): InspectionRenderer {
24+
if (this.installedRenderers.length > 0) {
25+
logger.warn('DefaultRenderer is already installed');
26+
return this;
27+
}
28+
workspace.onDidChangeConfiguration(event => {
29+
if (event.affectsConfiguration('java.copilot.inspection.renderer')) {
30+
const settings = this.reloadRenderers(context);
31+
sendInfo('java.copilot.inspection.renderer.changed', { 'settings': `${settings.join(',')}` });
32+
}
33+
});
34+
this.reloadRenderers(context);
35+
return this;
36+
}
37+
38+
public uninstall(): void {
39+
this.installedRenderers.forEach(r => r.uninstall());
40+
}
41+
42+
public clear(document?: TextDocument): void {
43+
this.installedRenderers.forEach(r => r.clear(document));
44+
}
45+
46+
public renderInspections(document: TextDocument, inspections: Inspection[]): void {
47+
this.installedRenderers.forEach(r => r.renderInspections(document, inspections));
48+
}
49+
50+
private reloadRenderers(context: ExtensionContext): string[] {
51+
this.installedRenderers.splice(0, this.installedRenderers.length);
52+
const settings = this.reloadSettings();
53+
Object.entries(this.renderers).forEach(([type, renderer]) => {
54+
if (settings.includes(type.toLowerCase())) {
55+
this.installedRenderers.push(renderer);
56+
renderer.install(context);
57+
} else {
58+
renderer.uninstall();
59+
}
60+
});
61+
return settings;
62+
}
63+
64+
private reloadSettings(): string[] {
65+
const config: WorkspaceConfiguration = workspace.getConfiguration('java.copilot.inspection.renderer');
66+
const types: string[] = Object.keys(this.renderers);
67+
const settings = types.map(type => config.get<boolean>(type) ? type.toLowerCase() : '').filter(t => t);
68+
if (settings.length === 0) {
69+
settings.push('diagnostics');
70+
settings.push('rulerhighlights');
71+
settings.push(isCodeLensDisabled() ? 'guttericons' : 'codelenses');
72+
}
73+
return settings;
74+
}
75+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/* eslint-disable @typescript-eslint/ban-ts-comment */
2+
import { CancellationToken, CodeAction, CodeActionContext, CodeActionKind, Diagnostic, DiagnosticCollection, DiagnosticSeverity, ExtensionContext, Range, Selection, TextDocument, languages } from "vscode";
3+
import { Inspection } from "../Inspection";
4+
import { InspectionRenderer } from "./InspectionRenderer";
5+
import { logger } from "../../../copilot/utils";
6+
import { COMMAND_FIX } from "../commands";
7+
8+
const DIAGNOSTICS_GROUP = 'java.copilot.inspection.diagnostics';
9+
10+
export class DiagnosticRenderer implements InspectionRenderer {
11+
private diagnostics: DiagnosticCollection | undefined;
12+
13+
public install(context: ExtensionContext): InspectionRenderer {
14+
if (this.diagnostics) return this;
15+
logger.debug('[DiagnosticRenderer] install...');
16+
this.diagnostics = languages.createDiagnosticCollection(DIAGNOSTICS_GROUP);
17+
context.subscriptions.push(this.diagnostics);
18+
return this;
19+
}
20+
21+
public uninstall(): void {
22+
if (!this.diagnostics) return;
23+
logger.debug('[DiagnosticRenderer] uninstall...');
24+
this.diagnostics.clear();
25+
this.diagnostics.dispose();
26+
this.diagnostics = undefined;
27+
}
28+
29+
public clear(document?: TextDocument): void {
30+
if (document) {
31+
this.diagnostics?.set(document.uri, []);
32+
} else {
33+
this.diagnostics?.clear();
34+
}
35+
}
36+
37+
public renderInspections(document: TextDocument, inspections: Inspection[]): void {
38+
if (inspections.length < 1 || !this.diagnostics) {
39+
return;
40+
}
41+
const newDiagnostics: Diagnostic[] = inspections.map(s => DiagnosticRenderer.toDiagnostic(s));
42+
const newDiagnosticsMessages = newDiagnostics.map(d => d.message.trim());
43+
const existingDiagnostics = this.diagnostics.get(document.uri) ?? [];
44+
const leftDiagnostics = existingDiagnostics.filter(d => !newDiagnosticsMessages.includes(d.message.trim()));
45+
newDiagnostics.push(...leftDiagnostics);
46+
this.diagnostics.set(document.uri, newDiagnostics);
47+
}
48+
49+
private static toDiagnostic(inspection: Inspection): Diagnostic {
50+
const range = Inspection.getIndicatorRangeOfInspection(inspection.problem);
51+
const severiy = inspection.severity.toUpperCase() === 'HIGH' ? DiagnosticSeverity.Information : DiagnosticSeverity.Hint;
52+
const diagnostic = new Diagnostic(range, inspection.problem.description, severiy);
53+
diagnostic.source = DIAGNOSTICS_GROUP;
54+
//@ts-ignore
55+
diagnostic.additional = inspection;
56+
return diagnostic;
57+
}
58+
}
59+
60+
export async function fixDiagnostic(document: TextDocument, _range: Range | Selection, context: CodeActionContext, _token: CancellationToken): Promise<CodeAction[]> {
61+
if (document?.languageId !== 'java') {
62+
return [];
63+
}
64+
const actions: CodeAction[] = [];
65+
for (const diagnostic of context.diagnostics) {
66+
if (diagnostic.source !== DIAGNOSTICS_GROUP) {
67+
continue;
68+
}
69+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
70+
// @ts-ignore
71+
const inspection: Inspection = diagnostic.additional as Inspection;
72+
const action: CodeAction = {
73+
title: inspection.solution,
74+
diagnostics: [diagnostic],
75+
kind: CodeActionKind.RefactorRewrite,
76+
command: {
77+
title: diagnostic.message,
78+
command: COMMAND_FIX,
79+
arguments: [inspection.problem, inspection.solution, 'diagnostics']
80+
}
81+
};
82+
actions.push(action);
83+
}
84+
return actions;
85+
}

0 commit comments

Comments
 (0)