From bdb8b3890be89599564c765f17a9995a35ac904f Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Thu, 11 Sep 2025 15:53:33 +0800 Subject: [PATCH 1/9] feat: copilot commands --- COPILOT_INTEGRATION.md | 154 +++++++++++++++++ GET_SYMBOLS_USAGE.md | 162 ++++++++++++++++++ .../jdtls/ext/core/CommandHandler.java | 2 + .../jdtls/ext/core/ProjectCommand.java | 154 +++++++++++++++++ package.json | 13 ++ src/commands.ts | 4 + src/copilotHelper.ts | 99 +++++++++++ src/extension.ts | 57 ++++++ src/java/jdtls.ts | 4 + 9 files changed, 649 insertions(+) create mode 100644 COPILOT_INTEGRATION.md create mode 100644 GET_SYMBOLS_USAGE.md create mode 100644 src/copilotHelper.ts diff --git a/COPILOT_INTEGRATION.md b/COPILOT_INTEGRATION.md new file mode 100644 index 00000000..d66952ba --- /dev/null +++ b/COPILOT_INTEGRATION.md @@ -0,0 +1,154 @@ +# Copilot Integration for Java Dependency Analysis + +这个功能为 Copilot 提供了分析 Java 项目本地依赖的能力。 + +## 功能概述 + +`resolveCopilotRequest` 功能可以: +1. 解析指定 Java 文件的所有 import 语句 +2. 过滤掉外部依赖(JAR 包、JRE 系统库等),只保留本地工程文件 +3. 提取每个本地文件的类型信息(class、interface、enum、annotation) +4. 返回格式化的类型信息列表 + +## API 接口 + +### Java 后端 API + +```java +public static String[] resolveCopilotRequest(List arguments, IProgressMonitor monitor) +``` + +**参数:** +- `arguments[0]`: 文件 URI 字符串 (如 "file:///path/to/MyClass.java") +- `monitor`: 进度监控器 + +**返回:** +- 字符串数组,每个元素格式为 `"type:fully.qualified.name"` +- `type` 可以是:`class`、`interface`、`enum`、`annotation` + +### VS Code 扩展 API + +```typescript +export async function resolveCopilotRequest(fileUri: string): Promise +``` + +**参数:** +- `fileUri`: 文件 URI 字符串 + +**返回:** +- Promise,解析到的本地类型信息 + +## 使用示例 + +### 1. 基本用法 + +```typescript +import { Uri } from "vscode"; +import { CopilotHelper } from "./copilotHelper"; + +// 分析当前活动文件的本地导入 +const currentFile = window.activeTextEditor?.document.uri; +if (currentFile) { + const localImports = await CopilotHelper.resolveLocalImports(currentFile); + console.log("Local imports:", localImports); + // 输出示例: + // [ + // "class:com.example.model.User", + // "interface:com.example.service.UserService", + // "enum:com.example.enums.Status" + // ] +} +``` + +### 2. 按类型分类 + +```typescript +const categorizedTypes = await CopilotHelper.getLocalImportsByType(currentFile); +console.log("Classes:", categorizedTypes.classes); +console.log("Interfaces:", categorizedTypes.interfaces); +console.log("Enums:", categorizedTypes.enums); + +// 输出示例: +// Classes: ["com.example.model.User", "com.example.util.Helper"] +// Interfaces: ["com.example.service.UserService"] +// Enums: ["com.example.enums.Status"] +``` + +### 3. 获取类型名称列表 + +```typescript +const typeNames = await CopilotHelper.getLocalImportTypeNames(currentFile); +console.log("Type names:", typeNames); + +// 输出示例: +// ["com.example.model.User", "com.example.service.UserService", "com.example.enums.Status"] +``` + +## 过滤逻辑 + +函数只返回**本地项目**中的类型,会过滤掉: +- ❌ 外部 JAR 包中的类 +- ❌ JRE 系统库中的类(如 `java.util.List`) +- ❌ Maven/Gradle 依赖中的类 +- ❌ 第三方库中的类 + +保留: +- ✅ 当前项目源码中的类 +- ✅ 当前项目源码中的接口 +- ✅ 当前项目源码中的枚举 +- ✅ 当前项目源码中的注解 + +## 示例场景 + +假设有一个 Java 文件: + +```java +package com.example.controller; + +import java.util.List; // ❌ JRE 系统库,会被过滤 +import org.springframework.web.bind.annotation.GetMapping; // ❌ 外部依赖,会被过滤 +import com.fasterxml.jackson.annotation.JsonProperty; // ❌ 外部依赖,会被过滤 + +import com.example.model.User; // ✅ 本地项目类 +import com.example.service.UserService; // ✅ 本地项目接口 +import com.example.enums.UserStatus; // ✅ 本地项目枚举 +import com.example.util.*; // ✅ 本地项目包(会展开为具体类型) + +public class UserController { + // ... +} +``` + +调用 `resolveCopilotRequest` 会返回: +``` +[ + "class:com.example.model.User", + "interface:com.example.service.UserService", + "enum:com.example.enums.UserStatus", + "class:com.example.util.DateHelper", + "class:com.example.util.StringUtil" +] +``` + +## 错误处理 + +函数内置了错误处理机制: +- 如果文件不存在或无法解析,返回空数组 +- 如果不是 Java 文件,返回空数组 +- 如果项目不是 Java 项目,返回空数组 +- 解析过程中的异常会被捕获并记录日志 + +## 性能考虑 + +- 使用缓存机制避免重复解析 +- 支持进度监控和取消操作 +- 懒加载包内容,只在需要时解析 +- 对大型项目进行了优化 + +## 集成到 Copilot + +这个功能专为 Copilot 设计,可以: +1. 帮助 Copilot 理解项目的本地代码结构 +2. 提供上下文信息用于代码生成 +3. 避免建议使用不存在的本地类型 +4. 提高代码补全的准确性 diff --git a/GET_SYMBOLS_USAGE.md b/GET_SYMBOLS_USAGE.md new file mode 100644 index 00000000..e3df4684 --- /dev/null +++ b/GET_SYMBOLS_USAGE.md @@ -0,0 +1,162 @@ +# 如何使用 getSymbolsFromFile 命令 + +我已经为 VS Code Java 依赖管理扩展添加了一个新的命令 `getSymbolsFromFile`,用于分析当前打开文件的本地项目符号。 + +## 使用方法 + +### 1. 通过键盘快捷键运行(推荐) + +1. 在 VS Code 中打开一个 Java 文件 +2. 按 `Ctrl+Shift+S` (Windows/Linux) 或 `Cmd+Shift+S` (macOS) +3. 查看开发者控制台输出结果 + +### 2. 通过命令面板运行 + +1. 在 VS Code 中打开一个 Java 文件 +2. 按 `Ctrl+Shift+P` (Windows/Linux) 或 `Cmd+Shift+P` (macOS) 打开命令面板 +3. 输入 `Get Local Symbols from Current File` +4. 按回车执行命令 +5. 查看开发者控制台输出结果 + +### 3. 查看输出结果 + +执行命令后,会在以下地方看到结果: + +**VS Code 通知消息:** +``` +Found 5 local symbols. Check console for details. +``` + +**开发者控制台输出(按 F12 打开):** +``` +=== Local Symbols from Current File === +File: /path/to/your/JavaFile.java +Total symbols found: 5 +1. class:com.example.model.User +2. interface:com.example.service.UserService +3. enum:com.example.enums.Status +4. class:com.example.util.DateHelper +5. annotation:com.example.annotations.Entity + +=== Categorized View === +Classes (2): ["com.example.model.User", "com.example.util.DateHelper"] +Interfaces (1): ["com.example.service.UserService"] +Enums (1): ["com.example.enums.Status"] +Annotations (1): ["com.example.annotations.Entity"] +=== End === +``` + +## 示例场景 + +### 示例 Java 文件 + +假设你有以下 Java 文件: + +```java +package com.example.controller; + +// 这些会被过滤掉(外部依赖) +import java.util.List; +import org.springframework.web.bind.annotation.GetMapping; +import com.fasterxml.jackson.annotation.JsonProperty; + +// 这些会被分析(本地项目符号) +import com.example.model.User; +import com.example.service.UserService; +import com.example.enums.Status; +import com.example.util.*; // 会展开为具体的类 +import com.example.annotations.Entity; + +@Entity +public class UserController { + private UserService userService; + + @GetMapping("/users") + public List getUsers() { + return userService.findByStatus(Status.ACTIVE); + } +} +``` + +### 执行命令后的输出 + +``` +=== Local Symbols from Current File === +File: /workspace/src/main/java/com/example/controller/UserController.java +Total symbols found: 5 +1. class:com.example.model.User +2. interface:com.example.service.UserService +3. enum:com.example.enums.Status +4. class:com.example.util.DateHelper +5. class:com.example.util.StringUtils +6. annotation:com.example.annotations.Entity + +=== Categorized View === +Classes (3): ["com.example.model.User", "com.example.util.DateHelper", "com.example.util.StringUtils"] +Interfaces (1): ["com.example.service.UserService"] +Enums (1): ["com.example.enums.Status"] +Annotations (1): ["com.example.annotations.Entity"] +=== End === +``` + +## 功能特点 + +### ✅ 会分析的内容 +- 本地项目中的类(class) +- 本地项目中的接口(interface) +- 本地项目中的枚举(enum) +- 本地项目中的注解(annotation) +- 包导入(`import com.example.util.*;`)会展开为具体类型 + +### ❌ 会过滤的内容 +- JRE 系统库(如 `java.util.List`) +- 外部 JAR 包中的类 +- Maven/Gradle 依赖中的类 +- 第三方框架的类(如 Spring、Jackson 等) + +## 错误处理 + +### 常见情况处理 + +1. **没有打开文件** + ``` + Warning: No active editor found. Please open a Java file first. + ``` + +2. **不是 Java 文件** + ``` + Warning: Please open a Java file to get symbols. + ``` + +3. **没有找到本地符号** + ``` + === Local Symbols from Current File === + File: /path/to/file.java + Total symbols found: 0 + No local project symbols found in imports. + === End === + ``` + +4. **解析错误** + ``` + Error: Error getting symbols: [具体错误信息] + ``` + +## 开发者控制台 + +要查看详细的控制台输出: + +1. 按 `F12` 打开开发者工具 +2. 点击 "Console" 选项卡 +3. 执行命令后查看输出 +4. 可以看到完整的符号列表和分类信息 + +## 用途 + +这个命令主要用于: + +1. **代码分析**:快速了解当前文件依赖了哪些本地项目组件 +2. **代码重构**:在重构时了解文件间的依赖关系 +3. **项目理解**:帮助快速理解代码结构和依赖关系 +4. **Copilot 集成**:为 AI 代码助手提供本地项目上下文 +5. **开发调试**:验证 import 语句是否正确引用本地组件 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 5d395719..6c6411a1 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 @@ -37,6 +37,8 @@ public Object executeCommand(String commandId, List arguments, IProgress return ProjectCommand.exportJar(arguments, monitor); case "java.project.checkImportStatus": return ProjectCommand.checkImportStatus(); + case "java.project.resolveCopilotRequest": + return ProjectCommand.resolveCopilotRequest(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 3f10d3e6..30c65a4d 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 @@ -332,6 +332,160 @@ public static boolean checkImportStatus() { return hasError; } + public static String[] resolveCopilotRequest(List arguments, IProgressMonitor monitor) { + if (arguments == null || arguments.isEmpty()) { + return new String[0]; + } + + try { + String fileUri = (String) arguments.get(0); + + // Parse URI manually to avoid restricted API + java.net.URI uri = new java.net.URI(fileUri); + String filePath = uri.getPath(); + if (filePath == null) { + return new String[0]; + } + + IPath path = new Path(filePath); + + // Get the file resource + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + IFile file = root.getFileForLocation(path); + if (file == null || !file.exists()) { + return new String[0]; + } + + // Get the Java project + IJavaProject javaProject = JavaCore.create(file.getProject()); + if (javaProject == null || !javaProject.exists()) { + return new String[0]; + } + + // Find the compilation unit + IJavaElement javaElement = JavaCore.create(file); + if (!(javaElement instanceof org.eclipse.jdt.core.ICompilationUnit)) { + return new String[0]; + } + + org.eclipse.jdt.core.ICompilationUnit compilationUnit = (org.eclipse.jdt.core.ICompilationUnit) javaElement; + + // Parse imports and resolve local project files + List result = new ArrayList<>(); + + // Get all imports from the compilation unit + org.eclipse.jdt.core.IImportDeclaration[] imports = compilationUnit.getImports(); + Set processedTypes = new HashSet<>(); + + for (org.eclipse.jdt.core.IImportDeclaration importDecl : imports) { + if (monitor.isCanceled()) { + break; + } + + String importName = importDecl.getElementName(); + if (importName.endsWith(".*")) { + // Handle package imports + String packageName = importName.substring(0, importName.length() - 2); + resolvePackageTypes(javaProject, packageName, result, processedTypes); + } else { + // Handle single type imports + resolveSingleType(javaProject, importName, result, processedTypes); + } + } + + return result.toArray(new String[0]); + + } catch (Exception e) { + JdtlsExtActivator.logException("Error in resolveCopilotRequest", e); + return new String[0]; + } + } + + private static void resolveSingleType(IJavaProject javaProject, String typeName, List result, Set processedTypes) { + try { + if (processedTypes.contains(typeName)) { + return; + } + processedTypes.add(typeName); + + // Find the type in the project + org.eclipse.jdt.core.IType type = javaProject.findType(typeName); + if (type != null && type.exists()) { + // Check if it's a local project type (not from external dependencies) + IPackageFragmentRoot packageRoot = (IPackageFragmentRoot) type.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT); + if (packageRoot != null && packageRoot.getKind() == IPackageFragmentRoot.K_SOURCE) { + // This is a source type from the local project + extractTypeInfo(type, result); + } + } + } catch (JavaModelException e) { + // Log but continue processing other types + JdtlsExtActivator.logException("Error resolving type: " + typeName, e); + } + } + + private static void resolvePackageTypes(IJavaProject javaProject, String packageName, List result, Set processedTypes) { + try { + // Find all package fragments with this name + IPackageFragmentRoot[] packageRoots = javaProject.getPackageFragmentRoots(); + for (IPackageFragmentRoot packageRoot : packageRoots) { + if (packageRoot.getKind() == IPackageFragmentRoot.K_SOURCE) { + org.eclipse.jdt.core.IPackageFragment packageFragment = packageRoot.getPackageFragment(packageName); + if (packageFragment != null && packageFragment.exists()) { + // Get all compilation units in this package + org.eclipse.jdt.core.ICompilationUnit[] compilationUnits = packageFragment.getCompilationUnits(); + for (org.eclipse.jdt.core.ICompilationUnit cu : compilationUnits) { + // Get all types in the compilation unit + org.eclipse.jdt.core.IType[] types = cu.getAllTypes(); + for (org.eclipse.jdt.core.IType type : types) { + String fullTypeName = type.getFullyQualifiedName(); + if (!processedTypes.contains(fullTypeName)) { + processedTypes.add(fullTypeName); + extractTypeInfo(type, result); + } + } + } + } + } + } + } catch (JavaModelException e) { + // Log but continue processing + JdtlsExtActivator.logException("Error resolving package: " + packageName, e); + } + } + + private static void extractTypeInfo(org.eclipse.jdt.core.IType type, List result) { + try { + String typeName = type.getFullyQualifiedName(); + String typeInfo = ""; + + // Determine type kind + if (type.isInterface()) { + typeInfo = "interface:" + typeName; + } else if (type.isClass()) { + typeInfo = "class:" + typeName; + } else if (type.isEnum()) { + typeInfo = "enum:" + typeName; + } else if (type.isAnnotation()) { + typeInfo = "annotation:" + typeName; + } else { + typeInfo = "type:" + typeName; + } + + result.add(typeInfo); + + // Also add nested types + org.eclipse.jdt.core.IType[] nestedTypes = type.getTypes(); + for (org.eclipse.jdt.core.IType nestedType : nestedTypes) { + extractTypeInfo(nestedType, result); + } + + } catch (JavaModelException e) { + // Log but continue processing other types + JdtlsExtActivator.logException("Error extracting type info for: " + type.getElementName(), e); + } + } + private static void reportExportJarMessage(String terminalId, int severity, String message) { if (StringUtils.isNotBlank(message) && StringUtils.isNotBlank(terminalId)) { String readableSeverity = getSeverityString(severity); diff --git a/package.json b/package.json index df286b18..2e2eb4f5 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,12 @@ "./server/com.microsoft.jdtls.ext.core-0.24.1.jar" ], "commands": [ + { + "command": "java.getSymbolsFromFile", + "title": "Get Local Symbols from Current File", + "category": "Java", + "icon": "$(symbol-class)" + }, { "command": "java.project.create", "title": "%contributes.commands.java.project.create%", @@ -349,6 +355,13 @@ } }, "keybindings": [ + { + "command": "java.getSymbolsFromFile", + "key": "ctrl+shift+s", + "win": "ctrl+shift+s", + "mac": "cmd+shift+s", + "when": "java:serverMode == Standard && editorLangId == java" + }, { "command": "java.view.package.revealFileInOS", "key": "ctrl+alt+r", diff --git a/src/commands.ts b/src/commands.ts index a2564835..49dcb10b 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -10,6 +10,8 @@ export namespace Commands { */ export const EXECUTE_WORKSPACE_COMMAND = "java.execute.workspaceCommand"; + export const GET_SYMBOLS_FROM_FILE = "java.getSymbolsFromFile"; + export const VIEW_PACKAGE_CHANGETOFLATPACKAGEVIEW = "java.view.package.changeToFlatPackageView"; export const VIEW_PACKAGE_CHANGETOHIERARCHICALPACKAGEVIEW = "java.view.package.changeToHierarchicalPackageView"; @@ -114,6 +116,8 @@ export namespace Commands { export const JAVA_PROJECT_GETMAINCLASSES = "java.project.getMainClasses"; + export const JAVA_PROJECT_RESOLVE_COPILOT_REQUEST = "java.project.resolveCopilotRequest"; + export const JAVA_PROJECT_GENERATEJAR = "java.project.generateJar"; export const JAVA_BUILD_WORKSPACE = "java.workspace.compile"; diff --git a/src/copilotHelper.ts b/src/copilotHelper.ts new file mode 100644 index 00000000..580f3e7d --- /dev/null +++ b/src/copilotHelper.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { Uri } from "vscode"; +import { Jdtls } from "./java/jdtls"; + +/** + * Helper class for Copilot integration to analyze Java project dependencies + */ +export class CopilotHelper { + + /** + * Resolves all local project types imported by the given file + * @param fileUri The URI of the Java file to analyze + * @returns Array of strings in format "type:fully.qualified.name" where type is class|interface|enum|annotation + */ + public static async resolveLocalImports(fileUri: Uri): Promise { + try { + const result = await Jdtls.resolveCopilotRequest(fileUri.toString()); + return result; + } catch (error) { + console.error("Error resolving copilot request:", error); + return []; + } + } + + /** + * Gets local project types imported by the given file, categorized by type + * @param fileUri The URI of the Java file to analyze + * @returns Object with categorized types + */ + public static async getLocalImportsByType(fileUri: Uri): Promise<{ + classes: string[]; + interfaces: string[]; + enums: string[]; + annotations: string[]; + others: string[]; + }> { + const result = { + classes: [] as string[], + interfaces: [] as string[], + enums: [] as string[], + annotations: [] as string[], + others: [] as string[] + }; + + try { + const imports = await this.resolveLocalImports(fileUri); + + for (const importInfo of imports) { + const [type, typeName] = importInfo.split(":", 2); + if (!typeName) { + result.others.push(importInfo); + continue; + } + + switch (type) { + case "class": + result.classes.push(typeName); + break; + case "interface": + result.interfaces.push(typeName); + break; + case "enum": + result.enums.push(typeName); + break; + case "annotation": + result.annotations.push(typeName); + break; + default: + result.others.push(typeName); + break; + } + } + } catch (error) { + console.error("Error categorizing imports:", error); + } + + return result; + } + + /** + * Gets a simple list of fully qualified type names imported from local project + * @param fileUri The URI of the Java file to analyze + * @returns Array of fully qualified type names + */ + public static async getLocalImportTypeNames(fileUri: Uri): Promise { + try { + const imports = await this.resolveLocalImports(fileUri); + return imports.map(importInfo => { + const [, typeName] = importInfo.split(":", 2); + return typeName || importInfo; + }); + } catch (error) { + console.error("Error getting type names:", error); + return []; + } + } +} diff --git a/src/extension.ts b/src/extension.ts index 531b5ac3..8d5f3b2c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,6 +6,7 @@ import { commands, Diagnostic, Extension, ExtensionContext, extensions, language Range, tasks, TextDocument, TextEditor, Uri, window, workspace } from "vscode"; import { dispose as disposeTelemetryWrapper, initializeFromJsonFile, instrumentOperation, instrumentOperationAsVsCodeCommand, sendInfo } from "vscode-extension-telemetry-wrapper"; import { Commands, contextManager } from "../extension.bundle"; +import { CopilotHelper } from "./copilotHelper"; import { BuildTaskProvider } from "./tasks/build/buildTaskProvider"; import { buildFiles, Context, ExtensionName } from "./constants"; import { LibraryController } from "./controllers/libraryController"; @@ -48,6 +49,62 @@ async function activateExtension(_operationId: string, context: ExtensionContext context.subscriptions.push(tasks.registerTaskProvider(BuildArtifactTaskProvider.exportJarType, new BuildArtifactTaskProvider())); context.subscriptions.push(tasks.registerTaskProvider(BuildTaskProvider.type, new BuildTaskProvider())); context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.VIEW_MENUS_FILE_NEW_JAVA_CLASS, newJavaFile)); + + // Add getSymbolsFromFile command + context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.GET_SYMBOLS_FROM_FILE, async () => { + const activeEditor = window.activeTextEditor; + if (!activeEditor) { + window.showWarningMessage("No active editor found. Please open a Java file first."); + return; + } + + const document = activeEditor.document; + if (!document.fileName.endsWith('.java')) { + window.showWarningMessage("Please open a Java file to get symbols."); + return; + } + + try { + const symbols = await CopilotHelper.resolveLocalImports(document.uri); + console.log("=== Local Symbols from Current File ==="); + console.log(`File: ${document.fileName}`); + console.log(`Total symbols found: ${symbols.length}`); + + if (symbols.length > 0) { + symbols.forEach((symbol, index) => { + console.log(`${index + 1}. ${symbol}`); + }); + + // Also show categorized view + const categorized = await CopilotHelper.getLocalImportsByType(document.uri); + console.log("\n=== Categorized View ==="); + if (categorized.classes.length > 0) { + console.log(`Classes (${categorized.classes.length}):`, categorized.classes); + } + if (categorized.interfaces.length > 0) { + console.log(`Interfaces (${categorized.interfaces.length}):`, categorized.interfaces); + } + if (categorized.enums.length > 0) { + console.log(`Enums (${categorized.enums.length}):`, categorized.enums); + } + if (categorized.annotations.length > 0) { + console.log(`Annotations (${categorized.annotations.length}):`, categorized.annotations); + } + if (categorized.others.length > 0) { + console.log(`Others (${categorized.others.length}):`, categorized.others); + } + } else { + console.log("No local project symbols found in imports."); + } + console.log("=== End ==="); + + window.showInformationMessage(`Found ${symbols.length} local symbols. Check console for details.`); + } catch (error) { + console.error("Error getting symbols:", error); + window.showErrorMessage(`Error getting symbols: ${error}`); + } + })); + context.subscriptions.push(window.onDidChangeActiveTextEditor((e: TextEditor | undefined) => { setContextForReloadProject(e?.document); })); diff --git a/src/java/jdtls.ts b/src/java/jdtls.ts index c1388253..4d8fb733 100644 --- a/src/java/jdtls.ts +++ b/src/java/jdtls.ts @@ -72,6 +72,10 @@ export namespace Jdtls { return await commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_GETMAINCLASSES, params) || []; } + export async function resolveCopilotRequest(fileUri: string): Promise { + return await commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_RESOLVE_COPILOT_REQUEST, fileUri) || []; + } + export async function exportJar(mainClass: string, classpaths: IClasspath[], destination: string, terminalId: string, token: CancellationToken): Promise { return commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_GENERATEJAR, From df2a6d2a7d89ca3913d74dbcbeb97d3448497bcd Mon Sep 17 00:00:00 2001 From: wenyutang Date: Fri, 12 Sep 2025 11:38:21 +0800 Subject: [PATCH 2/9] feat: add fix --- .../com.microsoft.jdtls.ext.core/plugin.xml | 1 + .../jdtls/ext/core/ProjectCommand.java | 177 ++++++++++++++---- src/extension.ts | 28 --- 3 files changed, 141 insertions(+), 65 deletions(-) diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml b/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml index 0352e983..c705c95c 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml @@ -10,6 +10,7 @@ + listProjects(List arguments, IProgressMo projects = ProjectUtils.getAllProjects(); } else { projects = Arrays.stream(ProjectUtils.getJavaProjects()) - .map(IJavaProject::getProject).toArray(IProject[]::new); + .map(IJavaProject::getProject).toArray(IProject[]::new); } ArrayList children = new ArrayList<>(); @@ -202,11 +202,14 @@ private static boolean exportJarExecution(String mainClass, Classpath[] classpat } if (classpath.isArtifact) { MultiStatus resultStatus = writeArchive(new ZipFile(classpath.source), - /* areDirectoryEntriesIncluded = */true, /* isCompressed = */true, target, directories, monitor); + /* areDirectoryEntriesIncluded = */true, /* isCompressed = */true, target, directories, + monitor); int severity = resultStatus.getSeverity(); if (severity == IStatus.OK) { java.nio.file.Path path = java.nio.file.Paths.get(classpath.source); - reportExportJarMessage(terminalId, IStatus.OK, "Successfully extracted the file to the exported jar: " + path.getFileName().toString()); + reportExportJarMessage(terminalId, IStatus.OK, + "Successfully extracted the file to the exported jar: " + + path.getFileName().toString()); continue; } if (resultStatus.isMultiStatus()) { @@ -218,9 +221,13 @@ private static boolean exportJarExecution(String mainClass, Classpath[] classpat } } else { try { - writeFile(new File(classpath.source), new Path(classpath.destination), /* areDirectoryEntriesIncluded = */true, - /* isCompressed = */true, target, directories); - reportExportJarMessage(terminalId, IStatus.OK, "Successfully added the file to the exported jar: " + classpath.destination); + writeFile(new File(classpath.source), new Path(classpath.destination), /* + * areDirectoryEntriesIncluded + * = + */true, + /* isCompressed = */true, target, directories); + reportExportJarMessage(terminalId, IStatus.OK, + "Successfully added the file to the exported jar: " + classpath.destination); } catch (CoreException e) { reportExportJarMessage(terminalId, IStatus.ERROR, e.getMessage()); } @@ -233,7 +240,8 @@ private static boolean exportJarExecution(String mainClass, Classpath[] classpat return true; } - public static List getMainClasses(List arguments, IProgressMonitor monitor) throws Exception { + public static List getMainClasses(List arguments, IProgressMonitor monitor) + throws Exception { List args = new ArrayList<>(arguments); if (args.size() <= 1) { args.add(Boolean.TRUE); @@ -246,7 +254,7 @@ public static List getMainClasses(List arguments, IProgre } final List res = new ArrayList<>(); List javaProjects = new ArrayList<>(); - for (PackageNode project: projectList) { + for (PackageNode project : projectList) { IJavaProject javaProject = PackageCommand.getJavaProject(project.getUri()); if (javaProject != null && javaProject.exists()) { javaProjects.add(javaProject); @@ -254,7 +262,7 @@ public static List getMainClasses(List arguments, IProgre } int includeMask = IJavaSearchScope.SOURCES; IJavaSearchScope scope = SearchEngine.createJavaSearchScope(javaProjects.toArray(new IJavaProject[0]), - includeMask); + includeMask); SearchPattern pattern1 = SearchPattern.createPattern("main(String[]) void", IJavaSearchConstants.METHOD, IJavaSearchConstants.DECLARATIONS, SearchPattern.R_CASE_SENSITIVE | SearchPattern.R_EXACT_MATCH); SearchPattern pattern2 = SearchPattern.createPattern("main() void", IJavaSearchConstants.METHOD, @@ -285,7 +293,7 @@ public void acceptSearchMatch(SearchMatch match) { }; SearchEngine searchEngine = new SearchEngine(); try { - searchEngine.search(pattern, new SearchParticipant[] {SearchEngine.getDefaultSearchParticipant()}, scope, + searchEngine.search(pattern, new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() }, scope, requestor, monitor); } catch (CoreException e) { // ignore @@ -336,52 +344,52 @@ public static String[] resolveCopilotRequest(List arguments, IProgressMo if (arguments == null || arguments.isEmpty()) { return new String[0]; } - + try { String fileUri = (String) arguments.get(0); - + // Parse URI manually to avoid restricted API java.net.URI uri = new java.net.URI(fileUri); String filePath = uri.getPath(); if (filePath == null) { return new String[0]; } - + IPath path = new Path(filePath); - + // Get the file resource IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); IFile file = root.getFileForLocation(path); if (file == null || !file.exists()) { return new String[0]; } - + // Get the Java project IJavaProject javaProject = JavaCore.create(file.getProject()); if (javaProject == null || !javaProject.exists()) { return new String[0]; } - + // Find the compilation unit IJavaElement javaElement = JavaCore.create(file); if (!(javaElement instanceof org.eclipse.jdt.core.ICompilationUnit)) { return new String[0]; } - + org.eclipse.jdt.core.ICompilationUnit compilationUnit = (org.eclipse.jdt.core.ICompilationUnit) javaElement; - + // Parse imports and resolve local project files List result = new ArrayList<>(); - + // Get all imports from the compilation unit org.eclipse.jdt.core.IImportDeclaration[] imports = compilationUnit.getImports(); Set processedTypes = new HashSet<>(); - + for (org.eclipse.jdt.core.IImportDeclaration importDecl : imports) { if (monitor.isCanceled()) { break; } - + String importName = importDecl.getElementName(); if (importName.endsWith(".*")) { // Handle package imports @@ -392,27 +400,29 @@ public static String[] resolveCopilotRequest(List arguments, IProgressMo resolveSingleType(javaProject, importName, result, processedTypes); } } - + return result.toArray(new String[0]); - + } catch (Exception e) { JdtlsExtActivator.logException("Error in resolveCopilotRequest", e); return new String[0]; } } - - private static void resolveSingleType(IJavaProject javaProject, String typeName, List result, Set processedTypes) { + + private static void resolveSingleType(IJavaProject javaProject, String typeName, List result, + Set processedTypes) { try { if (processedTypes.contains(typeName)) { return; } processedTypes.add(typeName); - + // Find the type in the project org.eclipse.jdt.core.IType type = javaProject.findType(typeName); if (type != null && type.exists()) { // Check if it's a local project type (not from external dependencies) - IPackageFragmentRoot packageRoot = (IPackageFragmentRoot) type.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT); + IPackageFragmentRoot packageRoot = (IPackageFragmentRoot) type + .getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT); if (packageRoot != null && packageRoot.getKind() == IPackageFragmentRoot.K_SOURCE) { // This is a source type from the local project extractTypeInfo(type, result); @@ -423,8 +433,9 @@ private static void resolveSingleType(IJavaProject javaProject, String typeName, JdtlsExtActivator.logException("Error resolving type: " + typeName, e); } } - - private static void resolvePackageTypes(IJavaProject javaProject, String packageName, List result, Set processedTypes) { + + private static void resolvePackageTypes(IJavaProject javaProject, String packageName, List result, + Set processedTypes) { try { // Find all package fragments with this name IPackageFragmentRoot[] packageRoots = javaProject.getPackageFragmentRoots(); @@ -433,7 +444,8 @@ private static void resolvePackageTypes(IJavaProject javaProject, String package org.eclipse.jdt.core.IPackageFragment packageFragment = packageRoot.getPackageFragment(packageName); if (packageFragment != null && packageFragment.exists()) { // Get all compilation units in this package - org.eclipse.jdt.core.ICompilationUnit[] compilationUnits = packageFragment.getCompilationUnits(); + org.eclipse.jdt.core.ICompilationUnit[] compilationUnits = packageFragment + .getCompilationUnits(); for (org.eclipse.jdt.core.ICompilationUnit cu : compilationUnits) { // Get all types in the compilation unit org.eclipse.jdt.core.IType[] types = cu.getAllTypes(); @@ -453,17 +465,17 @@ private static void resolvePackageTypes(IJavaProject javaProject, String package JdtlsExtActivator.logException("Error resolving package: " + packageName, e); } } - + private static void extractTypeInfo(org.eclipse.jdt.core.IType type, List result) { try { String typeName = type.getFullyQualifiedName(); String typeInfo = ""; - + // Determine type kind if (type.isInterface()) { typeInfo = "interface:" + typeName; } else if (type.isClass()) { - typeInfo = "class:" + typeName; + extractDetailedClassInfo(type, result); } else if (type.isEnum()) { typeInfo = "enum:" + typeName; } else if (type.isAnnotation()) { @@ -471,26 +483,117 @@ private static void extractTypeInfo(org.eclipse.jdt.core.IType type, List result) { + try { + if (!type.isClass()) { + return; // Only process classes + } + + String className = type.getFullyQualifiedName(); + List classDetails = new ArrayList<>(); + + // 1. Class declaration information + classDetails.add("class:" + className); + + // 2. Modifiers + int flags = type.getFlags(); + List modifiers = new ArrayList<>(); + if (org.eclipse.jdt.core.Flags.isPublic(flags)) + modifiers.add("public"); + if (org.eclipse.jdt.core.Flags.isAbstract(flags)) + modifiers.add("abstract"); + if (org.eclipse.jdt.core.Flags.isFinal(flags)) + modifiers.add("final"); + if (org.eclipse.jdt.core.Flags.isStatic(flags)) + modifiers.add("static"); + if (!modifiers.isEmpty()) { + classDetails.add("modifiers:" + String.join(",", modifiers)); + } + + // 3. Inheritance + String superclass = type.getSuperclassName(); + if (superclass != null && !"Object".equals(superclass)) { + classDetails.add("extends:" + superclass); + } + + // 4. Implemented interfaces + String[] interfaces = type.getSuperInterfaceNames(); + if (interfaces.length > 0) { + classDetails.add("implements:" + String.join(",", interfaces)); + } + + // 5. Constructors + IMethod[] methods = type.getMethods(); + List constructors = new ArrayList<>(); + List publicMethods = new ArrayList<>(); + + for (IMethod method : methods) { + if (method.isConstructor()) { + constructors.add(method.getElementName() + getParameterTypes(method)); + } else if (org.eclipse.jdt.core.Flags.isPublic(method.getFlags())) { + publicMethods.add(method.getElementName() + getParameterTypes(method)); + } + } + + if (!constructors.isEmpty()) { + classDetails.add("constructors:" + String.join(",", constructors)); + } + + if (!publicMethods.isEmpty()) { + classDetails.add("publicMethods:" + + String.join(",", publicMethods.subList(0, Math.min(publicMethods.size(), 10)))); + } + + // 6. Public fields + org.eclipse.jdt.core.IField[] fields = type.getFields(); + List publicFields = new ArrayList<>(); + for (org.eclipse.jdt.core.IField field : fields) { + if (org.eclipse.jdt.core.Flags.isPublic(field.getFlags())) { + publicFields.add(field.getElementName()); + } + } + + if (!publicFields.isEmpty()) { + classDetails.add("publicFields:" + String.join(",", publicFields)); + } + + // Combine all information into one string + result.add(String.join("|", classDetails)); + + } catch (JavaModelException e) { + JdtlsExtActivator.logException("Error extracting detailed class info", e); + } + } + + // Helper method: Get method parameter types + private static String getParameterTypes(IMethod method) { + String[] paramTypes = method.getParameterTypes(); + if (paramTypes.length == 0) { + return "()"; + } + return "(" + String.join(",", paramTypes) + ")"; + } + private static void reportExportJarMessage(String terminalId, int severity, String message) { if (StringUtils.isNotBlank(message) && StringUtils.isNotBlank(terminalId)) { String readableSeverity = getSeverityString(severity); JavaLanguageServerPlugin.getInstance().getClientConnection().executeClientCommand(COMMAND_EXPORT_JAR_REPORT, - terminalId, "[" + readableSeverity + "] " + message); + terminalId, "[" + readableSeverity + "] " + message); } } diff --git a/src/extension.ts b/src/extension.ts index 8d5f3b2c..82e9a1b0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -70,34 +70,6 @@ async function activateExtension(_operationId: string, context: ExtensionContext console.log(`File: ${document.fileName}`); console.log(`Total symbols found: ${symbols.length}`); - if (symbols.length > 0) { - symbols.forEach((symbol, index) => { - console.log(`${index + 1}. ${symbol}`); - }); - - // Also show categorized view - const categorized = await CopilotHelper.getLocalImportsByType(document.uri); - console.log("\n=== Categorized View ==="); - if (categorized.classes.length > 0) { - console.log(`Classes (${categorized.classes.length}):`, categorized.classes); - } - if (categorized.interfaces.length > 0) { - console.log(`Interfaces (${categorized.interfaces.length}):`, categorized.interfaces); - } - if (categorized.enums.length > 0) { - console.log(`Enums (${categorized.enums.length}):`, categorized.enums); - } - if (categorized.annotations.length > 0) { - console.log(`Annotations (${categorized.annotations.length}):`, categorized.annotations); - } - if (categorized.others.length > 0) { - console.log(`Others (${categorized.others.length}):`, categorized.others); - } - } else { - console.log("No local project symbols found in imports."); - } - console.log("=== End ==="); - window.showInformationMessage(`Found ${symbols.length} local symbols. Check console for details.`); } catch (error) { console.error("Error getting symbols:", error); From be99f7e74c7ed0741ffa72c2e9126022b9d8a202 Mon Sep 17 00:00:00 2001 From: wenyutang Date: Sun, 14 Sep 2025 22:52:00 +0800 Subject: [PATCH 3/9] feat: update --- .../com.microsoft.jdtls.ext.core/plugin.xml | 2 +- .../jdtls/ext/core/CommandHandler.java | 4 +- .../jdtls/ext/core/ProjectCommand.java | 95 +++- package-lock.json | 172 ++++++ package.json | 1 + src/commands.ts | 2 +- src/copilot/contextProvider.ts | 450 +++++++++++++++ .../copilotCompletionContextProvider.ts | 538 ++++++++++++++++++ src/copilotHelper.ts | 81 +-- src/java/jdtls.ts | 5 +- 10 files changed, 1259 insertions(+), 91 deletions(-) create mode 100644 src/copilot/contextProvider.ts create mode 100644 src/copilot/copilotCompletionContextProvider.ts diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml b/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml index c705c95c..6643c1a7 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml @@ -10,7 +10,7 @@ - + arguments, IProgress return ProjectCommand.exportJar(arguments, monitor); case "java.project.checkImportStatus": return ProjectCommand.checkImportStatus(); - case "java.project.resolveCopilotRequest": - return ProjectCommand.resolveCopilotRequest(arguments, monitor); + case "java.project.getImportClassContent": + return ProjectCommand.getImportClassContent(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 66c93aed..de7be7c3 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 @@ -42,7 +42,6 @@ import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.MultiStatus; -import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Path; import org.eclipse.jdt.core.IJavaElement; @@ -66,7 +65,6 @@ import org.eclipse.jdt.ls.core.internal.managers.ProjectsManager; import org.eclipse.jdt.ls.core.internal.managers.UpdateClasspathJob; import org.eclipse.jdt.ls.core.internal.preferences.Preferences.ReferencedLibraries; -import org.eclipse.jdt.ls.core.internal.preferences.Preferences.SearchScope; import org.eclipse.lsp4j.jsonrpc.json.adapters.CollectionTypeAdapter; import org.eclipse.lsp4j.jsonrpc.json.adapters.EnumTypeAdapter; @@ -88,6 +86,16 @@ public MainClassInfo(String name, String path) { } } + private static class ImportClassInfo { + public String uri; + public String className; + + public ImportClassInfo(String uri, String className) { + this.uri = uri; + this.className = className; + } + } + private static class Classpath { public String source; public String destination; @@ -340,9 +348,9 @@ public static boolean checkImportStatus() { return hasError; } - public static String[] resolveCopilotRequest(List arguments, IProgressMonitor monitor) { + public static ImportClassInfo[] getImportClassContent(List arguments, IProgressMonitor monitor) { if (arguments == null || arguments.isEmpty()) { - return new String[0]; + return new ImportClassInfo[0]; } try { @@ -352,7 +360,7 @@ public static String[] resolveCopilotRequest(List arguments, IProgressMo java.net.URI uri = new java.net.URI(fileUri); String filePath = uri.getPath(); if (filePath == null) { - return new String[0]; + return new ImportClassInfo[0]; } IPath path = new Path(filePath); @@ -361,25 +369,25 @@ public static String[] resolveCopilotRequest(List arguments, IProgressMo IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); IFile file = root.getFileForLocation(path); if (file == null || !file.exists()) { - return new String[0]; + return new ImportClassInfo[0]; } // Get the Java project IJavaProject javaProject = JavaCore.create(file.getProject()); if (javaProject == null || !javaProject.exists()) { - return new String[0]; + return new ImportClassInfo[0]; } // Find the compilation unit IJavaElement javaElement = JavaCore.create(file); if (!(javaElement instanceof org.eclipse.jdt.core.ICompilationUnit)) { - return new String[0]; + return new ImportClassInfo[0]; } org.eclipse.jdt.core.ICompilationUnit compilationUnit = (org.eclipse.jdt.core.ICompilationUnit) javaElement; // Parse imports and resolve local project files - List result = new ArrayList<>(); + List result = new ArrayList<>(); // Get all imports from the compilation unit org.eclipse.jdt.core.IImportDeclaration[] imports = compilationUnit.getImports(); @@ -401,15 +409,15 @@ public static String[] resolveCopilotRequest(List arguments, IProgressMo } } - return result.toArray(new String[0]); + return result.toArray(new ImportClassInfo[0]); } catch (Exception e) { JdtlsExtActivator.logException("Error in resolveCopilotRequest", e); - return new String[0]; + return new ImportClassInfo[0]; } } - private static void resolveSingleType(IJavaProject javaProject, String typeName, List result, + private static void resolveSingleType(IJavaProject javaProject, String typeName, List result, Set processedTypes) { try { if (processedTypes.contains(typeName)) { @@ -434,7 +442,7 @@ private static void resolveSingleType(IJavaProject javaProject, String typeName, } } - private static void resolvePackageTypes(IJavaProject javaProject, String packageName, List result, + private static void resolvePackageTypes(IJavaProject javaProject, String packageName, List result, Set processedTypes) { try { // Find all package fragments with this name @@ -466,7 +474,7 @@ private static void resolvePackageTypes(IJavaProject javaProject, String package } } - private static void extractTypeInfo(org.eclipse.jdt.core.IType type, List result) { + private static void extractTypeInfo(org.eclipse.jdt.core.IType type, List result) { try { String typeName = type.getFullyQualifiedName(); String typeInfo = ""; @@ -476,6 +484,7 @@ private static void extractTypeInfo(org.eclipse.jdt.core.IType type, List result) { + private static void extractDetailedClassInfo(org.eclipse.jdt.core.IType type, List result) { try { if (!type.isClass()) { return; // Only process classes @@ -572,14 +585,60 @@ private static void extractDetailedClassInfo(org.eclipse.jdt.core.IType type, Li classDetails.add("publicFields:" + String.join(",", publicFields)); } - // Combine all information into one string - result.add(String.join("|", classDetails)); + // Get URI for this type + String uri = getTypeUri(type); + if (uri != null) { + // Combine all information into one string + String classInfo = String.join("|", classDetails); + result.add(new ImportClassInfo(uri, classInfo)); + } } catch (JavaModelException e) { JdtlsExtActivator.logException("Error extracting detailed class info", e); } } + private static String getTypeUri(org.eclipse.jdt.core.IType type) { + try { + // Get the resource where the type is located + IResource resource = type.getResource(); + if (resource != null && resource.exists()) { + // Get the complete path of the file + IPath location = resource.getLocation(); + if (location != null) { + // 转换为 URI 格式 + return location.toFile().toURI().toString(); + } + + // If unable to get physical path, use workspace relative path + String workspacePath = resource.getFullPath().toString(); + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + IPath rootLocation = root.getLocation(); + if (rootLocation != null) { + IPath fullPath = rootLocation.append(workspacePath); + return fullPath.toFile().toURI().toString(); + } + } + + // As a fallback, try to get from compilation unit + org.eclipse.jdt.core.ICompilationUnit compilationUnit = type.getCompilationUnit(); + if (compilationUnit != null) { + IResource cuResource = compilationUnit.getResource(); + if (cuResource != null && cuResource.exists()) { + IPath cuLocation = cuResource.getLocation(); + if (cuLocation != null) { + return cuLocation.toFile().toURI().toString(); + } + } + } + + return null; + } catch (Exception e) { + JdtlsExtActivator.logException("Error getting type URI for: " + type.getElementName(), e); + return null; + } + } + // Helper method: Get method parameter types private static String getParameterTypes(IMethod method) { String[] paramTypes = method.getParameterTypes(); diff --git a/package-lock.json b/package-lock.json index 5b6b821b..534ccd77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.24.1", "license": "MIT", "dependencies": { + "@github/copilot-language-server": "^1.316.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.371.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server/-/copilot-language-server-1.371.0.tgz", + "integrity": "sha512-49CT02ElprSuG9zxM4y6TRQri0/a5doazxj3Qfz/whMtOTxiHhfHD/lmPUZXcEgOVVEovTUN7Znzx2ZLPtx3Fw==", + "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.371.0", + "@github/copilot-language-server-darwin-x64": "1.371.0", + "@github/copilot-language-server-linux-arm64": "1.371.0", + "@github/copilot-language-server-linux-x64": "1.371.0", + "@github/copilot-language-server-win32-x64": "1.371.0" + } + }, + "node_modules/@github/copilot-language-server-darwin-arm64": { + "version": "1.371.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-arm64/-/copilot-language-server-darwin-arm64-1.371.0.tgz", + "integrity": "sha512-1uWyuseYXFUIZhHFljP1O1ivTfnPxLRaDxIjqDxlU6+ugkuqp5s5LiHRED+4s4cIx4H9QMzkCVnE9Bms1nKN2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@github/copilot-language-server-darwin-x64": { + "version": "1.371.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-x64/-/copilot-language-server-darwin-x64-1.371.0.tgz", + "integrity": "sha512-nokRUPq4qPvJZ0QEZEEQPb+t2i/BrrjDDuNtBQtIIeBvJFW0YtwhbhEAtFpJtYZ5G+/nkbKMKYufFiLCvUnJ4A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@github/copilot-language-server-linux-arm64": { + "version": "1.371.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-arm64/-/copilot-language-server-linux-arm64-1.371.0.tgz", + "integrity": "sha512-1cCJCs5j3+wl6NcNs1AUXpqFFogHdQLRiBeMBPEUaFSI95H5WwPphwe0OlmrVfRJQ19rqDfeT58B1jHMX6fM/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@github/copilot-language-server-linux-x64": { + "version": "1.371.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-x64/-/copilot-language-server-linux-x64-1.371.0.tgz", + "integrity": "sha512-mMK5iGpaUQuM3x0H5I9iDRQQ3NZLzatXJtQkCPT30fXb2yZFNED+yU7nKAxwGX91MMeTyOzotNvh2ZzITmajDA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@github/copilot-language-server-win32-x64": { + "version": "1.371.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-win32-x64/-/copilot-language-server-win32-x64-1.371.0.tgz", + "integrity": "sha512-j7W1c6zTRUou4/l2M2HNfjfT8O588pRAf6zgllwnMrc2EYD8O4kzNiK1c5pHrlP1p5bnGqsUIrOuozu9EUkCLQ==", + "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", @@ -5736,6 +5821,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", @@ -6233,6 +6343,49 @@ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true }, + "@github/copilot-language-server": { + "version": "1.371.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server/-/copilot-language-server-1.371.0.tgz", + "integrity": "sha512-49CT02ElprSuG9zxM4y6TRQri0/a5doazxj3Qfz/whMtOTxiHhfHD/lmPUZXcEgOVVEovTUN7Znzx2ZLPtx3Fw==", + "requires": { + "@github/copilot-language-server-darwin-arm64": "1.371.0", + "@github/copilot-language-server-darwin-x64": "1.371.0", + "@github/copilot-language-server-linux-arm64": "1.371.0", + "@github/copilot-language-server-linux-x64": "1.371.0", + "@github/copilot-language-server-win32-x64": "1.371.0", + "vscode-languageserver-protocol": "^3.17.5" + } + }, + "@github/copilot-language-server-darwin-arm64": { + "version": "1.371.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-arm64/-/copilot-language-server-darwin-arm64-1.371.0.tgz", + "integrity": "sha512-1uWyuseYXFUIZhHFljP1O1ivTfnPxLRaDxIjqDxlU6+ugkuqp5s5LiHRED+4s4cIx4H9QMzkCVnE9Bms1nKN2w==", + "optional": true + }, + "@github/copilot-language-server-darwin-x64": { + "version": "1.371.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-x64/-/copilot-language-server-darwin-x64-1.371.0.tgz", + "integrity": "sha512-nokRUPq4qPvJZ0QEZEEQPb+t2i/BrrjDDuNtBQtIIeBvJFW0YtwhbhEAtFpJtYZ5G+/nkbKMKYufFiLCvUnJ4A==", + "optional": true + }, + "@github/copilot-language-server-linux-arm64": { + "version": "1.371.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-arm64/-/copilot-language-server-linux-arm64-1.371.0.tgz", + "integrity": "sha512-1cCJCs5j3+wl6NcNs1AUXpqFFogHdQLRiBeMBPEUaFSI95H5WwPphwe0OlmrVfRJQ19rqDfeT58B1jHMX6fM/Q==", + "optional": true + }, + "@github/copilot-language-server-linux-x64": { + "version": "1.371.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-x64/-/copilot-language-server-linux-x64-1.371.0.tgz", + "integrity": "sha512-mMK5iGpaUQuM3x0H5I9iDRQQ3NZLzatXJtQkCPT30fXb2yZFNED+yU7nKAxwGX91MMeTyOzotNvh2ZzITmajDA==", + "optional": true + }, + "@github/copilot-language-server-win32-x64": { + "version": "1.371.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-win32-x64/-/copilot-language-server-win32-x64-1.371.0.tgz", + "integrity": "sha512-j7W1c6zTRUou4/l2M2HNfjfT8O588pRAf6zgllwnMrc2EYD8O4kzNiK1c5pHrlP1p5bnGqsUIrOuozu9EUkCLQ==", + "optional": true + }, "@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -10356,6 +10509,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 2e2eb4f5..93ccdd11 100644 --- a/package.json +++ b/package.json @@ -1095,6 +1095,7 @@ "webpack-cli": "^4.10.0" }, "dependencies": { + "@github/copilot-language-server": "^1.316.0", "await-lock": "^2.2.2", "fmtr": "^1.1.4", "fs-extra": "^10.1.0", diff --git a/src/commands.ts b/src/commands.ts index 49dcb10b..e042750a 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -116,7 +116,7 @@ export namespace Commands { export const JAVA_PROJECT_GETMAINCLASSES = "java.project.getMainClasses"; - export const JAVA_PROJECT_RESOLVE_COPILOT_REQUEST = "java.project.resolveCopilotRequest"; + export const JAVA_PROJECT_GETIMPORTCLASSCONTENT = "java.project.getImportClassContent"; export const JAVA_PROJECT_GENERATEJAR = "java.project.generateJar"; diff --git a/src/copilot/contextProvider.ts b/src/copilot/contextProvider.ts new file mode 100644 index 00000000..a2862272 --- /dev/null +++ b/src/copilot/contextProvider.ts @@ -0,0 +1,450 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { + ContextProviderApiV1, + ResolveRequest, + SupportedContextItem, + type ContextProvider, +} from '@github/copilot-language-server'; +import * as vscode from 'vscode'; +import { CopilotHelper } from '../copilotHelper'; + +export enum NodeKind { + Workspace = 1, + Project = 2, + PackageRoot = 3, + Package = 4, + PrimaryType = 5, + CompilationUnit = 6, + ClassFile = 7, + Container = 8, + Folder = 9, + File = 10, +} + +export async function registerCopilotContextProviders( + context: vscode.ExtensionContext +) { + try { + const copilotClientApi = await getCopilotClientApi(); + const copilotChatApi = await getCopilotChatApi(); + if (!copilotClientApi && !copilotChatApi) { + console.log('Failed to find compatible version of GitHub Copilot extension installed. Skip registration of Copilot context provider.'); + return; + } + + const provider: ContextProvider = { + id: 'vscjava.vscode-java-pack', // use extension id as provider id for now + selector: [{ language: "*" }], + resolver: { + resolve: async (request, token) => { + console.log('======== java request:', request); + console.log('======== java token:', token); + const items = await resolveJavaContext(request, token); + console.log('======== java context end ===========') + return items; + } + } + }; + + let installCount = 0; + if (copilotClientApi) { + const disposable = await installContextProvider(copilotClientApi, provider); + if (disposable) { + context.subscriptions.push(disposable); + installCount++; + } + } + if (copilotChatApi) { + const disposable = await installContextProvider(copilotChatApi, provider); + if (disposable) { + context.subscriptions.push(disposable); + installCount++; + } + } + + if (installCount === 0) { + console.log('Incompatible GitHub Copilot extension installed. Skip registration of Java context providers.'); + return; + } + console.log('Registration of Java context provider for GitHub Copilot extension succeeded.'); + + // Register the Java completion context provider + const javaCompletionProvider = new JavaCopilotCompletionContextProvider(); + let completionProviderInstallCount = 0; + + if (copilotClientApi) { + const disposable = await installContextProvider(copilotClientApi, javaCompletionProvider); + if (disposable) { + context.subscriptions.push(disposable); + completionProviderInstallCount++; + } + } + if (copilotChatApi) { + const disposable = await installContextProvider(copilotChatApi, javaCompletionProvider); + if (disposable) { + context.subscriptions.push(disposable); + completionProviderInstallCount++; + } + } + + if (completionProviderInstallCount > 0) { + console.log('Registration of Java completion context provider for GitHub Copilot extension succeeded.'); + } else { + console.log('Failed to register Java completion context provider for GitHub Copilot extension.'); + } + } + catch (error) { + console.log('Error occurred while registering Java context provider for GitHub Copilot extension:', error); + } +} + +async function resolveJavaContext(_request: ResolveRequest, _token: vscode.CancellationToken): Promise { + const items: SupportedContextItem[] = []; + const start = performance.now(); + try { + // Get current document and position information + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor || activeEditor.document.languageId !== 'java') { + return items; + } + + const document = activeEditor.document; + + // const position = activeEditor.selection.active; + // const currentRange = activeEditor.selection.isEmpty + // ? new vscode.Range(position, position) + // : activeEditor.selection; + + // 1. Project basic information (High importance) + const projectContext = await collectProjectContext(document); + const packageName = await getPackageName(document); + + items.push({ + name: 'java.version', + value: projectContext.javaVersion, + importance: 90, + id: 'java-version', + origin: 'request' + }); + + items.push({ + name: 'java.file', + value: vscode.workspace.asRelativePath(document.uri), + importance: 80, + id: 'java-file-path', + origin: 'request' + }); + + items.push({ + name: 'java.package', + value: packageName, + importance: 85, + id: 'java-package-name', + origin: 'request' + }); + + const importClass = await CopilotHelper.resolveLocalImports(document.uri); + for(const cls of importClass) { + items.push({ + uri: cls.uri, + value: cls.className, + importance: 70, + origin: 'request' + }); + } + + console.log('tick time', performance.now() - start); + + } catch (error) { + console.log('Error resolving Java context:', error); + // Add error information as context to help with debugging + items.push({ + name: 'java.context.error', + value: `${error}`, + importance: 10, + id: 'java-context-error', + origin: 'request' + }); + } + console.log('Total context resolution time:', performance.now() - start); + console.log('===== Size of context items:', items.length); + return items; +} + +async function collectProjectContext(document: vscode.TextDocument): Promise<{ javaVersion: string }> { + try { + return await vscode.commands.executeCommand("java.project.getSettings", document.uri, ["java.home"]); + } catch (error) { + console.log('Failed to get Java version:', error); + return { javaVersion: 'unknown' }; + } +} + +async function getPackageName(document: vscode.TextDocument): Promise { + try { + const text = document.getText(); + const packageMatch = text.match(/^\s*package\s+([a-zA-Z_][a-zA-Z0-9_.]*)\s*;/m); + return packageMatch ? packageMatch[1] : 'default package'; + } catch (error) { + console.log('Failed to get package name:', error); + return 'unknown'; + } +} + +interface CopilotApi { + getContextProviderAPI(version: string): Promise; +} + +async function getCopilotClientApi(): Promise { + const extension = vscode.extensions.getExtension('github.copilot'); + if (!extension) { + return undefined; + } + try { + return await extension.activate(); + } catch { + return undefined; + } +} + +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); +} + +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; +} + +/** + * Java-specific Copilot completion context provider + * Similar to CopilotCompletionContextProvider but tailored for Java language + */ +export class JavaCopilotCompletionContextProvider implements ContextProvider { + public readonly id = 'java-completion'; + public readonly selector = [{ language: 'java' }]; + public readonly resolver = this.resolve.bind(this); + + // Cache for completion contexts with timeout + private cache = new Map(); + private readonly cacheTimeout = 30000; // 30 seconds + + public async resolve(request: ResolveRequest, cancellationToken: vscode.CancellationToken): Promise { + // Access document through request properties + const docUri = request.documentContext?.uri?.toString(); + const docOffset = request.documentContext?.offset; + + // Only process Java files + if (!docUri || !docUri.endsWith('.java')) { + return []; + } + + const cacheKey = `${docUri}:${docOffset}`; + const cached = this.cache.get(cacheKey); + + // Return cached result if still valid + if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { + return cached.context; + } + + try { + const context = await this.generateJavaCompletionContext(docUri, docOffset, cancellationToken); + + // Cache the result + this.cache.set(cacheKey, { + context, + timestamp: Date.now() + }); + + // Clean up old cache entries + this.cleanCache(); + + return context; + } catch (error) { + console.error('Error generating Java completion context:', error); + return []; + } + } + + private async generateJavaCompletionContext( + docUri: string, + docOffset: number | undefined, + cancellationToken: vscode.CancellationToken + ): Promise { + const context: SupportedContextItem[] = []; + + try { + // Check for cancellation + if (cancellationToken.isCancellationRequested) { + return []; + } + + // Get the document + const document = await vscode.workspace.openTextDocument(vscode.Uri.parse(docUri)); + if (!document) { + return []; + } + + // Get import class information from the Java project + const importClassInfo = await CopilotHelper.getImportClassContent(docUri); + + if (importClassInfo && importClassInfo.length > 0) { + // Convert import class information to context items + for (const classInfo of importClassInfo) { + context.push({ + name: `java.class.${this.extractClassName(classInfo.className)}`, + value: this.formatClassContext(classInfo.className), + importance: 75, + id: `java-class-${classInfo.uri}`, + origin: 'request' + }); + } + } + + // Get related project context + const projectContext = await this.getProjectContext(document); + context.push(...projectContext); + + // Get current file context (surrounding methods, classes) + const fileContext = await this.getCurrentFileContext(document, docOffset); + context.push(...fileContext); + + } catch (error) { + console.error('Error in generateJavaCompletionContext:', error); + } + + return context; + } + + private async getProjectContext(document: vscode.TextDocument): Promise { + const context: SupportedContextItem[] = []; + + try { + // Get local imports for better context + const localImports = await CopilotHelper.resolveLocalImports(document.uri); + + if (localImports) { + for (const importInfo of localImports) { + context.push({ + name: `java.import.${importInfo.className}`, + value: this.formatImportContext(importInfo.className), + importance: 60, + id: `java-import-${importInfo.uri}`, + origin: 'request' + }); + } + } + + // Get package information + const packageName = await getPackageName(document); + context.push({ + name: 'java.package', + value: packageName, + importance: 85, + id: 'java-package-context', + origin: 'request' + }); + + } catch (error) { + console.error('Error getting project context:', error); + } + + return context; + } + + private async getCurrentFileContext( + document: vscode.TextDocument, + docOffset: number | undefined + ): Promise { + const context: SupportedContextItem[] = []; + + try { + const text = document.getText(); + const lines = text.split('\n'); + + // Calculate current line from offset if provided + let currentLine = 0; + if (docOffset !== undefined) { + const textUpToOffset = text.substring(0, docOffset); + currentLine = textUpToOffset.split('\n').length - 1; + } + + // Get surrounding context (methods, classes around cursor) + const contextRange = this.getContextRange(lines, currentLine); + const contextContent = lines.slice(contextRange.start, contextRange.end).join('\n'); + + if (contextContent.trim()) { + context.push({ + name: 'java.current.file.context', + value: contextContent, + importance: 70, + id: 'java-current-file-context', + origin: 'request' + }); + } + } catch (error) { + console.error('Error getting current file context:', error); + } + + return context; + } + + private getContextRange(lines: string[], currentLine: number): { start: number; end: number } { + const contextLines = 20; // Lines of context to include + const start = Math.max(0, currentLine - contextLines); + const end = Math.min(lines.length, currentLine + contextLines); + + return { start, end }; + } + + private formatClassContext(className: string): string { + // Format class name for better Copilot understanding + return `// Related class: ${className}`; + } + + private formatImportContext(importName: string): string { + return `// Related import: ${importName}`; + } + + private extractClassName(className: string): string { + // Extract simple class name from fully qualified name + const parts = className.split('.'); + return parts[parts.length - 1] || 'Unknown'; + } + + private cleanCache(): void { + const now = Date.now(); + for (const [key, value] of this.cache.entries()) { + if (now - value.timestamp > this.cacheTimeout) { + this.cache.delete(key); + } + } + } +} \ No newline at end of file diff --git a/src/copilot/copilotCompletionContextProvider.ts b/src/copilot/copilotCompletionContextProvider.ts new file mode 100644 index 00000000..4452d873 --- /dev/null +++ b/src/copilot/copilotCompletionContextProvider.ts @@ -0,0 +1,538 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +import { ContextResolver, ResolveRequest, SupportedContextItem, type ContextProvider } from '@github/copilot-language-server'; +import { randomUUID } from 'crypto'; +import * as vscode from 'vscode'; +import { DocumentSelector } from 'vscode-languageserver-protocol'; +import { isBoolean, isNumber, isString } from '../common'; +import { getOutputChannelLogger, Logger } from '../logger'; +import * as telemetry from '../telemetry'; +import { CopilotCompletionContextResult } from './client'; +import { CopilotCompletionContextTelemetry } from './copilotCompletionContextTelemetry'; +import { getCopilotChatApi, getCopilotClientApi, type CopilotContextProviderAPI } from './copilotProviders'; +import { clients } from './extension'; +import { CppSettings } from './settings'; + +class DefaultValueFallback extends Error { + static readonly DefaultValue = "DefaultValue"; + constructor() { super(DefaultValueFallback.DefaultValue); } +} + +class CancellationError extends Error { + static readonly Canceled = "Canceled"; + constructor() { + super(CancellationError.Canceled); + this.name = this.message; + } +} + +class InternalCancellationError extends CancellationError { +} + +class CopilotCancellationError extends CancellationError { +} + +class CopilotContextProviderException extends Error { +} + +class WellKnownErrors extends Error { + static readonly ClientNotFound = "ClientNotFound"; + private constructor(message: string) { super(message); } + public static clientNotFound(): Error { + return new WellKnownErrors(WellKnownErrors.ClientNotFound); + } +} + +// A bit mask for enabling features in the completion context. +export enum CopilotCompletionContextFeatures { + None = 0, + Instant = 1, + Deferred = 2, +} + +// Mutually exclusive values for the kind of returned completion context. They either are: +// - computed. +// - obtained from the cache. +// - available in cache but stale (e.g. actual context is far away). +// - missing since the computation took too long and no cache is present (cache miss). The value +// is asynchronously computed and stored in cache. +// - the token is signaled as cancelled, in which case all the operations are aborted. +// - an unknown state. +export enum CopilotCompletionKind { + Computed = 'computed', + GotFromCache = 'gotFromCacheHit', + StaleCacheHit = 'staleCacheHit', + MissingCacheMiss = 'missingCacheMiss', + Canceled = 'canceled', + Unknown = 'unknown' +} + +type CacheEntry = [string, CopilotCompletionContextResult]; + +export class CopilotCompletionContextProvider implements ContextResolver { + private static readonly providerId = 'ms-vscode.cpptools'; + private readonly completionContextCache: Map = new Map(); + private static readonly defaultCppDocumentSelector: DocumentSelector = [{ language: 'cpp' }, { language: 'c' }, { language: 'cuda-cpp' }]; + // The default time budget for providing a value from resolve(). + private static readonly defaultTimeBudgetMs: number = 7; + // Assume the cache is stale when the distance to the current caret is greater than this value. + private static readonly defaultMaxCaretDistance = 8192; + private static readonly defaultMaxSnippetCount = 7; + private static readonly defaultMaxSnippetLength = 3 * 1024; + private static readonly defaultDoAggregateSnippets = true; + private completionContextCancellation = new vscode.CancellationTokenSource(); + private contextProviderDisposables: vscode.Disposable[] | undefined; + static readonly CppContextProviderEnabledFeatures = 'enabledFeatures'; + static readonly CppContextProviderTimeBudgetMs = 'timeBudgetMs'; + static readonly CppContextProviderMaxSnippetCount = 'maxSnippetCount'; + static readonly CppContextProviderMaxSnippetLength = 'maxSnippetLength'; + static readonly CppContextProviderMaxDistanceToCaret = 'maxDistanceToCaret'; + static readonly CppContextProviderDoAggregateSnippets = 'doAggregateSnippets'; + + constructor(private readonly logger: Logger) { + } + + private async waitForCompletionWithTimeoutAndCancellation(promise: Promise, defaultValue: T | undefined, + timeout: number, copilotToken: vscode.CancellationToken): Promise<[T | undefined, CopilotCompletionKind]> { + const defaultValuePromise = new Promise((_resolve, reject) => setTimeout(() => { + if (copilotToken.isCancellationRequested) { + reject(new CancellationError()); + } else { + reject(new DefaultValueFallback()); + } + }, timeout)); + const cancellationPromise = new Promise((_, reject) => { + copilotToken.onCancellationRequested(() => { + reject(new CancellationError()); + }); + }); + let snippetsOrNothing: T | undefined; + try { + snippetsOrNothing = await Promise.race([promise, cancellationPromise, defaultValuePromise]); + } catch (e) { + if (e instanceof DefaultValueFallback) { + return [defaultValue, defaultValue !== undefined ? CopilotCompletionKind.GotFromCache : CopilotCompletionKind.MissingCacheMiss]; + } else if (e instanceof CancellationError) { + return [undefined, CopilotCompletionKind.Canceled]; + } else { + throw e; + } + } + + return [snippetsOrNothing, CopilotCompletionKind.Computed]; + } + + private static normalizeFeatureFlag(featureFlag: CopilotCompletionContextFeatures): CopilotCompletionContextFeatures { + // eslint-disable-next-line no-bitwise + if ((featureFlag & CopilotCompletionContextFeatures.Instant) === CopilotCompletionContextFeatures.Instant) { return CopilotCompletionContextFeatures.Instant; } + // eslint-disable-next-line no-bitwise + if ((featureFlag & CopilotCompletionContextFeatures.Deferred) === CopilotCompletionContextFeatures.Deferred) { return CopilotCompletionContextFeatures.Deferred; } + return CopilotCompletionContextFeatures.None; + } + + // Get the completion context with a timeout and a cancellation token. + // The cancellationToken indicates that the value should not be returned nor cached. + private async getCompletionContextWithCancellation(context: ResolveRequest, featureFlag: CopilotCompletionContextFeatures, + maxSnippetCount: number, maxSnippetLength: number, doAggregateSnippets: boolean, startTime: number, telemetry: CopilotCompletionContextTelemetry, + internalToken: vscode.CancellationToken): + Promise { + const documentUri = context.documentContext.uri; + const caretOffset = context.documentContext.offset; + let logMessage = `Copilot: getCompletionContext(${documentUri}:${caretOffset}):`; + try { + const snippetsFeatureFlag = CopilotCompletionContextProvider.normalizeFeatureFlag(featureFlag); + telemetry.addRequestMetadata(documentUri, caretOffset, context.completionId, + context.documentContext.languageId, { featureFlag: snippetsFeatureFlag }); + const docUri = vscode.Uri.parse(documentUri); + const getClientForTime = performance.now(); + const client = clients.getClientFor(docUri); + const getClientForDuration = CopilotCompletionContextProvider.getRoundedDuration(getClientForTime); + telemetry.addGetClientForElapsed(getClientForDuration); + if (!client) { throw WellKnownErrors.clientNotFound(); } + const getCompletionContextStartTime = performance.now(); + const copilotCompletionContext: CopilotCompletionContextResult = + await client.getCompletionContext(docUri, caretOffset, snippetsFeatureFlag, maxSnippetCount, maxSnippetLength, doAggregateSnippets, internalToken); + telemetry.addRequestId(copilotCompletionContext.requestId); + logMessage += `(id: ${copilotCompletionContext.requestId})(getClientFor elapsed:${getClientForDuration}ms)`; + if (!copilotCompletionContext.areSnippetsMissing) { + const resultMismatch = copilotCompletionContext.sourceFileUri !== docUri.toString(); + if (resultMismatch) { logMessage += `(mismatch TU vs result)`; } + } + const cacheEntryId = randomUUID().toString(); + this.completionContextCache.set(copilotCompletionContext.sourceFileUri, [cacheEntryId, copilotCompletionContext]); + const duration = CopilotCompletionContextProvider.getRoundedDuration(startTime); + telemetry.addCacheComputedData(duration, cacheEntryId); + logMessage += ` cached in ${duration}ms ${copilotCompletionContext.traits.length} trait(s)`; + if (copilotCompletionContext.areSnippetsMissing) { logMessage += `(missing code snippets)`; } + else { + logMessage += ` and ${copilotCompletionContext.snippets.length} snippet(s)`; + logMessage += `(response.featureFlag:${copilotCompletionContext.featureFlag})`; + logMessage += `(response.uri:${copilotCompletionContext.sourceFileUri || ""}:${copilotCompletionContext.caretOffset})`; + } + + telemetry.addResponseMetadata(copilotCompletionContext.areSnippetsMissing, copilotCompletionContext.snippets.length, + copilotCompletionContext.traits.length, copilotCompletionContext.caretOffset, copilotCompletionContext.featureFlag); + telemetry.addComputeContextElapsed(CopilotCompletionContextProvider.getRoundedDuration(getCompletionContextStartTime)); + + return copilotCompletionContext; + } catch (e: any) { + if (e instanceof vscode.CancellationError || e.message === CancellationError.Canceled) { + telemetry.addInternalCanceled(CopilotCompletionContextProvider.getRoundedDuration(startTime)); + logMessage += `(internal cancellation)`; + throw InternalCancellationError; + } + + if (e instanceof WellKnownErrors) { + telemetry.addWellKnownError(e.message); + } + + telemetry.addError(); + this.logger.appendLineAtLevel(7, `Copilot: getCompletionContextWithCancellation(${documentUri}: ${caretOffset}): Error: '${e}'`); + return undefined; + } finally { + this.logger. + appendLineAtLevel(7, `[${new Date().toISOString().replace('T', ' ').replace('Z', '')}] ${logMessage}`); + telemetry.send("cache"); + } + } + + static readonly paramsCache: Record = {}; + static paramsCacheCreated = false; + private getContextProviderParam(paramName: string): T | undefined { + try { + if (!CopilotCompletionContextProvider.paramsCacheCreated) { + CopilotCompletionContextProvider.paramsCacheCreated = true; + const paramsJson = new CppSettings().cppContextProviderParams; + if (isString(paramsJson)) { + try { + const params = JSON.parse(paramsJson.replaceAll(/'/g, '"')); + for (const key in params) { + CopilotCompletionContextProvider.paramsCache[key] = params[key]; + } + } catch (e) { + console.warn(`getContextProviderParam(): error parsing getContextProviderParam: `, e); + } + } + } + return CopilotCompletionContextProvider.paramsCache[paramName] as T; + } catch (e) { + console.warn(`getContextProviderParam(): error fetching getContextProviderParam: `, e); + return undefined; + } + } + + private async fetchTimeBudgetMs(context: ResolveRequest): Promise { + try { + const timeBudgetMs = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderTimeBudgetMs) ?? + context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderTimeBudgetMs); + return isNumber(timeBudgetMs) ? timeBudgetMs : CopilotCompletionContextProvider.defaultTimeBudgetMs; + } catch (e) { + console.warn(`fetchTimeBudgetMs(): error fetching ${CopilotCompletionContextProvider.CppContextProviderTimeBudgetMs}, using default: `, e); + return CopilotCompletionContextProvider.defaultTimeBudgetMs; + } + } + + private async fetchMaxDistanceToCaret(context: ResolveRequest): Promise { + try { + const maxDistance = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderMaxDistanceToCaret) ?? + context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderMaxDistanceToCaret); + return isNumber(maxDistance) ? maxDistance : CopilotCompletionContextProvider.defaultMaxCaretDistance; + } catch (e) { + console.warn(`fetchMaxDistanceToCaret(): error fetching ${CopilotCompletionContextProvider.CppContextProviderMaxDistanceToCaret}, using default: `, e); + return CopilotCompletionContextProvider.defaultMaxCaretDistance; + } + } + + private async fetchMaxSnippetCount(context: ResolveRequest): Promise { + try { + const maxSnippetCount = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderMaxSnippetCount) ?? + context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderMaxSnippetCount); + return isNumber(maxSnippetCount) ? maxSnippetCount : CopilotCompletionContextProvider.defaultMaxSnippetCount; + } catch (e) { + console.warn(`fetchMaxSnippetCount(): error fetching ${CopilotCompletionContextProvider.defaultMaxSnippetCount}, using default: `, e); + return CopilotCompletionContextProvider.defaultMaxSnippetCount; + } + } + + private async fetchMaxSnippetLength(context: ResolveRequest): Promise { + try { + const maxSnippetLength = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderMaxSnippetLength) ?? + context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderMaxSnippetLength); + return isNumber(maxSnippetLength) ? maxSnippetLength : CopilotCompletionContextProvider.defaultMaxSnippetLength; + } catch (e) { + console.warn(`fetchMaxSnippetLength(): error fetching ${CopilotCompletionContextProvider.defaultMaxSnippetLength}, using default: `, e); + return CopilotCompletionContextProvider.defaultMaxSnippetLength; + } + } + + private async fetchDoAggregateSnippets(context: ResolveRequest): Promise { + try { + const doAggregateSnippets = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderDoAggregateSnippets) ?? + context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderDoAggregateSnippets); + return isBoolean(doAggregateSnippets) ? doAggregateSnippets : CopilotCompletionContextProvider.defaultDoAggregateSnippets; + } catch (e) { + console.warn(`fetchDoAggregateSnippets(): error fetching ${CopilotCompletionContextProvider.defaultDoAggregateSnippets}, using default: `, e); + return CopilotCompletionContextProvider.defaultDoAggregateSnippets; + } + } + + private async getEnabledFeatureNames(context: ResolveRequest): Promise { + try { + const enabledFeatureNames = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderEnabledFeatures) ?? + context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderEnabledFeatures); + if (isString(enabledFeatureNames)) { + return enabledFeatureNames.split(',').map(s => s.trim()); + } + } catch (e) { + console.warn(`getEnabledFeatureNames(): error fetching ${CopilotCompletionContextProvider.CppContextProviderEnabledFeatures}: `, e); + } + return undefined; + } + + private async getEnabledFeatureFlag(context: ResolveRequest): Promise { + let result; + for (const featureName of await this.getEnabledFeatureNames(context) ?? []) { + const flag = CopilotCompletionContextFeatures[featureName as keyof typeof CopilotCompletionContextFeatures]; + if (flag !== undefined) { result = (result ?? 0) + flag; } + } + return result; + } + + private static getRoundedDuration(startTime: number): number { + return Math.round(performance.now() - startTime); + } + + public static Create() { + const copilotCompletionProvider = new CopilotCompletionContextProvider(getOutputChannelLogger()); + copilotCompletionProvider.registerCopilotContextProvider(); + return copilotCompletionProvider; + } + + public dispose(): void { + this.completionContextCancellation.cancel(); + if (this.contextProviderDisposables) { + for (const disposable of this.contextProviderDisposables) { + disposable.dispose(); + } + this.contextProviderDisposables = undefined; + } + } + + public removeFile(fileUri: string): void { + this.completionContextCache.delete(fileUri); + } + + private computeSnippetsResolved: boolean = true; + + private async resolveResultAndKind(context: ResolveRequest, featureFlag: CopilotCompletionContextFeatures, + telemetry: CopilotCompletionContextTelemetry, defaultValue: CopilotCompletionContextResult | undefined, + resolveStartTime: number, timeBudgetMs: number, maxSnippetCount: number, maxSnippetLength: number, doAggregateSnippets: boolean, + copilotCancel: vscode.CancellationToken): Promise<[CopilotCompletionContextResult | undefined, CopilotCompletionKind]> { + if (this.computeSnippetsResolved) { + this.computeSnippetsResolved = false; + const computeSnippetsPromise = this.getCompletionContextWithCancellation(context, featureFlag, + maxSnippetCount, maxSnippetLength, doAggregateSnippets, resolveStartTime, telemetry.fork(), this.completionContextCancellation.token).finally( + () => this.computeSnippetsResolved = true + ); + const res = await this.waitForCompletionWithTimeoutAndCancellation( + computeSnippetsPromise, defaultValue, timeBudgetMs, copilotCancel); + return res; + } else { return [defaultValue, defaultValue ? CopilotCompletionKind.GotFromCache : CopilotCompletionKind.MissingCacheMiss]; } + } + + private static isStaleCacheHit(caretOffset: number, cacheCaretOffset: number, maxCaretDistance: number): boolean { + return Math.abs(caretOffset - caretOffset) > maxCaretDistance; + } + + private static createContextItems(copilotCompletionContext: CopilotCompletionContextResult | undefined): SupportedContextItem[] { + return [...copilotCompletionContext?.snippets ?? [], ...copilotCompletionContext?.traits ?? []] as SupportedContextItem[]; + } + + public async resolve(context: ResolveRequest, copilotCancel: vscode.CancellationToken): Promise { + const proposedEdits = context.documentContext.proposedEdits; + const resolveStartTime = performance.now(); + let logMessage = `Copilot: resolve(${context.documentContext.uri}:${context.documentContext.offset}):`; + const cppTimeBudgetMs = await this.fetchTimeBudgetMs(context); + const maxCaretDistance = await this.fetchMaxDistanceToCaret(context); + const maxSnippetCount = await this.fetchMaxSnippetCount(context); + const maxSnippetLength = await this.fetchMaxSnippetLength(context); + const doAggregateSnippets = await this.fetchDoAggregateSnippets(context); + const telemetry = new CopilotCompletionContextTelemetry(); + let copilotCompletionContext: CopilotCompletionContextResult | undefined; + let copilotCompletionContextKind: CopilotCompletionKind = CopilotCompletionKind.Unknown; + let featureFlag: CopilotCompletionContextFeatures | undefined; + const docUri = context.documentContext.uri; + const docOffset = context.documentContext.offset; + try { + featureFlag = await this.getEnabledFeatureFlag(context); + telemetry.addRequestMetadata(context.documentContext.uri, context.documentContext.offset, + context.completionId, context.documentContext.languageId, { + featureFlag, timeBudgetMs: cppTimeBudgetMs, maxCaretDistance, + maxSnippetCount, maxSnippetLength, doAggregateSnippets + }); + if (featureFlag === undefined) { return []; } + const cacheEntry: CacheEntry | undefined = this.completionContextCache.get(docUri.toString()); + if (proposedEdits) { + const defaultValue = cacheEntry?.[1]; + const isStaleCache = defaultValue !== undefined ? CopilotCompletionContextProvider.isStaleCacheHit(docOffset, defaultValue.caretOffset, maxCaretDistance) : true; + const contextItems = isStaleCache ? [] : CopilotCompletionContextProvider.createContextItems(defaultValue); + copilotCompletionContext = isStaleCache ? undefined : defaultValue; + copilotCompletionContextKind = isStaleCache ? CopilotCompletionKind.StaleCacheHit : CopilotCompletionKind.GotFromCache; + telemetry.addSpeculativeRequestMetadata(proposedEdits.length); + if (cacheEntry?.[0]) { + telemetry.addCacheHitEntryGuid(cacheEntry[0]); + } + return contextItems; + } + const [resultContext, resultKind] = await this.resolveResultAndKind(context, featureFlag, + telemetry.fork(), cacheEntry?.[1], resolveStartTime, cppTimeBudgetMs, maxSnippetCount, maxSnippetLength, doAggregateSnippets, copilotCancel); + copilotCompletionContext = resultContext; + copilotCompletionContextKind = resultKind; + logMessage += `(id: ${copilotCompletionContext?.requestId})`; + // Fix up copilotCompletionContextKind accounting for stale-cache-hits. + if (copilotCompletionContextKind === CopilotCompletionKind.GotFromCache && + copilotCompletionContext && cacheEntry) { + telemetry.addCacheHitEntryGuid(cacheEntry[0]); + const cachedData = cacheEntry[1]; + if (CopilotCompletionContextProvider.isStaleCacheHit(docOffset, cachedData.caretOffset, maxCaretDistance)) { + copilotCompletionContextKind = CopilotCompletionKind.StaleCacheHit; + copilotCompletionContext.snippets = []; + } + } + // Handle cancellation. + if (copilotCompletionContextKind === CopilotCompletionKind.Canceled) { + const duration: number = CopilotCompletionContextProvider.getRoundedDuration(resolveStartTime); + telemetry.addCopilotCanceled(duration); + throw new CopilotCancellationError(); + } + return CopilotCompletionContextProvider.createContextItems(copilotCompletionContext); + } catch (e: any) { + if (e instanceof CopilotCancellationError) { + telemetry.addCopilotCanceled(CopilotCompletionContextProvider.getRoundedDuration(resolveStartTime)); + logMessage += `(copilot cancellation)`; + throw e; + } + if (e instanceof InternalCancellationError) { + telemetry.addInternalCanceled(CopilotCompletionContextProvider.getRoundedDuration(resolveStartTime)); + logMessage += `(internal cancellation)`; + throw e; + } + if (e instanceof CancellationError) { throw e; } + + // For any other exception's type, it is an error. + telemetry.addError(); + throw e; + } finally { + const duration: number = CopilotCompletionContextProvider.getRoundedDuration(resolveStartTime); + logMessage += `(featureFlag:${featureFlag?.toString()})`; + if (proposedEdits) { logMessage += `(speculative request, proposedEdits:${proposedEdits.length})`; } + if (copilotCompletionContext === undefined) { + logMessage += `result is undefined and no code snippets provided(${copilotCompletionContextKind.toString()}), elapsed time:${duration} ms`; + } else { + logMessage += `for ${docUri}:${docOffset} provided ${copilotCompletionContext.snippets.length} code snippet(s)(${copilotCompletionContextKind.toString()}\ +${copilotCompletionContext?.areSnippetsMissing ? "(missing code snippets)" : ""}) and ${copilotCompletionContext.traits.length} trait(s), elapsed time:${duration} ms`; + } + telemetry.addCompletionContextKind(copilotCompletionContextKind); + telemetry.addResponseMetadata(copilotCompletionContext?.areSnippetsMissing ?? true, + copilotCompletionContext?.snippets.length, copilotCompletionContext?.traits.length, + copilotCompletionContext?.caretOffset, copilotCompletionContext?.featureFlag); + telemetry.addResolvedElapsed(duration); + telemetry.addCacheSize(this.completionContextCache.size); + telemetry.send(); + this.logger.appendLineAtLevel(7, `[${new Date().toISOString().replace('T', ' ').replace('Z', '')}] ${logMessage}`); + } + } + + public registerCopilotContextProvider(): void { + const registerCopilotContextProvider = 'registerCopilotContextProvider'; + const contextProvider = { + id: CopilotCompletionContextProvider.providerId, + selector: CopilotCompletionContextProvider.defaultCppDocumentSelector, + resolver: this + }; + type RegistrationResult = { message: string } | boolean; + const clientPromise: Promise = getCopilotClientApi().then(async (api) => { + if (!api) { + throw new CopilotContextProviderException("getCopilotApi() returned null, Copilot client is missing or inactive."); + } + const disposable = await this.installContextProvider(api, contextProvider); + if (disposable) { + this.contextProviderDisposables = this.contextProviderDisposables ?? []; + this.contextProviderDisposables.push(disposable); + return true; + } else { + throw new CopilotContextProviderException("getContextProviderAPI() is not available in Copilot client."); + } + }).catch((e) => { + console.debug("Failed to register the Copilot Context Provider with Copilot client."); + let message = "Failed to register the Copilot Context Provider with Copilot client"; + if (e instanceof CopilotContextProviderException) { + message += `: ${e.message} `; + } + return { message }; + }); + const chatPromise: Promise = getCopilotChatApi().then(async (api) => { + if (!api) { + throw new CopilotContextProviderException("getCopilotChatApi() returned null, Copilot Chat is missing or inactive."); + } + const disposable = await this.installContextProvider(api, contextProvider); + if (disposable) { + this.contextProviderDisposables = this.contextProviderDisposables ?? []; + this.contextProviderDisposables.push(disposable); + return true; + } else { + throw new CopilotContextProviderException("getContextProviderAPI() is not available in Copilot Chat."); + } + }).catch((e) => { + console.debug("Failed to register the Copilot Context Provider with Copilot Chat."); + let message = "Failed to register the Copilot Context Provider with Copilot Chat"; + if (e instanceof CopilotContextProviderException) { + message += `: ${e.message} `; + } + return { message }; + }); + // The client usually doesn't block. So test it first. + clientPromise.then((clientResult) => { + const properties: Record = {}; + if (isBoolean(clientResult) && clientResult) { + properties["cppCodeSnippetsProviderRegistered"] = "true"; + telemetry.logCopilotEvent(registerCopilotContextProvider, { ...properties }); + return; + } + return chatPromise.then((chatResult) => { + const properties: Record = {}; + if (isBoolean(chatResult) && chatResult) { + properties["cppCodeSnippetsProviderRegistered"] = "true"; + telemetry.logCopilotEvent(registerCopilotContextProvider, { ...properties }); + return; + } else if (!isBoolean(clientResult) && isString(clientResult.message)) { + properties["error"] = clientResult.message; + } else if (!isBoolean(chatResult) && isString(chatResult.message)) { + properties["error"] = chatResult.message; + } else { + properties["error"] = "Failed to register the Copilot Context Provider for unknown reason."; + } + telemetry.logCopilotEvent(registerCopilotContextProvider, { ...properties }); + }); + }).catch((e) => { + const properties: Record = {}; + properties["error"] = `Failed to register the Copilot Context Provider with exception: ${e}`; + telemetry.logCopilotEvent(registerCopilotContextProvider, { ...properties }); + }); + } + + private async installContextProvider(copilotAPI: CopilotContextProviderAPI, 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; + } else { + return undefined; + } + } +} diff --git a/src/copilotHelper.ts b/src/copilotHelper.ts index 580f3e7d..f72e3276 100644 --- a/src/copilotHelper.ts +++ b/src/copilotHelper.ts @@ -4,6 +4,11 @@ import { Uri } from "vscode"; import { Jdtls } from "./java/jdtls"; +export interface INodeImportClass { + uri: string; + className: string; // Changed from 'class' to 'className' to match Java code +} + /** * Helper class for Copilot integration to analyze Java project dependencies */ @@ -14,9 +19,9 @@ export class CopilotHelper { * @param fileUri The URI of the Java file to analyze * @returns Array of strings in format "type:fully.qualified.name" where type is class|interface|enum|annotation */ - public static async resolveLocalImports(fileUri: Uri): Promise { + public static async resolveLocalImports(fileUri: Uri): Promise { try { - const result = await Jdtls.resolveCopilotRequest(fileUri.toString()); + const result = await Jdtls.getImportClassContent(fileUri.toString()); return result; } catch (error) { console.error("Error resolving copilot request:", error); @@ -25,74 +30,16 @@ export class CopilotHelper { } /** - * Gets local project types imported by the given file, categorized by type - * @param fileUri The URI of the Java file to analyze - * @returns Object with categorized types + * Get import class content for the given file URI + * @param fileUri The URI of the Java file as string + * @returns Array of import class information with URI and content */ - public static async getLocalImportsByType(fileUri: Uri): Promise<{ - classes: string[]; - interfaces: string[]; - enums: string[]; - annotations: string[]; - others: string[]; - }> { - const result = { - classes: [] as string[], - interfaces: [] as string[], - enums: [] as string[], - annotations: [] as string[], - others: [] as string[] - }; - + public static async getImportClassContent(fileUri: string): Promise { try { - const imports = await this.resolveLocalImports(fileUri); - - for (const importInfo of imports) { - const [type, typeName] = importInfo.split(":", 2); - if (!typeName) { - result.others.push(importInfo); - continue; - } - - switch (type) { - case "class": - result.classes.push(typeName); - break; - case "interface": - result.interfaces.push(typeName); - break; - case "enum": - result.enums.push(typeName); - break; - case "annotation": - result.annotations.push(typeName); - break; - default: - result.others.push(typeName); - break; - } - } - } catch (error) { - console.error("Error categorizing imports:", error); - } - - return result; - } - - /** - * Gets a simple list of fully qualified type names imported from local project - * @param fileUri The URI of the Java file to analyze - * @returns Array of fully qualified type names - */ - public static async getLocalImportTypeNames(fileUri: Uri): Promise { - try { - const imports = await this.resolveLocalImports(fileUri); - return imports.map(importInfo => { - const [, typeName] = importInfo.split(":", 2); - return typeName || importInfo; - }); + const result = await Jdtls.getImportClassContent(fileUri); + return result; } catch (error) { - console.error("Error getting type names:", error); + console.error("Error getting import class content:", error); return []; } } diff --git a/src/java/jdtls.ts b/src/java/jdtls.ts index 4d8fb733..3f593115 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 "../copilotHelper"; export namespace Jdtls { export async function getProjects(params: string): Promise { @@ -72,8 +73,8 @@ export namespace Jdtls { return await commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_GETMAINCLASSES, params) || []; } - export async function resolveCopilotRequest(fileUri: string): Promise { - return await commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_RESOLVE_COPILOT_REQUEST, fileUri) || []; + export async function getImportClassContent(fileUri: string): Promise { + return await commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_GETIMPORTCLASSCONTENT, fileUri) || []; } export async function exportJar(mainClass: string, classpaths: IClasspath[], From da03139d5b7bb6a97295ac12a34168baf0f790f7 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Mon, 15 Sep 2025 16:23:59 +0800 Subject: [PATCH 4/9] feat: update the treatment vars --- src/{ => ext}/ExperimentationService.ts | 0 src/ext/treatmentVariables.ts | 13 +++++++++++++ src/extension.ts | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) rename src/{ => ext}/ExperimentationService.ts (100%) create mode 100644 src/ext/treatmentVariables.ts diff --git a/src/ExperimentationService.ts b/src/ext/ExperimentationService.ts similarity index 100% rename from src/ExperimentationService.ts rename to src/ext/ExperimentationService.ts diff --git a/src/ext/treatmentVariables.ts b/src/ext/treatmentVariables.ts new file mode 100644 index 00000000..cf4f29bd --- /dev/null +++ b/src/ext/treatmentVariables.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export class TreatmentVariables { + public static readonly VSCodeConfig = "vscode"; + public static readonly ContextProvider = "contextProvider"; +} + +export class TreatmentVariableValue { + // If this is true, user will see a different display title/description + // for notification/command/workflow bot during scaffolding. + public static contextProvider: boolean | undefined = undefined; +} diff --git a/src/extension.ts b/src/extension.ts index 82e9a1b0..8c30d6c3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -11,7 +11,7 @@ import { BuildTaskProvider } from "./tasks/build/buildTaskProvider"; import { buildFiles, Context, ExtensionName } from "./constants"; import { LibraryController } from "./controllers/libraryController"; import { ProjectController } from "./controllers/projectController"; -import { init as initExpService } from "./ExperimentationService"; +import { init as initExpService } from "./ext/ExperimentationService"; import { DeprecatedExportJarTaskProvider, BuildArtifactTaskProvider } from "./tasks/buildArtifact/BuildArtifactTaskProvider"; import { Settings } from "./settings"; import { syncHandler } from "./syncHandler"; From 622ca1dc5b18f0f8c4bbb0c1f9a2fa0427689ab9 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Mon, 15 Sep 2025 17:01:11 +0800 Subject: [PATCH 5/9] feat: add context provider into extension --- src/copilot/contextProvider.ts | 66 +++++++++++++++++----------------- src/extension.ts | 13 ++++++- 2 files changed, 45 insertions(+), 34 deletions(-) diff --git a/src/copilot/contextProvider.ts b/src/copilot/contextProvider.ts index a2862272..fe1ca33d 100644 --- a/src/copilot/contextProvider.ts +++ b/src/copilot/contextProvider.ts @@ -35,41 +35,41 @@ export async function registerCopilotContextProviders( return; } - const provider: ContextProvider = { - id: 'vscjava.vscode-java-pack', // use extension id as provider id for now - selector: [{ language: "*" }], - resolver: { - resolve: async (request, token) => { - console.log('======== java request:', request); - console.log('======== java token:', token); - const items = await resolveJavaContext(request, token); - console.log('======== java context end ===========') - return items; - } - } - }; + // const provider: ContextProvider = { + // id: 'vscjava.vscode-java-pack', // use extension id as provider id for now + // selector: [{ language: "*" }], + // resolver: { + // resolve: async (request, token) => { + // console.log('======== java request:', request); + // console.log('======== java token:', token); + // const items = await resolveJavaContext(request, token); + // console.log('======== java context end ===========') + // return items; + // } + // } + // }; - let installCount = 0; - if (copilotClientApi) { - const disposable = await installContextProvider(copilotClientApi, provider); - if (disposable) { - context.subscriptions.push(disposable); - installCount++; - } - } - if (copilotChatApi) { - const disposable = await installContextProvider(copilotChatApi, provider); - if (disposable) { - context.subscriptions.push(disposable); - installCount++; - } - } + // let installCount = 0; + // if (copilotClientApi) { + // const disposable = await installContextProvider(copilotClientApi, provider); + // if (disposable) { + // context.subscriptions.push(disposable); + // installCount++; + // } + // } + // if (copilotChatApi) { + // const disposable = await installContextProvider(copilotChatApi, provider); + // if (disposable) { + // context.subscriptions.push(disposable); + // installCount++; + // } + // } - if (installCount === 0) { - console.log('Incompatible GitHub Copilot extension installed. Skip registration of Java context providers.'); - return; - } - console.log('Registration of Java context provider for GitHub Copilot extension succeeded.'); + // if (installCount === 0) { + // console.log('Incompatible GitHub Copilot extension installed. Skip registration of Java context providers.'); + // return; + // } + // console.log('Registration of Java context provider for GitHub Copilot extension succeeded.'); // Register the Java completion context provider const javaCompletionProvider = new JavaCopilotCompletionContextProvider(); diff --git a/src/extension.ts b/src/extension.ts index 8c30d6c3..b312cc3e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -11,7 +11,7 @@ import { BuildTaskProvider } from "./tasks/build/buildTaskProvider"; import { buildFiles, Context, ExtensionName } from "./constants"; import { LibraryController } from "./controllers/libraryController"; import { ProjectController } from "./controllers/projectController"; -import { init as initExpService } from "./ext/ExperimentationService"; +import { init as initExpService, getExpService } from "./ext/ExperimentationService"; import { DeprecatedExportJarTaskProvider, BuildArtifactTaskProvider } from "./tasks/buildArtifact/BuildArtifactTaskProvider"; import { Settings } from "./settings"; import { syncHandler } from "./syncHandler"; @@ -21,6 +21,8 @@ import { DiagnosticProvider } from "./tasks/buildArtifact/migration/DiagnosticPr import { setContextForDeprecatedTasks, updateExportTaskType } from "./tasks/buildArtifact/migration/utils"; import { CodeActionProvider } from "./tasks/buildArtifact/migration/CodeActionProvider"; import { newJavaFile } from "./explorerCommands/new"; +import { registerCopilotContextProviders } from "./copilot/contextProvider"; +import { TreatmentVariables, TreatmentVariableValue } from "./ext/treatmentVariables"; export async function activate(context: ExtensionContext): Promise { contextManager.initialize(context); @@ -36,6 +38,7 @@ export async function activate(context: ExtensionContext): Promise { } }); contextManager.setContextValue(Context.EXTENSION_ACTIVATED, true); + await registerContextProviders(context); } async function activateExtension(_operationId: string, context: ExtensionContext): Promise { @@ -151,3 +154,11 @@ function setContextForReloadProject(document: TextDocument | undefined): void { } contextManager.setContextValue(Context.RELOAD_PROJECT_ACTIVE, false); } + +async function registerContextProviders(context: ExtensionContext): Promise { + TreatmentVariableValue.contextProvider = await getExpService().getTreatmentVariableAsync(TreatmentVariables.VSCodeConfig, TreatmentVariables.ContextProvider, true); + // Register additional context providers here + if(TreatmentVariableValue.contextProvider) { + registerCopilotContextProviders(context); + } +} \ No newline at end of file From e11a1a72774d8d129dd3a7e761ce1a4a3ff2f552 Mon Sep 17 00:00:00 2001 From: wenyutang Date: Mon, 15 Sep 2025 22:10:05 +0800 Subject: [PATCH 6/9] chore: update --- src/copilot/contextProvider.ts | 185 +----- .../copilotCompletionContextProvider.ts | 538 ------------------ src/ext/ExperimentationService.ts | 2 +- src/ext/treatmentVariables.ts | 4 +- src/extension.ts | 13 +- 5 files changed, 30 insertions(+), 712 deletions(-) delete mode 100644 src/copilot/copilotCompletionContextProvider.ts diff --git a/src/copilot/contextProvider.ts b/src/copilot/contextProvider.ts index fe1ca33d..a1e7105c 100644 --- a/src/copilot/contextProvider.ts +++ b/src/copilot/contextProvider.ts @@ -10,6 +10,9 @@ import { } from '@github/copilot-language-server'; import * as vscode from 'vscode'; import { CopilotHelper } from '../copilotHelper'; +import { TreatmentVariables } from '../ext/treatmentVariables'; +import { getExpService } from '../ext/ExperimentationService'; +import { sendInfo } from "vscode-extension-telemetry-wrapper"; export enum NodeKind { Workspace = 1, @@ -27,6 +30,16 @@ export enum NodeKind { export async function registerCopilotContextProviders( context: vscode.ExtensionContext ) { + const contextProviderIsEnabled = await getExpService().getTreatmentVariableAsync(TreatmentVariables.VSCodeConfig, TreatmentVariables.ContextProvider, true); + if (!contextProviderIsEnabled) { + sendInfo("", { + "contextProviderEnabled": "false", + }); + return; + } + sendInfo("", { + "contextProviderEnabled": "true", + }); try { const copilotClientApi = await getCopilotClientApi(); const copilotChatApi = await getCopilotChatApi(); @@ -74,7 +87,7 @@ export async function registerCopilotContextProviders( // Register the Java completion context provider const javaCompletionProvider = new JavaCopilotCompletionContextProvider(); let completionProviderInstallCount = 0; - + if (copilotClientApi) { const disposable = await installContextProvider(copilotClientApi, javaCompletionProvider); if (disposable) { @@ -147,7 +160,7 @@ async function resolveJavaContext(_request: ResolveRequest, _token: vscode.Cance }); const importClass = await CopilotHelper.resolveLocalImports(document.uri); - for(const cls of importClass) { + for (const cls of importClass) { items.push({ uri: cls.uri, value: cls.className, @@ -251,194 +264,48 @@ export class JavaCopilotCompletionContextProvider implements ContextProvider(); private readonly cacheTimeout = 30000; // 30 seconds - + public async resolve(request: ResolveRequest, cancellationToken: vscode.CancellationToken): Promise { // Access document through request properties const docUri = request.documentContext?.uri?.toString(); const docOffset = request.documentContext?.offset; - + // Only process Java files if (!docUri || !docUri.endsWith('.java')) { return []; } - + const cacheKey = `${docUri}:${docOffset}`; const cached = this.cache.get(cacheKey); - + // Return cached result if still valid if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { return cached.context; } - + try { - const context = await this.generateJavaCompletionContext(docUri, docOffset, cancellationToken); - + const context = await resolveJavaContext(request, cancellationToken); + // Cache the result this.cache.set(cacheKey, { context, timestamp: Date.now() }); - + // Clean up old cache entries this.cleanCache(); - + return context; } catch (error) { console.error('Error generating Java completion context:', error); return []; } } - - private async generateJavaCompletionContext( - docUri: string, - docOffset: number | undefined, - cancellationToken: vscode.CancellationToken - ): Promise { - const context: SupportedContextItem[] = []; - - try { - // Check for cancellation - if (cancellationToken.isCancellationRequested) { - return []; - } - - // Get the document - const document = await vscode.workspace.openTextDocument(vscode.Uri.parse(docUri)); - if (!document) { - return []; - } - - // Get import class information from the Java project - const importClassInfo = await CopilotHelper.getImportClassContent(docUri); - - if (importClassInfo && importClassInfo.length > 0) { - // Convert import class information to context items - for (const classInfo of importClassInfo) { - context.push({ - name: `java.class.${this.extractClassName(classInfo.className)}`, - value: this.formatClassContext(classInfo.className), - importance: 75, - id: `java-class-${classInfo.uri}`, - origin: 'request' - }); - } - } - - // Get related project context - const projectContext = await this.getProjectContext(document); - context.push(...projectContext); - - // Get current file context (surrounding methods, classes) - const fileContext = await this.getCurrentFileContext(document, docOffset); - context.push(...fileContext); - - } catch (error) { - console.error('Error in generateJavaCompletionContext:', error); - } - - return context; - } - - private async getProjectContext(document: vscode.TextDocument): Promise { - const context: SupportedContextItem[] = []; - - try { - // Get local imports for better context - const localImports = await CopilotHelper.resolveLocalImports(document.uri); - - if (localImports) { - for (const importInfo of localImports) { - context.push({ - name: `java.import.${importInfo.className}`, - value: this.formatImportContext(importInfo.className), - importance: 60, - id: `java-import-${importInfo.uri}`, - origin: 'request' - }); - } - } - - // Get package information - const packageName = await getPackageName(document); - context.push({ - name: 'java.package', - value: packageName, - importance: 85, - id: 'java-package-context', - origin: 'request' - }); - - } catch (error) { - console.error('Error getting project context:', error); - } - - return context; - } - - private async getCurrentFileContext( - document: vscode.TextDocument, - docOffset: number | undefined - ): Promise { - const context: SupportedContextItem[] = []; - - try { - const text = document.getText(); - const lines = text.split('\n'); - - // Calculate current line from offset if provided - let currentLine = 0; - if (docOffset !== undefined) { - const textUpToOffset = text.substring(0, docOffset); - currentLine = textUpToOffset.split('\n').length - 1; - } - - // Get surrounding context (methods, classes around cursor) - const contextRange = this.getContextRange(lines, currentLine); - const contextContent = lines.slice(contextRange.start, contextRange.end).join('\n'); - - if (contextContent.trim()) { - context.push({ - name: 'java.current.file.context', - value: contextContent, - importance: 70, - id: 'java-current-file-context', - origin: 'request' - }); - } - } catch (error) { - console.error('Error getting current file context:', error); - } - - return context; - } - - private getContextRange(lines: string[], currentLine: number): { start: number; end: number } { - const contextLines = 20; // Lines of context to include - const start = Math.max(0, currentLine - contextLines); - const end = Math.min(lines.length, currentLine + contextLines); - - return { start, end }; - } - - private formatClassContext(className: string): string { - // Format class name for better Copilot understanding - return `// Related class: ${className}`; - } - - private formatImportContext(importName: string): string { - return `// Related import: ${importName}`; - } - - private extractClassName(className: string): string { - // Extract simple class name from fully qualified name - const parts = className.split('.'); - return parts[parts.length - 1] || 'Unknown'; - } - + private cleanCache(): void { const now = Date.now(); for (const [key, value] of this.cache.entries()) { diff --git a/src/copilot/copilotCompletionContextProvider.ts b/src/copilot/copilotCompletionContextProvider.ts deleted file mode 100644 index 4452d873..00000000 --- a/src/copilot/copilotCompletionContextProvider.ts +++ /dev/null @@ -1,538 +0,0 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All Rights Reserved. - * See 'LICENSE' in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -import { ContextResolver, ResolveRequest, SupportedContextItem, type ContextProvider } from '@github/copilot-language-server'; -import { randomUUID } from 'crypto'; -import * as vscode from 'vscode'; -import { DocumentSelector } from 'vscode-languageserver-protocol'; -import { isBoolean, isNumber, isString } from '../common'; -import { getOutputChannelLogger, Logger } from '../logger'; -import * as telemetry from '../telemetry'; -import { CopilotCompletionContextResult } from './client'; -import { CopilotCompletionContextTelemetry } from './copilotCompletionContextTelemetry'; -import { getCopilotChatApi, getCopilotClientApi, type CopilotContextProviderAPI } from './copilotProviders'; -import { clients } from './extension'; -import { CppSettings } from './settings'; - -class DefaultValueFallback extends Error { - static readonly DefaultValue = "DefaultValue"; - constructor() { super(DefaultValueFallback.DefaultValue); } -} - -class CancellationError extends Error { - static readonly Canceled = "Canceled"; - constructor() { - super(CancellationError.Canceled); - this.name = this.message; - } -} - -class InternalCancellationError extends CancellationError { -} - -class CopilotCancellationError extends CancellationError { -} - -class CopilotContextProviderException extends Error { -} - -class WellKnownErrors extends Error { - static readonly ClientNotFound = "ClientNotFound"; - private constructor(message: string) { super(message); } - public static clientNotFound(): Error { - return new WellKnownErrors(WellKnownErrors.ClientNotFound); - } -} - -// A bit mask for enabling features in the completion context. -export enum CopilotCompletionContextFeatures { - None = 0, - Instant = 1, - Deferred = 2, -} - -// Mutually exclusive values for the kind of returned completion context. They either are: -// - computed. -// - obtained from the cache. -// - available in cache but stale (e.g. actual context is far away). -// - missing since the computation took too long and no cache is present (cache miss). The value -// is asynchronously computed and stored in cache. -// - the token is signaled as cancelled, in which case all the operations are aborted. -// - an unknown state. -export enum CopilotCompletionKind { - Computed = 'computed', - GotFromCache = 'gotFromCacheHit', - StaleCacheHit = 'staleCacheHit', - MissingCacheMiss = 'missingCacheMiss', - Canceled = 'canceled', - Unknown = 'unknown' -} - -type CacheEntry = [string, CopilotCompletionContextResult]; - -export class CopilotCompletionContextProvider implements ContextResolver { - private static readonly providerId = 'ms-vscode.cpptools'; - private readonly completionContextCache: Map = new Map(); - private static readonly defaultCppDocumentSelector: DocumentSelector = [{ language: 'cpp' }, { language: 'c' }, { language: 'cuda-cpp' }]; - // The default time budget for providing a value from resolve(). - private static readonly defaultTimeBudgetMs: number = 7; - // Assume the cache is stale when the distance to the current caret is greater than this value. - private static readonly defaultMaxCaretDistance = 8192; - private static readonly defaultMaxSnippetCount = 7; - private static readonly defaultMaxSnippetLength = 3 * 1024; - private static readonly defaultDoAggregateSnippets = true; - private completionContextCancellation = new vscode.CancellationTokenSource(); - private contextProviderDisposables: vscode.Disposable[] | undefined; - static readonly CppContextProviderEnabledFeatures = 'enabledFeatures'; - static readonly CppContextProviderTimeBudgetMs = 'timeBudgetMs'; - static readonly CppContextProviderMaxSnippetCount = 'maxSnippetCount'; - static readonly CppContextProviderMaxSnippetLength = 'maxSnippetLength'; - static readonly CppContextProviderMaxDistanceToCaret = 'maxDistanceToCaret'; - static readonly CppContextProviderDoAggregateSnippets = 'doAggregateSnippets'; - - constructor(private readonly logger: Logger) { - } - - private async waitForCompletionWithTimeoutAndCancellation(promise: Promise, defaultValue: T | undefined, - timeout: number, copilotToken: vscode.CancellationToken): Promise<[T | undefined, CopilotCompletionKind]> { - const defaultValuePromise = new Promise((_resolve, reject) => setTimeout(() => { - if (copilotToken.isCancellationRequested) { - reject(new CancellationError()); - } else { - reject(new DefaultValueFallback()); - } - }, timeout)); - const cancellationPromise = new Promise((_, reject) => { - copilotToken.onCancellationRequested(() => { - reject(new CancellationError()); - }); - }); - let snippetsOrNothing: T | undefined; - try { - snippetsOrNothing = await Promise.race([promise, cancellationPromise, defaultValuePromise]); - } catch (e) { - if (e instanceof DefaultValueFallback) { - return [defaultValue, defaultValue !== undefined ? CopilotCompletionKind.GotFromCache : CopilotCompletionKind.MissingCacheMiss]; - } else if (e instanceof CancellationError) { - return [undefined, CopilotCompletionKind.Canceled]; - } else { - throw e; - } - } - - return [snippetsOrNothing, CopilotCompletionKind.Computed]; - } - - private static normalizeFeatureFlag(featureFlag: CopilotCompletionContextFeatures): CopilotCompletionContextFeatures { - // eslint-disable-next-line no-bitwise - if ((featureFlag & CopilotCompletionContextFeatures.Instant) === CopilotCompletionContextFeatures.Instant) { return CopilotCompletionContextFeatures.Instant; } - // eslint-disable-next-line no-bitwise - if ((featureFlag & CopilotCompletionContextFeatures.Deferred) === CopilotCompletionContextFeatures.Deferred) { return CopilotCompletionContextFeatures.Deferred; } - return CopilotCompletionContextFeatures.None; - } - - // Get the completion context with a timeout and a cancellation token. - // The cancellationToken indicates that the value should not be returned nor cached. - private async getCompletionContextWithCancellation(context: ResolveRequest, featureFlag: CopilotCompletionContextFeatures, - maxSnippetCount: number, maxSnippetLength: number, doAggregateSnippets: boolean, startTime: number, telemetry: CopilotCompletionContextTelemetry, - internalToken: vscode.CancellationToken): - Promise { - const documentUri = context.documentContext.uri; - const caretOffset = context.documentContext.offset; - let logMessage = `Copilot: getCompletionContext(${documentUri}:${caretOffset}):`; - try { - const snippetsFeatureFlag = CopilotCompletionContextProvider.normalizeFeatureFlag(featureFlag); - telemetry.addRequestMetadata(documentUri, caretOffset, context.completionId, - context.documentContext.languageId, { featureFlag: snippetsFeatureFlag }); - const docUri = vscode.Uri.parse(documentUri); - const getClientForTime = performance.now(); - const client = clients.getClientFor(docUri); - const getClientForDuration = CopilotCompletionContextProvider.getRoundedDuration(getClientForTime); - telemetry.addGetClientForElapsed(getClientForDuration); - if (!client) { throw WellKnownErrors.clientNotFound(); } - const getCompletionContextStartTime = performance.now(); - const copilotCompletionContext: CopilotCompletionContextResult = - await client.getCompletionContext(docUri, caretOffset, snippetsFeatureFlag, maxSnippetCount, maxSnippetLength, doAggregateSnippets, internalToken); - telemetry.addRequestId(copilotCompletionContext.requestId); - logMessage += `(id: ${copilotCompletionContext.requestId})(getClientFor elapsed:${getClientForDuration}ms)`; - if (!copilotCompletionContext.areSnippetsMissing) { - const resultMismatch = copilotCompletionContext.sourceFileUri !== docUri.toString(); - if (resultMismatch) { logMessage += `(mismatch TU vs result)`; } - } - const cacheEntryId = randomUUID().toString(); - this.completionContextCache.set(copilotCompletionContext.sourceFileUri, [cacheEntryId, copilotCompletionContext]); - const duration = CopilotCompletionContextProvider.getRoundedDuration(startTime); - telemetry.addCacheComputedData(duration, cacheEntryId); - logMessage += ` cached in ${duration}ms ${copilotCompletionContext.traits.length} trait(s)`; - if (copilotCompletionContext.areSnippetsMissing) { logMessage += `(missing code snippets)`; } - else { - logMessage += ` and ${copilotCompletionContext.snippets.length} snippet(s)`; - logMessage += `(response.featureFlag:${copilotCompletionContext.featureFlag})`; - logMessage += `(response.uri:${copilotCompletionContext.sourceFileUri || ""}:${copilotCompletionContext.caretOffset})`; - } - - telemetry.addResponseMetadata(copilotCompletionContext.areSnippetsMissing, copilotCompletionContext.snippets.length, - copilotCompletionContext.traits.length, copilotCompletionContext.caretOffset, copilotCompletionContext.featureFlag); - telemetry.addComputeContextElapsed(CopilotCompletionContextProvider.getRoundedDuration(getCompletionContextStartTime)); - - return copilotCompletionContext; - } catch (e: any) { - if (e instanceof vscode.CancellationError || e.message === CancellationError.Canceled) { - telemetry.addInternalCanceled(CopilotCompletionContextProvider.getRoundedDuration(startTime)); - logMessage += `(internal cancellation)`; - throw InternalCancellationError; - } - - if (e instanceof WellKnownErrors) { - telemetry.addWellKnownError(e.message); - } - - telemetry.addError(); - this.logger.appendLineAtLevel(7, `Copilot: getCompletionContextWithCancellation(${documentUri}: ${caretOffset}): Error: '${e}'`); - return undefined; - } finally { - this.logger. - appendLineAtLevel(7, `[${new Date().toISOString().replace('T', ' ').replace('Z', '')}] ${logMessage}`); - telemetry.send("cache"); - } - } - - static readonly paramsCache: Record = {}; - static paramsCacheCreated = false; - private getContextProviderParam(paramName: string): T | undefined { - try { - if (!CopilotCompletionContextProvider.paramsCacheCreated) { - CopilotCompletionContextProvider.paramsCacheCreated = true; - const paramsJson = new CppSettings().cppContextProviderParams; - if (isString(paramsJson)) { - try { - const params = JSON.parse(paramsJson.replaceAll(/'/g, '"')); - for (const key in params) { - CopilotCompletionContextProvider.paramsCache[key] = params[key]; - } - } catch (e) { - console.warn(`getContextProviderParam(): error parsing getContextProviderParam: `, e); - } - } - } - return CopilotCompletionContextProvider.paramsCache[paramName] as T; - } catch (e) { - console.warn(`getContextProviderParam(): error fetching getContextProviderParam: `, e); - return undefined; - } - } - - private async fetchTimeBudgetMs(context: ResolveRequest): Promise { - try { - const timeBudgetMs = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderTimeBudgetMs) ?? - context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderTimeBudgetMs); - return isNumber(timeBudgetMs) ? timeBudgetMs : CopilotCompletionContextProvider.defaultTimeBudgetMs; - } catch (e) { - console.warn(`fetchTimeBudgetMs(): error fetching ${CopilotCompletionContextProvider.CppContextProviderTimeBudgetMs}, using default: `, e); - return CopilotCompletionContextProvider.defaultTimeBudgetMs; - } - } - - private async fetchMaxDistanceToCaret(context: ResolveRequest): Promise { - try { - const maxDistance = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderMaxDistanceToCaret) ?? - context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderMaxDistanceToCaret); - return isNumber(maxDistance) ? maxDistance : CopilotCompletionContextProvider.defaultMaxCaretDistance; - } catch (e) { - console.warn(`fetchMaxDistanceToCaret(): error fetching ${CopilotCompletionContextProvider.CppContextProviderMaxDistanceToCaret}, using default: `, e); - return CopilotCompletionContextProvider.defaultMaxCaretDistance; - } - } - - private async fetchMaxSnippetCount(context: ResolveRequest): Promise { - try { - const maxSnippetCount = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderMaxSnippetCount) ?? - context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderMaxSnippetCount); - return isNumber(maxSnippetCount) ? maxSnippetCount : CopilotCompletionContextProvider.defaultMaxSnippetCount; - } catch (e) { - console.warn(`fetchMaxSnippetCount(): error fetching ${CopilotCompletionContextProvider.defaultMaxSnippetCount}, using default: `, e); - return CopilotCompletionContextProvider.defaultMaxSnippetCount; - } - } - - private async fetchMaxSnippetLength(context: ResolveRequest): Promise { - try { - const maxSnippetLength = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderMaxSnippetLength) ?? - context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderMaxSnippetLength); - return isNumber(maxSnippetLength) ? maxSnippetLength : CopilotCompletionContextProvider.defaultMaxSnippetLength; - } catch (e) { - console.warn(`fetchMaxSnippetLength(): error fetching ${CopilotCompletionContextProvider.defaultMaxSnippetLength}, using default: `, e); - return CopilotCompletionContextProvider.defaultMaxSnippetLength; - } - } - - private async fetchDoAggregateSnippets(context: ResolveRequest): Promise { - try { - const doAggregateSnippets = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderDoAggregateSnippets) ?? - context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderDoAggregateSnippets); - return isBoolean(doAggregateSnippets) ? doAggregateSnippets : CopilotCompletionContextProvider.defaultDoAggregateSnippets; - } catch (e) { - console.warn(`fetchDoAggregateSnippets(): error fetching ${CopilotCompletionContextProvider.defaultDoAggregateSnippets}, using default: `, e); - return CopilotCompletionContextProvider.defaultDoAggregateSnippets; - } - } - - private async getEnabledFeatureNames(context: ResolveRequest): Promise { - try { - const enabledFeatureNames = this.getContextProviderParam(CopilotCompletionContextProvider.CppContextProviderEnabledFeatures) ?? - context.activeExperiments.get(CopilotCompletionContextProvider.CppContextProviderEnabledFeatures); - if (isString(enabledFeatureNames)) { - return enabledFeatureNames.split(',').map(s => s.trim()); - } - } catch (e) { - console.warn(`getEnabledFeatureNames(): error fetching ${CopilotCompletionContextProvider.CppContextProviderEnabledFeatures}: `, e); - } - return undefined; - } - - private async getEnabledFeatureFlag(context: ResolveRequest): Promise { - let result; - for (const featureName of await this.getEnabledFeatureNames(context) ?? []) { - const flag = CopilotCompletionContextFeatures[featureName as keyof typeof CopilotCompletionContextFeatures]; - if (flag !== undefined) { result = (result ?? 0) + flag; } - } - return result; - } - - private static getRoundedDuration(startTime: number): number { - return Math.round(performance.now() - startTime); - } - - public static Create() { - const copilotCompletionProvider = new CopilotCompletionContextProvider(getOutputChannelLogger()); - copilotCompletionProvider.registerCopilotContextProvider(); - return copilotCompletionProvider; - } - - public dispose(): void { - this.completionContextCancellation.cancel(); - if (this.contextProviderDisposables) { - for (const disposable of this.contextProviderDisposables) { - disposable.dispose(); - } - this.contextProviderDisposables = undefined; - } - } - - public removeFile(fileUri: string): void { - this.completionContextCache.delete(fileUri); - } - - private computeSnippetsResolved: boolean = true; - - private async resolveResultAndKind(context: ResolveRequest, featureFlag: CopilotCompletionContextFeatures, - telemetry: CopilotCompletionContextTelemetry, defaultValue: CopilotCompletionContextResult | undefined, - resolveStartTime: number, timeBudgetMs: number, maxSnippetCount: number, maxSnippetLength: number, doAggregateSnippets: boolean, - copilotCancel: vscode.CancellationToken): Promise<[CopilotCompletionContextResult | undefined, CopilotCompletionKind]> { - if (this.computeSnippetsResolved) { - this.computeSnippetsResolved = false; - const computeSnippetsPromise = this.getCompletionContextWithCancellation(context, featureFlag, - maxSnippetCount, maxSnippetLength, doAggregateSnippets, resolveStartTime, telemetry.fork(), this.completionContextCancellation.token).finally( - () => this.computeSnippetsResolved = true - ); - const res = await this.waitForCompletionWithTimeoutAndCancellation( - computeSnippetsPromise, defaultValue, timeBudgetMs, copilotCancel); - return res; - } else { return [defaultValue, defaultValue ? CopilotCompletionKind.GotFromCache : CopilotCompletionKind.MissingCacheMiss]; } - } - - private static isStaleCacheHit(caretOffset: number, cacheCaretOffset: number, maxCaretDistance: number): boolean { - return Math.abs(caretOffset - caretOffset) > maxCaretDistance; - } - - private static createContextItems(copilotCompletionContext: CopilotCompletionContextResult | undefined): SupportedContextItem[] { - return [...copilotCompletionContext?.snippets ?? [], ...copilotCompletionContext?.traits ?? []] as SupportedContextItem[]; - } - - public async resolve(context: ResolveRequest, copilotCancel: vscode.CancellationToken): Promise { - const proposedEdits = context.documentContext.proposedEdits; - const resolveStartTime = performance.now(); - let logMessage = `Copilot: resolve(${context.documentContext.uri}:${context.documentContext.offset}):`; - const cppTimeBudgetMs = await this.fetchTimeBudgetMs(context); - const maxCaretDistance = await this.fetchMaxDistanceToCaret(context); - const maxSnippetCount = await this.fetchMaxSnippetCount(context); - const maxSnippetLength = await this.fetchMaxSnippetLength(context); - const doAggregateSnippets = await this.fetchDoAggregateSnippets(context); - const telemetry = new CopilotCompletionContextTelemetry(); - let copilotCompletionContext: CopilotCompletionContextResult | undefined; - let copilotCompletionContextKind: CopilotCompletionKind = CopilotCompletionKind.Unknown; - let featureFlag: CopilotCompletionContextFeatures | undefined; - const docUri = context.documentContext.uri; - const docOffset = context.documentContext.offset; - try { - featureFlag = await this.getEnabledFeatureFlag(context); - telemetry.addRequestMetadata(context.documentContext.uri, context.documentContext.offset, - context.completionId, context.documentContext.languageId, { - featureFlag, timeBudgetMs: cppTimeBudgetMs, maxCaretDistance, - maxSnippetCount, maxSnippetLength, doAggregateSnippets - }); - if (featureFlag === undefined) { return []; } - const cacheEntry: CacheEntry | undefined = this.completionContextCache.get(docUri.toString()); - if (proposedEdits) { - const defaultValue = cacheEntry?.[1]; - const isStaleCache = defaultValue !== undefined ? CopilotCompletionContextProvider.isStaleCacheHit(docOffset, defaultValue.caretOffset, maxCaretDistance) : true; - const contextItems = isStaleCache ? [] : CopilotCompletionContextProvider.createContextItems(defaultValue); - copilotCompletionContext = isStaleCache ? undefined : defaultValue; - copilotCompletionContextKind = isStaleCache ? CopilotCompletionKind.StaleCacheHit : CopilotCompletionKind.GotFromCache; - telemetry.addSpeculativeRequestMetadata(proposedEdits.length); - if (cacheEntry?.[0]) { - telemetry.addCacheHitEntryGuid(cacheEntry[0]); - } - return contextItems; - } - const [resultContext, resultKind] = await this.resolveResultAndKind(context, featureFlag, - telemetry.fork(), cacheEntry?.[1], resolveStartTime, cppTimeBudgetMs, maxSnippetCount, maxSnippetLength, doAggregateSnippets, copilotCancel); - copilotCompletionContext = resultContext; - copilotCompletionContextKind = resultKind; - logMessage += `(id: ${copilotCompletionContext?.requestId})`; - // Fix up copilotCompletionContextKind accounting for stale-cache-hits. - if (copilotCompletionContextKind === CopilotCompletionKind.GotFromCache && - copilotCompletionContext && cacheEntry) { - telemetry.addCacheHitEntryGuid(cacheEntry[0]); - const cachedData = cacheEntry[1]; - if (CopilotCompletionContextProvider.isStaleCacheHit(docOffset, cachedData.caretOffset, maxCaretDistance)) { - copilotCompletionContextKind = CopilotCompletionKind.StaleCacheHit; - copilotCompletionContext.snippets = []; - } - } - // Handle cancellation. - if (copilotCompletionContextKind === CopilotCompletionKind.Canceled) { - const duration: number = CopilotCompletionContextProvider.getRoundedDuration(resolveStartTime); - telemetry.addCopilotCanceled(duration); - throw new CopilotCancellationError(); - } - return CopilotCompletionContextProvider.createContextItems(copilotCompletionContext); - } catch (e: any) { - if (e instanceof CopilotCancellationError) { - telemetry.addCopilotCanceled(CopilotCompletionContextProvider.getRoundedDuration(resolveStartTime)); - logMessage += `(copilot cancellation)`; - throw e; - } - if (e instanceof InternalCancellationError) { - telemetry.addInternalCanceled(CopilotCompletionContextProvider.getRoundedDuration(resolveStartTime)); - logMessage += `(internal cancellation)`; - throw e; - } - if (e instanceof CancellationError) { throw e; } - - // For any other exception's type, it is an error. - telemetry.addError(); - throw e; - } finally { - const duration: number = CopilotCompletionContextProvider.getRoundedDuration(resolveStartTime); - logMessage += `(featureFlag:${featureFlag?.toString()})`; - if (proposedEdits) { logMessage += `(speculative request, proposedEdits:${proposedEdits.length})`; } - if (copilotCompletionContext === undefined) { - logMessage += `result is undefined and no code snippets provided(${copilotCompletionContextKind.toString()}), elapsed time:${duration} ms`; - } else { - logMessage += `for ${docUri}:${docOffset} provided ${copilotCompletionContext.snippets.length} code snippet(s)(${copilotCompletionContextKind.toString()}\ -${copilotCompletionContext?.areSnippetsMissing ? "(missing code snippets)" : ""}) and ${copilotCompletionContext.traits.length} trait(s), elapsed time:${duration} ms`; - } - telemetry.addCompletionContextKind(copilotCompletionContextKind); - telemetry.addResponseMetadata(copilotCompletionContext?.areSnippetsMissing ?? true, - copilotCompletionContext?.snippets.length, copilotCompletionContext?.traits.length, - copilotCompletionContext?.caretOffset, copilotCompletionContext?.featureFlag); - telemetry.addResolvedElapsed(duration); - telemetry.addCacheSize(this.completionContextCache.size); - telemetry.send(); - this.logger.appendLineAtLevel(7, `[${new Date().toISOString().replace('T', ' ').replace('Z', '')}] ${logMessage}`); - } - } - - public registerCopilotContextProvider(): void { - const registerCopilotContextProvider = 'registerCopilotContextProvider'; - const contextProvider = { - id: CopilotCompletionContextProvider.providerId, - selector: CopilotCompletionContextProvider.defaultCppDocumentSelector, - resolver: this - }; - type RegistrationResult = { message: string } | boolean; - const clientPromise: Promise = getCopilotClientApi().then(async (api) => { - if (!api) { - throw new CopilotContextProviderException("getCopilotApi() returned null, Copilot client is missing or inactive."); - } - const disposable = await this.installContextProvider(api, contextProvider); - if (disposable) { - this.contextProviderDisposables = this.contextProviderDisposables ?? []; - this.contextProviderDisposables.push(disposable); - return true; - } else { - throw new CopilotContextProviderException("getContextProviderAPI() is not available in Copilot client."); - } - }).catch((e) => { - console.debug("Failed to register the Copilot Context Provider with Copilot client."); - let message = "Failed to register the Copilot Context Provider with Copilot client"; - if (e instanceof CopilotContextProviderException) { - message += `: ${e.message} `; - } - return { message }; - }); - const chatPromise: Promise = getCopilotChatApi().then(async (api) => { - if (!api) { - throw new CopilotContextProviderException("getCopilotChatApi() returned null, Copilot Chat is missing or inactive."); - } - const disposable = await this.installContextProvider(api, contextProvider); - if (disposable) { - this.contextProviderDisposables = this.contextProviderDisposables ?? []; - this.contextProviderDisposables.push(disposable); - return true; - } else { - throw new CopilotContextProviderException("getContextProviderAPI() is not available in Copilot Chat."); - } - }).catch((e) => { - console.debug("Failed to register the Copilot Context Provider with Copilot Chat."); - let message = "Failed to register the Copilot Context Provider with Copilot Chat"; - if (e instanceof CopilotContextProviderException) { - message += `: ${e.message} `; - } - return { message }; - }); - // The client usually doesn't block. So test it first. - clientPromise.then((clientResult) => { - const properties: Record = {}; - if (isBoolean(clientResult) && clientResult) { - properties["cppCodeSnippetsProviderRegistered"] = "true"; - telemetry.logCopilotEvent(registerCopilotContextProvider, { ...properties }); - return; - } - return chatPromise.then((chatResult) => { - const properties: Record = {}; - if (isBoolean(chatResult) && chatResult) { - properties["cppCodeSnippetsProviderRegistered"] = "true"; - telemetry.logCopilotEvent(registerCopilotContextProvider, { ...properties }); - return; - } else if (!isBoolean(clientResult) && isString(clientResult.message)) { - properties["error"] = clientResult.message; - } else if (!isBoolean(chatResult) && isString(chatResult.message)) { - properties["error"] = chatResult.message; - } else { - properties["error"] = "Failed to register the Copilot Context Provider for unknown reason."; - } - telemetry.logCopilotEvent(registerCopilotContextProvider, { ...properties }); - }); - }).catch((e) => { - const properties: Record = {}; - properties["error"] = `Failed to register the Copilot Context Provider with exception: ${e}`; - telemetry.logCopilotEvent(registerCopilotContextProvider, { ...properties }); - }); - } - - private async installContextProvider(copilotAPI: CopilotContextProviderAPI, 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; - } else { - return undefined; - } - } -} diff --git a/src/ext/ExperimentationService.ts b/src/ext/ExperimentationService.ts index a76741ea..d7dc3812 100644 --- a/src/ext/ExperimentationService.ts +++ b/src/ext/ExperimentationService.ts @@ -28,7 +28,7 @@ export function getExpService() { } export async function init(context: vscode.ExtensionContext): Promise { - const packageJson: {[key: string]: any} = require("../package.json"); + const packageJson: {[key: string]: any} = require("../../package.json"); // tslint:disable: no-string-literal const extensionName = `${packageJson["publisher"]}.${packageJson["name"]}`; const extensionVersion = packageJson["version"]; diff --git a/src/ext/treatmentVariables.ts b/src/ext/treatmentVariables.ts index cf4f29bd..05bec365 100644 --- a/src/ext/treatmentVariables.ts +++ b/src/ext/treatmentVariables.ts @@ -3,11 +3,9 @@ export class TreatmentVariables { public static readonly VSCodeConfig = "vscode"; - public static readonly ContextProvider = "contextProvider"; + public static readonly ContextProvider = "ContextProviderIsEnabled"; } export class TreatmentVariableValue { - // If this is true, user will see a different display title/description - // for notification/command/workflow bot during scaffolding. public static contextProvider: boolean | undefined = undefined; } diff --git a/src/extension.ts b/src/extension.ts index b312cc3e..9f06aafc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -11,7 +11,7 @@ import { BuildTaskProvider } from "./tasks/build/buildTaskProvider"; import { buildFiles, Context, ExtensionName } from "./constants"; import { LibraryController } from "./controllers/libraryController"; import { ProjectController } from "./controllers/projectController"; -import { init as initExpService, getExpService } from "./ext/ExperimentationService"; +import { init as initExpService } from "./ext/ExperimentationService"; import { DeprecatedExportJarTaskProvider, BuildArtifactTaskProvider } from "./tasks/buildArtifact/BuildArtifactTaskProvider"; import { Settings } from "./settings"; import { syncHandler } from "./syncHandler"; @@ -22,7 +22,6 @@ import { setContextForDeprecatedTasks, updateExportTaskType } from "./tasks/buil import { CodeActionProvider } from "./tasks/buildArtifact/migration/CodeActionProvider"; import { newJavaFile } from "./explorerCommands/new"; import { registerCopilotContextProviders } from "./copilot/contextProvider"; -import { TreatmentVariables, TreatmentVariableValue } from "./ext/treatmentVariables"; export async function activate(context: ExtensionContext): Promise { contextManager.initialize(context); @@ -38,7 +37,7 @@ export async function activate(context: ExtensionContext): Promise { } }); contextManager.setContextValue(Context.EXTENSION_ACTIVATED, true); - await registerContextProviders(context); + await registerCopilotContextProviders(context); } async function activateExtension(_operationId: string, context: ExtensionContext): Promise { @@ -153,12 +152,4 @@ function setContextForReloadProject(document: TextDocument | undefined): void { } } contextManager.setContextValue(Context.RELOAD_PROJECT_ACTIVE, false); -} - -async function registerContextProviders(context: ExtensionContext): Promise { - TreatmentVariableValue.contextProvider = await getExpService().getTreatmentVariableAsync(TreatmentVariables.VSCodeConfig, TreatmentVariables.ContextProvider, true); - // Register additional context providers here - if(TreatmentVariableValue.contextProvider) { - registerCopilotContextProviders(context); - } } \ No newline at end of file From bc8eba7201e34b9e4d151652025c4e7599b0bf6a Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Tue, 16 Sep 2025 10:42:02 +0800 Subject: [PATCH 7/9] feat: update --- src/copilot/contextProvider.ts | 37 ---------------------------------- src/extension.ts | 27 ------------------------- 2 files changed, 64 deletions(-) diff --git a/src/copilot/contextProvider.ts b/src/copilot/contextProvider.ts index a1e7105c..15784f9d 100644 --- a/src/copilot/contextProvider.ts +++ b/src/copilot/contextProvider.ts @@ -47,43 +47,6 @@ export async function registerCopilotContextProviders( console.log('Failed to find compatible version of GitHub Copilot extension installed. Skip registration of Copilot context provider.'); return; } - - // const provider: ContextProvider = { - // id: 'vscjava.vscode-java-pack', // use extension id as provider id for now - // selector: [{ language: "*" }], - // resolver: { - // resolve: async (request, token) => { - // console.log('======== java request:', request); - // console.log('======== java token:', token); - // const items = await resolveJavaContext(request, token); - // console.log('======== java context end ===========') - // return items; - // } - // } - // }; - - // let installCount = 0; - // if (copilotClientApi) { - // const disposable = await installContextProvider(copilotClientApi, provider); - // if (disposable) { - // context.subscriptions.push(disposable); - // installCount++; - // } - // } - // if (copilotChatApi) { - // const disposable = await installContextProvider(copilotChatApi, provider); - // if (disposable) { - // context.subscriptions.push(disposable); - // installCount++; - // } - // } - - // if (installCount === 0) { - // console.log('Incompatible GitHub Copilot extension installed. Skip registration of Java context providers.'); - // return; - // } - // console.log('Registration of Java context provider for GitHub Copilot extension succeeded.'); - // Register the Java completion context provider const javaCompletionProvider = new JavaCopilotCompletionContextProvider(); let completionProviderInstallCount = 0; diff --git a/src/extension.ts b/src/extension.ts index 9f06aafc..0a98c490 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -52,33 +52,6 @@ async function activateExtension(_operationId: string, context: ExtensionContext context.subscriptions.push(tasks.registerTaskProvider(BuildTaskProvider.type, new BuildTaskProvider())); context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.VIEW_MENUS_FILE_NEW_JAVA_CLASS, newJavaFile)); - // Add getSymbolsFromFile command - context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.GET_SYMBOLS_FROM_FILE, async () => { - const activeEditor = window.activeTextEditor; - if (!activeEditor) { - window.showWarningMessage("No active editor found. Please open a Java file first."); - return; - } - - const document = activeEditor.document; - if (!document.fileName.endsWith('.java')) { - window.showWarningMessage("Please open a Java file to get symbols."); - return; - } - - try { - const symbols = await CopilotHelper.resolveLocalImports(document.uri); - console.log("=== Local Symbols from Current File ==="); - console.log(`File: ${document.fileName}`); - console.log(`Total symbols found: ${symbols.length}`); - - window.showInformationMessage(`Found ${symbols.length} local symbols. Check console for details.`); - } catch (error) { - console.error("Error getting symbols:", error); - window.showErrorMessage(`Error getting symbols: ${error}`); - } - })); - context.subscriptions.push(window.onDidChangeActiveTextEditor((e: TextEditor | undefined) => { setContextForReloadProject(e?.document); })); From 418456660b3c11d2a9667edcfbec5f03ce81e60e Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Tue, 16 Sep 2025 15:01:40 +0800 Subject: [PATCH 8/9] feat: update --- src/copilot/contextProvider.ts | 230 +++++++++++++++++++++------------ src/extension.ts | 1 - 2 files changed, 147 insertions(+), 84 deletions(-) diff --git a/src/copilot/contextProvider.ts b/src/copilot/contextProvider.ts index 15784f9d..9a1129b8 100644 --- a/src/copilot/contextProvider.ts +++ b/src/copilot/contextProvider.ts @@ -9,10 +9,11 @@ import { type ContextProvider, } from '@github/copilot-language-server'; import * as vscode from 'vscode'; -import { CopilotHelper } from '../copilotHelper'; +import { CopilotHelper, INodeImportClass } from '../copilotHelper'; import { TreatmentVariables } from '../ext/treatmentVariables'; import { getExpService } from '../ext/ExperimentationService'; import { sendInfo } from "vscode-extension-telemetry-wrapper"; +import * as crypto from 'crypto'; export enum NodeKind { Workspace = 1, @@ -27,6 +28,71 @@ export enum NodeKind { File = 10, } +// Global cache for storing resolveLocalImports results +interface CacheEntry { + value: INodeImportClass[]; + timestamp: number; +} + +const globalImportsCache = new Map(); +const CACHE_EXPIRY_TIME = 5 * 60 * 1000; // 5 minutes + +/** + * Generate a hash for the document URI to use as cache key + * @param uri Document URI + * @returns Hashed URI string + */ +function generateCacheKey(uri: vscode.Uri): string { + return crypto.createHash('md5').update(uri.toString()).digest('hex'); +} + +/** + * Get cached imports for a document URI + * @param uri Document URI + * @returns Cached imports or null if not found/expired + */ +function getCachedImports(uri: vscode.Uri): INodeImportClass[] | null { + const key = generateCacheKey(uri); + const cached = globalImportsCache.get(key); + + if (!cached) { + return null; + } + + // Check if cache is expired + if (Date.now() - cached.timestamp > CACHE_EXPIRY_TIME) { + globalImportsCache.delete(key); + return null; + } + + return cached.value; +} + +/** + * Set cached imports for a document URI + * @param uri Document URI + * @param imports Import class array to cache + */ +function setCachedImports(uri: vscode.Uri, imports: INodeImportClass[]): void { + const key = generateCacheKey(uri); + globalImportsCache.set(key, { + value: imports, + timestamp: Date.now() + }); +} + +/** + * Clear expired cache entries + */ +function clearExpiredCache(): void { + const now = Date.now(); + for (const [key, entry] of globalImportsCache.entries()) { + if (now - entry.timestamp > CACHE_EXPIRY_TIME) { + globalImportsCache.delete(key); + } + } +} + export async function registerCopilotContextProviders( context: vscode.ExtensionContext ) { @@ -40,37 +106,90 @@ export async function registerCopilotContextProviders( sendInfo("", { "contextProviderEnabled": "true", }); + + // Start periodic cache cleanup + const cacheCleanupInterval = setInterval(() => { + clearExpiredCache(); + }, CACHE_EXPIRY_TIME); // Clean up every 5 minutes + + // Monitor file changes to invalidate cache + const fileWatcher = vscode.workspace.createFileSystemWatcher('**/*.java'); + + const invalidateCache = (uri: vscode.Uri) => { + const key = generateCacheKey(uri); + if (globalImportsCache.has(key)) { + globalImportsCache.delete(key); + console.log('======== Cache invalidated for:', uri.toString()); + } + }; + + fileWatcher.onDidChange(invalidateCache); + fileWatcher.onDidDelete(invalidateCache); + + // Dispose the interval and file watcher when extension is deactivated + context.subscriptions.push( + new vscode.Disposable(() => { + clearInterval(cacheCleanupInterval); + globalImportsCache.clear(); // Clear all cache on disposal + }), + fileWatcher + ); + try { const copilotClientApi = await getCopilotClientApi(); const copilotChatApi = await getCopilotChatApi(); - if (!copilotClientApi && !copilotChatApi) { - console.log('Failed to find compatible version of GitHub Copilot extension installed. Skip registration of Copilot context provider.'); + if (!copilotClientApi || !copilotChatApi) { + console.error('Failed to find compatible version of GitHub Copilot extension installed. Skip registration of Copilot context provider.'); return; } // Register the Java completion context provider - const javaCompletionProvider = new JavaCopilotCompletionContextProvider(); - let completionProviderInstallCount = 0; + const provider: ContextProvider = { + id: 'vscjava.vscode-java-pack', // use extension id as provider id for now + selector: [{ language: "java" }], + resolver: { + resolve: async (request, token) => { + // Check if we have a cached result for the current active editor + const activeEditor = vscode.window.activeTextEditor; + if (activeEditor && activeEditor.document.languageId === 'java') { + const cachedImports = getCachedImports(activeEditor.document.uri); + if (cachedImports) { + console.log('======== Using cached imports, cache size:', cachedImports.length); + // Return cached result as context items + return cachedImports.map(cls => ({ + uri: cls.uri, + value: cls.className, + importance: 70, + origin: 'request' as const + })); + } + } + + return await resolveJavaContext(request, token); + } + } + }; + let installCount = 0; if (copilotClientApi) { - const disposable = await installContextProvider(copilotClientApi, javaCompletionProvider); + const disposable = await installContextProvider(copilotClientApi, provider); if (disposable) { context.subscriptions.push(disposable); - completionProviderInstallCount++; + installCount++; } } if (copilotChatApi) { - const disposable = await installContextProvider(copilotChatApi, javaCompletionProvider); + const disposable = await installContextProvider(copilotChatApi, provider); if (disposable) { context.subscriptions.push(disposable); - completionProviderInstallCount++; + installCount++; } } - if (completionProviderInstallCount > 0) { - console.log('Registration of Java completion context provider for GitHub Copilot extension succeeded.'); - } else { - console.log('Failed to register Java completion context provider for GitHub Copilot extension.'); + if (installCount === 0) { + console.log('Incompatible GitHub Copilot extension installed. Skip registration of Java context providers.'); + return; } + console.log('Registration of Java context provider for GitHub Copilot extension succeeded.'); } catch (error) { console.log('Error occurred while registering Java context provider for GitHub Copilot extension:', error); @@ -89,11 +208,6 @@ async function resolveJavaContext(_request: ResolveRequest, _token: vscode.Cance const document = activeEditor.document; - // const position = activeEditor.selection.active; - // const currentRange = activeEditor.selection.isEmpty - // ? new vscode.Range(position, position) - // : activeEditor.selection; - // 1. Project basic information (High importance) const projectContext = await collectProjectContext(document); const packageName = await getPackageName(document); @@ -122,7 +236,17 @@ async function resolveJavaContext(_request: ResolveRequest, _token: vscode.Cance origin: 'request' }); - const importClass = await CopilotHelper.resolveLocalImports(document.uri); + // Try to get cached imports first + let importClass = getCachedImports(document.uri); + if (!importClass) { + // If not cached, resolve and cache the result + importClass = await CopilotHelper.resolveLocalImports(document.uri); + setCachedImports(document.uri, importClass); + console.log('======== Cached new imports, cache size:', importClass.length); + } else { + console.log('======== Using cached imports in resolveJavaContext, cache size:', importClass.length); + } + for (const cls of importClass) { items.push({ uri: cls.uri, @@ -145,16 +269,16 @@ async function resolveJavaContext(_request: ResolveRequest, _token: vscode.Cance origin: 'request' }); } - console.log('Total context resolution time:', performance.now() - start); - console.log('===== Size of context items:', items.length); + console.log('Total context resolution time:', performance.now() - start, 'ms', ' ,size:', items.length); + console.log('Context items:', items); return items; } async function collectProjectContext(document: vscode.TextDocument): Promise<{ javaVersion: string }> { try { - return await vscode.commands.executeCommand("java.project.getSettings", document.uri, ["java.home"]); + return await vscode.commands.executeCommand("java.project.getSettings", document.uri, ["java.compliance", "java.source", "java.target"]); } catch (error) { - console.log('Failed to get Java version:', error); + console.error('Failed to get Java version:', error); return { javaVersion: 'unknown' }; } } @@ -218,63 +342,3 @@ async function installContextProvider( } return undefined; } - -/** - * Java-specific Copilot completion context provider - * Similar to CopilotCompletionContextProvider but tailored for Java language - */ -export class JavaCopilotCompletionContextProvider implements ContextProvider { - public readonly id = 'java-completion'; - public readonly selector = [{ language: 'java' }]; - public readonly resolver = this.resolve.bind(this); - - // Cache for completion contexts with timeout - private cache = new Map(); - private readonly cacheTimeout = 30000; // 30 seconds - - public async resolve(request: ResolveRequest, cancellationToken: vscode.CancellationToken): Promise { - // Access document through request properties - const docUri = request.documentContext?.uri?.toString(); - const docOffset = request.documentContext?.offset; - - // Only process Java files - if (!docUri || !docUri.endsWith('.java')) { - return []; - } - - const cacheKey = `${docUri}:${docOffset}`; - const cached = this.cache.get(cacheKey); - - // Return cached result if still valid - if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { - return cached.context; - } - - try { - const context = await resolveJavaContext(request, cancellationToken); - - // Cache the result - this.cache.set(cacheKey, { - context, - timestamp: Date.now() - }); - - // Clean up old cache entries - this.cleanCache(); - - return context; - } catch (error) { - console.error('Error generating Java completion context:', error); - return []; - } - } - - private cleanCache(): void { - const now = Date.now(); - for (const [key, value] of this.cache.entries()) { - if (now - value.timestamp > this.cacheTimeout) { - this.cache.delete(key); - } - } - } -} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 0a98c490..1688d1b3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,7 +6,6 @@ import { commands, Diagnostic, Extension, ExtensionContext, extensions, language Range, tasks, TextDocument, TextEditor, Uri, window, workspace } from "vscode"; import { dispose as disposeTelemetryWrapper, initializeFromJsonFile, instrumentOperation, instrumentOperationAsVsCodeCommand, sendInfo } from "vscode-extension-telemetry-wrapper"; import { Commands, contextManager } from "../extension.bundle"; -import { CopilotHelper } from "./copilotHelper"; import { BuildTaskProvider } from "./tasks/build/buildTaskProvider"; import { buildFiles, Context, ExtensionName } from "./constants"; import { LibraryController } from "./controllers/libraryController"; From 5e34a13f8e4a8844ad44098f50d9ccb4966d75a7 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Tue, 16 Sep 2025 15:25:48 +0800 Subject: [PATCH 9/9] fix: update code --- COPILOT_INTEGRATION.md | 154 --------------- GET_SYMBOLS_USAGE.md | 162 ---------------- package.json | 13 -- src/commands.ts | 2 - src/copilot/contextCache.ts | 211 +++++++++++++++++++++ src/copilot/contextProvider.ts | 179 +++-------------- src/copilot/copilotHelper.ts | 44 +++++ src/copilotHelper.ts | 46 ----- src/{ext => exp}/ExperimentationService.ts | 0 src/{ext => exp}/treatmentVariables.ts | 0 src/extension.ts | 2 +- src/java/jdtls.ts | 6 +- 12 files changed, 286 insertions(+), 533 deletions(-) delete mode 100644 COPILOT_INTEGRATION.md delete mode 100644 GET_SYMBOLS_USAGE.md create mode 100644 src/copilot/contextCache.ts create mode 100644 src/copilot/copilotHelper.ts delete mode 100644 src/copilotHelper.ts rename src/{ext => exp}/ExperimentationService.ts (100%) rename src/{ext => exp}/treatmentVariables.ts (100%) diff --git a/COPILOT_INTEGRATION.md b/COPILOT_INTEGRATION.md deleted file mode 100644 index d66952ba..00000000 --- a/COPILOT_INTEGRATION.md +++ /dev/null @@ -1,154 +0,0 @@ -# Copilot Integration for Java Dependency Analysis - -这个功能为 Copilot 提供了分析 Java 项目本地依赖的能力。 - -## 功能概述 - -`resolveCopilotRequest` 功能可以: -1. 解析指定 Java 文件的所有 import 语句 -2. 过滤掉外部依赖(JAR 包、JRE 系统库等),只保留本地工程文件 -3. 提取每个本地文件的类型信息(class、interface、enum、annotation) -4. 返回格式化的类型信息列表 - -## API 接口 - -### Java 后端 API - -```java -public static String[] resolveCopilotRequest(List arguments, IProgressMonitor monitor) -``` - -**参数:** -- `arguments[0]`: 文件 URI 字符串 (如 "file:///path/to/MyClass.java") -- `monitor`: 进度监控器 - -**返回:** -- 字符串数组,每个元素格式为 `"type:fully.qualified.name"` -- `type` 可以是:`class`、`interface`、`enum`、`annotation` - -### VS Code 扩展 API - -```typescript -export async function resolveCopilotRequest(fileUri: string): Promise -``` - -**参数:** -- `fileUri`: 文件 URI 字符串 - -**返回:** -- Promise,解析到的本地类型信息 - -## 使用示例 - -### 1. 基本用法 - -```typescript -import { Uri } from "vscode"; -import { CopilotHelper } from "./copilotHelper"; - -// 分析当前活动文件的本地导入 -const currentFile = window.activeTextEditor?.document.uri; -if (currentFile) { - const localImports = await CopilotHelper.resolveLocalImports(currentFile); - console.log("Local imports:", localImports); - // 输出示例: - // [ - // "class:com.example.model.User", - // "interface:com.example.service.UserService", - // "enum:com.example.enums.Status" - // ] -} -``` - -### 2. 按类型分类 - -```typescript -const categorizedTypes = await CopilotHelper.getLocalImportsByType(currentFile); -console.log("Classes:", categorizedTypes.classes); -console.log("Interfaces:", categorizedTypes.interfaces); -console.log("Enums:", categorizedTypes.enums); - -// 输出示例: -// Classes: ["com.example.model.User", "com.example.util.Helper"] -// Interfaces: ["com.example.service.UserService"] -// Enums: ["com.example.enums.Status"] -``` - -### 3. 获取类型名称列表 - -```typescript -const typeNames = await CopilotHelper.getLocalImportTypeNames(currentFile); -console.log("Type names:", typeNames); - -// 输出示例: -// ["com.example.model.User", "com.example.service.UserService", "com.example.enums.Status"] -``` - -## 过滤逻辑 - -函数只返回**本地项目**中的类型,会过滤掉: -- ❌ 外部 JAR 包中的类 -- ❌ JRE 系统库中的类(如 `java.util.List`) -- ❌ Maven/Gradle 依赖中的类 -- ❌ 第三方库中的类 - -保留: -- ✅ 当前项目源码中的类 -- ✅ 当前项目源码中的接口 -- ✅ 当前项目源码中的枚举 -- ✅ 当前项目源码中的注解 - -## 示例场景 - -假设有一个 Java 文件: - -```java -package com.example.controller; - -import java.util.List; // ❌ JRE 系统库,会被过滤 -import org.springframework.web.bind.annotation.GetMapping; // ❌ 外部依赖,会被过滤 -import com.fasterxml.jackson.annotation.JsonProperty; // ❌ 外部依赖,会被过滤 - -import com.example.model.User; // ✅ 本地项目类 -import com.example.service.UserService; // ✅ 本地项目接口 -import com.example.enums.UserStatus; // ✅ 本地项目枚举 -import com.example.util.*; // ✅ 本地项目包(会展开为具体类型) - -public class UserController { - // ... -} -``` - -调用 `resolveCopilotRequest` 会返回: -``` -[ - "class:com.example.model.User", - "interface:com.example.service.UserService", - "enum:com.example.enums.UserStatus", - "class:com.example.util.DateHelper", - "class:com.example.util.StringUtil" -] -``` - -## 错误处理 - -函数内置了错误处理机制: -- 如果文件不存在或无法解析,返回空数组 -- 如果不是 Java 文件,返回空数组 -- 如果项目不是 Java 项目,返回空数组 -- 解析过程中的异常会被捕获并记录日志 - -## 性能考虑 - -- 使用缓存机制避免重复解析 -- 支持进度监控和取消操作 -- 懒加载包内容,只在需要时解析 -- 对大型项目进行了优化 - -## 集成到 Copilot - -这个功能专为 Copilot 设计,可以: -1. 帮助 Copilot 理解项目的本地代码结构 -2. 提供上下文信息用于代码生成 -3. 避免建议使用不存在的本地类型 -4. 提高代码补全的准确性 diff --git a/GET_SYMBOLS_USAGE.md b/GET_SYMBOLS_USAGE.md deleted file mode 100644 index e3df4684..00000000 --- a/GET_SYMBOLS_USAGE.md +++ /dev/null @@ -1,162 +0,0 @@ -# 如何使用 getSymbolsFromFile 命令 - -我已经为 VS Code Java 依赖管理扩展添加了一个新的命令 `getSymbolsFromFile`,用于分析当前打开文件的本地项目符号。 - -## 使用方法 - -### 1. 通过键盘快捷键运行(推荐) - -1. 在 VS Code 中打开一个 Java 文件 -2. 按 `Ctrl+Shift+S` (Windows/Linux) 或 `Cmd+Shift+S` (macOS) -3. 查看开发者控制台输出结果 - -### 2. 通过命令面板运行 - -1. 在 VS Code 中打开一个 Java 文件 -2. 按 `Ctrl+Shift+P` (Windows/Linux) 或 `Cmd+Shift+P` (macOS) 打开命令面板 -3. 输入 `Get Local Symbols from Current File` -4. 按回车执行命令 -5. 查看开发者控制台输出结果 - -### 3. 查看输出结果 - -执行命令后,会在以下地方看到结果: - -**VS Code 通知消息:** -``` -Found 5 local symbols. Check console for details. -``` - -**开发者控制台输出(按 F12 打开):** -``` -=== Local Symbols from Current File === -File: /path/to/your/JavaFile.java -Total symbols found: 5 -1. class:com.example.model.User -2. interface:com.example.service.UserService -3. enum:com.example.enums.Status -4. class:com.example.util.DateHelper -5. annotation:com.example.annotations.Entity - -=== Categorized View === -Classes (2): ["com.example.model.User", "com.example.util.DateHelper"] -Interfaces (1): ["com.example.service.UserService"] -Enums (1): ["com.example.enums.Status"] -Annotations (1): ["com.example.annotations.Entity"] -=== End === -``` - -## 示例场景 - -### 示例 Java 文件 - -假设你有以下 Java 文件: - -```java -package com.example.controller; - -// 这些会被过滤掉(外部依赖) -import java.util.List; -import org.springframework.web.bind.annotation.GetMapping; -import com.fasterxml.jackson.annotation.JsonProperty; - -// 这些会被分析(本地项目符号) -import com.example.model.User; -import com.example.service.UserService; -import com.example.enums.Status; -import com.example.util.*; // 会展开为具体的类 -import com.example.annotations.Entity; - -@Entity -public class UserController { - private UserService userService; - - @GetMapping("/users") - public List getUsers() { - return userService.findByStatus(Status.ACTIVE); - } -} -``` - -### 执行命令后的输出 - -``` -=== Local Symbols from Current File === -File: /workspace/src/main/java/com/example/controller/UserController.java -Total symbols found: 5 -1. class:com.example.model.User -2. interface:com.example.service.UserService -3. enum:com.example.enums.Status -4. class:com.example.util.DateHelper -5. class:com.example.util.StringUtils -6. annotation:com.example.annotations.Entity - -=== Categorized View === -Classes (3): ["com.example.model.User", "com.example.util.DateHelper", "com.example.util.StringUtils"] -Interfaces (1): ["com.example.service.UserService"] -Enums (1): ["com.example.enums.Status"] -Annotations (1): ["com.example.annotations.Entity"] -=== End === -``` - -## 功能特点 - -### ✅ 会分析的内容 -- 本地项目中的类(class) -- 本地项目中的接口(interface) -- 本地项目中的枚举(enum) -- 本地项目中的注解(annotation) -- 包导入(`import com.example.util.*;`)会展开为具体类型 - -### ❌ 会过滤的内容 -- JRE 系统库(如 `java.util.List`) -- 外部 JAR 包中的类 -- Maven/Gradle 依赖中的类 -- 第三方框架的类(如 Spring、Jackson 等) - -## 错误处理 - -### 常见情况处理 - -1. **没有打开文件** - ``` - Warning: No active editor found. Please open a Java file first. - ``` - -2. **不是 Java 文件** - ``` - Warning: Please open a Java file to get symbols. - ``` - -3. **没有找到本地符号** - ``` - === Local Symbols from Current File === - File: /path/to/file.java - Total symbols found: 0 - No local project symbols found in imports. - === End === - ``` - -4. **解析错误** - ``` - Error: Error getting symbols: [具体错误信息] - ``` - -## 开发者控制台 - -要查看详细的控制台输出: - -1. 按 `F12` 打开开发者工具 -2. 点击 "Console" 选项卡 -3. 执行命令后查看输出 -4. 可以看到完整的符号列表和分类信息 - -## 用途 - -这个命令主要用于: - -1. **代码分析**:快速了解当前文件依赖了哪些本地项目组件 -2. **代码重构**:在重构时了解文件间的依赖关系 -3. **项目理解**:帮助快速理解代码结构和依赖关系 -4. **Copilot 集成**:为 AI 代码助手提供本地项目上下文 -5. **开发调试**:验证 import 语句是否正确引用本地组件 diff --git a/package.json b/package.json index 93ccdd11..82c66728 100644 --- a/package.json +++ b/package.json @@ -49,12 +49,6 @@ "./server/com.microsoft.jdtls.ext.core-0.24.1.jar" ], "commands": [ - { - "command": "java.getSymbolsFromFile", - "title": "Get Local Symbols from Current File", - "category": "Java", - "icon": "$(symbol-class)" - }, { "command": "java.project.create", "title": "%contributes.commands.java.project.create%", @@ -355,13 +349,6 @@ } }, "keybindings": [ - { - "command": "java.getSymbolsFromFile", - "key": "ctrl+shift+s", - "win": "ctrl+shift+s", - "mac": "cmd+shift+s", - "when": "java:serverMode == Standard && editorLangId == java" - }, { "command": "java.view.package.revealFileInOS", "key": "ctrl+alt+r", diff --git a/src/commands.ts b/src/commands.ts index e042750a..8cf2d748 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -10,8 +10,6 @@ export namespace Commands { */ export const EXECUTE_WORKSPACE_COMMAND = "java.execute.workspaceCommand"; - export const GET_SYMBOLS_FROM_FILE = "java.getSymbolsFromFile"; - export const VIEW_PACKAGE_CHANGETOFLATPACKAGEVIEW = "java.view.package.changeToFlatPackageView"; export const VIEW_PACKAGE_CHANGETOHIERARCHICALPACKAGEVIEW = "java.view.package.changeToHierarchicalPackageView"; diff --git a/src/copilot/contextCache.ts b/src/copilot/contextCache.ts new file mode 100644 index 00000000..cb55ff79 --- /dev/null +++ b/src/copilot/contextCache.ts @@ -0,0 +1,211 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; +import * as crypto from 'crypto'; +import { INodeImportClass } from '../java/jdtls'; + +/** + * Cache entry interface for storing import data with timestamp + */ +interface CacheEntry { + value: INodeImportClass[]; + timestamp: number; +} + +/** + * Configuration options for the context cache + */ +interface ContextCacheOptions { + /** Cache expiry time in milliseconds. Default: 5 minutes */ + expiryTime?: number; + /** Enable automatic cleanup interval. Default: true */ + enableAutoCleanup?: boolean; + /** Enable file watching for cache invalidation. Default: true */ + enableFileWatching?: boolean; +} + +/** + * Context cache manager for storing and managing Java import contexts + */ +export class ContextCache { + private readonly cache = new Map(); + private readonly expiryTime: number; + private readonly enableAutoCleanup: boolean; + private readonly enableFileWatching: boolean; + + private cleanupInterval?: NodeJS.Timeout; + private fileWatcher?: vscode.FileSystemWatcher; + + constructor(options: ContextCacheOptions = {}) { + this.expiryTime = options.expiryTime ?? 5 * 60 * 1000; // 5 minutes default + this.enableAutoCleanup = options.enableAutoCleanup ?? true; + this.enableFileWatching = options.enableFileWatching ?? true; + } + + /** + * Initialize the cache with VS Code extension context + * @param context VS Code extension context for managing disposables + */ + public initialize(context: vscode.ExtensionContext): void { + if (this.enableAutoCleanup) { + this.startPeriodicCleanup(); + } + + if (this.enableFileWatching) { + this.setupFileWatcher(); + } + + // Register cleanup on extension disposal + context.subscriptions.push( + new vscode.Disposable(() => { + this.dispose(); + }) + ); + + if (this.fileWatcher) { + context.subscriptions.push(this.fileWatcher); + } + } + + /** + * Generate a hash for the document URI to use as cache key + * @param uri Document URI + * @returns Hashed URI string + */ + private generateCacheKey(uri: vscode.Uri): string { + return crypto.createHash('md5').update(uri.toString()).digest('hex'); + } + + /** + * Get cached imports for a document URI + * @param uri Document URI + * @returns Cached imports or null if not found/expired + */ + public get(uri: vscode.Uri): INodeImportClass[] | null { + const key = this.generateCacheKey(uri); + const cached = this.cache.get(key); + + if (!cached) { + return null; + } + + // Check if cache is expired + if (this.isExpired(cached)) { + this.cache.delete(key); + return null; + } + + return cached.value; + } + + /** + * Set cached imports for a document URI + * @param uri Document URI + * @param imports Import class array to cache + */ + public set(uri: vscode.Uri, imports: INodeImportClass[]): void { + const key = this.generateCacheKey(uri); + this.cache.set(key, { + value: imports, + timestamp: Date.now() + }); + } + + /** + * Check if a cache entry is expired + * @param entry Cache entry to check + * @returns True if expired, false otherwise + */ + private isExpired(entry: CacheEntry): boolean { + return Date.now() - entry.timestamp > this.expiryTime; + } + + /** + * Clear expired cache entries + */ + public clearExpired(): void { + const now = Date.now(); + for (const [key, entry] of this.cache.entries()) { + if (now - entry.timestamp > this.expiryTime) { + this.cache.delete(key); + } + } + } + + /** + * Clear all cache entries + */ + public clear(): void { + this.cache.clear(); + } + + /** + * Invalidate cache for specific URI + * @param uri URI to invalidate + */ + public invalidate(uri: vscode.Uri): void { + const key = this.generateCacheKey(uri); + if (this.cache.has(key)) { + this.cache.delete(key); + console.log('======== Cache invalidated for:', uri.toString()); + } + } + + /** + * Get cache statistics + * @returns Object containing cache size and other statistics + */ + public getStats(): { size: number; expiryTime: number } { + return { + size: this.cache.size, + expiryTime: this.expiryTime + }; + } + + /** + * Start periodic cleanup of expired cache entries + */ + private startPeriodicCleanup(): void { + this.cleanupInterval = setInterval(() => { + this.clearExpired(); + }, this.expiryTime); + } + + /** + * Setup file system watcher for Java files to invalidate cache on changes + */ + private setupFileWatcher(): void { + this.fileWatcher = vscode.workspace.createFileSystemWatcher('**/*.java'); + + const invalidateHandler = (uri: vscode.Uri) => { + this.invalidate(uri); + }; + + this.fileWatcher.onDidChange(invalidateHandler); + this.fileWatcher.onDidDelete(invalidateHandler); + } + + /** + * Dispose of all resources (intervals, watchers, etc.) + */ + public dispose(): void { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + this.cleanupInterval = undefined; + } + + if (this.fileWatcher) { + this.fileWatcher.dispose(); + this.fileWatcher = undefined; + } + + this.clear(); + } +} + +/** + * Default context cache instance + */ +export const contextCache = new ContextCache(); diff --git a/src/copilot/contextProvider.ts b/src/copilot/contextProvider.ts index 9a1129b8..c3255b35 100644 --- a/src/copilot/contextProvider.ts +++ b/src/copilot/contextProvider.ts @@ -9,89 +9,11 @@ import { type ContextProvider, } from '@github/copilot-language-server'; import * as vscode from 'vscode'; -import { CopilotHelper, INodeImportClass } from '../copilotHelper'; -import { TreatmentVariables } from '../ext/treatmentVariables'; -import { getExpService } from '../ext/ExperimentationService'; +import { CopilotHelper } from './copilotHelper'; +import { TreatmentVariables } from '../exp/treatmentVariables'; +import { getExpService } from '../exp/ExperimentationService'; import { sendInfo } from "vscode-extension-telemetry-wrapper"; -import * as crypto from 'crypto'; - -export enum NodeKind { - Workspace = 1, - Project = 2, - PackageRoot = 3, - Package = 4, - PrimaryType = 5, - CompilationUnit = 6, - ClassFile = 7, - Container = 8, - Folder = 9, - File = 10, -} - -// Global cache for storing resolveLocalImports results -interface CacheEntry { - value: INodeImportClass[]; - timestamp: number; -} - -const globalImportsCache = new Map(); -const CACHE_EXPIRY_TIME = 5 * 60 * 1000; // 5 minutes - -/** - * Generate a hash for the document URI to use as cache key - * @param uri Document URI - * @returns Hashed URI string - */ -function generateCacheKey(uri: vscode.Uri): string { - return crypto.createHash('md5').update(uri.toString()).digest('hex'); -} - -/** - * Get cached imports for a document URI - * @param uri Document URI - * @returns Cached imports or null if not found/expired - */ -function getCachedImports(uri: vscode.Uri): INodeImportClass[] | null { - const key = generateCacheKey(uri); - const cached = globalImportsCache.get(key); - - if (!cached) { - return null; - } - - // Check if cache is expired - if (Date.now() - cached.timestamp > CACHE_EXPIRY_TIME) { - globalImportsCache.delete(key); - return null; - } - - return cached.value; -} - -/** - * Set cached imports for a document URI - * @param uri Document URI - * @param imports Import class array to cache - */ -function setCachedImports(uri: vscode.Uri, imports: INodeImportClass[]): void { - const key = generateCacheKey(uri); - globalImportsCache.set(key, { - value: imports, - timestamp: Date.now() - }); -} - -/** - * Clear expired cache entries - */ -function clearExpiredCache(): void { - const now = Date.now(); - for (const [key, entry] of globalImportsCache.entries()) { - if (now - entry.timestamp > CACHE_EXPIRY_TIME) { - globalImportsCache.delete(key); - } - } -} +import { contextCache } from './contextCache'; export async function registerCopilotContextProviders( context: vscode.ExtensionContext @@ -107,33 +29,8 @@ export async function registerCopilotContextProviders( "contextProviderEnabled": "true", }); - // Start periodic cache cleanup - const cacheCleanupInterval = setInterval(() => { - clearExpiredCache(); - }, CACHE_EXPIRY_TIME); // Clean up every 5 minutes - - // Monitor file changes to invalidate cache - const fileWatcher = vscode.workspace.createFileSystemWatcher('**/*.java'); - - const invalidateCache = (uri: vscode.Uri) => { - const key = generateCacheKey(uri); - if (globalImportsCache.has(key)) { - globalImportsCache.delete(key); - console.log('======== Cache invalidated for:', uri.toString()); - } - }; - - fileWatcher.onDidChange(invalidateCache); - fileWatcher.onDidDelete(invalidateCache); - - // Dispose the interval and file watcher when extension is deactivated - context.subscriptions.push( - new vscode.Disposable(() => { - clearInterval(cacheCleanupInterval); - globalImportsCache.clear(); // Clear all cache on disposal - }), - fileWatcher - ); + // Initialize the context cache + contextCache.initialize(context); try { const copilotClientApi = await getCopilotClientApi(); @@ -151,11 +48,11 @@ export async function registerCopilotContextProviders( // Check if we have a cached result for the current active editor const activeEditor = vscode.window.activeTextEditor; if (activeEditor && activeEditor.document.languageId === 'java') { - const cachedImports = getCachedImports(activeEditor.document.uri); + const cachedImports = contextCache.get(activeEditor.document.uri); if (cachedImports) { console.log('======== Using cached imports, cache size:', cachedImports.length); // Return cached result as context items - return cachedImports.map(cls => ({ + return cachedImports.map((cls: any) => ({ uri: cls.uri, value: cls.className, importance: 70, @@ -209,8 +106,9 @@ async function resolveJavaContext(_request: ResolveRequest, _token: vscode.Cance const document = activeEditor.document; // 1. Project basic information (High importance) - const projectContext = await collectProjectContext(document); - const packageName = await getPackageName(document); + const copilotHelper = new CopilotHelper(); + const projectContext = await copilotHelper.collectProjectContext(document); + const packageName = await copilotHelper.getPackageName(document); items.push({ name: 'java.version', @@ -237,63 +135,36 @@ async function resolveJavaContext(_request: ResolveRequest, _token: vscode.Cance }); // Try to get cached imports first - let importClass = getCachedImports(document.uri); + let importClass = contextCache.get(document.uri); if (!importClass) { // If not cached, resolve and cache the result importClass = await CopilotHelper.resolveLocalImports(document.uri); - setCachedImports(document.uri, importClass); - console.log('======== Cached new imports, cache size:', importClass.length); + if (importClass) { + contextCache.set(document.uri, importClass); + console.log('======== Cached new imports, cache size:', importClass.length); + } } else { console.log('======== Using cached imports in resolveJavaContext, cache size:', importClass.length); } - for (const cls of importClass) { - items.push({ - uri: cls.uri, - value: cls.className, - importance: 70, - origin: 'request' - }); + if (importClass) { + for (const cls of importClass) { + items.push({ + uri: cls.uri, + value: cls.className, + importance: 70, + origin: 'request' + }); + } } - - console.log('tick time', performance.now() - start); - } catch (error) { console.log('Error resolving Java context:', error); - // Add error information as context to help with debugging - items.push({ - name: 'java.context.error', - value: `${error}`, - importance: 10, - id: 'java-context-error', - origin: 'request' - }); } console.log('Total context resolution time:', performance.now() - start, 'ms', ' ,size:', items.length); console.log('Context items:', items); return items; } -async function collectProjectContext(document: vscode.TextDocument): Promise<{ javaVersion: string }> { - try { - return await vscode.commands.executeCommand("java.project.getSettings", document.uri, ["java.compliance", "java.source", "java.target"]); - } catch (error) { - console.error('Failed to get Java version:', error); - return { javaVersion: 'unknown' }; - } -} - -async function getPackageName(document: vscode.TextDocument): Promise { - try { - const text = document.getText(); - const packageMatch = text.match(/^\s*package\s+([a-zA-Z_][a-zA-Z0-9_.]*)\s*;/m); - return packageMatch ? packageMatch[1] : 'default package'; - } catch (error) { - console.log('Failed to get package name:', error); - return 'unknown'; - } -} - interface CopilotApi { getContextProviderAPI(version: string): Promise; } diff --git a/src/copilot/copilotHelper.ts b/src/copilot/copilotHelper.ts new file mode 100644 index 00000000..86e0eb4a --- /dev/null +++ b/src/copilot/copilotHelper.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { commands, Uri, TextDocument } from "vscode"; +import { Jdtls, INodeImportClass } from "../java/jdtls"; +/** + * Helper class for Copilot integration to analyze Java project dependencies + */ +export class CopilotHelper { + /** + * Resolves all local project types imported by the given file + * @param fileUri The URI of the Java file to analyze + * @returns Array of strings in format "type:fully.qualified.name" where type is class|interface|enum|annotation + */ + public static async resolveLocalImports(fileUri: Uri): Promise { + try { + const result = await Jdtls.getImportClassContent(fileUri.toString()); + return result; + } catch (error) { + console.error("Error resolving copilot request:", error); + return []; + } + } + + public async collectProjectContext(document: TextDocument): Promise<{ javaVersion: string }> { + try { + return await commands.executeCommand("java.project.getSettings", document.uri, ["java.home", "java.compliance", "java.source", "java.target"]); + } catch (error) { + console.error('Failed to get Java version:', error); + return { javaVersion: 'unknown' }; + } + } + + public async getPackageName(document: TextDocument): Promise { + try { + const text = document.getText(); + const packageMatch = text.match(/^\s*package\s+([a-zA-Z_][a-zA-Z0-9_.]*)\s*;/m); + return packageMatch ? packageMatch[1] : 'default package'; + } catch (error) { + console.log('Failed to get package name:', error); + return 'unknown'; + } + } +} diff --git a/src/copilotHelper.ts b/src/copilotHelper.ts deleted file mode 100644 index f72e3276..00000000 --- a/src/copilotHelper.ts +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -import { Uri } from "vscode"; -import { Jdtls } from "./java/jdtls"; - -export interface INodeImportClass { - uri: string; - className: string; // Changed from 'class' to 'className' to match Java code -} - -/** - * Helper class for Copilot integration to analyze Java project dependencies - */ -export class CopilotHelper { - - /** - * Resolves all local project types imported by the given file - * @param fileUri The URI of the Java file to analyze - * @returns Array of strings in format "type:fully.qualified.name" where type is class|interface|enum|annotation - */ - public static async resolveLocalImports(fileUri: Uri): Promise { - try { - const result = await Jdtls.getImportClassContent(fileUri.toString()); - return result; - } catch (error) { - console.error("Error resolving copilot request:", error); - return []; - } - } - - /** - * Get import class content for the given file URI - * @param fileUri The URI of the Java file as string - * @returns Array of import class information with URI and content - */ - public static async getImportClassContent(fileUri: string): Promise { - try { - const result = await Jdtls.getImportClassContent(fileUri); - return result; - } catch (error) { - console.error("Error getting import class content:", error); - return []; - } - } -} diff --git a/src/ext/ExperimentationService.ts b/src/exp/ExperimentationService.ts similarity index 100% rename from src/ext/ExperimentationService.ts rename to src/exp/ExperimentationService.ts diff --git a/src/ext/treatmentVariables.ts b/src/exp/treatmentVariables.ts similarity index 100% rename from src/ext/treatmentVariables.ts rename to src/exp/treatmentVariables.ts diff --git a/src/extension.ts b/src/extension.ts index 1688d1b3..49293c39 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,7 +10,7 @@ import { BuildTaskProvider } from "./tasks/build/buildTaskProvider"; import { buildFiles, Context, ExtensionName } from "./constants"; import { LibraryController } from "./controllers/libraryController"; import { ProjectController } from "./controllers/projectController"; -import { init as initExpService } from "./ext/ExperimentationService"; +import { init as initExpService } from "./exp/ExperimentationService"; import { DeprecatedExportJarTaskProvider, BuildArtifactTaskProvider } from "./tasks/buildArtifact/BuildArtifactTaskProvider"; import { Settings } from "./settings"; import { syncHandler } from "./syncHandler"; diff --git a/src/java/jdtls.ts b/src/java/jdtls.ts index 3f593115..d4e1b42b 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 "../copilotHelper"; export namespace Jdtls { export async function getProjects(params: string): Promise { @@ -102,4 +101,9 @@ export namespace Jdtls { interface IPackageDataParam { projectUri: string | undefined; [key: string]: any; +} + +export interface INodeImportClass { + uri: string; + className: string; // Changed from 'class' to 'className' to match Java code } \ No newline at end of file