diff --git a/microprofile.jdt/org.eclipse.lsp4mp.jdt.core/src/main/java/org/eclipse/lsp4mp/jdt/internal/core/MicroProfilePropertiesListenerManager.java b/microprofile.jdt/org.eclipse.lsp4mp.jdt.core/src/main/java/org/eclipse/lsp4mp/jdt/internal/core/MicroProfilePropertiesListenerManager.java index c91aaf08..6e7efc9b 100644 --- a/microprofile.jdt/org.eclipse.lsp4mp.jdt.core/src/main/java/org/eclipse/lsp4mp/jdt/internal/core/MicroProfilePropertiesListenerManager.java +++ b/microprofile.jdt/org.eclipse.lsp4mp.jdt.core/src/main/java/org/eclipse/lsp4mp/jdt/internal/core/MicroProfilePropertiesListenerManager.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2019-2020 Red Hat Inc. and others. +* Copyright (c) 2019-2026 Red Hat Inc. and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -15,7 +15,10 @@ import java.util.HashSet; 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; @@ -61,6 +64,9 @@ public class MicroProfilePropertiesListenerManager { private static final MicroProfilePropertiesListenerManager INSTANCE = new MicroProfilePropertiesListenerManager(); + // Debounce delay to group multiple file changes into a single notification + private static final long DEBOUNCE_DELAY_MS = 2000; + public static MicroProfilePropertiesListenerManager getInstance() { return INSTANCE; } @@ -70,6 +76,13 @@ private class MicroProfileListener private static final String JAVA_FILE_EXTENSION = "java"; + // Lock for synchronizing access to pending event state + private final Object eventLock = new Object(); + // Event waiting to be fired after debounce delay + private MicroProfilePropertiesChangeEvent pendingEvent = null; + // Scheduled task for firing the pending event + private ScheduledFuture scheduledNotification = null; + @Override public void elementChanged(ElementChangedEvent event) { if (listeners.isEmpty()) { @@ -188,22 +201,72 @@ public boolean visit(IResourceDelta delta) throws CoreException { } private void fireAsyncEvent(MicroProfilePropertiesChangeEvent event) { - // IMPORTANT: The LSP notification 'microprofile/propertiesChanged' must be - // executed - // in background otherwise it breaks everything (JDT LS for Java completion, - // hover, etc are broken) - CompletableFuture.runAsync(() -> { - for (IMicroProfilePropertiesChangedListener listener : listeners) { - try { - listener.propertiesChanged(event); - } catch (Exception e) { - if (LOGGER.isLoggable(Level.SEVERE)) { - LOGGER.log(Level.SEVERE, - "Error while sending LSP 'microprofile/propertiesChanged' 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(() -> { + MicroProfilePropertiesChangeEvent 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 URIs and taking the widest + * scope. Scope hierarchy: SOURCES_AND_DEPENDENCIES > ONLY_SOURCES > + * ONLY_CONFIG_FILES + */ + private void mergeEvents(MicroProfilePropertiesChangeEvent target, MicroProfilePropertiesChangeEvent source) { + // Merge project URIs + if (source.getProjectURIs() != null) { + if (target.getProjectURIs() == null) { + target.setProjectURIs(new HashSet<>()); + } + target.getProjectURIs().addAll(source.getProjectURIs()); + } + + // Handle event type - take the widest scope + if (source.getType() == MicroProfilePropertiesScope.SOURCES_AND_DEPENDENCIES) { + target.setType(MicroProfilePropertiesScope.SOURCES_AND_DEPENDENCIES); + } else if (source.getType() == MicroProfilePropertiesScope.ONLY_SOURCES + && target.getType() != MicroProfilePropertiesScope.SOURCES_AND_DEPENDENCIES) { + target.setType(MicroProfilePropertiesScope.ONLY_SOURCES); + } + } + + /** + * Notifies all registered listeners about the properties change event. + */ + private void notifyListeners(MicroProfilePropertiesChangeEvent event) { + for (IMicroProfilePropertiesChangedListener listener : listeners) { + try { + listener.propertiesChanged(event); + } catch (Exception e) { + if (LOGGER.isLoggable(Level.SEVERE)) { + LOGGER.log(Level.SEVERE, + "Error while sending LSP 'microprofile/propertiesChanged' notification", e); } } - }); + } } private boolean isJavaFile(IFile file) { @@ -224,6 +287,8 @@ private boolean isFileContentChanged(IResourceDelta delta) { private final Set listeners; + private ScheduledExecutorService scheduler; + private MicroProfilePropertiesListenerManager() { listeners = new HashSet<>(); } @@ -257,6 +322,11 @@ public synchronized void initialize() { if (microprofileListener != null) { return; } + this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "MicroProfile-Properties-Debouncer"); + t.setDaemon(true); + return t; + }); this.microprofileListener = new MicroProfileListener(); JavaCore.addElementChangedListener(microprofileListener); ResourcesPlugin.getWorkspace().addResourceChangeListener(microprofileListener, @@ -272,6 +342,18 @@ public synchronized void destroy() { ResourcesPlugin.getWorkspace().removeResourceChangeListener(microprofileListener); this.microprofileListener = 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; + } } -} +} \ No newline at end of file