diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml b/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml index 0352e983..6643c1a7 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 @@ + arguments, IProgress return ProjectCommand.exportJar(arguments, monitor); case "java.project.checkImportStatus": return ProjectCommand.checkImportStatus(); + 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 3f10d3e6..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; @@ -110,7 +118,7 @@ public static List 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 +210,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 +229,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 +248,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 +262,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 +270,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 +301,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 @@ -332,11 +348,311 @@ public static boolean checkImportStatus() { return hasError; } + public static ImportClassInfo[] getImportClassContent(List arguments, IProgressMonitor monitor) { + if (arguments == null || arguments.isEmpty()) { + return new ImportClassInfo[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 ImportClassInfo[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 ImportClassInfo[0]; + } + + // Get the Java project + IJavaProject javaProject = JavaCore.create(file.getProject()); + if (javaProject == null || !javaProject.exists()) { + return new ImportClassInfo[0]; + } + + // Find the compilation unit + IJavaElement javaElement = JavaCore.create(file); + if (!(javaElement instanceof org.eclipse.jdt.core.ICompilationUnit)) { + 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<>(); + + // 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 ImportClassInfo[0]); + + } catch (Exception e) { + JdtlsExtActivator.logException("Error in resolveCopilotRequest", e); + return new ImportClassInfo[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()) { + extractDetailedClassInfo(type, result); + return; // extractDetailedClassInfo handles adding to result + } else if (type.isEnum()) { + typeInfo = "enum:" + typeName; + } else if (type.isAnnotation()) { + typeInfo = "annotation:" + typeName; + } else { + typeInfo = "type:" + typeName; + } + + // Get URI for this type + String uri = getTypeUri(type); + if (uri != null) { + result.add(new ImportClassInfo(uri, 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 extractDetailedClassInfo(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)); + } + + // 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(); + 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/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 df286b18..82c66728 100644 --- a/package.json +++ b/package.json @@ -1082,6 +1082,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 a2564835..8cf2d748 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -114,6 +114,8 @@ export namespace Commands { export const JAVA_PROJECT_GETMAINCLASSES = "java.project.getMainClasses"; + export const JAVA_PROJECT_GETIMPORTCLASSCONTENT = "java.project.getImportClassContent"; + export const JAVA_PROJECT_GENERATEJAR = "java.project.generateJar"; export const JAVA_BUILD_WORKSPACE = "java.workspace.compile"; 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 new file mode 100644 index 00000000..c3255b35 --- /dev/null +++ b/src/copilot/contextProvider.ts @@ -0,0 +1,215 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; +import { TreatmentVariables } from '../exp/treatmentVariables'; +import { getExpService } from '../exp/ExperimentationService'; +import { sendInfo } from "vscode-extension-telemetry-wrapper"; +import { contextCache } from './contextCache'; + +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", + }); + + // Initialize the context cache + contextCache.initialize(context); + + try { + const copilotClientApi = await getCopilotClientApi(); + const copilotChatApi = await getCopilotChatApi(); + 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 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 = 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: any) => ({ + 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, 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.'); + } + 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; + + // 1. Project basic information (High importance) + const copilotHelper = new CopilotHelper(); + const projectContext = await copilotHelper.collectProjectContext(document); + const packageName = await copilotHelper.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' + }); + + // Try to get cached imports first + let importClass = contextCache.get(document.uri); + if (!importClass) { + // If not cached, resolve and cache the result + importClass = await CopilotHelper.resolveLocalImports(document.uri); + 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); + } + + if (importClass) { + for (const cls of importClass) { + items.push({ + uri: cls.uri, + value: cls.className, + importance: 70, + origin: 'request' + }); + } + } + } catch (error) { + console.log('Error resolving Java context:', error); + } + console.log('Total context resolution time:', performance.now() - start, 'ms', ' ,size:', items.length); + console.log('Context items:', items); + return items; +} + +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; +} 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/ExperimentationService.ts b/src/exp/ExperimentationService.ts similarity index 94% rename from src/ExperimentationService.ts rename to src/exp/ExperimentationService.ts index a76741ea..d7dc3812 100644 --- a/src/ExperimentationService.ts +++ b/src/exp/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/exp/treatmentVariables.ts b/src/exp/treatmentVariables.ts new file mode 100644 index 00000000..05bec365 --- /dev/null +++ b/src/exp/treatmentVariables.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export class TreatmentVariables { + public static readonly VSCodeConfig = "vscode"; + public static readonly ContextProvider = "ContextProviderIsEnabled"; +} + +export class TreatmentVariableValue { + public static contextProvider: boolean | undefined = undefined; +} diff --git a/src/extension.ts b/src/extension.ts index 531b5ac3..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 "./ExperimentationService"; +import { init as initExpService } from "./exp/ExperimentationService"; import { DeprecatedExportJarTaskProvider, BuildArtifactTaskProvider } from "./tasks/buildArtifact/BuildArtifactTaskProvider"; import { Settings } from "./settings"; import { syncHandler } from "./syncHandler"; @@ -20,6 +20,7 @@ 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"; export async function activate(context: ExtensionContext): Promise { contextManager.initialize(context); @@ -35,6 +36,7 @@ export async function activate(context: ExtensionContext): Promise { } }); contextManager.setContextValue(Context.EXTENSION_ACTIVATED, true); + await registerCopilotContextProviders(context); } async function activateExtension(_operationId: string, context: ExtensionContext): Promise { @@ -48,6 +50,7 @@ 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)); + context.subscriptions.push(window.onDidChangeActiveTextEditor((e: TextEditor | undefined) => { setContextForReloadProject(e?.document); })); @@ -121,4 +124,4 @@ function setContextForReloadProject(document: TextDocument | undefined): void { } } contextManager.setContextValue(Context.RELOAD_PROJECT_ACTIVE, false); -} +} \ No newline at end of file diff --git a/src/java/jdtls.ts b/src/java/jdtls.ts index c1388253..d4e1b42b 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 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[], destination: string, terminalId: string, token: CancellationToken): Promise { return commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_GENERATEJAR, @@ -97,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