From 8f56d1642bd384ae011f92d1a99b23e6d627370e Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Mon, 27 Oct 2025 16:45:07 +0800 Subject: [PATCH 01/11] feat: add exp for dependes --- .../jdtls/ext/core/ProjectCommand.java | 4 + .../ext/core/parser/ContextResolver.java | 4 +- package-lock.json | 174 +++++++++++++- package.json | 3 +- src/commands.ts | 6 + src/copilot/contextProvider.ts | 194 ++++++++++++++++ src/copilot/copilotHelper.ts | 119 ++++++++++ src/copilot/utils.ts | 213 ++++++++++++++++++ src/extension.ts | 2 + src/java/jdtls.ts | 5 + 10 files changed, 720 insertions(+), 4 deletions(-) create mode 100644 src/copilot/contextProvider.ts create mode 100644 src/copilot/copilotHelper.ts create mode 100644 src/copilot/utils.ts diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java index 4aa6632b..2734f514 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java @@ -516,6 +516,10 @@ public static List getProjectDependencies(List arguments } String projectUri = (String) arguments.get(0); + if (projectUri == null || projectUri.isEmpty()) { + return new ArrayList<>(); + } + List resolverResult = ProjectResolver.resolveProjectDependencies(projectUri, monitor); // Convert ProjectResolver.DependencyInfo to ProjectCommand.DependencyInfo diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java index bf815c50..2d1ee610 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java @@ -71,11 +71,11 @@ public class ContextResolver { */ public static class ImportClassInfo { public String uri; // File URI (required) - public String className; // Human-readable class description with JavaDoc appended (required) + public String value; // Human-readable class description with JavaDoc appended (required) public ImportClassInfo(String uri, String value) { this.uri = uri; - this.className = value; + this.value = value; } } diff --git a/package-lock.json b/package-lock.json index f8fdb6bb..061cbfa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,9 +6,10 @@ "packages": { "": { "name": "vscode-java-dependency", - "version": "0.26.1", + "version": "0.26.2", "license": "MIT", "dependencies": { + "@github/copilot-language-server": "^1.388.0", "await-lock": "^2.2.2", "fmtr": "^1.1.4", "fs-extra": "^10.1.0", @@ -158,6 +159,90 @@ "node": ">=10.0.0" } }, + "node_modules/@github/copilot-language-server": { + "version": "1.388.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server/-/copilot-language-server-1.388.0.tgz", + "integrity": "sha512-u8+ePDN9U0DztUe7Y07GMBWvcJIEf6/VdGSHKIXPcyy/MrZpfY3aZ/ION1KSx7UR3OhNxXrLAGiXT9JH+DA35A==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "^3.17.5" + }, + "bin": { + "copilot-language-server": "dist/language-server.js" + }, + "optionalDependencies": { + "@github/copilot-language-server-darwin-arm64": "1.388.0", + "@github/copilot-language-server-darwin-x64": "1.388.0", + "@github/copilot-language-server-linux-arm64": "1.388.0", + "@github/copilot-language-server-linux-x64": "1.388.0", + "@github/copilot-language-server-win32-x64": "1.388.0" + } + }, + "node_modules/@github/copilot-language-server-darwin-arm64": { + "version": "1.388.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-arm64/-/copilot-language-server-darwin-arm64-1.388.0.tgz", + "integrity": "sha512-QWbkbE3W3TqWtvvMIMRzenBBZQviqUhaw5pNJnbqn+HLH7PrEGKa4OQE2Hd4eA4+3vss+BoUWYElKVaMh4AhMg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@github/copilot-language-server-darwin-x64": { + "version": "1.388.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-x64/-/copilot-language-server-darwin-x64-1.388.0.tgz", + "integrity": "sha512-4oZN6DVPgeel7GFskwtm5G59WVwJ8bUktmow8fDlSHkpImuFnnI3baPcIwfiJO6e2906Kzgr22rwKTHuFaTH1w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@github/copilot-language-server-linux-arm64": { + "version": "1.388.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-arm64/-/copilot-language-server-linux-arm64-1.388.0.tgz", + "integrity": "sha512-mgjMWtOY3DBFcgBJ0S13NOc0lVzY6GnGlqleaQjPZ8QscMvpMG75YIEmikXzb7wlScrCpBUw5S0oiUsYdjQFeQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@github/copilot-language-server-linux-x64": { + "version": "1.388.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-x64/-/copilot-language-server-linux-x64-1.388.0.tgz", + "integrity": "sha512-nqEHd7uyCWQRtwjjCt99c/HwDob2XhejpDuf5gT4crCsqj9dOuFU9/UO6TtKIivCHcI19cib21omiF7ynSF52g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@github/copilot-language-server-win32-x64": { + "version": "1.388.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-win32-x64/-/copilot-language-server-win32-x64-1.388.0.tgz", + "integrity": "sha512-aIURt6AZl0SWnPWxLuq7fZFp8gc3EcnoSjcaUh7+vSNTbdM/Pt3b3Gt3/mflm0twlAY4jtDqpWUBlzTikTDjmQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -5735,6 +5820,31 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, "node_modules/vscode-tas-client": { "version": "0.1.75", "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.75.tgz", @@ -6232,6 +6342,49 @@ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true }, + "@github/copilot-language-server": { + "version": "1.388.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server/-/copilot-language-server-1.388.0.tgz", + "integrity": "sha512-u8+ePDN9U0DztUe7Y07GMBWvcJIEf6/VdGSHKIXPcyy/MrZpfY3aZ/ION1KSx7UR3OhNxXrLAGiXT9JH+DA35A==", + "requires": { + "@github/copilot-language-server-darwin-arm64": "1.388.0", + "@github/copilot-language-server-darwin-x64": "1.388.0", + "@github/copilot-language-server-linux-arm64": "1.388.0", + "@github/copilot-language-server-linux-x64": "1.388.0", + "@github/copilot-language-server-win32-x64": "1.388.0", + "vscode-languageserver-protocol": "^3.17.5" + } + }, + "@github/copilot-language-server-darwin-arm64": { + "version": "1.388.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-arm64/-/copilot-language-server-darwin-arm64-1.388.0.tgz", + "integrity": "sha512-QWbkbE3W3TqWtvvMIMRzenBBZQviqUhaw5pNJnbqn+HLH7PrEGKa4OQE2Hd4eA4+3vss+BoUWYElKVaMh4AhMg==", + "optional": true + }, + "@github/copilot-language-server-darwin-x64": { + "version": "1.388.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-x64/-/copilot-language-server-darwin-x64-1.388.0.tgz", + "integrity": "sha512-4oZN6DVPgeel7GFskwtm5G59WVwJ8bUktmow8fDlSHkpImuFnnI3baPcIwfiJO6e2906Kzgr22rwKTHuFaTH1w==", + "optional": true + }, + "@github/copilot-language-server-linux-arm64": { + "version": "1.388.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-arm64/-/copilot-language-server-linux-arm64-1.388.0.tgz", + "integrity": "sha512-mgjMWtOY3DBFcgBJ0S13NOc0lVzY6GnGlqleaQjPZ8QscMvpMG75YIEmikXzb7wlScrCpBUw5S0oiUsYdjQFeQ==", + "optional": true + }, + "@github/copilot-language-server-linux-x64": { + "version": "1.388.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-x64/-/copilot-language-server-linux-x64-1.388.0.tgz", + "integrity": "sha512-nqEHd7uyCWQRtwjjCt99c/HwDob2XhejpDuf5gT4crCsqj9dOuFU9/UO6TtKIivCHcI19cib21omiF7ynSF52g==", + "optional": true + }, + "@github/copilot-language-server-win32-x64": { + "version": "1.388.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-win32-x64/-/copilot-language-server-win32-x64-1.388.0.tgz", + "integrity": "sha512-aIURt6AZl0SWnPWxLuq7fZFp8gc3EcnoSjcaUh7+vSNTbdM/Pt3b3Gt3/mflm0twlAY4jtDqpWUBlzTikTDjmQ==", + "optional": true + }, "@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -10355,6 +10508,25 @@ "dev": true, "requires": {} }, + "vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==" + }, + "vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "requires": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + }, "vscode-tas-client": { "version": "0.1.75", "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.75.tgz", diff --git a/package.json b/package.json index 9f0dc1b0..137f7103 100644 --- a/package.json +++ b/package.json @@ -1110,6 +1110,7 @@ "webpack-cli": "^4.10.0" }, "dependencies": { + "@github/copilot-language-server": "^1.388.0", "await-lock": "^2.2.2", "fmtr": "^1.1.4", "fs-extra": "^10.1.0", @@ -1120,4 +1121,4 @@ "vscode-extension-telemetry-wrapper": "^0.14.0", "vscode-tas-client": "^0.1.75" } -} \ No newline at end of file +} diff --git a/src/commands.ts b/src/commands.ts index a7338c0a..c59c2572 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -38,6 +38,10 @@ export namespace Commands { export const EXPORT_JAR_REPORT = "java.view.package.exportJarReport"; + export const IMPORT_CLASS_CONTENT_TELEMETRY = "java.importClassContent.telemetry"; + + export const PROJECT_DEPENDENCIES_TELEMETRY = "java.projectDependencies.telemetry"; + export const VIEW_PACKAGE_NEW = "java.view.package.new"; export const VIEW_PACKAGE_NEW_JAVA_CLASS = "java.view.package.newJavaClass"; @@ -134,6 +138,8 @@ export namespace Commands { export const JAVA_PROJECT_CHECK_IMPORT_STATUS = "java.project.checkImportStatus"; + export const JAVA_PROJECT_GET_IMPORT_CLASS_CONTENT = "java.project.getImportClassContent"; + export const JAVA_PROJECT_GET_DEPENDENCIES = "java.project.getDependencies"; export const JAVA_UPGRADE_WITH_COPILOT = "_java.upgradeWithCopilot"; diff --git a/src/copilot/contextProvider.ts b/src/copilot/contextProvider.ts new file mode 100644 index 00000000..132386cb --- /dev/null +++ b/src/copilot/contextProvider.ts @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { + ResolveRequest, + SupportedContextItem, + type ContextProvider, +} from '@github/copilot-language-server'; +import * as vscode from 'vscode'; +import { CopilotHelper } from './copilotHelper'; +import { sendError, sendInfo } from "vscode-extension-telemetry-wrapper"; +import { + JavaContextProviderUtils, + CancellationError, + InternalCancellationError, + CopilotCancellationError, + ContextResolverFunction, + CopilotApi, + ContextProviderRegistrationError, + ContextProviderResolverError +} from './utils'; + +export async function registerCopilotContextProviders( + context: vscode.ExtensionContext +) { + try { + const apis = await JavaContextProviderUtils.getCopilotApis(); + if (!apis.clientApi || !apis.chatApi) { + return; + } + + // Register the Java completion context provider + const provider: ContextProvider = { + id: 'vscjava.vscode-java-dependency', // use extension id as provider id for now + selector: [{ language: "java" }], + resolver: { resolve: createJavaContextResolver() } + }; + + const installCount = await JavaContextProviderUtils.installContextProviderOnApis(apis, provider, context, installContextProvider); + + if (installCount === 0) { + return; + } + + sendInfo("", { + "action": "registerCopilotContextProvider", + "extension": 'vscjava.vscode-java-dependency', + "status": "succeeded", + "installCount": installCount + }); + } + catch (error) { + sendError(new ContextProviderRegistrationError('Failed to register Copilot context provider: ' + ((error as Error).message || "unknown_error"))); + } +} + +/** + * Create the Java context resolver function + */ +function createJavaContextResolver(): ContextResolverFunction { + return async (request: ResolveRequest, copilotCancel: vscode.CancellationToken): Promise => { + try { + // Check for immediate cancellation + JavaContextProviderUtils.checkCancellation(copilotCancel); + + return await resolveJavaContext(request, copilotCancel); + } catch (error: any) { + sendError(new ContextProviderResolverError('Java Context Resolution Failed: ' + ((error as Error).message || "unknown_error"))); + // This should never be reached due to handleError throwing, but TypeScript requires it + return []; + } + }; +} + +/** + * Send telemetry data for Java context resolution + */ +function sendContextTelemetry(request: ResolveRequest, start: number, items: SupportedContextItem[], status: string, error?: string) { + const duration = Math.round(performance.now() - start); + const tokenCount = JavaContextProviderUtils.calculateTokenCount(items); + const telemetryData: any = { + "action": "resolveJavaContext", + "completionId": request.completionId, + "duration": duration, + "itemCount": items.length, + "tokenCount": tokenCount, + "status": status + }; + + if (error) { + telemetryData.error = error; + } + + sendInfo("", telemetryData); +} + +async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode.CancellationToken): Promise { + const items: SupportedContextItem[] = []; + const start = performance.now(); + + try { + // Check for cancellation before starting + JavaContextProviderUtils.checkCancellation(copilotCancel); + + // Get current document and position information + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor || activeEditor.document.languageId !== 'java') { + return items; + } + + const document = activeEditor.document; + + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + return items; + } + + const projectUri = workspaceFolders[0]; + + // Resolve project dependencies first + const projectDependencies = await CopilotHelper.resolveProjectDependencies(projectUri.uri, copilotCancel); + + // Check for cancellation after dependency resolution + JavaContextProviderUtils.checkCancellation(copilotCancel); + + // Convert project dependencies to Trait items + if (projectDependencies && Object.keys(projectDependencies).length > 0) { + for (const [key, value] of Object.entries(projectDependencies)) { + items.push({ + name: key, + value: value, + importance: 50 + }); + } + } + + // Check for cancellation before resolving imports + JavaContextProviderUtils.checkCancellation(copilotCancel); + + // Resolve imports directly without caching + const importClass = await CopilotHelper.resolveLocalImports(document.uri, copilotCancel); + + // Check for cancellation after resolution + JavaContextProviderUtils.checkCancellation(copilotCancel); + + // Check for cancellation before processing results + JavaContextProviderUtils.checkCancellation(copilotCancel); + + if (importClass) { + // Process imports in batches to reduce cancellation check overhead + const contextItems = JavaContextProviderUtils.createContextItemsFromImports(importClass); + + // Check cancellation once after creating all items + JavaContextProviderUtils.checkCancellation(copilotCancel); + + items.push(...contextItems); + } + } catch (error: any) { + if (error instanceof CopilotCancellationError) { + sendContextTelemetry(request, start, items, "cancelled_by_copilot"); + throw error; + } + if (error instanceof vscode.CancellationError || error.message === CancellationError.Canceled) { + sendContextTelemetry(request, start, items, "cancelled_internally"); + throw new InternalCancellationError(); + } + + // Send telemetry for general errors (but continue with partial results) + sendContextTelemetry(request, start, items, "error_partial_results", error.message || "unknown_error"); + + // Return partial results and log completion for error case + return items; + } + + // Send telemetry data once at the end for success case + sendContextTelemetry(request, start, items, "succeeded"); + + return items; +} + +export async function installContextProvider( + copilotAPI: CopilotApi, + contextProvider: ContextProvider +): Promise { + const hasGetContextProviderAPI = typeof copilotAPI.getContextProviderAPI === 'function'; + if (hasGetContextProviderAPI) { + const contextAPI = await copilotAPI.getContextProviderAPI('v1'); + if (contextAPI) { + return contextAPI.registerContextProvider(contextProvider); + } + } + return undefined; +} diff --git a/src/copilot/copilotHelper.ts b/src/copilot/copilotHelper.ts new file mode 100644 index 00000000..b46b975d --- /dev/null +++ b/src/copilot/copilotHelper.ts @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { commands, Uri, CancellationToken } from "vscode"; +import { sendError } from "vscode-extension-telemetry-wrapper"; +import { CopilotCancellationError, GetImportClassContentError, GetProjectDependenciesError } from "./utils"; + +export interface INodeImportClass { + uri: string; + value: string; // Changed from 'class' to 'className' to match Java code +} + +export interface IProjectDependency { + [key: string]: string; +} +/** + * Helper class for Copilot integration to analyze Java project dependencies + */ +export namespace CopilotHelper { + /** + * Resolves all local project types imported by the given file + * @param fileUri The URI of the Java file to analyze + * @param cancellationToken Optional cancellation token to abort the operation + * @returns Array of strings in format "type:fully.qualified.name" where type is class|interface|enum|annotation + */ + export async function resolveLocalImports(fileUri: Uri, cancellationToken?: CancellationToken): Promise { + if (cancellationToken?.isCancellationRequested) { + return []; + } + + try { + // Create a promise that can be cancelled + const commandPromise = commands.executeCommand("java.execute.workspaceCommand", "java.project.getImportClassContent", fileUri.toString()) as Promise; + if (cancellationToken) { + const result = await Promise.race([ + commandPromise, + new Promise((_, reject) => { + cancellationToken.onCancellationRequested(() => { + reject(new Error('Operation cancelled')); + }); + }), + new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('Operation timed out')); + }, 80); // 80ms timeout + }) + ]); + return result || []; + } else { + const result = await Promise.race([ + commandPromise, + new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('Operation timed out')); + }, 80); // 80ms timeout + }) + ]); + return result || []; + } + } catch (error: any) { + if (error.message === 'Operation cancelled') { + sendError(new CopilotCancellationError()); + return []; + } + sendError(new GetImportClassContentError('Failed to get import class content: ' + ((error as Error).message || "unknown_error"))); + return []; + } + } + + /** + * Resolves project dependencies for the given project URI + * @param projectUri The URI of the Java project to analyze + * @param cancellationToken Optional cancellation token to abort the operation + * @returns Object containing project dependencies as key-value pairs + */ + export async function resolveProjectDependencies(projectUri: Uri, cancellationToken?: CancellationToken): Promise { + if (cancellationToken?.isCancellationRequested) { + return {}; + } + + try { + // Create a promise that can be cancelled + const commandPromise = commands.executeCommand("java.execute.workspaceCommand", "java.project.getDependencies", projectUri.toString()) as Promise; + //set timeout + if (cancellationToken) { + const result = await Promise.race([ + commandPromise, + new Promise((_, reject) => { + cancellationToken.onCancellationRequested(() => { + reject(new Error('Operation cancelled')); + }); + }), + new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('Operation timed out')); + }, 40); // 40ms timeout + }) + ]); + return result || {}; + } else { + const result = await Promise.race([ + commandPromise, + new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('Operation timed out')); + }, 40); // 40ms timeout + }) + ]); + return result || {}; + } + } catch (error: any) { + if (error.message === 'Operation cancelled') { + return {}; + } + sendError(new GetProjectDependenciesError('Failed to get project dependencies: ' + ((error as Error).message || "unknown_error"))); + return {}; + } + } +} diff --git a/src/copilot/utils.ts b/src/copilot/utils.ts new file mode 100644 index 00000000..0a5c4d71 --- /dev/null +++ b/src/copilot/utils.ts @@ -0,0 +1,213 @@ +import * as vscode from 'vscode'; +import { + ContextProviderApiV1, + ResolveRequest, + SupportedContextItem, + type ContextProvider, +} from '@github/copilot-language-server'; +/** + * Error classes for Copilot context provider cancellation handling + */ +export class CancellationError extends Error { + static readonly Canceled = "Canceled"; + constructor() { + super(CancellationError.Canceled); + this.name = this.message; + } +} + +export class InternalCancellationError extends CancellationError { +} + +export class CopilotCancellationError extends CancellationError { +} + +/** + * Type definitions for common patterns + */ +export type ContextResolverFunction = (request: ResolveRequest, token: vscode.CancellationToken) => Promise; + +export interface CopilotApiWrapper { + clientApi?: CopilotApi; + chatApi?: CopilotApi; +} + +export interface CopilotApi { + getContextProviderAPI(version: string): Promise; +} + +/** + * Utility class for handling common operations in Java Context Provider + */ +export class JavaContextProviderUtils { + /** + * Check if operation should be cancelled and throw appropriate error + */ + static checkCancellation(token: vscode.CancellationToken): void { + if (token.isCancellationRequested) { + throw new CopilotCancellationError(); + } + } + + /** + * Create context items from import classes + */ + static createContextItemsFromImports(importClasses: any[]): SupportedContextItem[] { + return importClasses.map((cls: any) => ({ + uri: cls.uri, + value: cls.value, + importance: 70, + origin: 'request' as const + })); + } + + /** + * Create a basic Java version context item + */ + static createJavaVersionItem(javaVersion: string): SupportedContextItem { + return { + name: 'java.version', + value: javaVersion, + importance: 90, + id: 'java-version', + origin: 'request' + }; + } + + /** + * Get and validate Copilot APIs + */ + static async getCopilotApis(): Promise { + const copilotClientApi = await getCopilotClientApi(); + const copilotChatApi = await getCopilotChatApi(); + return { clientApi: copilotClientApi, chatApi: copilotChatApi }; + } + + /** + * Install context provider on available APIs + */ + static async installContextProviderOnApis( + apis: CopilotApiWrapper, + provider: ContextProvider, + context: vscode.ExtensionContext, + installFn: (api: CopilotApi, provider: ContextProvider) => Promise + ): Promise { + let installCount = 0; + + if (apis.clientApi) { + const disposable = await installFn(apis.clientApi, provider); + if (disposable) { + context.subscriptions.push(disposable); + installCount++; + } + } + + if (apis.chatApi) { + const disposable = await installFn(apis.chatApi, provider); + if (disposable) { + context.subscriptions.push(disposable); + installCount++; + } + } + + return installCount; + } + + /** + * Calculate approximate token count for context items + * Using a simple heuristic: ~4 characters per token + * Optimized for performance by using reduce and direct property access + */ + static calculateTokenCount(items: SupportedContextItem[]): number { + // Fast path: if no items, return 0 + if (items.length === 0) { + return 0; + } + + // Use reduce for better performance + const totalChars = items.reduce((sum, item) => { + let itemChars = 0; + // Direct property access is faster than 'in' operator + const value = (item as any).value; + const name = (item as any).name; + + if (value && typeof value === 'string') { + itemChars += value.length; + } + if (name && typeof name === 'string') { + itemChars += name.length; + } + + return sum + itemChars; + }, 0); + + // Approximate: 1 token ≈ 4 characters + // Use bitwise shift for faster division by 4 + return (totalChars >> 2) + (totalChars & 3 ? 1 : 0); + } +} + +/** + * Get Copilot client API + */ +export async function getCopilotClientApi(): Promise { + const extension = vscode.extensions.getExtension('github.copilot'); + if (!extension) { + return undefined; + } + try { + return await extension.activate(); + } catch { + return undefined; + } +} + +/** + * Get Copilot chat API + */ +export async function getCopilotChatApi(): Promise { + type CopilotChatApi = { getAPI?(version: number): CopilotApi | undefined }; + const extension = vscode.extensions.getExtension('github.copilot-chat'); + if (!extension) { + return undefined; + } + + let exports: CopilotChatApi | undefined; + try { + exports = await extension.activate(); + } catch { + return undefined; + } + if (!exports || typeof exports.getAPI !== 'function') { + return undefined; + } + return exports.getAPI(1); +} + +export class ContextProviderRegistrationError extends Error { + constructor(message: string) { + super(message); + this.name = 'ContextProviderRegistrationError'; + } +} + +export class GetImportClassContentError extends Error { + constructor(message: string) { + super(message); + this.name = 'GetImportClassContentError'; + } +} + +export class GetProjectDependenciesError extends Error { + constructor(message: string) { + super(message); + this.name = 'GetProjectDependenciesError'; + } +} + +export class ContextProviderResolverError extends Error { + constructor(message: string) { + super(message); + this.name = 'ContextProviderResolverError'; + } +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index d2fe424b..af169c32 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -21,6 +21,7 @@ import { setContextForDeprecatedTasks, updateExportTaskType } from "./tasks/buil import { CodeActionProvider } from "./tasks/buildArtifact/migration/CodeActionProvider"; import { newJavaFile } from "./explorerCommands/new"; import upgradeManager from "./upgrade/upgradeManager"; +import { registerCopilotContextProviders } from "./copilot/contextProvider"; export async function activate(context: ExtensionContext): Promise { contextManager.initialize(context); @@ -37,6 +38,7 @@ export async function activate(context: ExtensionContext): Promise { } }); contextManager.setContextValue(Context.EXTENSION_ACTIVATED, true); + await registerCopilotContextProviders(context); } async function activateExtension(_operationId: string, context: ExtensionContext): Promise { diff --git a/src/java/jdtls.ts b/src/java/jdtls.ts index 84a372a0..d92d708f 100644 --- a/src/java/jdtls.ts +++ b/src/java/jdtls.ts @@ -9,6 +9,7 @@ import { IClasspath } from "../tasks/buildArtifact/IStepMetadata"; import { IMainClassInfo } from "../tasks/buildArtifact/ResolveMainClassExecutor"; import { INodeData, NodeKind } from "./nodeData"; import { Settings } from "../settings"; +import { INodeImportClass } from "../copilot/copilotHelper"; export namespace Jdtls { export async function getProjects(params: string): Promise { @@ -86,6 +87,10 @@ export namespace Jdtls { return await commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_GET_DEPENDENCIES, projectUri) || []; } + export async function getImportClassContent(fileUri: string, token: CancellationToken): Promise { + return await commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_GET_IMPORT_CLASS_CONTENT, fileUri, token) || []; + } + export enum CompileWorkspaceStatus { Failed = 0, Succeed = 1, From acb7a99a240365ea75d57105a21d8f39beb56002 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Tue, 28 Oct 2025 13:24:15 +0800 Subject: [PATCH 02/11] feat: collect empty context reason --- .../jdtls/ext/core/CommandHandler.java | 2 + .../jdtls/ext/core/ProjectCommand.java | 228 +++++++++++++++--- src/copilot/contextProvider.ts | 46 +++- src/copilot/copilotHelper.ts | 177 +++++++++++--- 4 files changed, 383 insertions(+), 70 deletions(-) diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/CommandHandler.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/CommandHandler.java index 390dda27..a27042cc 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/CommandHandler.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/CommandHandler.java @@ -39,6 +39,8 @@ public Object executeCommand(String commandId, List arguments, IProgress return ProjectCommand.checkImportStatus(); case "java.project.getImportClassContent": return ProjectCommand.getImportClassContent(arguments, monitor); + case "java.project.getImportClassContentWithReason": + return ProjectCommand.getImportClassContentWithReason(arguments, monitor); case "java.project.getDependencies": return ProjectCommand.getProjectDependencies(arguments, monitor); default: diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java index 2734f514..e775789c 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java @@ -98,6 +98,101 @@ public DependencyInfo(String key, String value) { } } + /** + * Error reasons for ImportClassContent operation + */ + public enum ImportClassContentErrorReason { + NULL_ARGUMENTS("Arguments null or empty"), + INVALID_URI("URI invalid or empty"), + URI_PARSE_FAILED("URI parse failed"), + FILE_NOT_FOUND("File not found"), + FILE_NOT_EXISTS("File does not exist"), + NOT_JAVA_PROJECT("Not in Java project"), + PROJECT_NOT_EXISTS("Java project not exists"), + NOT_COMPILATION_UNIT("Not Java compilation unit"), + NO_IMPORTS("No import declarations"), + OPERATION_CANCELLED("Operation cancelled"), + TIME_LIMIT_EXCEEDED("Time limit exceeded"), + NO_RESULTS("No classes resolved"), + PROCESSING_EXCEPTION("Processing exception"); + + private final String message; + + ImportClassContentErrorReason(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + } + + /** + * Error reasons for ProjectDependencies operation + */ + public enum ProjectDependenciesErrorReason { + NULL_ARGUMENTS("Arguments null or empty"), + INVALID_URI("URI invalid or empty"), + URI_PARSE_FAILED("URI parse failed"), + MALFORMED_URI("Malformed URI syntax"), + OPERATION_CANCELLED("Operation cancelled"), + RESOLVER_NULL_RESULT("Resolver returned null"), + NO_DEPENDENCIES("No dependencies resolved"), + PROCESSING_EXCEPTION("Processing exception"); + + private final String message; + + ProjectDependenciesErrorReason(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + } + + /** + * Result wrapper for getImportClassContent method + */ + public static class ImportClassContentResult { + public List classInfoList; + public String errorReason; // Use String for JSON serialization compatibility + public boolean hasError; + + public ImportClassContentResult(List classInfoList) { + this.classInfoList = classInfoList; + this.errorReason = null; + this.hasError = false; + } + + public ImportClassContentResult(ImportClassContentErrorReason errorReason) { + this.classInfoList = Collections.emptyList(); + this.errorReason = errorReason.getMessage(); // Use enum message + this.hasError = true; + } + } + + /** + * Result wrapper for getProjectDependencies method + */ + public static class ProjectDependenciesResult { + public List dependencyInfoList; + public String errorReason; // Use String for JSON serialization compatibility + public boolean hasError; + + public ProjectDependenciesResult(List dependencyInfoList) { + this.dependencyInfoList = dependencyInfoList; + this.errorReason = null; + this.hasError = false; + } + + public ProjectDependenciesResult(ProjectDependenciesErrorReason errorReason) { + this.dependencyInfoList = new ArrayList<>(); + this.errorReason = errorReason.getMessage(); // Use enum message + this.hasError = true; + } + } + private static class Classpath { public String source; public String destination; @@ -351,17 +446,34 @@ public static boolean checkImportStatus() { } /** - * Get import class content for Copilot integration. - * This method extracts information about imported classes from a Java file. - * Uses a time-controlled strategy: prioritizes internal classes, adds external classes only if time permits. + * Get import class content for Copilot integration (backward compatibility wrapper). + * This method maintains compatibility with the original return type. * * @param arguments List containing the file URI as the first element * @param monitor Progress monitor for cancellation support * @return List of ImportClassInfo containing class information and JavaDoc */ public static List getImportClassContent(List arguments, IProgressMonitor monitor) { + ImportClassContentResult result = getImportClassContentWithReason(arguments, monitor); + if (result.hasError) { + // Log the error reason for debugging + JdtlsExtActivator.logError("getImportClassContent failed: " + result.errorReason); + } + return result.classInfoList; + } + + /** + * Get import class content for Copilot integration with detailed error reporting. + * This method extracts information about imported classes from a Java file. + * Uses a time-controlled strategy: prioritizes internal classes, adds external classes only if time permits. + * + * @param arguments List containing the file URI as the first element + * @param monitor Progress monitor for cancellation support + * @return ImportClassContentResult containing class information and error reason if applicable + */ + public static ImportClassContentResult getImportClassContentWithReason(List arguments, IProgressMonitor monitor) { if (arguments == null || arguments.isEmpty()) { - return Collections.emptyList(); + return new ImportClassContentResult(ImportClassContentErrorReason.NULL_ARGUMENTS); } // Time control: total budget 80ms, early return at 75ms @@ -371,12 +483,15 @@ public static List getImportClassContent(List arguments try { String fileUri = (String) arguments.get(0); + if (fileUri == null || fileUri.trim().isEmpty()) { + return new ImportClassContentResult(ImportClassContentErrorReason.INVALID_URI); + } // Parse URI manually to avoid restricted API java.net.URI uri = new java.net.URI(fileUri); String filePath = uri.getPath(); if (filePath == null) { - return Collections.emptyList(); + return new ImportClassContentResult(ImportClassContentErrorReason.URI_PARSE_FAILED); } IPath path = new Path(filePath); @@ -384,20 +499,26 @@ public static List getImportClassContent(List arguments // Get the file resource IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); IFile file = root.getFileForLocation(path); - if (file == null || !file.exists()) { - return Collections.emptyList(); + if (file == null) { + return new ImportClassContentResult(ImportClassContentErrorReason.FILE_NOT_FOUND); + } + if (!file.exists()) { + return new ImportClassContentResult(ImportClassContentErrorReason.FILE_NOT_EXISTS); } // Get the Java project IJavaProject javaProject = JavaCore.create(file.getProject()); - if (javaProject == null || !javaProject.exists()) { - return Collections.emptyList(); + if (javaProject == null) { + return new ImportClassContentResult(ImportClassContentErrorReason.NOT_JAVA_PROJECT); + } + if (!javaProject.exists()) { + return new ImportClassContentResult(ImportClassContentErrorReason.PROJECT_NOT_EXISTS); } // Find the compilation unit IJavaElement javaElement = JavaCore.create(file); if (!(javaElement instanceof org.eclipse.jdt.core.ICompilationUnit)) { - return Collections.emptyList(); + return new ImportClassContentResult(ImportClassContentErrorReason.NOT_COMPILATION_UNIT); } org.eclipse.jdt.core.ICompilationUnit compilationUnit = (org.eclipse.jdt.core.ICompilationUnit) javaElement; @@ -409,12 +530,20 @@ public static List getImportClassContent(List arguments org.eclipse.jdt.core.IImportDeclaration[] imports = compilationUnit.getImports(); Set processedTypes = new HashSet<>(); + // Check if file has no imports + if (imports == null || imports.length == 0) { + return new ImportClassContentResult(ImportClassContentErrorReason.NO_IMPORTS); + } + // Phase 1: Priority - Resolve project source classes (internal) for (org.eclipse.jdt.core.IImportDeclaration importDecl : imports) { // Check time budget before each operation long elapsed = System.currentTimeMillis() - startTime; - if (monitor.isCanceled() || elapsed >= EARLY_RETURN_MS) { - return classInfoList; // Early return if approaching time limit + if (monitor.isCanceled()) { + return new ImportClassContentResult(ImportClassContentErrorReason.OPERATION_CANCELLED); + } + if (elapsed >= EARLY_RETURN_MS) { + return new ImportClassContentResult(ImportClassContentErrorReason.TIME_LIMIT_EXCEEDED); } String importName = importDecl.getElementName(); @@ -470,11 +599,15 @@ public static List getImportClassContent(List arguments } } - return classInfoList; + // Success case - return the resolved class information + if (classInfoList.isEmpty()) { + return new ImportClassContentResult(ImportClassContentErrorReason.NO_RESULTS); + } + return new ImportClassContentResult(classInfoList); } catch (Exception e) { JdtlsExtActivator.logException("Error in getImportClassContent", e); - return Collections.emptyList(); + return new ImportClassContentResult(ImportClassContentErrorReason.PROCESSING_EXCEPTION); } } @@ -503,32 +636,67 @@ private static String getSeverityString(int severity) { } } + /** - * Get project dependencies information including JDK version. + * Get project dependencies information with detailed error reporting. + * This method extracts project dependency information including JDK version, build tool, etc. * * @param arguments List containing the project URI as the first element * @param monitor Progress monitor for cancellation support - * @return List of DependencyInfo containing key-value pairs of project information + * @return ProjectDependenciesResult containing dependency information and error reason if applicable */ - public static List getProjectDependencies(List arguments, IProgressMonitor monitor) { + public static ProjectDependenciesResult getProjectDependencies(List arguments, IProgressMonitor monitor) { if (arguments == null || arguments.isEmpty()) { - return new ArrayList<>(); + return new ProjectDependenciesResult(ProjectDependenciesErrorReason.NULL_ARGUMENTS); } - String projectUri = (String) arguments.get(0); - if (projectUri == null || projectUri.isEmpty()) { - return new ArrayList<>(); - } + try { + String projectUri = (String) arguments.get(0); + if (projectUri == null || projectUri.trim().isEmpty()) { + return new ProjectDependenciesResult(ProjectDependenciesErrorReason.INVALID_URI); + } + + // Validate URI format + try { + java.net.URI uri = new java.net.URI(projectUri); + if (uri.getPath() == null) { + return new ProjectDependenciesResult(ProjectDependenciesErrorReason.URI_PARSE_FAILED); + } + } catch (java.net.URISyntaxException e) { + return new ProjectDependenciesResult(ProjectDependenciesErrorReason.MALFORMED_URI); + } + + // Check if monitor is cancelled before processing + if (monitor.isCanceled()) { + return new ProjectDependenciesResult(ProjectDependenciesErrorReason.OPERATION_CANCELLED); + } - List resolverResult = ProjectResolver.resolveProjectDependencies(projectUri, monitor); - - // Convert ProjectResolver.DependencyInfo to ProjectCommand.DependencyInfo - List result = new ArrayList<>(); - for (ProjectResolver.DependencyInfo info : resolverResult) { - result.add(new DependencyInfo(info.key, info.value)); + List resolverResult = ProjectResolver.resolveProjectDependencies(projectUri, monitor); + + // Check if resolver returned null (should not happen, but defensive programming) + if (resolverResult == null) { + return new ProjectDependenciesResult(ProjectDependenciesErrorReason.RESOLVER_NULL_RESULT); + } + + // Convert ProjectResolver.DependencyInfo to ProjectCommand.DependencyInfo + List result = new ArrayList<>(); + for (ProjectResolver.DependencyInfo info : resolverResult) { + if (info != null) { + result.add(new DependencyInfo(info.key, info.value)); + } + } + + // Check if no dependencies were resolved + if (result.isEmpty()) { + return new ProjectDependenciesResult(ProjectDependenciesErrorReason.NO_DEPENDENCIES); + } + + return new ProjectDependenciesResult(result); + + } catch (Exception e) { + JdtlsExtActivator.logException("Error in getProjectDependenciesWithReason", e); + return new ProjectDependenciesResult(ProjectDependenciesErrorReason.PROCESSING_EXCEPTION); } - - return result; } private static final class LinkedFolderVisitor implements IResourceVisitor { diff --git a/src/copilot/contextProvider.ts b/src/copilot/contextProvider.ts index 132386cb..a8108741 100644 --- a/src/copilot/contextProvider.ts +++ b/src/copilot/contextProvider.ts @@ -49,6 +49,7 @@ export async function registerCopilotContextProviders( "status": "succeeded", "installCount": installCount }); + console.log('Java Copilot context provider registered successfully.'); } catch (error) { sendError(new ContextProviderRegistrationError('Failed to register Copilot context provider: ' + ((error as Error).message || "unknown_error"))); @@ -119,18 +120,29 @@ async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode const projectUri = workspaceFolders[0]; // Resolve project dependencies first - const projectDependencies = await CopilotHelper.resolveProjectDependencies(projectUri.uri, copilotCancel); - + const projectDependenciesResult = await CopilotHelper.resolveProjectDependenciesWithReason(projectUri.uri, copilotCancel); + console.dir(projectDependenciesResult); + // Check for cancellation after dependency resolution + JavaContextProviderUtils.checkCancellation(copilotCancel); + + // Send telemetry if there was an error resolving project dependencies + if (projectDependenciesResult.hasError && projectDependenciesResult.errorReason) { + sendInfo("", { + "action": "resolveProjectDependencies", + "status": "ContextEmpty", + "ContextEmptyReason": projectDependenciesResult.errorReason + }); + } // Check for cancellation after dependency resolution JavaContextProviderUtils.checkCancellation(copilotCancel); // Convert project dependencies to Trait items - if (projectDependencies && Object.keys(projectDependencies).length > 0) { - for (const [key, value] of Object.entries(projectDependencies)) { + if (projectDependenciesResult.dependencyInfoList && projectDependenciesResult.dependencyInfoList.length > 0) { + for (const dep of projectDependenciesResult.dependencyInfoList) { items.push({ - name: key, - value: value, - importance: 50 + name: dep.key, + value: dep.value, + importance: 70 }); } } @@ -139,18 +151,28 @@ async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode JavaContextProviderUtils.checkCancellation(copilotCancel); // Resolve imports directly without caching - const importClass = await CopilotHelper.resolveLocalImports(document.uri, copilotCancel); - + const importClassResult = await CopilotHelper.resolveLocalImportsWithReason(document.uri, copilotCancel); + console.dir(importClassResult); // Check for cancellation after resolution JavaContextProviderUtils.checkCancellation(copilotCancel); + // Send telemetry if there was an error resolving imports + if (importClassResult.hasError && importClassResult.errorReason) { + sendInfo("", { + "action": "resolveLocalImports", + "status": "ContextEmpty", + "ContextEmptyReason": importClassResult.errorReason + }); + console.log("Context resolution - local imports error: " + importClassResult.errorReason); + } + // Check for cancellation before processing results JavaContextProviderUtils.checkCancellation(copilotCancel); - if (importClass) { + if (importClassResult.classInfoList && importClassResult.classInfoList.length > 0) { // Process imports in batches to reduce cancellation check overhead - const contextItems = JavaContextProviderUtils.createContextItemsFromImports(importClass); - + const contextItems = JavaContextProviderUtils.createContextItemsFromImports(importClassResult.classInfoList); + console.dir(contextItems); // Check cancellation once after creating all items JavaContextProviderUtils.checkCancellation(copilotCancel); diff --git a/src/copilot/copilotHelper.ts b/src/copilot/copilotHelper.ts index b46b975d..c414a9b5 100644 --- a/src/copilot/copilotHelper.ts +++ b/src/copilot/copilotHelper.ts @@ -3,117 +3,238 @@ import { commands, Uri, CancellationToken } from "vscode"; import { sendError } from "vscode-extension-telemetry-wrapper"; -import { CopilotCancellationError, GetImportClassContentError, GetProjectDependenciesError } from "./utils"; +import { GetImportClassContentError, GetProjectDependenciesError } from "./utils"; export interface INodeImportClass { uri: string; value: string; // Changed from 'class' to 'className' to match Java code } +export interface IImportClassContentResult { + classInfoList: INodeImportClass[]; + errorReason?: string; + hasError: boolean; +} + export interface IProjectDependency { [key: string]: string; } + +export interface IProjectDependenciesResult { + dependencyInfoList: Array<{ key: string; value: string }>; + errorReason?: string; + hasError: boolean; +} /** * Helper class for Copilot integration to analyze Java project dependencies */ export namespace CopilotHelper { /** - * Resolves all local project types imported by the given file + * Resolves all local project types imported by the given file (backward compatibility version) * @param fileUri The URI of the Java file to analyze * @param cancellationToken Optional cancellation token to abort the operation - * @returns Array of strings in format "type:fully.qualified.name" where type is class|interface|enum|annotation + * @returns Array of import class information */ export async function resolveLocalImports(fileUri: Uri, cancellationToken?: CancellationToken): Promise { + const result = await resolveLocalImportsWithReason(fileUri, cancellationToken); + return result.classInfoList; + } + + /** + * Resolves all local project types imported by the given file with detailed error reporting + * @param fileUri The URI of the Java file to analyze + * @param cancellationToken Optional cancellation token to abort the operation + * @returns Result object containing import class information and error details + */ + export async function resolveLocalImportsWithReason(fileUri: Uri, cancellationToken?: CancellationToken): Promise { if (cancellationToken?.isCancellationRequested) { - return []; + return { + classInfoList: [], + errorReason: "Copilot_Cancellation_requested", + hasError: false + }; } try { - // Create a promise that can be cancelled - const commandPromise = commands.executeCommand("java.execute.workspaceCommand", "java.project.getImportClassContent", fileUri.toString()) as Promise; + // Use the new command with error reason support + const commandPromise = commands.executeCommand("java.execute.workspaceCommand", "java.project.getImportClassContent", fileUri.toString()) as Promise; + if (cancellationToken) { const result = await Promise.race([ commandPromise, - new Promise((_, reject) => { + new Promise((_, reject) => { cancellationToken.onCancellationRequested(() => { reject(new Error('Operation cancelled')); }); }), - new Promise((_, reject) => { + new Promise((_, reject) => { setTimeout(() => { reject(new Error('Operation timed out')); }, 80); // 80ms timeout }) ]); - return result || []; + + if (!result) { + return { + classInfoList: [], + errorReason: "Command returned null result", + hasError: true + }; + } + + return result; } else { const result = await Promise.race([ commandPromise, - new Promise((_, reject) => { + new Promise((_, reject) => { setTimeout(() => { reject(new Error('Operation timed out')); }, 80); // 80ms timeout }) ]); - return result || []; + + if (!result) { + return { + classInfoList: [], + errorReason: "Command returned null result", + hasError: true + }; + } + + return result; } } catch (error: any) { if (error.message === 'Operation cancelled') { - sendError(new CopilotCancellationError()); - return []; + return { + classInfoList: [], + errorReason: "Copilot_Cancellation_requested", + hasError: true + }; } - sendError(new GetImportClassContentError('Failed to get import class content: ' + ((error as Error).message || "unknown_error"))); - return []; + + if (error.message === 'Operation timed out') { + return { + classInfoList: [], + errorReason: "Operation timed out after 80ms", + hasError: true + }; + } + + const errorMessage = 'Failed_Get_Import_Info: ' + ((error as Error).message || "unknown_error"); + sendError(new GetImportClassContentError(errorMessage)); + return { + classInfoList: [], + errorReason: errorMessage, + hasError: true + }; } } /** - * Resolves project dependencies for the given project URI + * Resolves project dependencies for the given project URI (backward compatibility version) * @param projectUri The URI of the Java project to analyze * @param cancellationToken Optional cancellation token to abort the operation * @returns Object containing project dependencies as key-value pairs */ export async function resolveProjectDependencies(projectUri: Uri, cancellationToken?: CancellationToken): Promise { + const result = await resolveProjectDependenciesWithReason(projectUri, cancellationToken); + + // Convert to legacy format + const dependencies: IProjectDependency = {}; + for (const dep of result.dependencyInfoList) { + dependencies[dep.key] = dep.value; + } + + return dependencies; + } + + /** + * Resolves project dependencies with detailed error reporting + * @param projectUri The URI of the Java project to analyze + * @param cancellationToken Optional cancellation token to abort the operation + * @returns Result object containing project dependencies and error information + */ + export async function resolveProjectDependenciesWithReason(projectUri: Uri, cancellationToken?: CancellationToken): Promise { if (cancellationToken?.isCancellationRequested) { - return {}; + return { + dependencyInfoList: [], + errorReason: "Copilot_Cancellation_requested", + hasError: true + }; } try { - // Create a promise that can be cancelled - const commandPromise = commands.executeCommand("java.execute.workspaceCommand", "java.project.getDependencies", projectUri.toString()) as Promise; - //set timeout + // Use the new command with error reason support + const commandPromise = commands.executeCommand("java.execute.workspaceCommand", "java.project.getDependencies", projectUri.toString()) as Promise; + if (cancellationToken) { const result = await Promise.race([ commandPromise, - new Promise((_, reject) => { + new Promise((_, reject) => { cancellationToken.onCancellationRequested(() => { reject(new Error('Operation cancelled')); }); }), - new Promise((_, reject) => { + new Promise((_, reject) => { setTimeout(() => { reject(new Error('Operation timed out')); }, 40); // 40ms timeout }) ]); - return result || {}; + + if (!result) { + return { + dependencyInfoList: [], + errorReason: "Command returned null result", + hasError: true + }; + } + + return result; } else { const result = await Promise.race([ commandPromise, - new Promise((_, reject) => { + new Promise((_, reject) => { setTimeout(() => { reject(new Error('Operation timed out')); }, 40); // 40ms timeout }) ]); - return result || {}; + + if (!result) { + return { + dependencyInfoList: [], + errorReason: "Command returned null result", + hasError: true + }; + } + + return result; } } catch (error: any) { if (error.message === 'Operation cancelled') { - return {}; + return { + dependencyInfoList: [], + errorReason: 'Copilot_Cancellation_requested', + hasError: true + }; + } + + if (error.message === 'Operation timed out') { + return { + dependencyInfoList: [], + errorReason: "Operation timed out after 40ms", + hasError: true + }; } - sendError(new GetProjectDependenciesError('Failed to get project dependencies: ' + ((error as Error).message || "unknown_error"))); - return {}; + + const errorMessage = 'Failed to get project dependencies: ' + ((error as Error).message || "unknown_error"); + sendError(new GetProjectDependenciesError(errorMessage)); + return { + dependencyInfoList: [], + errorReason: errorMessage, + hasError: true + }; } } } From 6bc41363f042297815b3e14462aecceb0b8f69c0 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 29 Oct 2025 11:11:54 +0800 Subject: [PATCH 03/11] feat: update --- .../jdtls/ext/core/CommandHandler.java | 2 - .../jdtls/ext/core/ProjectCommand.java | 89 ++++----- src/copilot/contextProvider.ts | 104 ++++------ src/copilot/copilotHelper.ts | 183 +++++++++++++++--- 4 files changed, 225 insertions(+), 153 deletions(-) diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/CommandHandler.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/CommandHandler.java index a27042cc..390dda27 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/CommandHandler.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/CommandHandler.java @@ -39,8 +39,6 @@ public Object executeCommand(String commandId, List arguments, IProgress return ProjectCommand.checkImportStatus(); case "java.project.getImportClassContent": return ProjectCommand.getImportClassContent(arguments, monitor); - case "java.project.getImportClassContentWithReason": - return ProjectCommand.getImportClassContentWithReason(arguments, monitor); case "java.project.getDependencies": return ProjectCommand.getProjectDependencies(arguments, monitor); default: diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java index e775789c..f55c0142 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java @@ -99,22 +99,22 @@ public DependencyInfo(String key, String value) { } /** - * Error reasons for ImportClassContent operation + * Empty reasons for ImportClassContent operation */ public enum ImportClassContentErrorReason { - NULL_ARGUMENTS("Arguments null or empty"), - INVALID_URI("URI invalid or empty"), - URI_PARSE_FAILED("URI parse failed"), - FILE_NOT_FOUND("File not found"), - FILE_NOT_EXISTS("File does not exist"), - NOT_JAVA_PROJECT("Not in Java project"), - PROJECT_NOT_EXISTS("Java project not exists"), - NOT_COMPILATION_UNIT("Not Java compilation unit"), - NO_IMPORTS("No import declarations"), - OPERATION_CANCELLED("Operation cancelled"), - TIME_LIMIT_EXCEEDED("Time limit exceeded"), - NO_RESULTS("No classes resolved"), - PROCESSING_EXCEPTION("Processing exception"); + NULL_ARGUMENTS("NullArgs"), + INVALID_URI("InvalidUri"), + URI_PARSE_FAILED("UriParseFail"), + FILE_NOT_FOUND("FileNotFound"), + FILE_NOT_EXISTS("FileNotExists"), + NOT_JAVA_PROJECT("NotJavaProject"), + PROJECT_NOT_EXISTS("ProjectNotExists"), + NOT_COMPILATION_UNIT("NotCompilationUnit"), + NO_IMPORTS("NoImports"), + OPERATION_CANCELLED("Cancelled"), + TIME_LIMIT_EXCEEDED("Timeout"), + NO_RESULTS("NoResults"), + PROCESSING_EXCEPTION("ProcessingError"); private final String message; @@ -128,17 +128,17 @@ public String getMessage() { } /** - * Error reasons for ProjectDependencies operation + * Empty reasons for ProjectDependencies operation */ public enum ProjectDependenciesErrorReason { - NULL_ARGUMENTS("Arguments null or empty"), - INVALID_URI("URI invalid or empty"), - URI_PARSE_FAILED("URI parse failed"), - MALFORMED_URI("Malformed URI syntax"), - OPERATION_CANCELLED("Operation cancelled"), - RESOLVER_NULL_RESULT("Resolver returned null"), - NO_DEPENDENCIES("No dependencies resolved"), - PROCESSING_EXCEPTION("Processing exception"); + NULL_ARGUMENTS("NullArgs"), + INVALID_URI("InvalidUri"), + URI_PARSE_FAILED("UriParseFail"), + MALFORMED_URI("MalformedUri"), + OPERATION_CANCELLED("Cancelled"), + RESOLVER_NULL_RESULT("ResolverNull"), + NO_DEPENDENCIES("NoDependencies"), + PROCESSING_EXCEPTION("ProcessingError"); private final String message; @@ -156,19 +156,19 @@ public String getMessage() { */ public static class ImportClassContentResult { public List classInfoList; - public String errorReason; // Use String for JSON serialization compatibility - public boolean hasError; + public String emptyReason; // Reason why the result is empty + public boolean isEmpty; public ImportClassContentResult(List classInfoList) { this.classInfoList = classInfoList; - this.errorReason = null; - this.hasError = false; + this.emptyReason = null; + this.isEmpty = false; } public ImportClassContentResult(ImportClassContentErrorReason errorReason) { this.classInfoList = Collections.emptyList(); - this.errorReason = errorReason.getMessage(); // Use enum message - this.hasError = true; + this.emptyReason = errorReason.getMessage(); // Use enum message + this.isEmpty = true; } } @@ -177,19 +177,19 @@ public ImportClassContentResult(ImportClassContentErrorReason errorReason) { */ public static class ProjectDependenciesResult { public List dependencyInfoList; - public String errorReason; // Use String for JSON serialization compatibility - public boolean hasError; + public String emptyReason; // Reason why the result is empty + public boolean isEmpty; public ProjectDependenciesResult(List dependencyInfoList) { this.dependencyInfoList = dependencyInfoList; - this.errorReason = null; - this.hasError = false; + this.emptyReason = null; + this.isEmpty = false; } public ProjectDependenciesResult(ProjectDependenciesErrorReason errorReason) { this.dependencyInfoList = new ArrayList<>(); - this.errorReason = errorReason.getMessage(); // Use enum message - this.hasError = true; + this.emptyReason = errorReason.getMessage(); // Use enum message + this.isEmpty = true; } } @@ -445,23 +445,6 @@ public static boolean checkImportStatus() { return hasError; } - /** - * Get import class content for Copilot integration (backward compatibility wrapper). - * This method maintains compatibility with the original return type. - * - * @param arguments List containing the file URI as the first element - * @param monitor Progress monitor for cancellation support - * @return List of ImportClassInfo containing class information and JavaDoc - */ - public static List getImportClassContent(List arguments, IProgressMonitor monitor) { - ImportClassContentResult result = getImportClassContentWithReason(arguments, monitor); - if (result.hasError) { - // Log the error reason for debugging - JdtlsExtActivator.logError("getImportClassContent failed: " + result.errorReason); - } - return result.classInfoList; - } - /** * Get import class content for Copilot integration with detailed error reporting. * This method extracts information about imported classes from a Java file. @@ -471,7 +454,7 @@ public static List getImportClassContent(List arguments * @param monitor Progress monitor for cancellation support * @return ImportClassContentResult containing class information and error reason if applicable */ - public static ImportClassContentResult getImportClassContentWithReason(List arguments, IProgressMonitor monitor) { + public static ImportClassContentResult getImportClassContent(List arguments, IProgressMonitor monitor) { if (arguments == null || arguments.isEmpty()) { return new ImportClassContentResult(ImportClassContentErrorReason.NULL_ARGUMENTS); } diff --git a/src/copilot/contextProvider.ts b/src/copilot/contextProvider.ts index a8108741..e164a7fc 100644 --- a/src/copilot/contextProvider.ts +++ b/src/copilot/contextProvider.ts @@ -104,80 +104,48 @@ async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode // Check for cancellation before starting JavaContextProviderUtils.checkCancellation(copilotCancel); - // Get current document and position information - const activeEditor = vscode.window.activeTextEditor; - if (!activeEditor || activeEditor.document.languageId !== 'java') { - return items; - } - - const document = activeEditor.document; - - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders || workspaceFolders.length === 0) { - return items; - } - - const projectUri = workspaceFolders[0]; - - // Resolve project dependencies first - const projectDependenciesResult = await CopilotHelper.resolveProjectDependenciesWithReason(projectUri.uri, copilotCancel); - console.dir(projectDependenciesResult); - // Check for cancellation after dependency resolution - JavaContextProviderUtils.checkCancellation(copilotCancel); - - // Send telemetry if there was an error resolving project dependencies - if (projectDependenciesResult.hasError && projectDependenciesResult.errorReason) { - sendInfo("", { - "action": "resolveProjectDependencies", - "status": "ContextEmpty", - "ContextEmptyReason": projectDependenciesResult.errorReason - }); - } - // Check for cancellation after dependency resolution - JavaContextProviderUtils.checkCancellation(copilotCancel); - - // Convert project dependencies to Trait items - if (projectDependenciesResult.dependencyInfoList && projectDependenciesResult.dependencyInfoList.length > 0) { - for (const dep of projectDependenciesResult.dependencyInfoList) { - items.push({ - name: dep.key, - value: dep.value, - importance: 70 - }); + // Resolve project dependencies and convert to context items + const projectDependencyItems = await CopilotHelper.resolveAndConvertProjectDependencies( + vscode.workspace.workspaceFolders, + copilotCancel, + JavaContextProviderUtils.checkCancellation, + (action: string, status: string, reason?: string) => { + const telemetryData: any = { + "action": action, + "status": status + }; + if (reason) { + telemetryData.ContextEmptyReason = reason; + } + sendInfo("", telemetryData); } - } - - // Check for cancellation before resolving imports + ); JavaContextProviderUtils.checkCancellation(copilotCancel); + + items.push(...projectDependencyItems); - // Resolve imports directly without caching - const importClassResult = await CopilotHelper.resolveLocalImportsWithReason(document.uri, copilotCancel); - console.dir(importClassResult); - // Check for cancellation after resolution JavaContextProviderUtils.checkCancellation(copilotCancel); - // Send telemetry if there was an error resolving imports - if (importClassResult.hasError && importClassResult.errorReason) { - sendInfo("", { - "action": "resolveLocalImports", - "status": "ContextEmpty", - "ContextEmptyReason": importClassResult.errorReason - }); - console.log("Context resolution - local imports error: " + importClassResult.errorReason); - } - - // Check for cancellation before processing results + // Resolve local imports and convert to context items + const localImportItems = await CopilotHelper.resolveAndConvertLocalImports( + vscode.window.activeTextEditor, + copilotCancel, + JavaContextProviderUtils.checkCancellation, + (action: string, status: string, reason?: string) => { + const telemetryData: any = { + "action": action, + "status": status + }; + if (reason) { + telemetryData.ContextEmptyReason = reason; + } + sendInfo("", telemetryData); + }, + JavaContextProviderUtils.createContextItemsFromImports + ); JavaContextProviderUtils.checkCancellation(copilotCancel); - - if (importClassResult.classInfoList && importClassResult.classInfoList.length > 0) { - // Process imports in batches to reduce cancellation check overhead - const contextItems = JavaContextProviderUtils.createContextItemsFromImports(importClassResult.classInfoList); - console.dir(contextItems); - // Check cancellation once after creating all items - JavaContextProviderUtils.checkCancellation(copilotCancel); - - items.push(...contextItems); - } + + items.push(...localImportItems); } catch (error: any) { if (error instanceof CopilotCancellationError) { sendContextTelemetry(request, start, items, "cancelled_by_copilot"); diff --git a/src/copilot/copilotHelper.ts b/src/copilot/copilotHelper.ts index c414a9b5..3d98ad1c 100644 --- a/src/copilot/copilotHelper.ts +++ b/src/copilot/copilotHelper.ts @@ -12,8 +12,8 @@ export interface INodeImportClass { export interface IImportClassContentResult { classInfoList: INodeImportClass[]; - errorReason?: string; - hasError: boolean; + emptyReason?: string; + isEmpty: boolean; } export interface IProjectDependency { @@ -22,8 +22,8 @@ export interface IProjectDependency { export interface IProjectDependenciesResult { dependencyInfoList: Array<{ key: string; value: string }>; - errorReason?: string; - hasError: boolean; + emptyReason?: string; + isEmpty: boolean; } /** * Helper class for Copilot integration to analyze Java project dependencies @@ -50,8 +50,8 @@ export namespace CopilotHelper { if (cancellationToken?.isCancellationRequested) { return { classInfoList: [], - errorReason: "Copilot_Cancellation_requested", - hasError: false + emptyReason: "CopilotCancelled", + isEmpty: true }; } @@ -77,8 +77,8 @@ export namespace CopilotHelper { if (!result) { return { classInfoList: [], - errorReason: "Command returned null result", - hasError: true + emptyReason: "CommandNullResult", + isEmpty: true }; } @@ -96,8 +96,8 @@ export namespace CopilotHelper { if (!result) { return { classInfoList: [], - errorReason: "Command returned null result", - hasError: true + emptyReason: "CommandNullResult", + isEmpty: true }; } @@ -107,25 +107,25 @@ export namespace CopilotHelper { if (error.message === 'Operation cancelled') { return { classInfoList: [], - errorReason: "Copilot_Cancellation_requested", - hasError: true + emptyReason: "CopilotCancelled", + isEmpty: true }; } if (error.message === 'Operation timed out') { return { classInfoList: [], - errorReason: "Operation timed out after 80ms", - hasError: true + emptyReason: "Timeout", + isEmpty: true }; } - const errorMessage = 'Failed_Get_Import_Info: ' + ((error as Error).message || "unknown_error"); + const errorMessage = 'TsException_' + ((error as Error).message || "unknown"); sendError(new GetImportClassContentError(errorMessage)); return { classInfoList: [], - errorReason: errorMessage, - hasError: true + emptyReason: errorMessage, + isEmpty: true }; } } @@ -158,8 +158,8 @@ export namespace CopilotHelper { if (cancellationToken?.isCancellationRequested) { return { dependencyInfoList: [], - errorReason: "Copilot_Cancellation_requested", - hasError: true + emptyReason: "CopilotCancelled", + isEmpty: true }; } @@ -185,8 +185,8 @@ export namespace CopilotHelper { if (!result) { return { dependencyInfoList: [], - errorReason: "Command returned null result", - hasError: true + emptyReason: "CommandNullResult", + isEmpty: true }; } @@ -204,8 +204,8 @@ export namespace CopilotHelper { if (!result) { return { dependencyInfoList: [], - errorReason: "Command returned null result", - hasError: true + emptyReason: "CommandNullResult", + isEmpty: true }; } @@ -215,26 +215,149 @@ export namespace CopilotHelper { if (error.message === 'Operation cancelled') { return { dependencyInfoList: [], - errorReason: 'Copilot_Cancellation_requested', - hasError: true + emptyReason: 'CopilotCancelled', + isEmpty: true }; } if (error.message === 'Operation timed out') { return { dependencyInfoList: [], - errorReason: "Operation timed out after 40ms", - hasError: true + emptyReason: "Timeout", + isEmpty: true }; } - const errorMessage = 'Failed to get project dependencies: ' + ((error as Error).message || "unknown_error"); + const errorMessage = 'TsException_' + ((error as Error).message || "unknown"); sendError(new GetProjectDependenciesError(errorMessage)); return { dependencyInfoList: [], - errorReason: errorMessage, - hasError: true + emptyReason: errorMessage, + isEmpty: true }; } } + + /** + * Resolves project dependencies and converts them to context items with cancellation support + * @param workspaceFolders The workspace folders, or undefined if none + * @param copilotCancel Cancellation token from Copilot + * @param checkCancellation Function to check for cancellation + * @param sendTelemetry Function to send telemetry data + * @returns Array of context items for project dependencies, or empty array if no workspace folders + */ + export async function resolveAndConvertProjectDependencies( + workspaceFolders: readonly { uri: Uri }[] | undefined, + copilotCancel: CancellationToken, + checkCancellation: (token: CancellationToken) => void, + sendTelemetry: (action: string, status: string, reason?: string) => void + ): Promise> { + const items: Array<{ name: string; value: string; importance: number }> = []; + + // Check if workspace folders exist + if (!workspaceFolders || workspaceFolders.length === 0) { + sendTelemetry("resolveProjectDependencies", "ContextEmpty", "NoWorkspace"); + return items; + } + + const projectUri = workspaceFolders[0]; + + // Resolve project dependencies + const projectDependenciesResult = await resolveProjectDependenciesWithReason(projectUri.uri, copilotCancel); + console.dir(projectDependenciesResult); + + // Check for cancellation after dependency resolution + checkCancellation(copilotCancel); + + // Send telemetry if result is empty + if (projectDependenciesResult.isEmpty && projectDependenciesResult.emptyReason) { + sendTelemetry("resolveProjectDependencies", "ContextEmpty", projectDependenciesResult.emptyReason); + } else if (projectDependenciesResult.dependencyInfoList.length === 0) { + // No error but still empty - likely no dependencies in project + sendTelemetry("resolveProjectDependencies", "ContextEmpty", "NoDependenciesResults"); + } + + // Check for cancellation after telemetry + checkCancellation(copilotCancel); + + // Convert project dependencies to context items + if (projectDependenciesResult.dependencyInfoList && projectDependenciesResult.dependencyInfoList.length > 0) { + for (const dep of projectDependenciesResult.dependencyInfoList) { + items.push({ + name: dep.key, + value: dep.value, + importance: 70 + }); + } + } + + return items; + } + + /** + * Resolves local imports and converts them to context items with cancellation support + * @param activeEditor The active text editor, or undefined if none + * @param copilotCancel Cancellation token from Copilot + * @param checkCancellation Function to check for cancellation + * @param sendTelemetry Function to send telemetry data + * @param createContextItems Function to create context items from imports + * @returns Array of context items for local imports, or empty array if no valid editor + */ + export async function resolveAndConvertLocalImports( + activeEditor: { document: { uri: Uri; languageId: string } } | undefined, + copilotCancel: CancellationToken, + checkCancellation: (token: CancellationToken) => void, + sendTelemetry: (action: string, status: string, reason?: string) => void, + createContextItems: (classInfoList: any[]) => any[] + ): Promise { + const items: any[] = []; + + // Check if there's an active editor with a Java document + if (!activeEditor) { + sendTelemetry("resolveLocalImports", "ContextEmpty", "NoActiveEditor"); + return items; + } + + if (activeEditor.document.languageId !== 'java') { + sendTelemetry("resolveLocalImports", "ContextEmpty", "NotJavaFile"); + return items; + } + + const documentUri = activeEditor.document.uri; + + // Check for cancellation before resolving imports + checkCancellation(copilotCancel); + + // Resolve imports directly without caching + const importClassResult = await resolveLocalImportsWithReason(documentUri, copilotCancel); + console.dir(importClassResult); + + // Check for cancellation after resolution + checkCancellation(copilotCancel); + + // Send telemetry if result is empty + if (importClassResult.isEmpty && importClassResult.emptyReason) { + sendTelemetry("resolveLocalImports", "ContextEmpty", importClassResult.emptyReason); + console.log("Context resolution - local imports empty: " + importClassResult.emptyReason); + } else if (importClassResult.classInfoList.length === 0) { + // No error but still empty - likely no imports in file + sendTelemetry("resolveLocalImports", "ContextEmpty", "NoImportsResults"); + } + + // Check for cancellation before processing results + checkCancellation(copilotCancel); + + if (importClassResult.classInfoList && importClassResult.classInfoList.length > 0) { + // Process imports in batches to reduce cancellation check overhead + const contextItems = createContextItems(importClassResult.classInfoList); + console.dir(contextItems); + + // Check cancellation once after creating all items + checkCancellation(copilotCancel); + + items.push(...contextItems); + } + + return items; + } } From 92046682148632ca5e58d72aa6338b592d048237 Mon Sep 17 00:00:00 2001 From: wenyutang Date: Wed, 29 Oct 2025 11:44:36 +0800 Subject: [PATCH 04/11] feat: remove console log --- src/copilot/contextProvider.ts | 1 - src/copilot/copilotHelper.ts | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/copilot/contextProvider.ts b/src/copilot/contextProvider.ts index e164a7fc..397fb039 100644 --- a/src/copilot/contextProvider.ts +++ b/src/copilot/contextProvider.ts @@ -49,7 +49,6 @@ export async function registerCopilotContextProviders( "status": "succeeded", "installCount": installCount }); - console.log('Java Copilot context provider registered successfully.'); } catch (error) { sendError(new ContextProviderRegistrationError('Failed to register Copilot context provider: ' + ((error as Error).message || "unknown_error"))); diff --git a/src/copilot/copilotHelper.ts b/src/copilot/copilotHelper.ts index 3d98ad1c..20bffb93 100644 --- a/src/copilot/copilotHelper.ts +++ b/src/copilot/copilotHelper.ts @@ -264,7 +264,6 @@ export namespace CopilotHelper { // Resolve project dependencies const projectDependenciesResult = await resolveProjectDependenciesWithReason(projectUri.uri, copilotCancel); - console.dir(projectDependenciesResult); // Check for cancellation after dependency resolution checkCancellation(copilotCancel); @@ -330,7 +329,6 @@ export namespace CopilotHelper { // Resolve imports directly without caching const importClassResult = await resolveLocalImportsWithReason(documentUri, copilotCancel); - console.dir(importClassResult); // Check for cancellation after resolution checkCancellation(copilotCancel); @@ -338,7 +336,6 @@ export namespace CopilotHelper { // Send telemetry if result is empty if (importClassResult.isEmpty && importClassResult.emptyReason) { sendTelemetry("resolveLocalImports", "ContextEmpty", importClassResult.emptyReason); - console.log("Context resolution - local imports empty: " + importClassResult.emptyReason); } else if (importClassResult.classInfoList.length === 0) { // No error but still empty - likely no imports in file sendTelemetry("resolveLocalImports", "ContextEmpty", "NoImportsResults"); @@ -350,7 +347,6 @@ export namespace CopilotHelper { if (importClassResult.classInfoList && importClassResult.classInfoList.length > 0) { // Process imports in batches to reduce cancellation check overhead const contextItems = createContextItems(importClassResult.classInfoList); - console.dir(contextItems); // Check cancellation once after creating all items checkCancellation(copilotCancel); From 118ef8d7aa80b2111277afcc457b7bdf44fa575e Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 29 Oct 2025 13:58:32 +0800 Subject: [PATCH 05/11] feat: add cache for project info --- .../ext/core/parser/ProjectResolver.java | 228 +++++++++++++++++- 1 file changed, 223 insertions(+), 5 deletions(-) diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java index 2f317a41..24c9ad77 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java @@ -2,13 +2,25 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IResourceDeltaVisitor; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.ElementChangedEvent; import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IElementChangedListener; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaElementDelta; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; @@ -19,6 +31,179 @@ public class ProjectResolver { + // Cache for project dependency information + private static final Map dependencyCache = new ConcurrentHashMap<>(); + + // Flag to track if listeners are registered + private static volatile boolean listenersRegistered = false; + + // Lock for listener registration + private static final Object listenerLock = new Object(); + + /** + * Cached dependency information with timestamp + */ + private static class CachedDependencyInfo { + final List dependencies; + final long timestamp; + final long classpathHash; + + CachedDependencyInfo(List dependencies, long classpathHash) { + this.dependencies = new ArrayList<>(dependencies); + this.timestamp = System.currentTimeMillis(); + this.classpathHash = classpathHash; + } + + boolean isValid() { + // Cache is valid for 5 minutes + return (System.currentTimeMillis() - timestamp) < 300000; + } + } + + /** + * Listener for Java element changes (classpath changes, project references, etc.) + */ + private static final IElementChangedListener javaElementListener = new IElementChangedListener() { + @Override + public void elementChanged(ElementChangedEvent event) { + IJavaElementDelta delta = event.getDelta(); + processDelta(delta); + } + + private void processDelta(IJavaElementDelta delta) { + IJavaElement element = delta.getElement(); + int flags = delta.getFlags(); + + // Check for classpath changes + if ((flags & IJavaElementDelta.F_CLASSPATH_CHANGED) != 0 || + (flags & IJavaElementDelta.F_RESOLVED_CLASSPATH_CHANGED) != 0) { + + if (element instanceof IJavaProject) { + IJavaProject project = (IJavaProject) element; + invalidateCache(project.getProject()); + } + } + + // Recursively process children + for (IJavaElementDelta child : delta.getAffectedChildren()) { + processDelta(child); + } + } + }; + + /** + * Listener for resource changes (pom.xml, build.gradle, etc.) + */ + private static final IResourceChangeListener resourceListener = new IResourceChangeListener() { + @Override + public void resourceChanged(IResourceChangeEvent event) { + if (event.getType() != IResourceChangeEvent.POST_CHANGE) { + return; + } + + IResourceDelta delta = event.getDelta(); + if (delta == null) { + return; + } + + try { + delta.accept(new IResourceDeltaVisitor() { + @Override + public boolean visit(IResourceDelta delta) throws CoreException { + IResource resource = delta.getResource(); + + // Check for build file changes + if (resource.getType() == IResource.FILE) { + String fileName = resource.getName(); + if ("pom.xml".equals(fileName) || + "build.gradle".equals(fileName) || + "build.gradle.kts".equals(fileName) || + ".classpath".equals(fileName) || + ".project".equals(fileName)) { + + IProject project = resource.getProject(); + if (project != null) { + invalidateCache(project); + } + } + } + return true; + } + }); + } catch (CoreException e) { + JdtlsExtActivator.logException("Error processing resource delta", e); + } + } + }; + + /** + * Initialize listeners for cache invalidation + */ + private static void ensureListenersRegistered() { + if (!listenersRegistered) { + synchronized (listenerLock) { + if (!listenersRegistered) { + try { + // Register Java element change listener + JavaCore.addElementChangedListener(javaElementListener, + ElementChangedEvent.POST_CHANGE); + + // Register resource change listener + ResourcesPlugin.getWorkspace().addResourceChangeListener( + resourceListener, + IResourceChangeEvent.POST_CHANGE); + + listenersRegistered = true; + JdtlsExtActivator.logInfo("ProjectResolver cache listeners registered successfully"); + } catch (Exception e) { + JdtlsExtActivator.logException("Failed to register ProjectResolver listeners", e); + } + } + } + } + } + + /** + * Invalidate cache for a specific project + */ + private static void invalidateCache(IProject project) { + if (project == null) { + return; + } + + String projectPath = project.getLocation() != null ? + project.getLocation().toOSString() : project.getName(); + + if (dependencyCache.remove(projectPath) != null) { + JdtlsExtActivator.logInfo("Cache invalidated for project: " + project.getName()); + } + } + + /** + * Clear all cached dependency information + */ + public static void clearCache() { + dependencyCache.clear(); + JdtlsExtActivator.logInfo("ProjectResolver cache cleared"); + } + + /** + * Calculate a simple hash of classpath entries for cache validation + */ + private static long calculateClasspathHash(IJavaProject javaProject) { + try { + IClasspathEntry[] entries = javaProject.getResolvedClasspath(true); + long hash = 0; + for (IClasspathEntry entry : entries) { + hash = hash * 31 + entry.getPath().toString().hashCode(); + hash = hash * 31 + entry.getEntryKind(); + } + return hash; + } catch (JavaModelException e) { + return 0; + } + } + // Constants for dependency info keys private static final String KEY_BUILD_TOOL = "buildTool"; private static final String KEY_PROJECT_NAME = "projectName"; @@ -29,7 +214,6 @@ public class ProjectResolver { private static final String KEY_MODULE_NAME = "moduleName"; private static final String KEY_TOTAL_LIBRARIES = "totalLibraries"; private static final String KEY_TOTAL_PROJECT_REFS = "totalProjectReferences"; - private static final String KEY_JRE_CONTAINER_PATH = "jreContainerPath"; private static final String KEY_JRE_CONTAINER = "jreContainer"; public static class DependencyInfo { @@ -44,12 +228,16 @@ public DependencyInfo(String key, String value) { /** * Resolve project dependencies information including JDK version. + * Uses cache with automatic invalidation on project changes. * * @param projectUri The project URI * @param monitor Progress monitor for cancellation support * @return List of DependencyInfo containing key-value pairs of project information */ public static List resolveProjectDependencies(String projectUri, IProgressMonitor monitor) { + // Ensure listeners are registered for cache invalidation + ensureListenersRegistered(); + List result = new ArrayList<>(); try { @@ -68,6 +256,22 @@ public static List resolveProjectDependencies(String projectUri, return result; } + // Generate cache key based on project location + String cacheKey = projectPath.toOSString(); + + // Calculate current classpath hash for validation + long currentClasspathHash = calculateClasspathHash(javaProject); + + // Try to get from cache + CachedDependencyInfo cached = dependencyCache.get(cacheKey); + if (cached != null && cached.isValid() && cached.classpathHash == currentClasspathHash) { + JdtlsExtActivator.logInfo("Using cached dependencies for project: " + project.getName()); + return new ArrayList<>(cached.dependencies); + } + + // Cache miss or invalid - resolve dependencies + JdtlsExtActivator.logInfo("Resolving dependencies for project: " + project.getName()); + // Add basic project information addBasicProjectInfo(result, project, javaProject); @@ -77,6 +281,9 @@ public static List resolveProjectDependencies(String projectUri, // Add build tool info by checking for build files detectBuildTool(result, project); + // Store in cache + dependencyCache.put(cacheKey, new CachedDependencyInfo(result, currentClasspathHash)); + } catch (Exception e) { JdtlsExtActivator.logException("Error in resolveProjectDependencies", e); } @@ -158,12 +365,13 @@ private static void processClasspathEntries(List result, IJavaPr /** * Process a library classpath entry. + * Only returns the library file name without full path to reduce data size. */ private static void processLibraryEntry(List result, IClasspathEntry entry, int libCount) { IPath libPath = entry.getPath(); if (libPath != null) { - result.add(new DependencyInfo("library_" + libCount, - libPath.lastSegment() + " (" + libPath.toOSString() + ")")); + // Only keep the file name, remove the full path + result.add(new DependencyInfo("library_" + libCount, libPath.lastSegment())); } } @@ -180,17 +388,27 @@ private static void processProjectEntry(List result, IClasspathE /** * Process a container classpath entry (JRE, Maven, Gradle containers). + * Simplified to only extract essential information. */ private static void processContainerEntry(List result, IClasspathEntry entry) { String containerPath = entry.getPath().toString(); if (containerPath.contains("JRE_CONTAINER")) { - result.add(new DependencyInfo(KEY_JRE_CONTAINER_PATH, containerPath)); + // Only extract the JRE version, not the full container path try { String vmInstallName = JavaRuntime.getVMInstallName(entry.getPath()); addIfNotNull(result, KEY_JRE_CONTAINER, vmInstallName); } catch (Exception e) { - // Ignore if unable to get VM install name + // Fallback: try to extract version from path + if (containerPath.contains("JavaSE-")) { + int startIdx = containerPath.lastIndexOf("JavaSE-"); + String version = containerPath.substring(startIdx); + // Clean up any trailing characters + if (version.contains("/")) { + version = version.substring(0, version.indexOf("/")); + } + result.add(new DependencyInfo(KEY_JRE_CONTAINER, version)); + } } } else if (containerPath.contains("MAVEN")) { result.add(new DependencyInfo(KEY_BUILD_TOOL, "Maven")); From 8dc14c35872094b4f29dcd76f55af16fe6a08dfe Mon Sep 17 00:00:00 2001 From: wenyutang Date: Wed, 29 Oct 2025 14:40:39 +0800 Subject: [PATCH 06/11] feat: remove useless code --- src/commands.ts | 6 ------ src/copilot/contextProvider.ts | 3 +++ src/copilot/copilotHelper.ts | 2 ++ src/java/jdtls.ts | 5 ----- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index c59c2572..f7b2d880 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -38,10 +38,6 @@ export namespace Commands { export const EXPORT_JAR_REPORT = "java.view.package.exportJarReport"; - export const IMPORT_CLASS_CONTENT_TELEMETRY = "java.importClassContent.telemetry"; - - export const PROJECT_DEPENDENCIES_TELEMETRY = "java.projectDependencies.telemetry"; - export const VIEW_PACKAGE_NEW = "java.view.package.new"; export const VIEW_PACKAGE_NEW_JAVA_CLASS = "java.view.package.newJavaClass"; @@ -136,8 +132,6 @@ export namespace Commands { export const JAVA_UPDATE_DEPRECATED_TASK = "java.updateDeprecatedTask"; - export const JAVA_PROJECT_CHECK_IMPORT_STATUS = "java.project.checkImportStatus"; - export const JAVA_PROJECT_GET_IMPORT_CLASS_CONTENT = "java.project.getImportClassContent"; export const JAVA_PROJECT_GET_DEPENDENCIES = "java.project.getDependencies"; diff --git a/src/copilot/contextProvider.ts b/src/copilot/contextProvider.ts index 397fb039..410508a7 100644 --- a/src/copilot/contextProvider.ts +++ b/src/copilot/contextProvider.ts @@ -42,6 +42,7 @@ export async function registerCopilotContextProviders( if (installCount === 0) { return; } + console.log('===== register Copilot Java context provider succeeded ====='); sendInfo("", { "action": "registerCopilotContextProvider", @@ -119,6 +120,7 @@ async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode sendInfo("", telemetryData); } ); + console.dir(projectDependencyItems); JavaContextProviderUtils.checkCancellation(copilotCancel); items.push(...projectDependencyItems); @@ -142,6 +144,7 @@ async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode }, JavaContextProviderUtils.createContextItemsFromImports ); + console.dir(localImportItems); JavaContextProviderUtils.checkCancellation(copilotCancel); items.push(...localImportItems); diff --git a/src/copilot/copilotHelper.ts b/src/copilot/copilotHelper.ts index 20bffb93..02edec4b 100644 --- a/src/copilot/copilotHelper.ts +++ b/src/copilot/copilotHelper.ts @@ -289,6 +289,7 @@ export namespace CopilotHelper { }); } } + // console.dir(items); return items; } @@ -353,6 +354,7 @@ export namespace CopilotHelper { items.push(...contextItems); } + // console.dir(items); return items; } diff --git a/src/java/jdtls.ts b/src/java/jdtls.ts index d92d708f..84a372a0 100644 --- a/src/java/jdtls.ts +++ b/src/java/jdtls.ts @@ -9,7 +9,6 @@ import { IClasspath } from "../tasks/buildArtifact/IStepMetadata"; import { IMainClassInfo } from "../tasks/buildArtifact/ResolveMainClassExecutor"; import { INodeData, NodeKind } from "./nodeData"; import { Settings } from "../settings"; -import { INodeImportClass } from "../copilot/copilotHelper"; export namespace Jdtls { export async function getProjects(params: string): Promise { @@ -87,10 +86,6 @@ export namespace Jdtls { return await commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_GET_DEPENDENCIES, projectUri) || []; } - export async function getImportClassContent(fileUri: string, token: CancellationToken): Promise { - return await commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_GET_IMPORT_CLASS_CONTENT, fileUri, token) || []; - } - export enum CompileWorkspaceStatus { Failed = 0, Succeed = 1, From de08a66d505c59078e9124a56eb22a559746dfc8 Mon Sep 17 00:00:00 2001 From: wenyutang Date: Wed, 29 Oct 2025 15:05:29 +0800 Subject: [PATCH 07/11] chore: update --- src/commands.ts | 2 ++ src/copilot/copilotHelper.ts | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index f7b2d880..ac7e5632 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -132,6 +132,8 @@ export namespace Commands { export const JAVA_UPDATE_DEPRECATED_TASK = "java.updateDeprecatedTask"; + export const JAVA_PROJECT_CHECK_IMPORT_STATUS = "java.project.checkImportStatus"; + export const JAVA_PROJECT_GET_IMPORT_CLASS_CONTENT = "java.project.getImportClassContent"; export const JAVA_PROJECT_GET_DEPENDENCIES = "java.project.getDependencies"; diff --git a/src/copilot/copilotHelper.ts b/src/copilot/copilotHelper.ts index 02edec4b..e7e703ee 100644 --- a/src/copilot/copilotHelper.ts +++ b/src/copilot/copilotHelper.ts @@ -4,7 +4,7 @@ import { commands, Uri, CancellationToken } from "vscode"; import { sendError } from "vscode-extension-telemetry-wrapper"; import { GetImportClassContentError, GetProjectDependenciesError } from "./utils"; - +import { Commands } from '../commands'; export interface INodeImportClass { uri: string; value: string; // Changed from 'class' to 'className' to match Java code @@ -57,7 +57,7 @@ export namespace CopilotHelper { try { // Use the new command with error reason support - const commandPromise = commands.executeCommand("java.execute.workspaceCommand", "java.project.getImportClassContent", fileUri.toString()) as Promise; + const commandPromise = commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_GET_IMPORT_CLASS_CONTENT, fileUri.toString()) as Promise; if (cancellationToken) { const result = await Promise.race([ @@ -165,7 +165,7 @@ export namespace CopilotHelper { try { // Use the new command with error reason support - const commandPromise = commands.executeCommand("java.execute.workspaceCommand", "java.project.getDependencies", projectUri.toString()) as Promise; + const commandPromise = commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_GET_DEPENDENCIES, projectUri.toString()) as Promise; if (cancellationToken) { const result = await Promise.race([ From 7aca40462ffbd6ed9f80c34a12064d5fcd167dee Mon Sep 17 00:00:00 2001 From: wenyutang Date: Wed, 29 Oct 2025 16:58:01 +0800 Subject: [PATCH 08/11] perf: update --- .../com.microsoft.jdtls.ext.core/plugin.xml | 2 ++ .../microsoft/jdtls/ext/core/CommandHandler.java | 4 ++++ .../microsoft/jdtls/ext/core/ProjectCommand.java | 16 ++++++++++++++-- src/commands.ts | 4 ++-- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml b/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml index a469ec92..3c2ead52 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml @@ -12,6 +12,8 @@ + + arguments, IProgress return ProjectCommand.getImportClassContent(arguments, monitor); case "java.project.getDependencies": return ProjectCommand.getProjectDependencies(arguments, monitor); + case "java.project.getImportClassContentWithResult": + return ProjectCommand.getImportClassContentWithResult(arguments, monitor); + case "java.project.getProjectDependenciesWithResult": + return ProjectCommand.getProjectDependenciesWithResult(arguments, monitor); default: break; } diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java index f55c0142..b5a54273 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java @@ -445,6 +445,12 @@ public static boolean checkImportStatus() { return hasError; } + // This method reserver for pack. + public static List getImportClassContent(List arguments, IProgressMonitor monitor) { + ImportClassContentResult result = getImportClassContentWithResult(arguments, monitor); + return result == null ? Collections.emptyList() : result.classInfoList; + } + /** * Get import class content for Copilot integration with detailed error reporting. * This method extracts information about imported classes from a Java file. @@ -454,7 +460,7 @@ public static boolean checkImportStatus() { * @param monitor Progress monitor for cancellation support * @return ImportClassContentResult containing class information and error reason if applicable */ - public static ImportClassContentResult getImportClassContent(List arguments, IProgressMonitor monitor) { + public static ImportClassContentResult getImportClassContentWithResult(List arguments, IProgressMonitor monitor) { if (arguments == null || arguments.isEmpty()) { return new ImportClassContentResult(ImportClassContentErrorReason.NULL_ARGUMENTS); } @@ -619,6 +625,12 @@ private static String getSeverityString(int severity) { } } + // resverved for pack. + public static List getProjectDependencies(List arguments, IProgressMonitor monitor) { + ProjectDependenciesResult result = getProjectDependenciesWithResult(arguments, monitor); + return result == null ? Collections.emptyList() : result.dependencyInfoList; + } + /** * Get project dependencies information with detailed error reporting. @@ -628,7 +640,7 @@ private static String getSeverityString(int severity) { * @param monitor Progress monitor for cancellation support * @return ProjectDependenciesResult containing dependency information and error reason if applicable */ - public static ProjectDependenciesResult getProjectDependencies(List arguments, IProgressMonitor monitor) { + public static ProjectDependenciesResult getProjectDependenciesWithResult(List arguments, IProgressMonitor monitor) { if (arguments == null || arguments.isEmpty()) { return new ProjectDependenciesResult(ProjectDependenciesErrorReason.NULL_ARGUMENTS); } diff --git a/src/commands.ts b/src/commands.ts index ac7e5632..1daec7e6 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -134,9 +134,9 @@ export namespace Commands { export const JAVA_PROJECT_CHECK_IMPORT_STATUS = "java.project.checkImportStatus"; - export const JAVA_PROJECT_GET_IMPORT_CLASS_CONTENT = "java.project.getImportClassContent"; + export const JAVA_PROJECT_GET_IMPORT_CLASS_CONTENT = "java.project.getImportClassContentWithResult"; - export const JAVA_PROJECT_GET_DEPENDENCIES = "java.project.getDependencies"; + export const JAVA_PROJECT_GET_DEPENDENCIES = "java.project.getProjectDependenciesWithResult"; export const JAVA_UPGRADE_WITH_COPILOT = "_java.upgradeWithCopilot"; From c0f03ae7ce60d2459e9467c9c9c070022d4e28be Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Wed, 29 Oct 2025 17:02:09 +0800 Subject: [PATCH 09/11] style: unify code --- src/copilot/copilotHelper.ts | 73 ++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/src/copilot/copilotHelper.ts b/src/copilot/copilotHelper.ts index e7e703ee..44ceff79 100644 --- a/src/copilot/copilotHelper.ts +++ b/src/copilot/copilotHelper.ts @@ -5,6 +5,29 @@ import { commands, Uri, CancellationToken } from "vscode"; import { sendError } from "vscode-extension-telemetry-wrapper"; import { GetImportClassContentError, GetProjectDependenciesError } from "./utils"; import { Commands } from '../commands'; + +/** + * Enum for error messages used in Promise rejection + */ +export enum ErrorMessage { + OperationCancelled = "Operation cancelled", + OperationTimedOut = "Operation timed out" +} + +/** + * Enum for empty reason codes when operations return empty results + */ +export enum EmptyReason { + CopilotCancelled = "CopilotCancelled", + CommandNullResult = "CommandNullResult", + Timeout = "Timeout", + NoWorkspace = "NoWorkspace", + NoDependenciesResults = "NoDependenciesResults", + NoActiveEditor = "NoActiveEditor", + NotJavaFile = "NotJavaFile", + NoImportsResults = "NoImportsResults" +} + export interface INodeImportClass { uri: string; value: string; // Changed from 'class' to 'className' to match Java code @@ -50,7 +73,7 @@ export namespace CopilotHelper { if (cancellationToken?.isCancellationRequested) { return { classInfoList: [], - emptyReason: "CopilotCancelled", + emptyReason: EmptyReason.CopilotCancelled, isEmpty: true }; } @@ -64,12 +87,12 @@ export namespace CopilotHelper { commandPromise, new Promise((_, reject) => { cancellationToken.onCancellationRequested(() => { - reject(new Error('Operation cancelled')); + reject(new Error(ErrorMessage.OperationCancelled)); }); }), new Promise((_, reject) => { setTimeout(() => { - reject(new Error('Operation timed out')); + reject(new Error(ErrorMessage.OperationTimedOut)); }, 80); // 80ms timeout }) ]); @@ -77,7 +100,7 @@ export namespace CopilotHelper { if (!result) { return { classInfoList: [], - emptyReason: "CommandNullResult", + emptyReason: EmptyReason.CommandNullResult, isEmpty: true }; } @@ -88,7 +111,7 @@ export namespace CopilotHelper { commandPromise, new Promise((_, reject) => { setTimeout(() => { - reject(new Error('Operation timed out')); + reject(new Error(ErrorMessage.OperationTimedOut)); }, 80); // 80ms timeout }) ]); @@ -96,7 +119,7 @@ export namespace CopilotHelper { if (!result) { return { classInfoList: [], - emptyReason: "CommandNullResult", + emptyReason: EmptyReason.CommandNullResult, isEmpty: true }; } @@ -104,18 +127,18 @@ export namespace CopilotHelper { return result; } } catch (error: any) { - if (error.message === 'Operation cancelled') { + if (error.message === ErrorMessage.OperationCancelled) { return { classInfoList: [], - emptyReason: "CopilotCancelled", + emptyReason: EmptyReason.CopilotCancelled, isEmpty: true }; } - if (error.message === 'Operation timed out') { + if (error.message === ErrorMessage.OperationTimedOut) { return { classInfoList: [], - emptyReason: "Timeout", + emptyReason: EmptyReason.Timeout, isEmpty: true }; } @@ -158,7 +181,7 @@ export namespace CopilotHelper { if (cancellationToken?.isCancellationRequested) { return { dependencyInfoList: [], - emptyReason: "CopilotCancelled", + emptyReason: EmptyReason.CopilotCancelled, isEmpty: true }; } @@ -172,12 +195,12 @@ export namespace CopilotHelper { commandPromise, new Promise((_, reject) => { cancellationToken.onCancellationRequested(() => { - reject(new Error('Operation cancelled')); + reject(new Error(ErrorMessage.OperationCancelled)); }); }), new Promise((_, reject) => { setTimeout(() => { - reject(new Error('Operation timed out')); + reject(new Error(ErrorMessage.OperationTimedOut)); }, 40); // 40ms timeout }) ]); @@ -185,7 +208,7 @@ export namespace CopilotHelper { if (!result) { return { dependencyInfoList: [], - emptyReason: "CommandNullResult", + emptyReason: EmptyReason.CommandNullResult, isEmpty: true }; } @@ -196,7 +219,7 @@ export namespace CopilotHelper { commandPromise, new Promise((_, reject) => { setTimeout(() => { - reject(new Error('Operation timed out')); + reject(new Error(ErrorMessage.OperationTimedOut)); }, 40); // 40ms timeout }) ]); @@ -204,7 +227,7 @@ export namespace CopilotHelper { if (!result) { return { dependencyInfoList: [], - emptyReason: "CommandNullResult", + emptyReason: EmptyReason.CommandNullResult, isEmpty: true }; } @@ -212,18 +235,18 @@ export namespace CopilotHelper { return result; } } catch (error: any) { - if (error.message === 'Operation cancelled') { + if (error.message === ErrorMessage.OperationCancelled) { return { dependencyInfoList: [], - emptyReason: 'CopilotCancelled', + emptyReason: EmptyReason.CopilotCancelled, isEmpty: true }; } - if (error.message === 'Operation timed out') { + if (error.message === ErrorMessage.OperationTimedOut) { return { dependencyInfoList: [], - emptyReason: "Timeout", + emptyReason: EmptyReason.Timeout, isEmpty: true }; } @@ -256,7 +279,7 @@ export namespace CopilotHelper { // Check if workspace folders exist if (!workspaceFolders || workspaceFolders.length === 0) { - sendTelemetry("resolveProjectDependencies", "ContextEmpty", "NoWorkspace"); + sendTelemetry("resolveProjectDependencies", "ContextEmpty", EmptyReason.NoWorkspace); return items; } @@ -273,7 +296,7 @@ export namespace CopilotHelper { sendTelemetry("resolveProjectDependencies", "ContextEmpty", projectDependenciesResult.emptyReason); } else if (projectDependenciesResult.dependencyInfoList.length === 0) { // No error but still empty - likely no dependencies in project - sendTelemetry("resolveProjectDependencies", "ContextEmpty", "NoDependenciesResults"); + sendTelemetry("resolveProjectDependencies", "ContextEmpty", EmptyReason.NoDependenciesResults); } // Check for cancellation after telemetry @@ -314,12 +337,12 @@ export namespace CopilotHelper { // Check if there's an active editor with a Java document if (!activeEditor) { - sendTelemetry("resolveLocalImports", "ContextEmpty", "NoActiveEditor"); + sendTelemetry("resolveLocalImports", "ContextEmpty", EmptyReason.NoActiveEditor); return items; } if (activeEditor.document.languageId !== 'java') { - sendTelemetry("resolveLocalImports", "ContextEmpty", "NotJavaFile"); + sendTelemetry("resolveLocalImports", "ContextEmpty", EmptyReason.NotJavaFile); return items; } @@ -339,7 +362,7 @@ export namespace CopilotHelper { sendTelemetry("resolveLocalImports", "ContextEmpty", importClassResult.emptyReason); } else if (importClassResult.classInfoList.length === 0) { // No error but still empty - likely no imports in file - sendTelemetry("resolveLocalImports", "ContextEmpty", "NoImportsResults"); + sendTelemetry("resolveLocalImports", "ContextEmpty", EmptyReason.NoImportsResults); } // Check for cancellation before processing results From 7e671c80a3b4e0a083e0fdb38cbcd19901da71fc Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Thu, 30 Oct 2025 09:18:43 +0800 Subject: [PATCH 10/11] update --- src/copilot/contextProvider.ts | 3 --- src/copilot/copilotHelper.ts | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/copilot/contextProvider.ts b/src/copilot/contextProvider.ts index 410508a7..397fb039 100644 --- a/src/copilot/contextProvider.ts +++ b/src/copilot/contextProvider.ts @@ -42,7 +42,6 @@ export async function registerCopilotContextProviders( if (installCount === 0) { return; } - console.log('===== register Copilot Java context provider succeeded ====='); sendInfo("", { "action": "registerCopilotContextProvider", @@ -120,7 +119,6 @@ async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode sendInfo("", telemetryData); } ); - console.dir(projectDependencyItems); JavaContextProviderUtils.checkCancellation(copilotCancel); items.push(...projectDependencyItems); @@ -144,7 +142,6 @@ async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode }, JavaContextProviderUtils.createContextItemsFromImports ); - console.dir(localImportItems); JavaContextProviderUtils.checkCancellation(copilotCancel); items.push(...localImportItems); diff --git a/src/copilot/copilotHelper.ts b/src/copilot/copilotHelper.ts index 44ceff79..bd686972 100644 --- a/src/copilot/copilotHelper.ts +++ b/src/copilot/copilotHelper.ts @@ -312,7 +312,6 @@ export namespace CopilotHelper { }); } } - // console.dir(items); return items; } @@ -377,7 +376,6 @@ export namespace CopilotHelper { items.push(...contextItems); } - // console.dir(items); return items; } From 29870e8aa671e0e7f27ec57e1a5e15a33865a5a5 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Thu, 30 Oct 2025 14:26:05 +0800 Subject: [PATCH 11/11] feat: fix path issue and aggregate project --- .../ext/core/parser/ProjectResolver.java | 213 +++++++++++++++++- src/copilot/contextProvider.ts | 25 +- src/copilot/copilotHelper.ts | 56 ++--- src/copilot/utils.ts | 46 ++-- 4 files changed, 268 insertions(+), 72 deletions(-) diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java index 24c9ad77..73aa001b 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java @@ -229,6 +229,7 @@ public DependencyInfo(String key, String value) { /** * Resolve project dependencies information including JDK version. * Uses cache with automatic invalidation on project changes. + * Supports both single projects and multi-module aggregator projects. * * @param projectUri The project URI * @param monitor Progress monitor for cancellation support @@ -252,8 +253,14 @@ public static List resolveProjectDependencies(String projectUri, } IJavaProject javaProject = JavaCore.create(project); + + // Check if this is a Java project if (javaProject == null || !javaProject.exists()) { - return result; + // Not a Java project - might be an aggregator/parent project + // Try to find Java sub-projects under this path + JdtlsExtActivator.logInfo("Not a Java project: " + project.getName() + + ", checking for sub-projects"); + return resolveAggregatorProjectDependencies(root, projectPath, monitor); } // Generate cache key based on project location @@ -291,6 +298,210 @@ public static List resolveProjectDependencies(String projectUri, return result; } + /** + * Resolve dependencies for an aggregator/parent project by finding and processing all Java sub-projects. + * This handles multi-module Maven/Gradle projects where the parent is not a Java project itself. + * Returns aggregated information useful for AI context (Java version, common dependencies, build tool). + * + * @param root The workspace root + * @param parentPath The path of the parent/aggregator project + * @param monitor Progress monitor + * @return Aggregated dependency information from all sub-projects + */ + private static List resolveAggregatorProjectDependencies( + IWorkspaceRoot root, IPath parentPath, IProgressMonitor monitor) { + + List result = new ArrayList<>(); + List javaProjects = new ArrayList<>(); + + // Find all Java projects under the parent path + IProject[] allProjects = root.getProjects(); + for (IProject p : allProjects) { + if (p.getLocation() != null && parentPath.isPrefixOf(p.getLocation())) { + try { + if (p.isAccessible() && p.hasNature(JavaCore.NATURE_ID)) { + IJavaProject jp = JavaCore.create(p); + if (jp != null && jp.exists()) { + javaProjects.add(jp); + } + } + } catch (CoreException e) { + // Skip this project + } + } + } + + if (javaProjects.isEmpty()) { + JdtlsExtActivator.logInfo("No Java sub-projects found under: " + parentPath.toOSString()); + return result; + } + + JdtlsExtActivator.logInfo("Found " + javaProjects.size() + + " Java sub-project(s) under: " + parentPath.toOSString()); + + // Mark as aggregator project + result.add(new DependencyInfo("aggregatorProject", "true")); + result.add(new DependencyInfo("totalSubProjects", String.valueOf(javaProjects.size()))); + + // Collect sub-project names for reference + StringBuilder projectNames = new StringBuilder(); + for (int i = 0; i < javaProjects.size(); i++) { + if (i > 0) projectNames.append(", "); + projectNames.append(javaProjects.get(i).getProject().getName()); + } + result.add(new DependencyInfo("subProjectNames", projectNames.toString())); + + // Determine the primary/representative Java version (most common or highest) + String primaryJavaVersion = determinePrimaryJavaVersion(javaProjects); + if (primaryJavaVersion != null) { + result.add(new DependencyInfo(KEY_JAVA_VERSION, primaryJavaVersion)); + } + + // Collect all unique libraries across sub-projects (top 10 most common) + Map libraryFrequency = collectLibraryFrequency(javaProjects, monitor); + addTopLibraries(result, libraryFrequency, 10); + + // Detect build tool from parent directory + IProject parentProject = findProjectByPath(root, parentPath); + if (parentProject != null) { + detectBuildTool(result, parentProject); + } + + // Get JRE container info from first sub-project (usually consistent across modules) + if (!javaProjects.isEmpty()) { + extractJreInfo(result, javaProjects.get(0)); + } + + return result; + } + + /** + * Determine the primary Java version from all sub-projects. + * Returns the most common version, or the highest if there's a tie. + */ + private static String determinePrimaryJavaVersion(List javaProjects) { + Map versionCount = new ConcurrentHashMap<>(); + + for (IJavaProject jp : javaProjects) { + String version = jp.getOption(JavaCore.COMPILER_COMPLIANCE, true); + if (version != null) { + versionCount.put(version, versionCount.getOrDefault(version, 0) + 1); + } + } + + if (versionCount.isEmpty()) { + return null; + } + + // Find most common version (or highest if tie) + return versionCount.entrySet().stream() + .max((e1, e2) -> { + int countCompare = Integer.compare(e1.getValue(), e2.getValue()); + if (countCompare != 0) return countCompare; + // If same count, prefer higher version + return e1.getKey().compareTo(e2.getKey()); + }) + .map(Map.Entry::getKey) + .orElse(null); + } + + /** + * Collect frequency of all libraries across sub-projects. + * Returns a map of library name to frequency count. + */ + private static Map collectLibraryFrequency( + List javaProjects, IProgressMonitor monitor) { + + Map libraryFrequency = new ConcurrentHashMap<>(); + + for (IJavaProject jp : javaProjects) { + if (monitor.isCanceled()) { + break; + } + + try { + IClasspathEntry[] entries = jp.getResolvedClasspath(true); + for (IClasspathEntry entry : entries) { + if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) { + IPath libPath = entry.getPath(); + if (libPath != null) { + String libName = libPath.lastSegment(); + libraryFrequency.put(libName, + libraryFrequency.getOrDefault(libName, 0) + 1); + } + } + } + } catch (JavaModelException e) { + // Skip this project + } + } + + return libraryFrequency; + } + + /** + * Add top N most common libraries to result. + */ + private static void addTopLibraries(List result, + Map libraryFrequency, int topN) { + + if (libraryFrequency.isEmpty()) { + result.add(new DependencyInfo(KEY_TOTAL_LIBRARIES, "0")); + return; + } + + // Sort by frequency (descending) and take top N + List> topLibs = libraryFrequency.entrySet().stream() + .sorted((e1, e2) -> Integer.compare(e2.getValue(), e1.getValue())) + .limit(topN) + .collect(java.util.stream.Collectors.toList()); + + result.add(new DependencyInfo(KEY_TOTAL_LIBRARIES, + String.valueOf(libraryFrequency.size()))); + + // Add top common libraries + int index = 1; + for (Map.Entry entry : topLibs) { + result.add(new DependencyInfo("commonLibrary_" + index, + entry.getKey() + " (used in " + entry.getValue() + " modules)")); + index++; + } + } + + /** + * Extract JRE container information from a Java project. + */ + private static void extractJreInfo(List result, IJavaProject javaProject) { + try { + IClasspathEntry[] entries = javaProject.getResolvedClasspath(true); + for (IClasspathEntry entry : entries) { + if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) { + String containerPath = entry.getPath().toString(); + if (containerPath.contains("JRE_CONTAINER")) { + try { + String vmInstallName = JavaRuntime.getVMInstallName(entry.getPath()); + addIfNotNull(result, KEY_JRE_CONTAINER, vmInstallName); + return; + } catch (Exception e) { + // Fallback: extract from path + if (containerPath.contains("JavaSE-")) { + int startIdx = containerPath.lastIndexOf("JavaSE-"); + String version = containerPath.substring(startIdx); + if (version.contains("/")) { + version = version.substring(0, version.indexOf("/")); + } + result.add(new DependencyInfo(KEY_JRE_CONTAINER, version)); + return; + } + } + } + } + } + } catch (JavaModelException e) { + // Ignore + } + } + /** * Find project by path from all projects in workspace. */ diff --git a/src/copilot/contextProvider.ts b/src/copilot/contextProvider.ts index 397fb039..fd3be432 100644 --- a/src/copilot/contextProvider.ts +++ b/src/copilot/contextProvider.ts @@ -107,17 +107,7 @@ async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode const projectDependencyItems = await CopilotHelper.resolveAndConvertProjectDependencies( vscode.workspace.workspaceFolders, copilotCancel, - JavaContextProviderUtils.checkCancellation, - (action: string, status: string, reason?: string) => { - const telemetryData: any = { - "action": action, - "status": status - }; - if (reason) { - telemetryData.ContextEmptyReason = reason; - } - sendInfo("", telemetryData); - } + JavaContextProviderUtils.checkCancellation ); JavaContextProviderUtils.checkCancellation(copilotCancel); @@ -129,18 +119,7 @@ async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode const localImportItems = await CopilotHelper.resolveAndConvertLocalImports( vscode.window.activeTextEditor, copilotCancel, - JavaContextProviderUtils.checkCancellation, - (action: string, status: string, reason?: string) => { - const telemetryData: any = { - "action": action, - "status": status - }; - if (reason) { - telemetryData.ContextEmptyReason = reason; - } - sendInfo("", telemetryData); - }, - JavaContextProviderUtils.createContextItemsFromImports + JavaContextProviderUtils.checkCancellation ); JavaContextProviderUtils.checkCancellation(copilotCancel); diff --git a/src/copilot/copilotHelper.ts b/src/copilot/copilotHelper.ts index bd686972..2238512f 100644 --- a/src/copilot/copilotHelper.ts +++ b/src/copilot/copilotHelper.ts @@ -2,8 +2,8 @@ // Licensed under the MIT license. import { commands, Uri, CancellationToken } from "vscode"; -import { sendError } from "vscode-extension-telemetry-wrapper"; -import { GetImportClassContentError, GetProjectDependenciesError } from "./utils"; +import { sendError, sendInfo } from "vscode-extension-telemetry-wrapper"; +import { GetImportClassContentError, GetProjectDependenciesError, sendContextOperationTelemetry, JavaContextProviderUtils } from "./utils"; import { Commands } from '../commands'; /** @@ -79,8 +79,9 @@ export namespace CopilotHelper { } try { - // Use the new command with error reason support - const commandPromise = commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_GET_IMPORT_CLASS_CONTENT, fileUri.toString()) as Promise; + const normalizedUri = decodeURIComponent(Uri.file(fileUri.fsPath).toString()); + + const commandPromise = commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_GET_IMPORT_CLASS_CONTENT, normalizedUri) as Promise; if (cancellationToken) { const result = await Promise.race([ @@ -187,8 +188,9 @@ export namespace CopilotHelper { } try { - // Use the new command with error reason support - const commandPromise = commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_GET_DEPENDENCIES, projectUri.toString()) as Promise; + const normalizedUri = decodeURIComponent(Uri.file(projectUri.fsPath).toString()); + + const commandPromise = commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_GET_DEPENDENCIES, normalizedUri) as Promise; if (cancellationToken) { const result = await Promise.race([ @@ -266,20 +268,18 @@ export namespace CopilotHelper { * @param workspaceFolders The workspace folders, or undefined if none * @param copilotCancel Cancellation token from Copilot * @param checkCancellation Function to check for cancellation - * @param sendTelemetry Function to send telemetry data * @returns Array of context items for project dependencies, or empty array if no workspace folders */ export async function resolveAndConvertProjectDependencies( workspaceFolders: readonly { uri: Uri }[] | undefined, copilotCancel: CancellationToken, - checkCancellation: (token: CancellationToken) => void, - sendTelemetry: (action: string, status: string, reason?: string) => void + checkCancellation: (token: CancellationToken) => void ): Promise> { - const items: Array<{ name: string; value: string; importance: number }> = []; + const items: any[] = []; // Check if workspace folders exist if (!workspaceFolders || workspaceFolders.length === 0) { - sendTelemetry("resolveProjectDependencies", "ContextEmpty", EmptyReason.NoWorkspace); + sendContextOperationTelemetry("resolveProjectDependencies", "ContextEmpty", sendInfo, EmptyReason.NoWorkspace); return items; } @@ -293,10 +293,7 @@ export namespace CopilotHelper { // Send telemetry if result is empty if (projectDependenciesResult.isEmpty && projectDependenciesResult.emptyReason) { - sendTelemetry("resolveProjectDependencies", "ContextEmpty", projectDependenciesResult.emptyReason); - } else if (projectDependenciesResult.dependencyInfoList.length === 0) { - // No error but still empty - likely no dependencies in project - sendTelemetry("resolveProjectDependencies", "ContextEmpty", EmptyReason.NoDependenciesResults); + sendContextOperationTelemetry("resolveProjectDependencies", "ContextEmpty", sendInfo, projectDependenciesResult.emptyReason); } // Check for cancellation after telemetry @@ -304,13 +301,11 @@ export namespace CopilotHelper { // Convert project dependencies to context items if (projectDependenciesResult.dependencyInfoList && projectDependenciesResult.dependencyInfoList.length > 0) { - for (const dep of projectDependenciesResult.dependencyInfoList) { - items.push({ - name: dep.key, - value: dep.value, - importance: 70 - }); - } + const contextItems = JavaContextProviderUtils.createContextItemsFromProjectDependencies(projectDependenciesResult.dependencyInfoList); + + // Check cancellation once after creating all items + checkCancellation(copilotCancel); + items.push(...contextItems); } return items; @@ -321,27 +316,24 @@ export namespace CopilotHelper { * @param activeEditor The active text editor, or undefined if none * @param copilotCancel Cancellation token from Copilot * @param checkCancellation Function to check for cancellation - * @param sendTelemetry Function to send telemetry data * @param createContextItems Function to create context items from imports * @returns Array of context items for local imports, or empty array if no valid editor */ export async function resolveAndConvertLocalImports( activeEditor: { document: { uri: Uri; languageId: string } } | undefined, copilotCancel: CancellationToken, - checkCancellation: (token: CancellationToken) => void, - sendTelemetry: (action: string, status: string, reason?: string) => void, - createContextItems: (classInfoList: any[]) => any[] + checkCancellation: (token: CancellationToken) => void ): Promise { const items: any[] = []; // Check if there's an active editor with a Java document if (!activeEditor) { - sendTelemetry("resolveLocalImports", "ContextEmpty", EmptyReason.NoActiveEditor); + sendContextOperationTelemetry("resolveLocalImports", "ContextEmpty", sendInfo, EmptyReason.NoActiveEditor); return items; } if (activeEditor.document.languageId !== 'java') { - sendTelemetry("resolveLocalImports", "ContextEmpty", EmptyReason.NotJavaFile); + sendContextOperationTelemetry("resolveLocalImports", "ContextEmpty", sendInfo, EmptyReason.NotJavaFile); return items; } @@ -358,18 +350,14 @@ export namespace CopilotHelper { // Send telemetry if result is empty if (importClassResult.isEmpty && importClassResult.emptyReason) { - sendTelemetry("resolveLocalImports", "ContextEmpty", importClassResult.emptyReason); - } else if (importClassResult.classInfoList.length === 0) { - // No error but still empty - likely no imports in file - sendTelemetry("resolveLocalImports", "ContextEmpty", EmptyReason.NoImportsResults); + sendContextOperationTelemetry("resolveLocalImports", "ContextEmpty", sendInfo, importClassResult.emptyReason); } - // Check for cancellation before processing results checkCancellation(copilotCancel); if (importClassResult.classInfoList && importClassResult.classInfoList.length > 0) { // Process imports in batches to reduce cancellation check overhead - const contextItems = createContextItems(importClassResult.classInfoList); + const contextItems = JavaContextProviderUtils.createContextItemsFromImports(importClassResult.classInfoList); // Check cancellation once after creating all items checkCancellation(copilotCancel); diff --git a/src/copilot/utils.ts b/src/copilot/utils.ts index 0a5c4d71..5ed2e6ce 100644 --- a/src/copilot/utils.ts +++ b/src/copilot/utils.ts @@ -49,6 +49,14 @@ export class JavaContextProviderUtils { } } + static createContextItemsFromProjectDependencies(projectDepsResults: Array<{ key: string; value: string }>): SupportedContextItem[] { + return projectDepsResults.map(dep => ({ + name: dep.key, + value: dep.value, + importance: 70 + })); + } + /** * Create context items from import classes */ @@ -56,24 +64,11 @@ export class JavaContextProviderUtils { return importClasses.map((cls: any) => ({ uri: cls.uri, value: cls.value, - importance: 70, + importance: 80, origin: 'request' as const })); } - /** - * Create a basic Java version context item - */ - static createJavaVersionItem(javaVersion: string): SupportedContextItem { - return { - name: 'java.version', - value: javaVersion, - importance: 90, - id: 'java-version', - origin: 'request' - }; - } - /** * Get and validate Copilot APIs */ @@ -210,4 +205,27 @@ export class ContextProviderResolverError extends Error { super(message); this.name = 'ContextProviderResolverError'; } +} + +/** + * Send telemetry data for context operations (like resolveProjectDependencies, resolveLocalImports) + * @param action The action being performed + * @param status The status of the action (e.g., "ContextEmpty", "succeeded") + * @param reason Optional reason for empty context + * @param sendInfo The sendInfo function from vscode-extension-telemetry-wrapper + */ +export function sendContextOperationTelemetry( + action: string, + status: string, + sendInfo: (eventName: string, properties?: any) => void, + reason?: string +): void { + const telemetryData: any = { + "action": action, + "status": status + }; + if (reason) { + telemetryData.ContextEmptyReason = reason; + } + sendInfo("", telemetryData); } \ No newline at end of file