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 77d2d256..b41aaa6c 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 @@ -6,12 +6,21 @@ 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; @@ -21,6 +30,179 @@ import com.microsoft.jdtls.ext.core.JdtlsExtActivator; 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"; @@ -54,6 +236,9 @@ public DependencyInfo(String key, String value) { * @return List of DependencyInfo containing key-value pairs of project information */ public static List resolveProjectDependencies(String fileUri, IProgressMonitor monitor) { + // Ensure listeners are registered for cache invalidation + ensureListenersRegistered(); + List result = new ArrayList<>(); try { @@ -73,6 +258,19 @@ public static List resolveProjectDependencies(String fileUri, IP return result; } + // Generate cache key based on project location + String cacheKey = project.getLocation().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); + } + // Add basic project information addBasicProjectInfo(result, project, javaProject); @@ -81,6 +279,9 @@ public static List resolveProjectDependencies(String fileUri, IP // 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);