Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

Expand Down Expand Up @@ -53,12 +56,22 @@ public class JavaDataModelListenerManager {

private static final JavaDataModelListenerManager INSTANCE = new JavaDataModelListenerManager();

// Debounce delay to group multiple file changes into a single notification
private static final long DEBOUNCE_DELAY_MS = 2000;

public static JavaDataModelListenerManager getInstance() {
return INSTANCE;
}

private class QuteListener implements IElementChangedListener {

// Lock for synchronizing access to pending event state
private final Object eventLock = new Object();
// Event waiting to be fired after debounce delay
private JavaDataModelChangeEvent pendingEvent = null;
// Scheduled task for firing the pending event
private ScheduledFuture<?> scheduledNotification = null;

@Override
public void elementChanged(ElementChangedEvent event) {
if (listeners.isEmpty()) {
Expand Down Expand Up @@ -87,7 +100,7 @@ public void elementChanged(ElementChangedEvent event) {
// }
// ]
JavaDataModelChangeEvent mpEvent = new JavaDataModelChangeEvent();
mpEvent.setProjects(new HashSet(changedProjects.values()));
mpEvent.setProjects(new HashSet<>(changedProjects.values()));
fireAsyncEvent(mpEvent);
}

Expand Down Expand Up @@ -192,22 +205,88 @@ private boolean isClasspathChanged(int flags) {
}

private void fireAsyncEvent(JavaDataModelChangeEvent event) {
// IMPORTANT: The LSP notification 'qute/javaDataModelChanged' must be
// executed
// in background otherwise it breaks everything (JDT LS for Java completion,
// hover, etc are broken)
CompletableFuture.runAsync(() -> {
for (IJavaDataModelChangedListener listener : listeners) {
try {
listener.dataModelChanged(event);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE, "Error while sending LSP 'qute/javaDataModelChanged' notification",
e);
synchronized (eventLock) {
// Merge with pending event if one exists
if (pendingEvent == null) {
pendingEvent = event;
} else {
mergeEvents(pendingEvent, event);
}

// Cancel previous timer if it exists
if (scheduledNotification != null && !scheduledNotification.isDone()) {
scheduledNotification.cancel(false);
}

// Schedule notification after debounce delay
scheduledNotification = scheduler.schedule(() -> {
JavaDataModelChangeEvent eventToFire;
synchronized (eventLock) {
eventToFire = pendingEvent;
pendingEvent = null;
scheduledNotification = null;
}

if (eventToFire != null) {
notifyListeners(eventToFire);
}
}, DEBOUNCE_DELAY_MS, TimeUnit.MILLISECONDS);
}
}

/**
* Merges two events by combining their project information. For each project,
* combines the source class names from both events.
*/
private void mergeEvents(JavaDataModelChangeEvent target, JavaDataModelChangeEvent source) {
if (source.getProjects() == null) {
return;
}

if (target.getProjects() == null) {
target.setProjects(new HashSet<>());
}

// Create a map for quick lookup of existing project info by URI
Map<String, JavaDataModelChangeEvent.ProjectChangeInfo> targetProjectMap = new HashMap<>();
for (JavaDataModelChangeEvent.ProjectChangeInfo projectInfo : target.getProjects()) {
targetProjectMap.put(projectInfo.getUri(), projectInfo);
}

// Merge source projects into target
for (JavaDataModelChangeEvent.ProjectChangeInfo sourceProject : source.getProjects()) {
String uri = sourceProject.getUri();
JavaDataModelChangeEvent.ProjectChangeInfo targetProject = targetProjectMap.get(uri);

if (targetProject == null) {
// Project doesn't exist in target, add it
target.getProjects().add(sourceProject);
targetProjectMap.put(uri, sourceProject);
} else {
// Project exists, merge the sources
if (sourceProject.getSources() != null) {
if (targetProject.getSources() == null) {
targetProject.setSources(new HashSet<>());
}
targetProject.getSources().addAll(sourceProject.getSources());
}
}
});
}
}

/**
* Notifies all registered listeners about the java data model change event.
*/
private void notifyListeners(JavaDataModelChangeEvent event) {
for (IJavaDataModelChangedListener listener : listeners) {
try {
listener.dataModelChanged(event);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE, "Error while sending LSP 'qute/dataModelChanged' notification", e);
}
}
}
}

}
Expand All @@ -216,12 +295,14 @@ private void fireAsyncEvent(JavaDataModelChangeEvent event) {

private final Set<IJavaDataModelChangedListener> listeners;

private ScheduledExecutorService scheduler;

private JavaDataModelListenerManager() {
listeners = new HashSet<>();
}

/**
* Add the given MicroProfile properties changed listener.
* Add the given Java data model changed listener.
*
* @param listener the listener to add
*/
Expand All @@ -232,7 +313,7 @@ public void addJavaDataModelChangedListener(IJavaDataModelChangedListener listen
}

/**
* Remove the given MicroProfile properties changed listener.
* Remove the given Java data model changed listener.
*
* @param listener the listener to remove
*/
Expand All @@ -249,6 +330,11 @@ public synchronized void initialize() {
if (quteListener != null) {
return;
}
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "Qute_JavaDataModel-Debouncer");
t.setDaemon(true);
return t;
});
this.quteListener = new QuteListener();
JavaCore.addElementChangedListener(quteListener);
}
Expand All @@ -261,6 +347,18 @@ public synchronized void destroy() {
JavaCore.removeElementChangedListener(quteListener);
this.quteListener = null;
}
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
scheduler = null;
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ private static synchronized void initialize() {
return;
}
// Add a classpath changed listener to execute client command
// "qute/javaDataModelChanged"
// "qute/dataModelChanged"
JavaDataModelListenerManager.getInstance().addJavaDataModelChangedListener(LISTENER);
initialized = true;
}
Expand Down
Loading