Skip to content

Commit d6921eb

Browse files
support ignoring inspections. (#1326)
* feat: support ignore inspections. * resolve comments.
1 parent b937c94 commit d6921eb

File tree

9 files changed

+99
-30
lines changed

9 files changed

+99
-30
lines changed

src/copilot/inspect/InspectActionCodeLensProvider.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { CodeLens, CodeLensProvider, Event, EventEmitter, ExtensionContext, TextDocument, Uri, languages } from "vscode";
22
import { getTopLevelClassesOfDocument, logger } from "../utils";
3-
import { COMMAND_INSPECT_CLASS } from "./commands";
3+
import { COMMAND_IGNORE_INSPECTIONS, COMMAND_INSPECT_CLASS } from "./commands";
4+
import InspectionCache from "./InspectionCache";
45

56
export class InspectActionCodeLensProvider implements CodeLensProvider {
67
private inspectCodeLenses: Map<Uri, CodeLens[]> = new Map();
@@ -18,14 +19,22 @@ export class InspectActionCodeLensProvider implements CodeLensProvider {
1819
public async rerender(document: TextDocument) {
1920
if (document.languageId !== 'java') return;
2021
logger.debug('[InspectCodeLensProvider] rerender inspect codelenses...');
21-
const docCodeLenses: CodeLens[] = [];
22+
const topLevelCodeLenses: CodeLens[] = [];
2223
const classes = await getTopLevelClassesOfDocument(document);
23-
classes.forEach(clazz => docCodeLenses.push(new CodeLens(clazz.range, {
24+
classes.map(clazz => new CodeLens(clazz.range, {
2425
title: "Rewrite with new syntax",
2526
command: COMMAND_INSPECT_CLASS,
2627
arguments: [document, clazz]
27-
})));
28-
this.inspectCodeLenses.set(document.uri, docCodeLenses);
28+
})).forEach(codeLens => topLevelCodeLenses.push(codeLens));
29+
30+
const results = await Promise.all(classes.map(clazz => InspectionCache.hasCache(document, clazz)));
31+
classes.filter((_, i) => results[i]).map(clazz => new CodeLens(clazz.range, {
32+
title: "Ignore all",
33+
command: COMMAND_IGNORE_INSPECTIONS,
34+
arguments: [document, clazz]
35+
})).forEach(codeLens => topLevelCodeLenses.push(codeLens));
36+
37+
this.inspectCodeLenses.set(document.uri, topLevelCodeLenses);
2938
this.emitter.fire();
3039
}
3140

src/copilot/inspect/Inspection.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { TextDocument, workspace, window, Selection, Range, Position } from "vscode";
2+
import { SymbolNode } from "./SymbolNode";
23

34
export interface InspectionProblem {
45
/**
@@ -27,6 +28,7 @@ export interface InspectionProblem {
2728

2829
export interface Inspection {
2930
document?: TextDocument;
31+
symbol?: SymbolNode;
3032
problem: InspectionProblem;
3133
solution: string;
3234
severity: string;

src/copilot/inspect/InspectionCache.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { SymbolKind, TextDocument } from 'vscode';
2-
import { METHOD_KINDS, getClassesAndMethodsOfDocument, logger } from '../utils';
2+
import { METHOD_KINDS, getClassesAndMethodsContainedInRange, getClassesAndMethodsOfDocument, logger } from '../utils';
33
import { Inspection } from './Inspection';
44
import * as crypto from "crypto";
55
import { SymbolNode } from './SymbolNode';
@@ -11,20 +11,37 @@ import { SymbolNode } from './SymbolNode';
1111
const DOC_SYMBOL_VERSION_INSPECTIONS: Map<string, Map<string, [string, Inspection[]]>> = new Map();
1212

1313
export default class InspectionCache {
14-
public static hasCache(document: TextDocument, symbol?: SymbolNode): boolean {
14+
/**
15+
* check if the document or the symbol is cached.
16+
* if the symbol is provided, check if the symbol or its contained symbols are cached.
17+
*/
18+
public static async hasCache(document: TextDocument, symbol?: SymbolNode): Promise<boolean> {
1519
const documentKey = document.uri.fsPath;
1620
if (!symbol) {
1721
return DOC_SYMBOL_VERSION_INSPECTIONS.has(documentKey);
1822
}
1923
const symbolInspections = DOC_SYMBOL_VERSION_INSPECTIONS.get(documentKey);
20-
const versionInspections = symbolInspections?.get(symbol.qualifiedName);
21-
const symbolVersionId = InspectionCache.calculateSymbolVersionId(document, symbol);
22-
return versionInspections?.[0] === symbolVersionId;
24+
// check if the symbol or its contained symbols are cached
25+
const symbols = await getClassesAndMethodsContainedInRange(symbol.range, document);
26+
for (const s of symbols) {
27+
const versionInspections = symbolInspections?.get(s.qualifiedName);
28+
const symbolVersionId = InspectionCache.calculateSymbolVersionId(document, s);
29+
if (versionInspections?.[0] === symbolVersionId) {
30+
return true;
31+
}
32+
}
33+
return false;
2334
}
2435

36+
/**
37+
* Get cached inspections of a document, if the document is not cached, return an empty array.
38+
* Cached inspections of outdated symbols are filtered out.Symbols are considered outdated if
39+
* their content has changed.
40+
*/
2541
public static async getCachedInspectionsOfDoc(document: TextDocument): Promise<Inspection[]> {
2642
const symbols: SymbolNode[] = await getClassesAndMethodsOfDocument(document);
2743
const inspections: Inspection[] = [];
44+
// we don't get cached inspections directly from the cache, because we need to filter out outdated symbols
2845
for (const symbol of symbols) {
2946
const cachedInspections = InspectionCache.getCachedInspectionsOfSymbol(document, symbol);
3047
inspections.push(...cachedInspections);
@@ -109,6 +126,10 @@ export default class InspectionCache {
109126
const documentKey = document.uri.fsPath;
110127
const symbolVersionId = InspectionCache.calculateSymbolVersionId(document, symbol);
111128
const cachedSymbolInspections = DOC_SYMBOL_VERSION_INSPECTIONS.get(documentKey) ?? new Map();
129+
inspections.forEach(s => {
130+
s.document = document;
131+
s.symbol = symbol;
132+
});
112133
// use qualified name to prevent conflicts between symbols with the same signature in same document
113134
cachedSymbolInspections.set(symbol.qualifiedName, [symbolVersionId, inspections]);
114135
DOC_SYMBOL_VERSION_INSPECTIONS.set(documentKey, cachedSymbolInspections);

src/copilot/inspect/InspectionCopilot.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { getClassesContainedInRange, getInnermostClassContainsRange, getIntersec
44
import { Inspection } from "./Inspection";
55
import path from "path";
66
import { TextDocument, SymbolKind, ProgressLocation, commands, Position, Range, Selection, window, LanguageModelChatSystemMessage, LanguageModelChatMessage, LanguageModelChatUserMessage, LanguageModelChatAssistantMessage } from "vscode";
7-
import { COMMAND_FIX } from "./commands";
7+
import { COMMAND_FIX_INSPECTION } from "./commands";
88
import InspectionCache from "./InspectionCache";
99
import { SymbolNode } from "./SymbolNode";
1010

@@ -172,7 +172,7 @@ export default class InspectionCopilot extends Copilot {
172172
void window.showInformationMessage(`Inspected ${symbolKind} ${symbolName}... of \"${path.basename(document.fileName)}\" and got 0 suggestions.`);
173173
} else if (inspections.length == 1) {
174174
// apply the only suggestion automatically
175-
void commands.executeCommand(COMMAND_FIX, inspections[0].problem, inspections[0].solution, 'auto');
175+
void commands.executeCommand(COMMAND_FIX_INSPECTION, inspections[0].problem, inspections[0].solution, 'auto');
176176
} else {
177177
// show message to go to the first suggestion
178178
void window.showInformationMessage(`Inspected ${symbolKind} ${symbolName}... of \"${path.basename(document.fileName)}\" and got ${inspections.length} suggestions.`, "Go to").then(selection => {

src/copilot/inspect/commands.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import { Inspection, InspectionProblem } from "./Inspection";
55
import { uncapitalize } from "../utils";
66
import { SymbolNode } from "./SymbolNode";
77
import { DocumentRenderer } from "./DocumentRenderer";
8+
import InspectionCache from "./InspectionCache";
89

910
export const COMMAND_INSPECT_CLASS = 'java.copilot.inspect.class';
1011
export const COMMAND_INSPECT_RANGE = 'java.copilot.inspect.range';
11-
export const COMMAND_FIX = 'java.copilot.fix.inspection';
12+
export const COMMAND_FIX_INSPECTION = 'java.copilot.inspection.fix';
13+
export const COMMAND_IGNORE_INSPECTIONS = 'java.copilot.inspection.ignore';
1214

1315
export function registerCommands(copilot: InspectionCopilot, renderer: DocumentRenderer) {
1416
instrumentOperationAsVsCodeCommand(COMMAND_INSPECT_CLASS, async (document: TextDocument, clazz: SymbolNode) => {
@@ -21,10 +23,10 @@ export function registerCommands(copilot: InspectionCopilot, renderer: DocumentR
2123
renderer.rerender(document);
2224
});
2325

24-
instrumentOperationAsVsCodeCommand(COMMAND_FIX, async (problem: InspectionProblem, solution: string, source: string) => {
26+
instrumentOperationAsVsCodeCommand(COMMAND_FIX_INSPECTION, async (problem: InspectionProblem, solution: string, source: string) => {
2527
// source is where is this command triggered from, e.g. "gutter", "codelens", "diagnostic"
2628
const range = Inspection.getIndicatorRangeOfInspection(problem);
27-
sendInfo(`${COMMAND_FIX}.info`, { problem: problem.description, solution, source });
29+
sendInfo(`${COMMAND_FIX_INSPECTION}.info`, { problem: problem.description, solution, source });
2830
void commands.executeCommand('vscode.editorChat.start', {
2931
autoSend: true,
3032
message: `/fix ${problem.description}, maybe ${uncapitalize(solution)}`,
@@ -33,4 +35,12 @@ export function registerCommands(copilot: InspectionCopilot, renderer: DocumentR
3335
initialRange: range
3436
});
3537
});
38+
39+
instrumentOperationAsVsCodeCommand(COMMAND_IGNORE_INSPECTIONS, async (document: TextDocument, symbol?: SymbolNode, inspection?: Inspection) => {
40+
if (inspection) {
41+
sendInfo(`${COMMAND_IGNORE_INSPECTIONS}.info`, { problem: inspection.problem.description, solution: inspection.solution });
42+
}
43+
InspectionCache.invalidateInspectionCache(document, symbol, inspection);
44+
renderer.rerender(document);
45+
});
3646
}

src/copilot/inspect/render/CodeLensRenderer.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
import { CodeLens, CodeLensProvider, Disposable, Event, EventEmitter, ExtensionContext, TextDocument, Uri, languages } from "vscode";
33
import { Inspection } from "../Inspection";
44
import { InspectionRenderer } from "./InspectionRenderer";
5-
import { logger } from "../../../copilot/utils";
6-
import { COMMAND_FIX } from "../commands";
5+
import { logger, uncapitalize } from "../../../copilot/utils";
6+
import { COMMAND_IGNORE_INSPECTIONS, COMMAND_FIX_INSPECTION } from "../commands";
7+
import { capitalize } from "lodash";
78

89
export class CodeLensRenderer implements InspectionRenderer {
910
private readonly codeLenses: Map<Uri, CodeLens[]> = new Map();
@@ -40,7 +41,7 @@ export class CodeLensRenderer implements InspectionRenderer {
4041
if (inspections.length < 1 || !this.codeLenses) {
4142
return;
4243
}
43-
const newCodeLenses: CodeLens[] = inspections.map(s => CodeLensRenderer.toCodeLens(s));
44+
const newCodeLenses: CodeLens[] = inspections.flatMap(s => CodeLensRenderer.toCodeLenses(document, s));
4445
const newCodeLensesMessages = newCodeLenses.map(c => c.command?.title.trim());
4546
const existingCodeLenses = this.codeLenses.get(document.uri) ?? [];
4647
const leftCodeLenses = existingCodeLenses.filter(c => !newCodeLensesMessages.includes(c.command?.title.trim()));
@@ -49,15 +50,25 @@ export class CodeLensRenderer implements InspectionRenderer {
4950
this.provider.refresh();
5051
}
5152

52-
private static toCodeLens(inspection: Inspection): CodeLens {
53+
private static toCodeLenses(document: TextDocument, inspection: Inspection): CodeLens[] {
54+
const codeLenses = [];
5355
const range = Inspection.getIndicatorRangeOfInspection(inspection.problem);
54-
const codeLens = new CodeLens(range, {
55-
title: inspection.solution,
56+
const inspectionCodeLens = new CodeLens(range, {
57+
title: capitalize(inspection.solution),
5658
tooltip: inspection.problem.description,
57-
command: COMMAND_FIX,
59+
command: COMMAND_FIX_INSPECTION,
5860
arguments: [inspection.problem, inspection.solution, 'codelenses']
5961
});
60-
return codeLens;
62+
codeLenses.push(inspectionCodeLens);
63+
64+
const ignoreCodeLens = new CodeLens(range, {
65+
title: 'Ignore',
66+
tooltip: `Ignore "${uncapitalize(inspection.problem.description)}"`,
67+
command: COMMAND_IGNORE_INSPECTIONS,
68+
arguments: [document, inspection.symbol, inspection]
69+
});
70+
codeLenses.push(ignoreCodeLens);
71+
return codeLenses;
6172
}
6273
}
6374

src/copilot/inspect/render/DiagnosticRenderer.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import { CancellationToken, CodeAction, CodeActionContext, CodeActionKind, Diagnostic, DiagnosticCollection, DiagnosticSeverity, ExtensionContext, Range, Selection, TextDocument, languages } from "vscode";
33
import { Inspection } from "../Inspection";
44
import { InspectionRenderer } from "./InspectionRenderer";
5-
import { logger } from "../../../copilot/utils";
6-
import { COMMAND_FIX } from "../commands";
5+
import { logger, uncapitalize } from "../../../copilot/utils";
6+
import { COMMAND_IGNORE_INSPECTIONS, COMMAND_FIX_INSPECTION } from "../commands";
77

88
const DIAGNOSTICS_GROUP = 'java.copilot.inspection.diagnostics';
99

@@ -69,17 +69,28 @@ export async function fixDiagnostic(document: TextDocument, _range: Range | Sele
6969
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
7070
// @ts-ignore
7171
const inspection: Inspection = diagnostic.additional as Inspection;
72-
const action: CodeAction = {
72+
const fixAction: CodeAction = {
7373
title: inspection.solution,
7474
diagnostics: [diagnostic],
7575
kind: CodeActionKind.RefactorRewrite,
7676
command: {
7777
title: diagnostic.message,
78-
command: COMMAND_FIX,
78+
command: COMMAND_FIX_INSPECTION,
7979
arguments: [inspection.problem, inspection.solution, 'diagnostics']
8080
}
8181
};
82-
actions.push(action);
82+
actions.push(fixAction);
83+
const ignoreAction: CodeAction = {
84+
title: `Ignore "${uncapitalize(inspection.problem.description)}"`,
85+
diagnostics: [diagnostic],
86+
kind: CodeActionKind.RefactorRewrite,
87+
command: {
88+
title: `Ignore "${uncapitalize(inspection.problem.description)}"`,
89+
command: COMMAND_IGNORE_INSPECTIONS,
90+
arguments: [document, inspection.symbol, inspection]
91+
}
92+
};
93+
actions.push(ignoreAction);
8394
}
8495
return actions;
8596
}

src/copilot/inspect/render/GutterIconRenderer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Inspection } from "../Inspection";
44
import { InspectionRenderer } from "./InspectionRenderer";
55
import { logger } from "../../../copilot/utils";
66
import path = require("path");
7-
import { COMMAND_FIX } from "../commands";
7+
import { COMMAND_FIX_INSPECTION } from "../commands";
88

99
export class GutterIconRenderer implements InspectionRenderer {
1010
private readonly gutterIcons: Map<Uri, GutterIcon[]> = new Map();
@@ -59,7 +59,7 @@ export class GutterIconRenderer implements InspectionRenderer {
5959
private static toGutterIcon(inspection: Inspection): GutterIcon {
6060
const range = Inspection.getIndicatorRangeOfInspection(inspection.problem);
6161
const args = [inspection.problem, inspection.solution, 'guttericons'];
62-
const commandUri = Uri.parse(`command:${COMMAND_FIX}?${encodeURIComponent(JSON.stringify(args))}`);
62+
const commandUri = Uri.parse(`command:${COMMAND_FIX_INSPECTION}?${encodeURIComponent(JSON.stringify(args))}`);
6363
const hoverMessage = new MarkdownString(`${inspection.problem.description}\n\n$(copilot) [${inspection.solution}](${commandUri})`, true);
6464
hoverMessage.isTrusted = true;
6565
return { range, hoverMessage, inspection };

src/copilot/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ export async function getClassesContainedInRange(range: Range | Selection, docum
1515
.filter(clazz => range.contains(clazz.range));
1616
}
1717

18+
export async function getClassesAndMethodsContainedInRange(range: Range | Selection, document: TextDocument): Promise<SymbolNode[]> {
19+
const symbols = await getClassesAndMethodsOfDocument(document);
20+
return symbols.filter(symbol => range.contains(symbol.range));
21+
}
22+
1823
/**
1924
* get the innermost class symbol that completely contains the `range` in the `document`
2025
*/

0 commit comments

Comments
 (0)