diff --git a/src/copilot/Copilot.ts b/src/copilot/Copilot.ts deleted file mode 100644 index f9036841..00000000 --- a/src/copilot/Copilot.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { LanguageModelChatMessage, lm, Disposable, CancellationToken, LanguageModelChatRequestOptions, LanguageModelChatMessageRole, LanguageModelChatSelector } from "vscode"; -import { fixedInstrumentSimpleOperation, logger } from "./utils"; -import { sendInfo } from "vscode-extension-telemetry-wrapper"; - -export default class Copilot { - public static readonly DEFAULT_END_MARK = '<|endofresponse|>'; - public static readonly DEFAULT_MAX_ROUNDS = 10; - public static readonly DEFAULT_MODEL = { family: 'gpt-4' }; - public static readonly DEFAULT_MODEL_OPTIONS: LanguageModelChatRequestOptions = { modelOptions: {} }; - public static readonly NOT_CANCELLABEL: CancellationToken = { isCancellationRequested: false, onCancellationRequested: () => Disposable.from() }; - - public constructor( - private readonly systemMessagesOrSamples: LanguageModelChatMessage[], - private readonly modelSelector: LanguageModelChatSelector = Copilot.DEFAULT_MODEL, - private readonly modelOptions: LanguageModelChatRequestOptions = Copilot.DEFAULT_MODEL_OPTIONS, - private readonly maxRounds: number = Copilot.DEFAULT_MAX_ROUNDS, - private readonly endMark: string = Copilot.DEFAULT_END_MARK - ) { - } - - private async doSend( - userMessage: string, - modelOptions: LanguageModelChatRequestOptions = Copilot.DEFAULT_MODEL_OPTIONS, - cancellationToken: CancellationToken = Copilot.NOT_CANCELLABEL - ): Promise { - let answer: string = ''; - let rounds: number = 0; - const messages = [...this.systemMessagesOrSamples]; - const _send = async (message: string): Promise => { - rounds++; - logger.debug(`User: \n`, message); - logger.info(`User: ${message.split('\n')[0]}...`); - messages.push(new LanguageModelChatMessage(LanguageModelChatMessageRole.User, message)); - logger.info('Copilot: thinking...'); - - let rawAnswer: string = ''; - try { - const model = (await lm.selectChatModels(this.modelSelector))?.[0]; - if (!model) { - const models = await lm.selectChatModels(); - throw new Error(`No suitable model, available models: [${models.map(m => m.name).join(', ')}]. Please make sure you have installed the latest "GitHub Copilot Chat" (v0.16.0 or later).`); - } - const response = await model.sendRequest(messages, modelOptions ?? this.modelOptions, cancellationToken); - for await (const item of response.text) { - rawAnswer += item; - } - } catch (e) { - //@ts-ignore - const cause = e.cause || e; - logger.error(`Failed to chat with copilot`, cause); - throw cause; - } - messages.push(new LanguageModelChatMessage(LanguageModelChatMessageRole.Assistant, rawAnswer)); - logger.debug(`Copilot: \n`, rawAnswer); - logger.info(`Copilot: ${rawAnswer.split('\n')[0]}...`); - answer += rawAnswer; - return answer.trim().endsWith(this.endMark); - }; - let complete: boolean = await _send(userMessage); - while (!complete && rounds < this.maxRounds) { - complete = await _send('continue where you left off.'); - } - logger.debug('rounds', rounds); - sendInfo('java.copilot.sendRequest.info', { rounds: rounds }); - return answer.replace(this.endMark, ""); - } - - public async send( - userMessage: string, - modelOptions: LanguageModelChatRequestOptions = Copilot.DEFAULT_MODEL_OPTIONS, - cancellationToken: CancellationToken = Copilot.NOT_CANCELLABEL - ): Promise { - return fixedInstrumentSimpleOperation("java.copilot.sendRequest", this.doSend.bind(this))(userMessage, modelOptions, cancellationToken); - } -} diff --git a/src/copilot/inspect/DocumentRenderer.ts b/src/copilot/inspect/DocumentRenderer.ts deleted file mode 100644 index 4b6971eb..00000000 --- a/src/copilot/inspect/DocumentRenderer.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import { ExtensionContext, TextDocument, WorkspaceConfiguration, workspace } from "vscode"; -import { CodeLensRenderer } from "./render/CodeLensRenderer"; -import { DiagnosticRenderer } from "./render/DiagnosticRenderer"; -import { GutterIconRenderer } from "./render/GutterIconRenderer"; -import { RulerHighlightRenderer } from "./render/RulerHighlightRenderer"; -import { InspectionRenderer } from "./render/InspectionRenderer"; -import { sendInfo } from "vscode-extension-telemetry-wrapper"; -import { isCodeLensDisabled, logger } from "../utils"; -import { InspectActionCodeLensProvider } from "./InspectActionCodeLensProvider"; -import { debounce } from "lodash"; -import InspectionCache from "./InspectionCache"; - -/** - * `DocumentRenderer` is responsible for - * - managing `Rewrite with new Java syntax` code lenses renderer - * - managing inspection renderers based on settings - * - rendering inspections for a document - */ -export class DocumentRenderer { - private readonly availableRenderers: { [type: string]: InspectionRenderer } = {}; - private readonly installedRenderers: InspectionRenderer[] = []; - private readonly inspectActionCodeLensProvider: InspectActionCodeLensProvider; - private readonly rerenderDebouncelyMap: { [key: string]: (document: TextDocument) => void } = {}; - - public constructor() { - this.inspectActionCodeLensProvider = new InspectActionCodeLensProvider(); - this.availableRenderers['diagnostics'] = new DiagnosticRenderer(); - this.availableRenderers['guttericons'] = new GutterIconRenderer(); - this.availableRenderers['codelenses'] = new CodeLensRenderer(); - this.availableRenderers['rulerhighlights'] = new RulerHighlightRenderer(); - } - - public install(context: ExtensionContext): DocumentRenderer { - if (this.installedRenderers.length > 0) { - logger.warn('DefaultRenderer is already installed'); - return this; - } - this.inspectActionCodeLensProvider.install(context); - // watch for inspection renderers configuration changes - workspace.onDidChangeConfiguration(event => { - if (event.affectsConfiguration('java.copilot.inspection.renderer')) { - const settings = this.reloadInspectionRenderers(context); - sendInfo('java.copilot.inspection.renderer.changed', { 'settings': `${settings.join(',')}` }); - } - }); - this.reloadInspectionRenderers(context); - return this; - } - - /** - * rerender all inspections for the given document - * @param document the document to rerender - * @param debounced whether to rerender debouncely - */ - public async rerender(document: TextDocument, debounced: boolean = false): Promise { - if (document.languageId !== 'java') return; - if (!debounced) { - this.inspectActionCodeLensProvider.rerender(document); - this.rerenderInspections(document); - return; - } - // clear all rendered inspections first - this.installedRenderers.forEach(r => r.clear(document)); - const key = document.uri.fsPath; - if (!this.rerenderDebouncelyMap[key]) { - this.rerenderDebouncelyMap[key] = debounce((document: TextDocument) => { - this.inspectActionCodeLensProvider.rerender(document); - this.rerenderInspections(document); - }); - } - this.rerenderDebouncelyMap[key](document); - } - - private async rerenderInspections(document: TextDocument): Promise { - const inspections = await InspectionCache.getCachedInspectionsOfDoc(document); - this.installedRenderers.forEach(r => r.clear(document)); - this.installedRenderers.forEach(r => { - r.renderInspections(document, inspections); - }); - } - - private reloadInspectionRenderers(context: ExtensionContext): string[] { - this.installedRenderers.splice(0, this.installedRenderers.length); - const settings = this.reloadInspectionRendererSettings(); - Object.entries(this.availableRenderers).forEach(([type, renderer]) => { - if (settings.includes(type.toLowerCase())) { // if enabled - this.installedRenderers.push(renderer); - renderer.install(context); - } else { - renderer.uninstall(); - } - }); - return settings; - } - - /** - * get the enabled inspection renderer names - */ - private reloadInspectionRendererSettings(): string[] { - const config: WorkspaceConfiguration = workspace.getConfiguration('java.copilot.inspection.renderer'); - const types: string[] = Object.keys(this.availableRenderers); - const settings = types.map(type => config.get(type) ? type.toLowerCase() : '').filter(t => t); - if (settings.length === 0) { - settings.push('diagnostics'); - settings.push('rulerhighlights'); - const disabled = isCodeLensDisabled(); - if (disabled) { - logger.warn('CodeLens is disabled, fallback to GutterIcons'); - } - settings.push(disabled ? 'guttericons' : 'codelenses'); - } - return settings; - } -} diff --git a/src/copilot/inspect/InspectActionCodeLensProvider.ts b/src/copilot/inspect/InspectActionCodeLensProvider.ts deleted file mode 100644 index cd5e0f75..00000000 --- a/src/copilot/inspect/InspectActionCodeLensProvider.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { CodeLens, CodeLensProvider, Event, EventEmitter, ExtensionContext, TextDocument, Uri, languages } from "vscode"; -import { getTopLevelClassesOfDocument, logger } from "../utils"; -import { COMMAND_IGNORE_INSPECTIONS, COMMAND_INSPECT_CLASS } from "./commands"; -import InspectionCache from "./InspectionCache"; - -export class InspectActionCodeLensProvider implements CodeLensProvider { - private inspectCodeLenses: Map = new Map(); - private emitter: EventEmitter = new EventEmitter(); - public readonly onDidChangeCodeLenses: Event = 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 topLevelCodeLenses: CodeLens[] = []; - const classes = await getTopLevelClassesOfDocument(document); - classes.map(clazz => new CodeLens(clazz.range, { - title: "✨ Rewrite with new Java syntax", - command: COMMAND_INSPECT_CLASS, - arguments: [document, clazz] - })).forEach(codeLens => topLevelCodeLenses.push(codeLens)); - - const results = await Promise.all(classes.map(clazz => InspectionCache.hasCache(document, clazz))); - classes.filter((_, i) => results[i]).map(clazz => new CodeLens(clazz.range, { - title: "Ignore all", - command: COMMAND_IGNORE_INSPECTIONS, - arguments: [document, clazz] - })).forEach(codeLens => topLevelCodeLenses.push(codeLens)); - - this.inspectCodeLenses.set(document.uri, topLevelCodeLenses); - this.emitter.fire(); - } - - public provideCodeLenses(document: TextDocument): CodeLens[] { - return this.inspectCodeLenses.get(document.uri) ?? []; - } -} \ No newline at end of file diff --git a/src/copilot/inspect/Inspection.ts b/src/copilot/inspect/Inspection.ts deleted file mode 100644 index c27b1af3..00000000 --- a/src/copilot/inspect/Inspection.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { TextDocument, workspace, window, Selection, Range, Position } from "vscode"; -import { SymbolNode } from "./SymbolNode"; - -export interface InspectionProblem { - /** - * short description of the problem - */ - 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; - }; - /** - * indicator of the problematic code block, e.g. method name/class name, keywork, etc. - */ - indicator: string; -} - -export interface Inspection { - id: string; - document?: TextDocument; - symbol?: SymbolNode; - problem: InspectionProblem; - solution: string; - severity: string; -} - -export namespace Inspection { - export function revealFirstLineOfInspection(inspection: Inspection) { - inspection.document && void workspace.openTextDocument(inspection.document.uri).then(document => { - void window.showTextDocument(document).then(editor => { - const range = getIndicatorRangeOfInspection(inspection.problem); - editor.selection = new Selection(range.start, range.end); - editor.revealRange(range); - }); - }); - } - - /** - * 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)); - } -} \ No newline at end of file diff --git a/src/copilot/inspect/InspectionCache.ts b/src/copilot/inspect/InspectionCache.ts deleted file mode 100644 index fc5beb20..00000000 --- a/src/copilot/inspect/InspectionCache.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { SymbolKind, TextDocument } from 'vscode'; -import { METHOD_KINDS, getSymbolsContainedInRange, getSymbolsOfDocument, logger } from '../utils'; -import { Inspection } from './Inspection'; -import { SymbolNode } from './SymbolNode'; - -/** - * A map based cache for inspections of a document. - * format: `Map> = new Map(); - -export default class InspectionCache { - /** - * check if the document or the symbol is cached. - * if the symbol is provided, check if the symbol or its contained symbols are cached. - */ - public static async hasCache(document: TextDocument, symbol?: SymbolNode): Promise { - const documentKey = document.uri.fsPath; - if (!symbol) { - return DOC_SYMBOL_SNAPSHOT_INSPECTIONS.has(documentKey); - } - const symbolInspections = DOC_SYMBOL_SNAPSHOT_INSPECTIONS.get(documentKey); - // check if the symbol or its contained symbols are cached - const symbols = await getSymbolsContainedInRange(symbol.range, document); - for (const s of symbols) { - const snapshotInspections = symbolInspections?.get(s.qualifiedName); - if (snapshotInspections?.[0] === s.snapshotId && snapshotInspections[1].length > 0) { - return true; - } - } - return false; - } - - /** - * Get cached inspections of a document, if the document is not cached, return an empty array. - * Cached inspections of outdated symbols are filtered out.Symbols are considered outdated if - * their content has changed. - */ - public static async getCachedInspectionsOfDoc(document: TextDocument): Promise { - const symbols: SymbolNode[] = await getSymbolsOfDocument(document); - const inspections: Inspection[] = []; - // we don't get cached inspections directly from the cache, because we need to filter out outdated symbols - for (const symbol of symbols) { - const cachedInspections = InspectionCache.getCachedInspectionsOfSymbol(document, symbol); - inspections.push(...cachedInspections); - } - return inspections; - } - - /** - * @returns the cached inspections, or undefined if not found - */ - public static getCachedInspectionsOfSymbol(document: TextDocument, symbol: SymbolNode): Inspection[] { - const documentKey = document.uri.fsPath; - const symbolInspections = DOC_SYMBOL_SNAPSHOT_INSPECTIONS.get(documentKey); - const snapshotInspections = symbolInspections?.get(symbol.qualifiedName); - if (snapshotInspections?.[0] === symbol.snapshotId) { - logger.debug(`cache hit for ${SymbolKind[symbol.kind]} ${symbol.qualifiedName} of ${document.uri.fsPath}`); - const inspections = snapshotInspections[1]; - inspections.forEach(s => { - s.document = document; - s.problem.position.line = s.problem.position.relativeLine + symbol.range.start.line; - }); - return inspections; - } - logger.debug(`cache miss for ${SymbolKind[symbol.kind]} ${symbol.qualifiedName} of ${document.uri.fsPath}`); - return []; - } - - public static cache(document: TextDocument, symbols: SymbolNode[], inspections: Inspection[]): void { - for (const symbol of symbols) { - const isMethod = METHOD_KINDS.includes(symbol.kind); - const symbolInspections: Inspection[] = inspections.filter(inspection => { - const inspectionLine = inspection.problem.position.line; - return isMethod ? - // NOTE: method inspections are inspections whose `position.line` is within the method's range - inspectionLine >= symbol.range.start.line && inspectionLine <= symbol.range.end.line : - // NOTE: class/field inspections are inspections whose `position.line` is exactly the first line number of the class/field - inspectionLine === symbol.range.start.line; - }); - // re-calculate `relativeLine` of method inspections, `relativeLine` is the relative line number to the start of the method - symbolInspections.forEach(inspection => inspection.problem.position.relativeLine = inspection.problem.position.line - symbol.range.start.line); - InspectionCache.cacheSymbolInspections(document, symbol, symbolInspections); - } - } - - /** - * invalidate the cache of a document, a symbol, or an inspection. - * NOTE: the cached inspections of the symbol and its contained symbols will be removed when invalidating a symbol. - */ - public static invalidateInspectionCache(document?: TextDocument, symbol?: SymbolNode, inspeciton?: Inspection): void { - if (!document) { - DOC_SYMBOL_SNAPSHOT_INSPECTIONS.clear(); - } else if (!symbol) { - const documentKey = document.uri.fsPath; - DOC_SYMBOL_SNAPSHOT_INSPECTIONS.delete(documentKey); - } else if (!inspeciton) { - const documentKey = document.uri.fsPath; - const symbolInspections = DOC_SYMBOL_SNAPSHOT_INSPECTIONS.get(documentKey); - // remove the cached inspections of the symbol - symbolInspections?.delete(symbol.qualifiedName); - // remove the cached inspections of contained symbols - symbolInspections?.forEach((_, key) => { - if (key.startsWith(symbol.qualifiedName)) { - symbolInspections.delete(key); - } - }); - } else { - const documentKey = document.uri.fsPath; - const symbolInspections = DOC_SYMBOL_SNAPSHOT_INSPECTIONS.get(documentKey); - const snapshotInspections = symbolInspections?.get(symbol.qualifiedName); - if (snapshotInspections?.[0] === symbol.snapshotId) { - const inspections = snapshotInspections[1]; - // remove the inspection - inspections.splice(inspections.indexOf(inspeciton), 1); - } - } - } - - private static cacheSymbolInspections(document: TextDocument, symbol: SymbolNode, inspections: Inspection[]): void { - logger.debug(`cache ${inspections.length} inspections for ${SymbolKind[symbol.kind]} ${symbol.qualifiedName} of ${document.uri.fsPath}`); - const documentKey = document.uri.fsPath; - const cachedSymbolInspections = DOC_SYMBOL_SNAPSHOT_INSPECTIONS.get(documentKey) ?? new Map(); - inspections.forEach(s => { - s.document = document; - s.symbol = symbol; - }); - // use qualified name to prevent conflicts between symbols with the same signature in same document - cachedSymbolInspections.set(symbol.qualifiedName, [symbol.snapshotId, inspections]); - DOC_SYMBOL_SNAPSHOT_INSPECTIONS.set(documentKey, cachedSymbolInspections); - } -} diff --git a/src/copilot/inspect/InspectionCopilot.ts b/src/copilot/inspect/InspectionCopilot.ts deleted file mode 100644 index feb54c2d..00000000 --- a/src/copilot/inspect/InspectionCopilot.ts +++ /dev/null @@ -1,358 +0,0 @@ -import { sendInfo } from "vscode-extension-telemetry-wrapper"; -import Copilot from "../Copilot"; -import { fixedInstrumentSimpleOperation, getClassesContainedInRange, getInnermostClassContainsRange, getIntersectionSymbolsOfRange, getProjectJavaVersion, getUnionRange, logger } from "../utils"; -import { Inspection } from "./Inspection"; -import { TextDocument, SymbolKind, ProgressLocation, commands, Position, Range, Selection, window, LanguageModelChatMessage } from "vscode"; -import { COMMAND_FIX_INSPECTION } from "./commands"; -import InspectionCache from "./InspectionCache"; -import { SymbolNode } from "./SymbolNode"; -import { randomUUID } from "crypto"; - -export default class InspectionCopilot extends Copilot { - public static readonly FORMAT_CODE = (context: ProjectContext, code: string) => ` - Current project uses Java ${context.javaVersion}. please suggest improvements compatible with this version for code below (do not format the reponse, and do not respond markdown): - ${code} - `; - public static readonly SYSTEM_MESSAGE = ` - **You are expert at Java and promoting newer built-in features of Java.** - Your identify and suggest improvements for Java code blocks that can be optimized using newer features of Java. Keep the following guidelines in mind: - - Focus on utilizing built-in features from recent Java versions (Java 8 and onwards) to make the code more readable, efficient, and concise. - - Do not suggest the use of third-party libraries or frameworks. - - Comment directly on the code that can be improved. Use the following format for comments: - \`\`\` - other code... - // @PROBLEM: Briefly describe the issue in the code, preferably in less than 10 words. Start with a gerund/noun word, e.g., "Using". - // @SOLUTION: Suggest a solution to the problem in less than 10 words. Start with a verb. - // @INDICATOR: Identify the problematic code block with a single word contained in the block. It could be a Java keyword, a method/field/variable name, or a value (e.g., magic number). Use '' if cannot be identified. - // @SEVERITY: Rate the severity of the problem as either HIGH, MIDDLE, or LOW. - the original code that can be improved... - \`\`\` - - Place your comment directly above the code that needs to be improved, without making any changes to the original code. - - Your response should be the complete original code with your added comments. Do not make any other modifications. - - Do not comment on code that is not certain to have issues. - - Do not comment on code that is well-written or simple enough to understand. - - Do not add any explanations, do not format the output, and do not output markdown. - - Conclude your response with "//${Copilot.DEFAULT_END_MARK}". - Remember, your aim is to enhance the code, and promote the use of newer built-in Java features at the same time! - `; - public static readonly EXAMPLE_USER_MESSAGE = this.FORMAT_CODE({ javaVersion: '17' }, ` - @Entity - public class EmployeePojo implements Employee { - private final String name; - public EmployeePojo(String name) { - this.name = name; - } - public String getName() { - return name; - } - public String getRole() { - String result = ""; - if (this.name.equals("Miller")) { - result = "Senior"; - } else if (this.name.equals("Mike")) { - result = "HR"; - } else { - result = "FTE"; - } - return result; - } - }`); - public static readonly EXAMPLE_ASSISTANT_MESSAGE = ` - @Entity - // @PROBLEM: Using traditional POJO - // @SOLUTION: Use record - // @INDICATOR: EmployeePojo - // @SEVERITY: MIDDLE - public class EmployeePojo implements Employee { - private final String name; - public EmployeePojo(String name) { - this.name = name; - } - public String getName() { - return name; - } - public String getRole() { - String result = ""; - // @PROBLEM: Using multiple if-else - // @SOLUTION: Use enhanced switch expression - // @INDICATOR: if - // @SEVERITY: MIDDLE - if (this.name.equals("Miller")) { - result = "Senior"; - } else if (this.name.equals("Mike")) { - result = "HR"; - } else { - result = "FTE"; - } - return result; - } - } - //${Copilot.DEFAULT_END_MARK} - `; - - // Initialize regex patterns - private static readonly COMMENT_PATTERN: RegExp = /\/\/ @[A-Z]+: (.*)/; - private static readonly PROBLEM_PATTERN: RegExp = /\/\/ @PROBLEM: (.*)/; - private static readonly SOLUTION_PATTERN: RegExp = /\/\/ @SOLUTION: (.*)/; - private static readonly INDICATOR_PATTERN: RegExp = /\/\/ @INDICATOR: (.*)/; - private static readonly LEVEL_PATTERN: RegExp = /\/\/ @SEVERITY: (.*)/; - private static readonly INSPECTION_COMMENT_LINE_COUNT = 4; - - private static readonly DEFAULT_MAX_CONCURRENCIES: number = 3; - - private readonly debounceMap = new Map(); - private readonly inspecting: Set = new Set(); - - public constructor( - private readonly maxConcurrencies: number = InspectionCopilot.DEFAULT_MAX_CONCURRENCIES, - ) { - super([ - LanguageModelChatMessage.User(InspectionCopilot.SYSTEM_MESSAGE), - LanguageModelChatMessage.User(InspectionCopilot.EXAMPLE_USER_MESSAGE), - LanguageModelChatMessage.Assistant(InspectionCopilot.EXAMPLE_ASSISTANT_MESSAGE), - ]); - } - - public get busy(): boolean { - return this.inspecting.size >= this.maxConcurrencies; - } - - public async inspectDocument(document: TextDocument): Promise { - logger.info('inspecting document:', document.fileName); - const range = new Range(document.lineAt(0).range.start, document.lineAt(document.lineCount - 1).range.end); - return this.inspectRange(document, range); - } - - public async inspectClass(document: TextDocument, clazz: SymbolNode): Promise { - logger.info('inspecting class:', clazz.qualifiedName); - return this.inspectRange(document, clazz.range, clazz); - } - - public async inspectSymbol(document: TextDocument, symbol: SymbolNode): Promise { - logger.info(`inspecting symbol ${SymbolKind[symbol.kind]} ${symbol.qualifiedName}`); - return this.inspectRange(document, symbol.range, symbol); - } - - public async inspectRange(document: TextDocument, range: Range, symbol?: SymbolNode): Promise { - if (this.busy) { - logger.warn('Copilot is busy, please retry after current inspecting tasks is finished.'); - void window.showWarningMessage(`Copilot is busy, please retry after current inspecting tasks are finished.`); - return Promise.resolve([]); - } - if (this.inspecting.has(document)) { - return Promise.resolve([]); - } - try { - this.inspecting.add(document); - // ajust the range to the minimal container class or method symbols - const methodAndFields: SymbolNode[] = await getIntersectionSymbolsOfRange(range, document); - const classes: SymbolNode[] = await getClassesContainedInRange(range, document); - const symbols: SymbolNode[] = [...classes, ...methodAndFields]; - if (symbols.length < 1) { - const containingClass: SymbolNode = await getInnermostClassContainsRange(range, document); - symbols.push(containingClass); - } - - // get the union range of the container symbols, which will be insepcted by copilot - const expandedRange: Range = getUnionRange(symbols); - - // inspect the expanded union range - const target = symbol ? symbol.toString() : (symbols[0].toString() + (symbols.length > 1 ? ", etc." : "")); - const inspections = await window.withProgress({ - location: ProgressLocation.Notification, - title: `Inspecting ${target}...`, - cancellable: false - }, (_progress) => { - return this.doInspectRange(document, expandedRange); - }); - - // show message based on the number of inspections - if (inspections.length < 1) { - void window.showInformationMessage(`Inspected ${target}, and got 0 suggestions.`); - } else if (inspections.length == 1) { - // apply the only suggestion automatically - void commands.executeCommand(COMMAND_FIX_INSPECTION, inspections[0], 'auto'); - } else { - // show message to go to the first suggestion - // inspected a, ..., etc. and got n suggestions. - void window.showInformationMessage(`Inspected ${target}, and got ${inspections.length} suggestions.`, "Go to").then(selection => { - selection === "Go to" && void Inspection.revealFirstLineOfInspection(inspections[0]); - }); - } - InspectionCache.cache(document, symbols, inspections); - return inspections; - } finally { - this.inspecting.delete(document); - } - } - - /** - * inspect the given code (debouncely if `key` is provided) using copilot and return the inspections - * @param code code to inspect - * @param key key to debounce the inspecting, which is used to support multiple debouncing. Consider - * the case that we have multiple documents, and we only want to debounce the method calls on the - * same document (identified by `key`). - * @param wait debounce time in milliseconds, default is 3000ms - * @returns inspections provided by copilot - */ - public inspectCode(code: string, context: ProjectContext, key?: string, wait: number = 3000): Promise { - const _doInspectCode: (code: string, context: ProjectContext) => Promise = fixedInstrumentSimpleOperation("java.copilot.inspect.code", this.doInspectCode.bind(this)); - if (!key) { // inspect code immediately without debounce - return this.doInspectCode(code, context); - } - // inspect code with debounce if key is provided - if (this.debounceMap.has(key)) { - clearTimeout(this.debounceMap.get(key) as NodeJS.Timeout); - logger.debug(`debounced`, key); - } - return new Promise((resolve) => { - this.debounceMap.set(key, setTimeout(() => { - void _doInspectCode(code, context).then(inspections => { - this.debounceMap.delete(key); - resolve(inspections); - }); - }, wait <= 0 ? 3000 : wait)); - }); - } - - private async doInspectRange(document: TextDocument, range: Range | Selection): Promise { - const adjustedRange = new Range(new Position(range.start.line, 0), new Position(range.end.line, document.lineAt(range.end.line).text.length)); - const content: string = document.getText(adjustedRange); - const startLine = range.start.line; - const projectContext = await this.collectProjectContext(document); - const inspections = await this.inspectCode(content, projectContext); - inspections.forEach(s => { - s.document = document; - // real line index to the start of the document - s.problem.position.line = s.problem.position.relativeLine + startLine; - }); - return inspections; - } - - private async doInspectCode(code: string, context: ProjectContext): Promise { - const originalLines: string[] = code.split(/\r?\n/); - // code lines without empty lines and comments - const codeLines: { originalLineIndex: number, content: string }[] = this.extractCodeLines(originalLines) - const codeLinesContent = codeLines.map(l => l.content).join('\n'); - - if (codeLines.length < 1) { - return Promise.resolve([]); - } - - const codeWithInspectionComments = await this.send(InspectionCopilot.FORMAT_CODE(context, codeLinesContent)); - const inspections = this.extractInspections(codeWithInspectionComments, codeLines); - // add properties for telemetry - sendInfo('java.copilot.inspect.code', { - javaVersion: context.javaVersion, - codeLength: code.length, - codeLines: codeLines.length, - insectionsCount: inspections.length, - inspections: `[${inspections.map(i => JSON.stringify({ - problem: i.problem.description, - solution: i.solution, - })).join(',')}]`, - }); - return inspections; - } - - /** - * extract inspections from the code with inspection comments - * @param codeWithInspectionComments response from the copilot, code with inspection comments - * @param codeLines code lines without empty lines and comments - */ - private extractInspections(codeWithInspectionComments: string, codeLines: { originalLineIndex: number, content: string }[]): Inspection[] { - const lines = codeWithInspectionComments.split('\n').filter(line => line.trim().length > 0); - const inspections: Inspection[] = []; - let commentLineCount = 0; - - for (let i = 0; i < lines.length;) { - const commentMatch = lines[i].match(InspectionCopilot.COMMENT_PATTERN); - if (commentMatch) { - const inspection: Inspection | undefined = this.extractInspection(i, lines); - if (inspection) { - const codeLineIndex = i - commentLineCount; - // relative line number to the start of the code inspected, which will be ajusted relative to the start of container symbol later when caching. - inspection.problem.position.relativeLine = codeLines[codeLineIndex].originalLineIndex ?? -1; - inspection.problem.position.code = codeLines[codeLineIndex].content; - inspections.push(inspection); - i += InspectionCopilot.INSPECTION_COMMENT_LINE_COUNT; // inspection comment has 4 lines - commentLineCount += InspectionCopilot.INSPECTION_COMMENT_LINE_COUNT; - continue; - } else { - commentLineCount++; - } - } - i++; - } - - return inspections.filter(i => i.problem.indicator.trim() !== '').sort((a, b) => a.problem.position.relativeLine - b.problem.position.relativeLine); - } - - /** - * Extract inspection from the 4 line starting at the given index - * @param index the index of the first line of the inspection comment - * @param lines all lines of the code with inspection comments - * @returns inspection object - */ - private extractInspection(index: number, lines: string[]): Inspection | undefined { - const problemMatch = lines[index + 0].match(InspectionCopilot.PROBLEM_PATTERN); - const solutionMatch = lines[index + 1].match(InspectionCopilot.SOLUTION_PATTERN); - const indicatorMatch = lines[index + 2].match(InspectionCopilot.INDICATOR_PATTERN); - const severityMatch = lines[index + 3].match(InspectionCopilot.LEVEL_PATTERN); - if (problemMatch && solutionMatch && indicatorMatch && severityMatch) { - return { - id: randomUUID().toString(), - problem: { - description: problemMatch[1].trim(), - position: { line: -1, relativeLine: -1, code: '' }, - indicator: indicatorMatch[1].trim() - }, - solution: solutionMatch[1].trim(), - severity: severityMatch[1].trim() - }; - } else { - logger.error('Failed to extract inspection from the lines:', lines.slice(index, index + 4)); - return undefined; - } - } - - /** - * Extract code lines only without empty and comment lines - * @param originalLines original code lines with comments and empty lines - * @returns code lines (including line content and corresponding original line index) without empty lines and comments - */ - private extractCodeLines(originalLines: string[]): { originalLineIndex: number, content: string }[] { - const codeLines: { originalLineIndex: number, content: string }[] = []; - let inBlockComment = false; - for (let originalLineIndex = 0; originalLineIndex < originalLines.length; originalLineIndex++) { - const trimmedLine = originalLines[originalLineIndex].trim(); - - // Check for block comment start - if (trimmedLine.startsWith('/*')) { - inBlockComment = true; - } - - // If we're not in a block comment, add the line to the output - if (trimmedLine !== '' && !inBlockComment && !trimmedLine.startsWith('//')) { - codeLines.push({ content: originalLines[originalLineIndex], originalLineIndex }); - } - - // Check for block comment end - if (trimmedLine.endsWith('*/')) { - inBlockComment = false; - } - } - return codeLines; - } - - async collectProjectContext(document: TextDocument): Promise { - logger.info('colleteting project context info (java version)...'); - const javaVersion = await getProjectJavaVersion(document); - logger.info('project java version:', javaVersion); - return { javaVersion }; - } -} - -export interface ProjectContext { - javaVersion: string; -} \ No newline at end of file diff --git a/src/copilot/inspect/SymbolNode.ts b/src/copilot/inspect/SymbolNode.ts deleted file mode 100644 index 2396f9aa..00000000 --- a/src/copilot/inspect/SymbolNode.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { DocumentSymbol, SymbolKind, Range, TextDocument } from "vscode"; -import * as crypto from "crypto"; - -/** - * A wrapper class for DocumentSymbol to provide additional functionalities: - * - parent: the parent symbol - * - qualifiedName: the fully qualified name of the symbol - */ -export class SymbolNode { - public readonly snapshotId: string; - - public constructor( - public readonly document: TextDocument, - public readonly symbol: DocumentSymbol, - public readonly parent?: SymbolNode - ) { - // calculate the snapshot id of the symbol immediately because the symbol content may change. - this.snapshotId = SymbolNode.calculateSymbolSnapshotId(document, symbol); - } - - public get range(): Range { - return this.symbol.range; - } - - public get kind(): SymbolKind { - return this.symbol.kind; - } - - /** - * The fully qualified name of the symbol. - */ - public get qualifiedName(): string { - if (this.parent) { - return this.parent.qualifiedName + "." + this.symbol.name; - } else { - return this.symbol.name; - } - } - - public get children(): SymbolNode[] { - return this.symbol.children.map(symbol => new SymbolNode(this.document, symbol, this)); - } - - /** - * generate a unique id for the symbol based on its content, so that we can detect if the symbol has changed - */ - private static calculateSymbolSnapshotId(document: TextDocument, symbol: DocumentSymbol): string { - const body = document.getText(symbol.range); - return crypto.createHash('sha256').update(body).digest("hex") - } - - public toString(): string { - return `${SymbolKind[this.kind].toLowerCase()} ${this.symbol.name}`; - } -} \ No newline at end of file diff --git a/src/copilot/inspect/commands.ts b/src/copilot/inspect/commands.ts deleted file mode 100644 index 93c23990..00000000 --- a/src/copilot/inspect/commands.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { TextDocument, Range, Selection, commands, window, Uri, env } from "vscode"; -import { instrumentOperationAsVsCodeCommand, sendInfo } from "vscode-extension-telemetry-wrapper"; -import InspectionCopilot from "./InspectionCopilot"; -import { Inspection, InspectionProblem } from "./Inspection"; -import { logger, uncapitalize } from "../utils"; -import { SymbolNode } from "./SymbolNode"; -import { DocumentRenderer } from "./DocumentRenderer"; -import InspectionCache from "./InspectionCache"; -import path from "path"; - -export const COMMAND_INSPECT_CLASS = 'java.copilot.inspect.class'; -export const COMMAND_INSPECT_RANGE = 'java.copilot.inspect.range'; -export const COMMAND_FIX_INSPECTION = 'java.copilot.inspection.fix'; -export const COMMAND_IGNORE_INSPECTIONS = 'java.copilot.inspection.ignore'; - -const LEARN_MORE_RESPONSE_FILTERED = 'https://docs.github.com/en/copilot/configuring-github-copilot/configuring-github-copilot-settings-on-githubcom#enabling-or-disabling-duplication-detection'; - -export function registerCommands(copilot: InspectionCopilot, renderer: DocumentRenderer) { - instrumentOperationAsVsCodeCommand(COMMAND_INSPECT_CLASS, async (document: TextDocument, clazz: SymbolNode) => { - try { - await copilot.inspectClass(document, clazz); - } catch (e) { - showErrorMessage(e, document, clazz); - logger.error(`Failed to inspect class "${clazz.symbol.name}".`, e); - throw e; - } - renderer.rerender(document); - }); - - instrumentOperationAsVsCodeCommand(COMMAND_INSPECT_RANGE, async (document: TextDocument, range: Range | Selection) => { - try { - await copilot.inspectRange(document, range); - } catch (e) { - showErrorMessage(e, document, range); - logger.error(`Failed to inspect range of "${path.basename(document.fileName)}".`, e); - throw e; - } - renderer.rerender(document); - }); - - instrumentOperationAsVsCodeCommand(COMMAND_FIX_INSPECTION, 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_INSPECTION}.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!.start), - initialRange: new Range(range!.start, range!.start) - }); - }); - - instrumentOperationAsVsCodeCommand(COMMAND_IGNORE_INSPECTIONS, async (document: TextDocument, symbol?: SymbolNode, inspection?: Inspection) => { - if (inspection) { - sendInfo(`${COMMAND_IGNORE_INSPECTIONS}.info`, { problem: inspection.problem.description, solution: inspection.solution }); - } - InspectionCache.invalidateInspectionCache(document, symbol, inspection); - renderer.rerender(document); - }); -} - -function showErrorMessage(e: unknown, document: TextDocument, target: SymbolNode | Range) { - let message = target instanceof Range ? - `Failed to inspect range of "${path.basename(document.fileName)}", ${e}` : - `Failed to inspect class "${target.symbol.name}", ${e}`; - - const actions = new Map void>(); - if (e instanceof Error && e.message.toLowerCase().includes('response got filtered')) { - actions.set('Learn more', () => env.openExternal(Uri.parse(LEARN_MORE_RESPONSE_FILTERED))); - message += ', possibly because it matches existing public code'; - } - window.showErrorMessage(`${message}.`, ...actions.keys()).then(choice => { - if (choice) { - actions.get(choice)!(); - } - }); -} diff --git a/src/copilot/inspect/index.ts b/src/copilot/inspect/index.ts deleted file mode 100644 index 595915f5..00000000 --- a/src/copilot/inspect/index.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { CancellationToken, CodeAction, CodeActionContext, CodeActionKind, ExtensionContext, TextDocument, languages, window, workspace, Range, Selection, extensions } from "vscode"; -import { COMMAND_INSPECT_RANGE, registerCommands } from "./commands"; -import { DocumentRenderer } from "./DocumentRenderer"; -import { fixDiagnostic } from "./render/DiagnosticRenderer"; -import InspectionCache from "./InspectionCache"; -import { logger } from "../utils"; -import { sendInfo } from "vscode-extension-telemetry-wrapper"; -import InspectionCopilot from "./InspectionCopilot"; - -export const DEPENDENT_EXTENSIONS = ['github.copilot-chat', 'redhat.java']; - -export async function activateCopilotInspection(context: ExtensionContext): Promise { - logger.info('Waiting for dependent extensions to be ready...'); - await waitUntilExtensionsInstalled(DEPENDENT_EXTENSIONS); - await waitUntilExtensionsActivated(DEPENDENT_EXTENSIONS); - logger.info('Activating Java Copilot features...'); - doActivate(context); - logger.info('Java Copilot features activated.'); -} - -export function doActivate(context: ExtensionContext): void { - const copilot = new InspectionCopilot(); - const renderer: DocumentRenderer = new DocumentRenderer().install(context); - registerCommands(copilot, renderer); - - context.subscriptions.push( - languages.registerCodeActionsProvider({ language: 'java' }, { provideCodeActions: fixDiagnostic }), // Fix using Copilot - languages.registerCodeActionsProvider({ language: 'java' }, { provideCodeActions: rewrite }), // Inspect using Copilot - workspace.onDidOpenTextDocument(doc => renderer.rerender(doc)), // Rerender class codelens and cached suggestions on document open - workspace.onDidChangeTextDocument(e => renderer.rerender(e.document, true)), // Rerender class codelens and cached suggestions debouncely on document change - window.onDidChangeVisibleTextEditors(editors => editors.forEach(editor => renderer.rerender(editor.document))), // rerender in case of renderers changed. - workspace.onDidCloseTextDocument(doc => InspectionCache.invalidateInspectionCache(doc)), // Rerender class codelens and cached suggestions debouncely on document change - ); - window.visibleTextEditors.forEach(editor => renderer.rerender(editor.document)); -} - -async function rewrite(document: TextDocument, range: Range | Selection, _context: CodeActionContext, _token: CancellationToken): Promise { - const action: CodeAction = { - title: "Rewrite with new Java syntax", - kind: CodeActionKind.RefactorRewrite, - command: { - title: "Rewrite selected code", - command: COMMAND_INSPECT_RANGE, - arguments: [document, range] - } - }; - return [action]; -} - -export async function waitUntilExtensionsActivated(extensionIds: string[], interval: number = 1500) { - const start = Date.now(); - return new Promise((resolve) => { - const notActivatedExtensionIds = extensionIds.filter(id => !extensions.getExtension(id)?.isActive); - if (notActivatedExtensionIds.length == 0) { - logger.info(`All dependent extensions [${extensionIds.join(', ')}] are activated.`); - return resolve(); - } - logger.info(`Dependent extensions [${notActivatedExtensionIds.join(', ')}] are not activated, waiting...`); - const id = setInterval(() => { - if (extensionIds.every(id => extensions.getExtension(id)?.isActive)) { - clearInterval(id); - sendInfo('java.copilot.inspection.dependentExtensions.waitActivated', { time: Date.now() - start }); - logger.info(`waited for ${Date.now() - start}ms for all dependent extensions [${extensionIds.join(', ')}] to be activated.`); - resolve(); - } - }, interval); - }); -} - -export async function waitUntilExtensionsInstalled(extensionIds: string[]) { - const start = Date.now(); - return new Promise((resolve) => { - const notInstalledExtensionIds = extensionIds.filter(id => !extensions.getExtension(id)); - if (notInstalledExtensionIds.length == 0) { - logger.info(`All dependent extensions [${extensionIds.join(', ')}] are installed.`); - return resolve(); - } - sendInfo('java.copilot.inspection.dependentExtensions.notInstalledExtensions', { extensionIds: `[${notInstalledExtensionIds.join(',')}]` }); - logger.info(`Dependent extensions [${notInstalledExtensionIds.join(', ')}] are not installed, waiting...`); - - const disposable = extensions.onDidChange(() => { - if (extensionIds.every(id => extensions.getExtension(id))) { - disposable.dispose(); - sendInfo('java.copilot.inspection.dependentExtensions.waitInstalled', { time: Date.now() - start }); - logger.info(`waited for ${Date.now() - start}ms for all dependent extensions [${extensionIds.join(', ')}] to be installed.`); - resolve(); - } - }); - }); -} \ No newline at end of file diff --git a/src/copilot/inspect/render/CodeLensRenderer.ts b/src/copilot/inspect/render/CodeLensRenderer.ts deleted file mode 100644 index 47b97129..00000000 --- a/src/copilot/inspect/render/CodeLensRenderer.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import { CodeLens, CodeLensProvider, Command, Range, Disposable, Event, EventEmitter, ExtensionContext, TextDocument, Uri, languages } from "vscode"; -import { Inspection } from "../Inspection"; -import { InspectionRenderer } from "./InspectionRenderer"; -import { logger, uncapitalize } from "../../../copilot/utils"; -import { COMMAND_IGNORE_INSPECTIONS, COMMAND_FIX_INSPECTION } from "../commands"; -import { capitalize } from "lodash"; -import _ from "lodash"; - -export class CodeLensRenderer implements InspectionRenderer { - private readonly codeLenses: Map = new Map(); - private readonly provider = new InspectionCodeLensProvider(this.codeLenses); - private disposableRegistry: Disposable | undefined; - - public install(context: ExtensionContext): InspectionRenderer { - if (this.disposableRegistry) return this; - logger.debug(`[CodeLensRenderer] install`); - this.disposableRegistry = languages.registerCodeLensProvider({ language: 'java' }, this.provider); - context.subscriptions.push(this.disposableRegistry); - return this; - } - - public uninstall(): void { - if (!this.disposableRegistry) return; - logger.debug(`[CodeLensRenderer] uninstall`); - this.codeLenses.clear(); - this.disposableRegistry.dispose(); - this.provider.refresh(); - this.disposableRegistry = undefined; - } - - public clear(document?: TextDocument): void { - if (document) { - this.codeLenses?.set(document.uri, []); - } else { - this.codeLenses.clear(); - } - this.provider.refresh(); - } - - public renderInspections(document: TextDocument, inspections: Inspection[]): void { - if (inspections.length < 1 || !this.codeLenses) { - return; - } - const oldItems = this.codeLenses.get(document.uri) ?? []; - const oldIds: string[] = _.uniq(oldItems).map(c => c.inspection.id); - const newIds: string[] = inspections.map(i => i.id); - const toKeep: InspectionCodeLens[] = _.intersection(oldIds, newIds).map(id => oldItems.find(c => c.inspection.id === id)!) ?? []; - const toAdd: InspectionCodeLens[] = _.difference(newIds, oldIds).map(id => inspections.find(i => i.id === id)!) - .flatMap(i => CodeLensRenderer.toCodeLenses(document, i)); - this.codeLenses.set(document.uri, [...toKeep, ...toAdd]); - this.provider.refresh(); - } - - private static toCodeLenses(document: TextDocument, inspection: Inspection): InspectionCodeLens[] { - const codeLenses = []; - const range = Inspection.getIndicatorRangeOfInspection(inspection.problem); - const inspectionCodeLens = new InspectionCodeLens(inspection, range, { - title: capitalize(inspection.solution), - tooltip: inspection.problem.description, - command: COMMAND_FIX_INSPECTION, - arguments: [inspection.problem, inspection.solution, 'codelenses'] - }); - codeLenses.push(inspectionCodeLens); - - const ignoreCodeLens = new InspectionCodeLens(inspection, range, { - title: 'Ignore', - tooltip: `Ignore "${uncapitalize(inspection.problem.description)}"`, - command: COMMAND_IGNORE_INSPECTIONS, - arguments: [document, inspection.symbol, inspection] - }); - codeLenses.push(ignoreCodeLens); - return codeLenses; - } -} - -class InspectionCodeLens extends CodeLens { - public constructor(public readonly inspection: Inspection, range: Range, command?: Command) { - super(range, command); - } -} - -class InspectionCodeLensProvider implements CodeLensProvider { - private readonly emitter: EventEmitter = new EventEmitter(); - public readonly onDidChangeCodeLenses: Event = this.emitter.event; - - constructor(private readonly codeLenses: Map) { } - - provideCodeLenses(document: TextDocument): CodeLens[] { - return this.codeLenses.get(document.uri) ?? []; - } - - refresh(): void { - this.emitter.fire(); - } -} - diff --git a/src/copilot/inspect/render/DiagnosticRenderer.ts b/src/copilot/inspect/render/DiagnosticRenderer.ts deleted file mode 100644 index 3bca6b6a..00000000 --- a/src/copilot/inspect/render/DiagnosticRenderer.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import { CancellationToken, CodeAction, CodeActionContext, CodeActionKind, Diagnostic, DiagnosticCollection, DiagnosticSeverity, ExtensionContext, Range, Selection, TextDocument, languages } from "vscode"; -import { Inspection } from "../Inspection"; -import { InspectionRenderer } from "./InspectionRenderer"; -import { logger } from "../../../copilot/utils"; -import { COMMAND_FIX_INSPECTION } from "../commands"; -import _ from "lodash"; - -const DIAGNOSTICS_GROUP = 'java.copilot.inspection.diagnostics'; - -export class DiagnosticRenderer implements InspectionRenderer { - private diagnostics: DiagnosticCollection | undefined; - - public install(context: ExtensionContext): InspectionRenderer { - if (this.diagnostics) return this; - logger.debug('[DiagnosticRenderer] install...'); - this.diagnostics = languages.createDiagnosticCollection(DIAGNOSTICS_GROUP); - context.subscriptions.push(this.diagnostics); - return this; - } - - public uninstall(): void { - if (!this.diagnostics) return; - logger.debug('[DiagnosticRenderer] uninstall...'); - this.diagnostics.clear(); - this.diagnostics.dispose(); - this.diagnostics = undefined; - } - - public clear(document?: TextDocument): void { - if (document) { - this.diagnostics?.set(document.uri, []); - } else { - this.diagnostics?.clear(); - } - } - - public renderInspections(document: TextDocument, inspections: Inspection[]): void { - if (inspections.length < 1 || !this.diagnostics) { - return; - } - const oldItems: readonly InspectionDiagnostic[] = (this.diagnostics.get(document.uri) ?? []) as InspectionDiagnostic[]; - const oldIds: string[] = _.uniq(oldItems).map(c => c.inspection.id); - const newIds: string[] = inspections.map(i => i.id); - const toKeep: InspectionDiagnostic[] = _.intersection(oldIds, newIds).map(id => oldItems.find(c => c.inspection.id === id)!) ?? []; - const toAdd: InspectionDiagnostic[] = _.difference(newIds, oldIds).map(id => inspections.find(i => i.id === id)!).map(i => new InspectionDiagnostic(i)); - this.diagnostics.set(document.uri, [...toKeep, ...toAdd]); - } -} - -class InspectionDiagnostic extends Diagnostic { - public constructor(public readonly inspection: Inspection) { - const range = Inspection.getIndicatorRangeOfInspection(inspection.problem); - const severiy = inspection.severity.toUpperCase() === 'HIGH' ? DiagnosticSeverity.Information : DiagnosticSeverity.Hint; - super(range, inspection.problem.description, severiy); - this.source = DIAGNOSTICS_GROUP; - } -} - -export async function fixDiagnostic(document: TextDocument, _range: Range | Selection, context: CodeActionContext, _token: CancellationToken): Promise { - if (document?.languageId !== 'java') { - return []; - } - const actions: CodeAction[] = []; - for (const diagnostic of context.diagnostics) { - if (diagnostic.source !== DIAGNOSTICS_GROUP) { - continue; - } - const inspection: Inspection = (diagnostic as InspectionDiagnostic).inspection as Inspection; - const fixAction: CodeAction = { - title: inspection.solution, - diagnostics: [diagnostic], - kind: CodeActionKind.RefactorRewrite, - isPreferred: true, - command: { - title: diagnostic.message, - command: COMMAND_FIX_INSPECTION, - arguments: [inspection.problem, inspection.solution, 'diagnostics'] - } - }; - actions.push(fixAction); - } - return actions; -} diff --git a/src/copilot/inspect/render/GutterIconRenderer.ts b/src/copilot/inspect/render/GutterIconRenderer.ts deleted file mode 100644 index 100f9383..00000000 --- a/src/copilot/inspect/render/GutterIconRenderer.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import { DecorationOptions, ExtensionContext, MarkdownString, TextDocument, TextEditorDecorationType, Uri, window } from "vscode"; -import { Inspection } from "../Inspection"; -import { InspectionRenderer } from "./InspectionRenderer"; -import { logger } from "../../../copilot/utils"; -import path = require("path"); -import { COMMAND_FIX_INSPECTION } from "../commands"; -import _ from "lodash"; - -export class GutterIconRenderer implements InspectionRenderer { - private readonly gutterIcons: Map = new Map(); - private gutterIconDecorationType: TextEditorDecorationType | undefined; - - public install(context: ExtensionContext): InspectionRenderer { - if (this.gutterIconDecorationType) return this; - logger.debug(`[GutterIconRenderer] install`); - const icon = Uri.file(path.join(context.asAbsolutePath('resources'), `gutter-blue.svg`)); - this.gutterIconDecorationType = window.createTextEditorDecorationType({ - isWholeLine: true, - gutterIconPath: icon, - gutterIconSize: 'contain' - }); - return this; - } - - public uninstall(): void { - if (!this.gutterIconDecorationType) return; - logger.debug(`[GutterIconRenderer] uninstall`); - this.gutterIcons.clear(); - window.visibleTextEditors.forEach(editor => this.gutterIconDecorationType && editor.setDecorations(this.gutterIconDecorationType, [])); - this.gutterIconDecorationType.dispose(); - this.gutterIconDecorationType = undefined; - } - - public clear(document?: TextDocument): void { - if (!this.gutterIconDecorationType) return; - if (document) { - this.gutterIcons?.set(document.uri, []); - } else { - this.gutterIcons?.clear(); - } - window.visibleTextEditors.forEach(editor => this.gutterIconDecorationType && editor.setDecorations(this.gutterIconDecorationType, [])); - } - - public renderInspections(document: TextDocument, inspections: Inspection[]): void { - const editor = window.visibleTextEditors.find(e => e.document.uri === document.uri); - if (inspections.length < 1 || !editor || !this.gutterIconDecorationType) { - return; - } - - const oldItems: readonly InspectionGutterIcon[] = this.gutterIcons.get(document.uri) ?? []; - const oldIds: string[] = _.uniq(oldItems).map(c => c.inspection.id); - const newIds: string[] = inspections.map(i => i.id); - const toKeep: InspectionGutterIcon[] = _.intersection(oldIds, newIds).map(id => oldItems.find(c => c.inspection.id === id)!) ?? []; - const toAdd: InspectionGutterIcon[] = _.difference(newIds, oldIds).map(id => inspections.find(i => i.id === id)!).map(i => GutterIconRenderer.toGutterIcon(i)); - const newGutterIcons: InspectionGutterIcon[] = [...toKeep, ...toAdd]; - this.gutterIcons.set(document.uri, newGutterIcons); - - editor.setDecorations(this.gutterIconDecorationType, newGutterIcons); - } - - private static toGutterIcon(inspection: Inspection): InspectionGutterIcon { - const range = Inspection.getIndicatorRangeOfInspection(inspection.problem); - const args = [inspection.problem, inspection.solution, 'guttericons']; - const commandUri = Uri.parse(`command:${COMMAND_FIX_INSPECTION}?${encodeURIComponent(JSON.stringify(args))}`); - const hoverMessage = new MarkdownString(`${inspection.problem.description}\n\n$(copilot) [${inspection.solution}](${commandUri})`, true); - hoverMessage.isTrusted = true; - return { range, hoverMessage, inspection }; - } -} - -interface InspectionGutterIcon extends DecorationOptions { - inspection: Inspection; -} diff --git a/src/copilot/inspect/render/InspectionRenderer.ts b/src/copilot/inspect/render/InspectionRenderer.ts deleted file mode 100644 index db507745..00000000 --- a/src/copilot/inspect/render/InspectionRenderer.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ExtensionContext, TextDocument } from "vscode"; -import { Inspection } from "../Inspection"; - -export interface InspectionRenderer { - install(context: ExtensionContext): InspectionRenderer; - uninstall(): void; - clear(document?: TextDocument): void; - renderInspections(document: TextDocument, inspections: Inspection[]): void; -} \ No newline at end of file diff --git a/src/copilot/inspect/render/RulerHighlightRenderer.ts b/src/copilot/inspect/render/RulerHighlightRenderer.ts deleted file mode 100644 index 1e9469a5..00000000 --- a/src/copilot/inspect/render/RulerHighlightRenderer.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import { DecorationOptions, ExtensionContext, OverviewRulerLane, TextDocument, TextEditorDecorationType, ThemeColor, Uri, window } from "vscode"; -import { Inspection } from "../Inspection"; -import { InspectionRenderer } from "./InspectionRenderer"; -import { logger } from "../../../copilot/utils"; -import _ from "lodash"; - -export class RulerHighlightRenderer implements InspectionRenderer { - private readonly rulerHighlights: Map = new Map(); - private rulerDecorationType: TextEditorDecorationType | undefined; - - public install(_context: ExtensionContext): InspectionRenderer { - if (this.rulerDecorationType) return this; - logger.debug(`[RulerRenderer] install`); - const color = new ThemeColor('textLink.foreground'); - this.rulerDecorationType = window.createTextEditorDecorationType({ - isWholeLine: true, - overviewRulerLane: OverviewRulerLane.Right, - overviewRulerColor: color - }); - return this; - } - - public uninstall(): void { - if (!this.rulerDecorationType) return; - logger.debug(`[RulerRenderer] uninstall`); - this.rulerHighlights.clear(); - window.visibleTextEditors.forEach(editor => this.rulerDecorationType && editor.setDecorations(this.rulerDecorationType, [])); - this.rulerDecorationType.dispose(); - this.rulerDecorationType = undefined; - } - - public clear(document?: TextDocument): void { - if (!this.rulerDecorationType) return; - if (document) { - this.rulerHighlights?.set(document.uri, []); - } else { - this.rulerHighlights?.clear(); - } - window.visibleTextEditors.forEach(editor => this.rulerDecorationType && editor.setDecorations(this.rulerDecorationType, [])); - } - - public renderInspections(document: TextDocument, inspections: Inspection[]): void { - const editor = window.visibleTextEditors.find(e => e.document.uri === document.uri); - if (inspections.length < 1 || !editor || !this.rulerDecorationType) { - return; - } - const oldItems: readonly InspectionRulerHighlight[] = this.rulerHighlights.get(document.uri) ?? []; - const oldIds: string[] = _.uniq(oldItems).map(c => c.inspection.id); - const newIds: string[] = inspections.map(i => i.id); - const toKeep: InspectionRulerHighlight[] = _.intersection(oldIds, newIds).map(id => oldItems.find(c => c.inspection.id === id)!) ?? []; - const toAdd: InspectionRulerHighlight[] = _.difference(newIds, oldIds).map(id => inspections.find(i => i.id === id)!).map(i => RulerHighlightRenderer.toRulerHighlight(i)); - const newRulerHightlights: InspectionRulerHighlight[] = [...toKeep, ...toAdd]; - this.rulerHighlights.set(document.uri, newRulerHightlights); - - editor.setDecorations(this.rulerDecorationType, newRulerHightlights); - } - - private static toRulerHighlight(inspection: Inspection): InspectionRulerHighlight { - const range = Inspection.getIndicatorRangeOfInspection(inspection.problem); - return { range, inspection }; - } -} - -interface InspectionRulerHighlight extends DecorationOptions { - inspection: Inspection; -} diff --git a/src/copilot/utils.ts b/src/copilot/utils.ts deleted file mode 100644 index 8ad431db..00000000 --- a/src/copilot/utils.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { LogOutputChannel, SymbolKind, TextDocument, commands, window, Range, Selection, workspace, DocumentSymbol, version } from "vscode"; -import { SymbolNode } from "./inspect/SymbolNode"; -import { SemVer } from "semver"; -import { createUuid, sendOperationEnd, sendOperationError, sendOperationStart } from "vscode-extension-telemetry-wrapper"; - -export const CLASS_KINDS: SymbolKind[] = [SymbolKind.Class, SymbolKind.Interface, SymbolKind.Enum]; -export const METHOD_KINDS: SymbolKind[] = [SymbolKind.Method, SymbolKind.Constructor]; -export const FIELD_KINDS: SymbolKind[] = [SymbolKind.Field, SymbolKind.Property, SymbolKind.Constant]; - -export const logger: LogOutputChannel = window.createOutputChannel("Java Rewriting Suggestions", { log: true }); - -/** - * get all the class symbols contained in the `range` in the `document` - */ -export async function getClassesContainedInRange(range: Range | Selection, document: TextDocument): Promise { - const symbols = await getSymbolsOfDocument(document); - return symbols.filter(symbol => CLASS_KINDS.includes(symbol.kind)) - .filter(clazz => range.contains(clazz.range)); -} - -export async function getSymbolsContainedInRange(range: Range | Selection, document: TextDocument): Promise { - const symbols = await getSymbolsOfDocument(document); - return symbols.filter(symbol => range.contains(symbol.range)); -} - -/** - * get the innermost class symbol that completely contains the `range` in the `document` - */ -export async function getInnermostClassContainsRange(range: Range | Selection, document: TextDocument): Promise { - const symbols = await getSymbolsOfDocument(document); - return symbols.filter(symbol => CLASS_KINDS.includes(symbol.kind)) - // reverse the classes to get the innermost class first - .reverse().filter(clazz => clazz.range.contains(range))[0]; -} - -/** - * get all the method/field symbols that are completely or partially contained in the `range` in the `document` - */ -export async function getIntersectionSymbolsOfRange(range: Range | Selection, document: TextDocument): Promise { - const symbols = await getSymbolsOfDocument(document); - return symbols.filter(symbol => METHOD_KINDS.includes(symbol.kind) || FIELD_KINDS.includes(symbol.kind)) - .filter(method => method.range.intersection(range)); -} - -export function getUnionRange(symbols: SymbolNode[]): Range { - let result: Range = new Range(symbols[0].range.start, symbols[0].range.end); - for (const symbol of symbols) { - result = result.union(symbol.range); - } - return result; -} - -/** - * get all classes (classes inside methods are not considered) and methods of a document in a pre-order traversal manner - */ -export async function getSymbolsOfDocument(document: TextDocument): Promise { - const stack = ((await commands.executeCommand('vscode.executeDocumentSymbolProvider', document.uri)) ?? []).reverse().map(symbol => new SymbolNode(document, symbol)); - - const result: SymbolNode[] = []; - while (stack.length > 0) { - const symbol = stack.pop() as SymbolNode; - if (CLASS_KINDS.includes(symbol.kind)) { - result.push(symbol); - stack.push(...symbol.children.reverse()); - } else if (METHOD_KINDS.includes(symbol.kind) || FIELD_KINDS.includes(symbol.kind)) { - result.push(symbol); - } - } - return result; -} - -export async function getTopLevelClassesOfDocument(document: TextDocument): Promise { - const symbols = ((await commands.executeCommand('vscode.executeDocumentSymbolProvider', document.uri)) ?? []); - return symbols.filter(symbol => CLASS_KINDS.includes(symbol.kind)).map(symbol => new SymbolNode(document, symbol)); -} - -export function uncapitalize(str: string): string { - return str.charAt(0).toLowerCase() + str.slice(1); -} - -export function isCodeLensDisabled(): boolean { - const editorConfig = workspace.getConfiguration('editor'); - const enabled = editorConfig.get('codeLens'); - // If it's explicitly set to false, CodeLens is turned off - return enabled === false; -} - -export async function getProjectJavaVersion(document: TextDocument): Promise { - const uri = document.uri.toString(); - const key = "org.eclipse.jdt.core.compiler.source"; - try { - const settings: { [key]: string } = await retryOnFailure(async () => { - return await commands.executeCommand("java.project.getSettings", uri, [key]); - }); - return settings[key] ?? '17'; - } catch (e) { - throw new Error(`Failed to get Java version, please check if the project is loaded normally: ${e}`); - } -} - -export async function retryOnFailure(task: () => Promise, timeout: number = 15000, retryInterval: number = 3000): Promise { - const startTime = Date.now(); - - while (true) { - try { - return await task(); - } catch (error) { - if (Date.now() - startTime >= timeout) { - throw error; - } else { - await new Promise(resolve => setTimeout(resolve, retryInterval)); - } - } - } -} - -export function isLlmApiReady(): boolean { - return new SemVer(version).compare(new SemVer("1.90.0-insider")) >= 0; -} - -/** - * copied from vscode-extension-telemetry-wrapper, the only difference is we re-throw the error to the caller but the original one doesn't. - */ -export function fixedInstrumentOperation( - operationName: string, - cb: (operationId: string, ...args: any[]) => any, - thisArg?: any, -): (...args: any[]) => any { - return async (...args: any[]) => { - let error; - const operationId = createUuid(); - const startAt: number = Date.now(); - - try { - sendOperationStart(operationId, operationName); - return await cb.apply(thisArg, [operationId, ...args]); - } catch (e) { - error = e as Error; - sendOperationError(operationId, operationName, error); - // NOTE: re-throw the error to the caller - throw e; - } finally { - const duration = Date.now() - startAt; - sendOperationEnd(operationId, operationName, duration, error); - } - }; -} - -/** - * copied from vscode-extension-telemetry-wrapper, the only difference is we re-throw the error to the caller but the original one doesn't. - */ -export function fixedInstrumentSimpleOperation(operationName: string, cb: (...args: any[]) => any, thisArg?: any): (...args: any[]) => any { - return fixedInstrumentOperation(operationName, async (_operationId, ...args) => await cb.apply(thisArg, args), thisArg /** unnecessary */); -} \ No newline at end of file