From 226d7c81372daaafe4e120af71c42723bff3b312 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Thu, 30 Oct 2025 15:22:48 +0800 Subject: [PATCH 1/3] feat: add cache to project resolver --- .../ext/core/parser/ProjectResolver.java | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) 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..bef73372 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; @@ -18,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"; @@ -50,6 +235,9 @@ public DependencyInfo(String key, String value) { * @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 +252,24 @@ 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; } + // 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); + } + // Add basic project information addBasicProjectInfo(result, project, javaProject); @@ -76,6 +278,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); From f968cd3ef7cc6e4cb7dd5b60da4fadc688892826 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Mon, 3 Nov 2025 13:35:52 +0800 Subject: [PATCH 2/3] perf: update project location as key --- .../com/microsoft/jdtls/ext/core/parser/ProjectResolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bef73372..880e0280 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 @@ -258,7 +258,7 @@ public static List resolveProjectDependencies(String projectUri, } // Generate cache key based on project location - String cacheKey = projectPath.toOSString(); + String cacheKey = project.getLocation().toOSString(); // Calculate current classpath hash for validation long currentClasspathHash = calculateClasspathHash(javaProject); From c734c917bb57517e60193aed74ded37153ec14ae Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Mon, 3 Nov 2025 15:36:41 +0800 Subject: [PATCH 3/3] fix: fix error --- .../com/microsoft/jdtls/ext/core/parser/ProjectResolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 eb673245..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 @@ -235,7 +235,7 @@ public DependencyInfo(String key, String value) { * @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) { + public static List resolveProjectDependencies(String fileUri, IProgressMonitor monitor) { // Ensure listeners are registered for cache invalidation ensureListenersRegistered();