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