Skip to content

Commit

Permalink
add Rewrite with new syntax code lens on top level classes and code…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
wangmingliang-ms authored Apr 29, 2024
1 parent e22a87d commit 96eb385
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 40 deletions.
35 changes: 35 additions & 0 deletions src/copilot/inspect/InspectActionCodeLensProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { CodeLens, CodeLensProvider, Event, EventEmitter, ExtensionContext, TextDocument, Uri, languages } from "vscode";
import { getTopLevelClassesOfDocument, logger } from "../utils";
import { COMMAND_INSPECT_CLASS } from "./commands";

export class InspectActionCodeLensProvider implements CodeLensProvider {
private inspectCodeLenses: Map<Uri, CodeLens[]> = new Map();
private emitter: EventEmitter<void> = new EventEmitter<void>();
public readonly onDidChangeCodeLenses: Event<void> = this.emitter.event;

public install(context: ExtensionContext): InspectActionCodeLensProvider {
logger.debug('[InspectCodeLensProvider] install...');
context.subscriptions.push(
languages.registerCodeLensProvider({ language: 'java' }, this)
);
return this;
}

public async rerender(document: TextDocument) {
if (document.languageId !== 'java') return;
logger.debug('[InspectCodeLensProvider] rerender inspect codelenses...');
const docCodeLenses: CodeLens[] = [];
const classes = await getTopLevelClassesOfDocument(document);
classes.forEach(clazz => docCodeLenses.push(new CodeLens(clazz.range, {
title: "Rewrite with new syntax",
command: COMMAND_INSPECT_CLASS,
arguments: [document, clazz]
})));
this.inspectCodeLenses.set(document.uri, docCodeLenses);
this.emitter.fire();
}

public provideCodeLenses(document: TextDocument): CodeLens[] {
return this.inspectCodeLenses.get(document.uri) ?? [];
}
}
79 changes: 52 additions & 27 deletions src/copilot/inspect/Inspection.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,66 @@
import { TextDocument } from "vscode";
import { TextDocument, workspace, window, Selection, Range, Position } from "vscode";

export interface Inspection {
document?: TextDocument;
problem: {
export interface InspectionProblem {
/**
* short description of the problem
*/
description: string;
position: {
/**
* short description of the problem
* real line number to the start of the document, will change
*/
description: string;
position: {
/**
* real line number to the start of the document, will change
*/
line: number;
/**
* relative line number to the start of the symbol(method/class), won't change
*/
relativeLine: number;
/**
* code of the first line of the problematic code block
*/
code: string;
};
line: number;
/**
* symbol name of the problematic code block, e.g. method name/class name, keywork, etc.
* relative line number to the start of the symbol(method/class), won't change
*/
symbol: string;
}
relativeLine: number;
/**
* code of the first line of the problematic code block
*/
code: string;
};
/**
* indicator of the problematic code block, e.g. method name/class name, keywork, etc.
*/
indicator: string;
}

export interface Inspection {
document?: TextDocument;
problem: InspectionProblem;
solution: string;
severity: string;
}

export namespace Inspection {
export function fix(inspection: Inspection, source: string) {
//TODO: implement me
export function revealFirstLineOfInspection(inspection: Inspection) {
inspection.document && void workspace.openTextDocument(inspection.document.uri).then(document => {
void window.showTextDocument(document).then(editor => {
const range = document.lineAt(inspection.problem.position.line).range;
editor.selection = new Selection(range.start, range.end);
editor.revealRange(range);
});
});
}

export function highlight(inspection: Inspection) {
//TODO: implement me
/**
* get the range of the indicator of the inspection.
* `indicator` will be used as the position of code lens/diagnostics and also used as initial selection for fix commands.
*/
export function getIndicatorRangeOfInspection(problem: InspectionProblem): Range {
const position = problem.position;
const startLine: number = position.line;
let startColumn: number = position.code.indexOf(problem.indicator), endLine: number = -1, endColumn: number = -1;
if (startColumn > -1) {
// highlight only the symbol
endLine = position.line;
endColumn = startColumn + problem.indicator?.length;
} else {
// highlight entire first line
startColumn = position.code.search(/\S/) ?? 0; // first non-whitespace character
endLine = position.line;
endColumn = position.code.length; // last character
}
return new Range(new Position(startLine, startColumn), new Position(endLine, endColumn));
}
}
27 changes: 14 additions & 13 deletions src/copilot/inspect/InspectionCopilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import Copilot from "../Copilot";
import { getClassesContainedInRange, getInnermostClassContainsRange, getIntersectionMethodsOfRange, getUnionRange, logger } from "../utils";
import { Inspection } from "./Inspection";
import path from "path";
import { TextDocument, DocumentSymbol, SymbolKind, ProgressLocation, Position, Range, Selection, window } from "vscode";
import { TextDocument, DocumentSymbol, SymbolKind, ProgressLocation, commands, Position, Range, Selection, window } from "vscode";
import { COMMAND_FIX } from "./commands";

export default class InspectionCopilot extends Copilot {

Expand All @@ -16,7 +17,7 @@ export default class InspectionCopilot extends Copilot {
other code...
// @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".
// @SOLUTION: solution to fix the problem in less than 10 words, should be as short as possible, starts with a verb.
// @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
// @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
// @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
the original problematic code...
\`\`\`
Expand Down Expand Up @@ -58,7 +59,7 @@ export default class InspectionCopilot extends Copilot {
@Entity
// @PROBLEM: Using a traditional POJO
// @SOLUTION: transform into a record
// @SYMBOL: EmployeePojo
// @INDICATOR: EmployeePojo
// @SEVERITY: MIDDLE
public class EmployeePojo implements Employee {
public final String name;
Expand All @@ -69,7 +70,7 @@ export default class InspectionCopilot extends Copilot {
String result = '';
// @PROBLEM: Using if-else statements to check the type of animal
// @SOLUTION: Use switch expression
// @SYMBOL: if
// @INDICATOR: if
// @SEVERITY: MIDDLE
if (this.name.equals("Miller")) {
result = "Senior";
Expand All @@ -86,7 +87,7 @@ export default class InspectionCopilot extends Copilot {
} catch (Exception e) {
// @PROBLEM: Print stack trace in case of an exception
// @SOLUTION: Log errors to a logger
// @SYMBOL: ex.printStackTrace
// @INDICATOR: ex.printStackTrace
// @SEVERITY: LOW
e.printStackTrace();
}
Expand All @@ -98,7 +99,7 @@ export default class InspectionCopilot extends Copilot {
// Initialize regex patterns
private static readonly PROBLEM_PATTERN: RegExp = /\/\/ @PROBLEM: (.*)/;
private static readonly SOLUTION_PATTERN: RegExp = /\/\/ @SOLUTION: (.*)/;
private static readonly SYMBOL_PATTERN: RegExp = /\/\/ @SYMBOL: (.*)/;
private static readonly INDICATOR_PATTERN: RegExp = /\/\/ @INDICATOR: (.*)/;
private static readonly LEVEL_PATTERN: RegExp = /\/\/ @SEVERITY: (.*)/;

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

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

/**
Expand All @@ -271,23 +272,23 @@ export default class InspectionCopilot extends Copilot {
problem: {
description: '',
position: { line: -1, relativeLine: -1, code: '' },
symbol: ''
indicator: ''
},
solution: '',
severity: ''
};
const problemMatch = lines[index + 0].match(InspectionCopilot.PROBLEM_PATTERN);
const solutionMatch = lines[index + 1].match(InspectionCopilot.SOLUTION_PATTERN);
const symbolMatch = lines[index + 2].match(InspectionCopilot.SYMBOL_PATTERN);
const indicatorMatch = lines[index + 2].match(InspectionCopilot.INDICATOR_PATTERN);
const severityMatch = lines[index + 3].match(InspectionCopilot.LEVEL_PATTERN);
if (problemMatch) {
inspection.problem.description = problemMatch[1].trim();
}
if (solutionMatch) {
inspection.solution = solutionMatch[1].trim();
}
if (symbolMatch) {
inspection.problem.symbol = symbolMatch[1].trim();
if (indicatorMatch) {
inspection.problem.indicator = indicatorMatch[1].trim();
}
if (severityMatch) {
inspection.severity = severityMatch[1].trim();
Expand Down
34 changes: 34 additions & 0 deletions src/copilot/inspect/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { DocumentSymbol, TextDocument, Range, Selection, commands } from "vscode";
import { instrumentOperationAsVsCodeCommand, sendInfo } from "vscode-extension-telemetry-wrapper";
import InspectionCopilot from "./InspectionCopilot";
import { Inspection, InspectionProblem } from "./Inspection";
import { uncapitalize } from "../utils";

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

export function registerCommands() {
instrumentOperationAsVsCodeCommand(COMMAND_INSPECT_CLASS, async (document: TextDocument, clazz: DocumentSymbol) => {
const copilot = new InspectionCopilot();
void copilot.inspectClass(document, clazz);
});

instrumentOperationAsVsCodeCommand(COMMAND_INSPECT_RANGE, async (document: TextDocument, range: Range | Selection) => {
const copilot = new InspectionCopilot();
void copilot.inspectRange(document, range);
});

instrumentOperationAsVsCodeCommand(COMMAND_FIX, async (problem: InspectionProblem, solution: string, source: string) => {
// source is where is this command triggered from, e.g. "gutter", "codelens", "diagnostic"
const range = Inspection.getIndicatorRangeOfInspection(problem);
sendInfo(`${COMMAND_FIX}.info`, { problem: problem.description, solution, source });
void commands.executeCommand('vscode.editorChat.start', {
autoSend: true,
message: `/fix ${problem.description}, maybe ${uncapitalize(solution)}`,
position: range.start,
initialSelection: new Selection(range.start, range.end),
initialRange: range
});
});
}
32 changes: 32 additions & 0 deletions src/copilot/inspect/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { CancellationToken, CodeAction, CodeActionContext, CodeActionKind, ExtensionContext, TextDocument, languages, window, workspace, Range, Selection } from "vscode";
import { COMMAND_INSPECT_RANGE, registerCommands } from "./commands";
import { InspectActionCodeLensProvider } from "./InspectActionCodeLensProvider";

export const DEPENDENT_EXTENSIONS = ['github.copilot-chat', 'redhat.java'];

export async function activateCopilotInspection(context: ExtensionContext): Promise<void> {

registerCommands();

const inspectActionCodeLenses = new InspectActionCodeLensProvider().install(context);

context.subscriptions.push(
workspace.onDidOpenTextDocument(doc => inspectActionCodeLenses.rerender(doc)), // Rerender class codelens when open a new document
workspace.onDidChangeTextDocument(e => inspectActionCodeLenses.rerender(e.document)), // Rerender class codelens when change a document
languages.registerCodeActionsProvider({ language: 'java' }, { provideCodeActions: rewrite }), // add code action to rewrite code
);
window.visibleTextEditors.forEach(editor => inspectActionCodeLenses.rerender(editor.document));
}

async function rewrite(document: TextDocument, range: Range | Selection, _context: CodeActionContext, _token: CancellationToken): Promise<CodeAction[]> {
const action: CodeAction = {
title: "Rewrite with new syntax",
kind: CodeActionKind.RefactorRewrite,
command: {
title: "Rewrite selected code",
command: COMMAND_INSPECT_RANGE,
arguments: [document, range]
}
};
return [action];
}
9 changes: 9 additions & 0 deletions src/copilot/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,12 @@ async function getClassesAndMethodsOfDocument(document: TextDocument): Promise<D
}
return result;
}

export async function getTopLevelClassesOfDocument(document: TextDocument): Promise<DocumentSymbol[]> {
const symbols = ((await commands.executeCommand<DocumentSymbol[]>('vscode.executeDocumentSymbolProvider', document.uri)) ?? []);
return symbols.filter(symbol => CLASS_KINDS.includes(symbol.kind));
}

export function uncapitalize(str: string): string {
return str.charAt(0).toLowerCase() + str.slice(1);
}
3 changes: 3 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { initialize as initUtils } from "./utils";
import { KEY_SHOW_WHEN_USING_JAVA } from "./utils/globalState";
import { scheduleAction } from "./utils/scheduler";
import { showWelcomeWebview, WelcomeViewSerializer } from "./welcome";
import { activateCopilotInspection } from "./copilot/inspect";

let cleanJavaWorkspaceIndicator: string;
let activatedTimestamp: number;
Expand Down Expand Up @@ -81,6 +82,8 @@ async function initializeExtension(_operationId: string, context: vscode.Extensi
vscode.commands.executeCommand("java.runtime");
});
}

activateCopilotInspection(context);
}

async function presentFirstView(context: vscode.ExtensionContext) {
Expand Down

0 comments on commit 96eb385

Please sign in to comment.