Skip to content

Commit 96eb385

Browse files
add Rewrite with new syntax code lens on top level classes and code actions. (#1317)
* show `Rewrite with new syntax` code lens on first level classes of java documents. * add code action to inspect java code selection * implement `Inspection.highlight` to highlight the first line of an inspeciton. * delegate to copilot inline chat to fix inspection * chore: Use `sendInfo` to attach properties to telemetry event * rename symbols. * resolve command: renaming/inline/js docs. * extract interface InspectionProblem and rename inspection.problem.symbol as `inspection.problem.indicator` to avoid conflicts
1 parent e22a87d commit 96eb385

File tree

7 files changed

+179
-40
lines changed

7 files changed

+179
-40
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { CodeLens, CodeLensProvider, Event, EventEmitter, ExtensionContext, TextDocument, Uri, languages } from "vscode";
2+
import { getTopLevelClassesOfDocument, logger } from "../utils";
3+
import { COMMAND_INSPECT_CLASS } from "./commands";
4+
5+
export class InspectActionCodeLensProvider implements CodeLensProvider {
6+
private inspectCodeLenses: Map<Uri, CodeLens[]> = new Map();
7+
private emitter: EventEmitter<void> = new EventEmitter<void>();
8+
public readonly onDidChangeCodeLenses: Event<void> = this.emitter.event;
9+
10+
public install(context: ExtensionContext): InspectActionCodeLensProvider {
11+
logger.debug('[InspectCodeLensProvider] install...');
12+
context.subscriptions.push(
13+
languages.registerCodeLensProvider({ language: 'java' }, this)
14+
);
15+
return this;
16+
}
17+
18+
public async rerender(document: TextDocument) {
19+
if (document.languageId !== 'java') return;
20+
logger.debug('[InspectCodeLensProvider] rerender inspect codelenses...');
21+
const docCodeLenses: CodeLens[] = [];
22+
const classes = await getTopLevelClassesOfDocument(document);
23+
classes.forEach(clazz => docCodeLenses.push(new CodeLens(clazz.range, {
24+
title: "Rewrite with new syntax",
25+
command: COMMAND_INSPECT_CLASS,
26+
arguments: [document, clazz]
27+
})));
28+
this.inspectCodeLenses.set(document.uri, docCodeLenses);
29+
this.emitter.fire();
30+
}
31+
32+
public provideCodeLenses(document: TextDocument): CodeLens[] {
33+
return this.inspectCodeLenses.get(document.uri) ?? [];
34+
}
35+
}

src/copilot/inspect/Inspection.ts

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,66 @@
1-
import { TextDocument } from "vscode";
1+
import { TextDocument, workspace, window, Selection, Range, Position } from "vscode";
22

3-
export interface Inspection {
4-
document?: TextDocument;
5-
problem: {
3+
export interface InspectionProblem {
4+
/**
5+
* short description of the problem
6+
*/
7+
description: string;
8+
position: {
69
/**
7-
* short description of the problem
10+
* real line number to the start of the document, will change
811
*/
9-
description: string;
10-
position: {
11-
/**
12-
* real line number to the start of the document, will change
13-
*/
14-
line: number;
15-
/**
16-
* relative line number to the start of the symbol(method/class), won't change
17-
*/
18-
relativeLine: number;
19-
/**
20-
* code of the first line of the problematic code block
21-
*/
22-
code: string;
23-
};
12+
line: number;
2413
/**
25-
* symbol name of the problematic code block, e.g. method name/class name, keywork, etc.
14+
* relative line number to the start of the symbol(method/class), won't change
2615
*/
27-
symbol: string;
28-
}
16+
relativeLine: number;
17+
/**
18+
* code of the first line of the problematic code block
19+
*/
20+
code: string;
21+
};
22+
/**
23+
* indicator of the problematic code block, e.g. method name/class name, keywork, etc.
24+
*/
25+
indicator: string;
26+
}
27+
28+
export interface Inspection {
29+
document?: TextDocument;
30+
problem: InspectionProblem;
2931
solution: string;
3032
severity: string;
3133
}
3234

3335
export namespace Inspection {
34-
export function fix(inspection: Inspection, source: string) {
35-
//TODO: implement me
36+
export function revealFirstLineOfInspection(inspection: Inspection) {
37+
inspection.document && void workspace.openTextDocument(inspection.document.uri).then(document => {
38+
void window.showTextDocument(document).then(editor => {
39+
const range = document.lineAt(inspection.problem.position.line).range;
40+
editor.selection = new Selection(range.start, range.end);
41+
editor.revealRange(range);
42+
});
43+
});
3644
}
3745

38-
export function highlight(inspection: Inspection) {
39-
//TODO: implement me
46+
/**
47+
* get the range of the indicator of the inspection.
48+
* `indicator` will be used as the position of code lens/diagnostics and also used as initial selection for fix commands.
49+
*/
50+
export function getIndicatorRangeOfInspection(problem: InspectionProblem): Range {
51+
const position = problem.position;
52+
const startLine: number = position.line;
53+
let startColumn: number = position.code.indexOf(problem.indicator), endLine: number = -1, endColumn: number = -1;
54+
if (startColumn > -1) {
55+
// highlight only the symbol
56+
endLine = position.line;
57+
endColumn = startColumn + problem.indicator?.length;
58+
} else {
59+
// highlight entire first line
60+
startColumn = position.code.search(/\S/) ?? 0; // first non-whitespace character
61+
endLine = position.line;
62+
endColumn = position.code.length; // last character
63+
}
64+
return new Range(new Position(startLine, startColumn), new Position(endLine, endColumn));
4065
}
4166
}

src/copilot/inspect/InspectionCopilot.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import Copilot from "../Copilot";
33
import { getClassesContainedInRange, getInnermostClassContainsRange, getIntersectionMethodsOfRange, getUnionRange, logger } from "../utils";
44
import { Inspection } from "./Inspection";
55
import path from "path";
6-
import { TextDocument, DocumentSymbol, SymbolKind, ProgressLocation, Position, Range, Selection, window } from "vscode";
6+
import { TextDocument, DocumentSymbol, SymbolKind, ProgressLocation, commands, Position, Range, Selection, window } from "vscode";
7+
import { COMMAND_FIX } from "./commands";
78

89
export default class InspectionCopilot extends Copilot {
910

@@ -16,7 +17,7 @@ export default class InspectionCopilot extends Copilot {
1617
other code...
1718
// @PROBLEM: problem of the code in less than 10 words, should be as short as possible, starts with a gerund/noun word, e.g., "Using".
1819
// @SOLUTION: solution to fix the problem in less than 10 words, should be as short as possible, starts with a verb.
19-
// @SYMBOL: symbol of the problematic code block, must be a single word contained by the problematic code. it's usually a Java keyword, a method/field/variable name, or a value(e.g. magic number)... but NOT multiple, '<null>' if cannot be identified
20+
// @INDICATOR: indicator of the problematic code block, must be a single word contained by the problematic code. it's usually a Java keyword, a method/field/variable name, or a value(e.g. magic number)... but NOT multiple, '<null>' if cannot be identified
2021
// @SEVERITY: severity of the problem, must be one of **[HIGH, MIDDLE, LOW]**, *HIGH* for Probable bugs, Security risks, Exception handling or Resource management(e.g. memory leaks); *MIDDLE* for Error handling, Performance, Reflective accesses issues and Verbose or redundant code; *LOW* for others
2122
the original problematic code...
2223
\`\`\`
@@ -58,7 +59,7 @@ export default class InspectionCopilot extends Copilot {
5859
@Entity
5960
// @PROBLEM: Using a traditional POJO
6061
// @SOLUTION: transform into a record
61-
// @SYMBOL: EmployeePojo
62+
// @INDICATOR: EmployeePojo
6263
// @SEVERITY: MIDDLE
6364
public class EmployeePojo implements Employee {
6465
public final String name;
@@ -69,7 +70,7 @@ export default class InspectionCopilot extends Copilot {
6970
String result = '';
7071
// @PROBLEM: Using if-else statements to check the type of animal
7172
// @SOLUTION: Use switch expression
72-
// @SYMBOL: if
73+
// @INDICATOR: if
7374
// @SEVERITY: MIDDLE
7475
if (this.name.equals("Miller")) {
7576
result = "Senior";
@@ -86,7 +87,7 @@ export default class InspectionCopilot extends Copilot {
8687
} catch (Exception e) {
8788
// @PROBLEM: Print stack trace in case of an exception
8889
// @SOLUTION: Log errors to a logger
89-
// @SYMBOL: ex.printStackTrace
90+
// @INDICATOR: ex.printStackTrace
9091
// @SEVERITY: LOW
9192
e.printStackTrace();
9293
}
@@ -98,7 +99,7 @@ export default class InspectionCopilot extends Copilot {
9899
// Initialize regex patterns
99100
private static readonly PROBLEM_PATTERN: RegExp = /\/\/ @PROBLEM: (.*)/;
100101
private static readonly SOLUTION_PATTERN: RegExp = /\/\/ @SOLUTION: (.*)/;
101-
private static readonly SYMBOL_PATTERN: RegExp = /\/\/ @SYMBOL: (.*)/;
102+
private static readonly INDICATOR_PATTERN: RegExp = /\/\/ @INDICATOR: (.*)/;
102103
private static readonly LEVEL_PATTERN: RegExp = /\/\/ @SEVERITY: (.*)/;
103104

104105
private readonly debounceMap = new Map<string, NodeJS.Timeout>();
@@ -157,11 +158,11 @@ export default class InspectionCopilot extends Copilot {
157158
void window.showInformationMessage(`Inspected ${symbolKind} ${symbolName}... of \"${path.basename(document.fileName)}\" and got 0 suggestions.`);
158159
} else if (inspections.length == 1) {
159160
// apply the only suggestion automatically
160-
void Inspection.fix(inspections[0], 'auto');
161+
void commands.executeCommand(COMMAND_FIX, inspections[0].problem, inspections[0].solution, 'auto');
161162
} else {
162163
// show message to go to the first suggestion
163164
void window.showInformationMessage(`Inspected ${symbolKind} ${symbolName}... of \"${path.basename(document.fileName)}\" and got ${inspections.length} suggestions.`, "Go to").then(selection => {
164-
selection === "Go to" && void Inspection.highlight(inspections[0]);
165+
selection === "Go to" && void Inspection.revealFirstLineOfInspection(inspections[0]);
165166
});
166167
}
167168
return inspections;
@@ -257,7 +258,7 @@ export default class InspectionCopilot extends Copilot {
257258
i++;
258259
}
259260

260-
return inspections.filter(i => i.problem.symbol.trim() !== '<null>').sort((a, b) => a.problem.position.relativeLine - b.problem.position.relativeLine);
261+
return inspections.filter(i => i.problem.indicator.trim() !== '<null>').sort((a, b) => a.problem.position.relativeLine - b.problem.position.relativeLine);
261262
}
262263

263264
/**
@@ -271,23 +272,23 @@ export default class InspectionCopilot extends Copilot {
271272
problem: {
272273
description: '',
273274
position: { line: -1, relativeLine: -1, code: '' },
274-
symbol: ''
275+
indicator: ''
275276
},
276277
solution: '',
277278
severity: ''
278279
};
279280
const problemMatch = lines[index + 0].match(InspectionCopilot.PROBLEM_PATTERN);
280281
const solutionMatch = lines[index + 1].match(InspectionCopilot.SOLUTION_PATTERN);
281-
const symbolMatch = lines[index + 2].match(InspectionCopilot.SYMBOL_PATTERN);
282+
const indicatorMatch = lines[index + 2].match(InspectionCopilot.INDICATOR_PATTERN);
282283
const severityMatch = lines[index + 3].match(InspectionCopilot.LEVEL_PATTERN);
283284
if (problemMatch) {
284285
inspection.problem.description = problemMatch[1].trim();
285286
}
286287
if (solutionMatch) {
287288
inspection.solution = solutionMatch[1].trim();
288289
}
289-
if (symbolMatch) {
290-
inspection.problem.symbol = symbolMatch[1].trim();
290+
if (indicatorMatch) {
291+
inspection.problem.indicator = indicatorMatch[1].trim();
291292
}
292293
if (severityMatch) {
293294
inspection.severity = severityMatch[1].trim();

src/copilot/inspect/commands.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { DocumentSymbol, TextDocument, Range, Selection, commands } from "vscode";
2+
import { instrumentOperationAsVsCodeCommand, sendInfo } from "vscode-extension-telemetry-wrapper";
3+
import InspectionCopilot from "./InspectionCopilot";
4+
import { Inspection, InspectionProblem } from "./Inspection";
5+
import { uncapitalize } from "../utils";
6+
7+
export const COMMAND_INSPECT_CLASS = 'java.copilot.inspect.class';
8+
export const COMMAND_INSPECT_RANGE = 'java.copilot.inspect.range';
9+
export const COMMAND_FIX = 'java.copilot.fix.inspection';
10+
11+
export function registerCommands() {
12+
instrumentOperationAsVsCodeCommand(COMMAND_INSPECT_CLASS, async (document: TextDocument, clazz: DocumentSymbol) => {
13+
const copilot = new InspectionCopilot();
14+
void copilot.inspectClass(document, clazz);
15+
});
16+
17+
instrumentOperationAsVsCodeCommand(COMMAND_INSPECT_RANGE, async (document: TextDocument, range: Range | Selection) => {
18+
const copilot = new InspectionCopilot();
19+
void copilot.inspectRange(document, range);
20+
});
21+
22+
instrumentOperationAsVsCodeCommand(COMMAND_FIX, async (problem: InspectionProblem, solution: string, source: string) => {
23+
// source is where is this command triggered from, e.g. "gutter", "codelens", "diagnostic"
24+
const range = Inspection.getIndicatorRangeOfInspection(problem);
25+
sendInfo(`${COMMAND_FIX}.info`, { problem: problem.description, solution, source });
26+
void commands.executeCommand('vscode.editorChat.start', {
27+
autoSend: true,
28+
message: `/fix ${problem.description}, maybe ${uncapitalize(solution)}`,
29+
position: range.start,
30+
initialSelection: new Selection(range.start, range.end),
31+
initialRange: range
32+
});
33+
});
34+
}

src/copilot/inspect/index.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { CancellationToken, CodeAction, CodeActionContext, CodeActionKind, ExtensionContext, TextDocument, languages, window, workspace, Range, Selection } from "vscode";
2+
import { COMMAND_INSPECT_RANGE, registerCommands } from "./commands";
3+
import { InspectActionCodeLensProvider } from "./InspectActionCodeLensProvider";
4+
5+
export const DEPENDENT_EXTENSIONS = ['github.copilot-chat', 'redhat.java'];
6+
7+
export async function activateCopilotInspection(context: ExtensionContext): Promise<void> {
8+
9+
registerCommands();
10+
11+
const inspectActionCodeLenses = new InspectActionCodeLensProvider().install(context);
12+
13+
context.subscriptions.push(
14+
workspace.onDidOpenTextDocument(doc => inspectActionCodeLenses.rerender(doc)), // Rerender class codelens when open a new document
15+
workspace.onDidChangeTextDocument(e => inspectActionCodeLenses.rerender(e.document)), // Rerender class codelens when change a document
16+
languages.registerCodeActionsProvider({ language: 'java' }, { provideCodeActions: rewrite }), // add code action to rewrite code
17+
);
18+
window.visibleTextEditors.forEach(editor => inspectActionCodeLenses.rerender(editor.document));
19+
}
20+
21+
async function rewrite(document: TextDocument, range: Range | Selection, _context: CodeActionContext, _token: CancellationToken): Promise<CodeAction[]> {
22+
const action: CodeAction = {
23+
title: "Rewrite with new syntax",
24+
kind: CodeActionKind.RefactorRewrite,
25+
command: {
26+
title: "Rewrite selected code",
27+
command: COMMAND_INSPECT_RANGE,
28+
arguments: [document, range]
29+
}
30+
};
31+
return [action];
32+
}

src/copilot/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,12 @@ async function getClassesAndMethodsOfDocument(document: TextDocument): Promise<D
5959
}
6060
return result;
6161
}
62+
63+
export async function getTopLevelClassesOfDocument(document: TextDocument): Promise<DocumentSymbol[]> {
64+
const symbols = ((await commands.executeCommand<DocumentSymbol[]>('vscode.executeDocumentSymbolProvider', document.uri)) ?? []);
65+
return symbols.filter(symbol => CLASS_KINDS.includes(symbol.kind));
66+
}
67+
68+
export function uncapitalize(str: string): string {
69+
return str.charAt(0).toLowerCase() + str.slice(1);
70+
}

src/extension.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { initialize as initUtils } from "./utils";
2323
import { KEY_SHOW_WHEN_USING_JAVA } from "./utils/globalState";
2424
import { scheduleAction } from "./utils/scheduler";
2525
import { showWelcomeWebview, WelcomeViewSerializer } from "./welcome";
26+
import { activateCopilotInspection } from "./copilot/inspect";
2627

2728
let cleanJavaWorkspaceIndicator: string;
2829
let activatedTimestamp: number;
@@ -81,6 +82,8 @@ async function initializeExtension(_operationId: string, context: vscode.Extensi
8182
vscode.commands.executeCommand("java.runtime");
8283
});
8384
}
85+
86+
activateCopilotInspection(context);
8487
}
8588

8689
async function presentFirstView(context: vscode.ExtensionContext) {

0 commit comments

Comments
 (0)