diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jdtls/JdtLsIndex.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jdtls/JdtLsIndex.java index 286aa05f2..857526555 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jdtls/JdtLsIndex.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jdtls/JdtLsIndex.java @@ -23,7 +23,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.commons.java.ClasspathIndex; import org.springframework.ide.vscode.commons.java.IJavaModuleData; -import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.java.IType; import org.springframework.ide.vscode.commons.java.JavaUtils; import org.springframework.ide.vscode.commons.javadoc.JdtLsJavadocProvider; @@ -65,31 +64,23 @@ public class JdtLsIndex implements ClasspathIndex { final private Cache>> supertypesCache = CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.SECONDS).build(); final private Cache>> subtypesCache = CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.SECONDS).build(); - - final private Listener projectListener = new Listener() { - - @Override - public void deleted(IJavaProject project) { - // ignore - } - - @Override - public void created(IJavaProject project) { - // ignore - } - - @Override - public void changed(IJavaProject project) { - binaryTypeCache.invalidateAll(); - sourceTypeCache.invalidateAll(); - } - }; + + final private Listener projectListener; public JdtLsIndex(STS4LanguageClient client, URI projectUri, ProjectObserver projectObserver) { this.client = client; this.projectUri = projectUri; this.projectObserver = projectObserver; this.javadocProvider = new JdtLsJavadocProvider(client, projectUri.toString()); + + this.projectListener = ProjectObserver.onAny(project -> { + if (Objects.equals(project.getLocationUri(), projectUri)) { + binaryTypeCache.invalidateAll(); + sourceTypeCache.invalidateAll(); + supertypesCache.invalidateAll(); + subtypesCache.invalidateAll(); + } + }); this.projectObserver.addListener(projectListener); } diff --git a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/ClasspathListenerManager.java b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/ClasspathListenerManager.java index 1f8cfeca3..56b412b1b 100644 --- a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/ClasspathListenerManager.java +++ b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/ClasspathListenerManager.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Pivotal, Inc. + * Copyright (c) 2019 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -13,6 +13,11 @@ package org.springframework.tooling.jdt.ls.commons.classpath; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; @@ -51,7 +56,8 @@ public class ClasspathListenerManager { }; public interface ClasspathListener { - public abstract void classpathChanged(IJavaProject jp); + void classpathChanged(IJavaProject jp); + default void projectBuilt(IJavaProject jp) {}; } private class MyListener implements IElementChangedListener { @@ -102,12 +108,31 @@ public class ClasspathListenerManager { private ClasspathListener listener; private MyListener myListener; private final Logger logger; + + private IResourceChangeListener workspaceListener = (event) -> { + if (event.getSource() instanceof IProject) { + projectBuilt((IProject) event.getSource()); + } else if (event.getSource() instanceof IWorkspace) { + for (IProject p : ((IWorkspace)event.getSource()).getRoot().getProjects()) { + projectBuilt(p); + } + } + }; public ClasspathListenerManager(Logger logger, ClasspathListener listener) { this.logger = logger; logger.log("Setting up ClasspathListenerManager"); this.listener = listener; JavaCore.addElementChangedListener(myListener=new MyListener(), ElementChangedEvent.POST_CHANGE); + final IWorkspace workspace = ResourcesPlugin.getWorkspace(); + workspace.addResourceChangeListener(workspaceListener, IResourceChangeEvent.POST_BUILD); + } + + private void projectBuilt(IProject project) { + IJavaProject jp = JavaCore.create(project); + if (jp != null) { + listener.projectBuilt(jp); + } } public void dispose() { @@ -115,6 +140,7 @@ public class ClasspathListenerManager { JavaCore.removeElementChangedListener(myListener); myListener = null; } + ResourcesPlugin.getWorkspace().removeResourceChangeListener(workspaceListener); } } \ No newline at end of file diff --git a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/ReusableClasspathListenerHandler.java b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/ReusableClasspathListenerHandler.java index 95be46750..d67a1d366 100644 --- a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/ReusableClasspathListenerHandler.java +++ b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/ReusableClasspathListenerHandler.java @@ -65,6 +65,12 @@ public class ReusableClasspathListenerHandler { public void classpathChanged(IJavaProject jp) { sendNotification(jp, subscribers.keySet()); } + + @Override + public void projectBuilt(IJavaProject jp) { + sendNotificationOnProjectBuilt(jp, subscribers.keySet()); + } + }); subscribers.put(callbackCommandId, new SendClasspathNotificationsJob(logger, conn, callbackCommandId, isBatched)); logger.log("subsribers = " + subscribers); @@ -106,6 +112,16 @@ public class ReusableClasspathListenerHandler { } } + private synchronized void sendNotificationOnProjectBuilt(IJavaProject jp, Collection callbackIds) { + if (subscribers!=null) { + for (String callbackId : callbackIds) { + SendClasspathNotificationsJob sendNotificationJob = subscribers.get(callbackId); + sendNotificationJob.builtProjectQueue.add(jp); + sendNotificationJob.schedule(); + } + } + } + public synchronized void unsubscribe(String callbackCommandId) { logger.log("unsubscribing from classpath changes: " + callbackCommandId); if (subscribers != null) { diff --git a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/SendClasspathNotificationsJob.java b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/SendClasspathNotificationsJob.java index 9ba26da2a..61a86f6b5 100644 --- a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/SendClasspathNotificationsJob.java +++ b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/SendClasspathNotificationsJob.java @@ -14,9 +14,11 @@ import java.io.File; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Queue; +import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import org.eclipse.core.runtime.IProgressMonitor; @@ -26,6 +28,7 @@ import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; import org.springframework.ide.vscode.commons.protocol.java.Classpath; +import org.springframework.ide.vscode.commons.protocol.java.Classpath.CPE; import org.springframework.tooling.jdt.ls.commons.Logger; import com.google.common.collect.ImmutableList; @@ -49,7 +52,10 @@ public class SendClasspathNotificationsJob extends Job { */ private Map projectLocations = new HashMap<>(); public final Queue queue = new ConcurrentLinkedQueue<>(); - + public final Queue builtProjectQueue = new ConcurrentLinkedQueue<>(); + + private final Set notReadyProjects = new HashSet<>(); + public SendClasspathNotificationsJob(Logger logger, ClientCommandExecutor conn, String callbackId, boolean isBatched) { super("Send Classpath Notifications"); this.logger = logger; @@ -99,8 +105,19 @@ public class SendClasspathNotificationsJob extends Job { protected IStatus run(IProgressMonitor monitor) { synchronized (projectLocations) { //Could use some Eclipse job rule. But its really a bit of a PITA to create the right one. try { + // Try to see if classpath needs to be sent for the projects that have been + // built since classpath JAR may not have existed (not downloaded) at the time + // of classpath changed event + for (IJavaProject jp = builtProjectQueue.poll(); jp!=null; jp = builtProjectQueue.poll()) { + if (notReadyProjects.remove(jp)) { + queue.add(jp); + } + } for (IJavaProject jp = queue.poll(); jp!=null; jp = queue.poll()) { logger.log("Preparing classpath changed notification " + jp.getElementName()); + // Project wasn't ready before but now it's about to be processed for Classpath again. + // Remove it from the set of not readt projects + notReadyProjects.remove(jp); URI projectLoc = getProjectLocation(jp); if (projectLoc==null) { logger.log("Could not send event for project because no project location: "+jp.getElementName()); @@ -117,11 +134,26 @@ public class SendClasspathNotificationsJob extends Job { Classpath classpath = Classpath.EMPTY; if (deleted) { + // Project has been removed no need to keep in not ready projects set + notReadyProjects.remove(jp); // projectLocations.remove(projectName); } else { projectLocations.put(projectName, projectLoc); try { classpath = ClasspathUtil.resolve(jp, logger); + List filteredCPEs = new ArrayList<>(classpath.getEntries().size()); + for (CPE cpe : classpath.getEntries()) { + // Some classpath entries don't exist yet (to be downloaded during the build). Filter them out as these JARs won't be indexed until they exist + if (!Classpath.isBinary(cpe) || new File(cpe.getPath()).exists()) { + filteredCPEs.add(cpe); + } + } + if (filteredCPEs.size() != classpath.getEntries().size()) { + // If some entries in the classpath don't exist yet add the project to not ready projects set to process later when project is built + notReadyProjects.add(jp); + // Only send effective classpath that has all entries physically present. + classpath = new Classpath(filteredCPEs); + } } catch (Exception e) { logger.log(e); }