Skip to content

Commit 5f777ab

Browse files
committed
Send 'qute/dataModelChanged' notification with debounce mode
Signed-off-by: azerr <azerr@redhat.com>
1 parent e81ea07 commit 5f777ab

2 files changed

Lines changed: 117 additions & 19 deletions

File tree

qute.jdt/com.redhat.qute.jdt/src/main/java/com/redhat/qute/jdt/internal/JavaDataModelListenerManager.java

Lines changed: 116 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
import java.util.HashSet;
1616
import java.util.Map;
1717
import java.util.Set;
18-
import java.util.concurrent.CompletableFuture;
18+
import java.util.concurrent.Executors;
19+
import java.util.concurrent.ScheduledExecutorService;
20+
import java.util.concurrent.ScheduledFuture;
21+
import java.util.concurrent.TimeUnit;
1922
import java.util.logging.Level;
2023
import java.util.logging.Logger;
2124

@@ -53,12 +56,22 @@ public class JavaDataModelListenerManager {
5356

5457
private static final JavaDataModelListenerManager INSTANCE = new JavaDataModelListenerManager();
5558

59+
// Debounce delay to group multiple file changes into a single notification
60+
private static final long DEBOUNCE_DELAY_MS = 2000;
61+
5662
public static JavaDataModelListenerManager getInstance() {
5763
return INSTANCE;
5864
}
5965

6066
private class QuteListener implements IElementChangedListener {
6167

68+
// Lock for synchronizing access to pending event state
69+
private final Object eventLock = new Object();
70+
// Event waiting to be fired after debounce delay
71+
private JavaDataModelChangeEvent pendingEvent = null;
72+
// Scheduled task for firing the pending event
73+
private ScheduledFuture<?> scheduledNotification = null;
74+
6275
@Override
6376
public void elementChanged(ElementChangedEvent event) {
6477
if (listeners.isEmpty()) {
@@ -87,7 +100,7 @@ public void elementChanged(ElementChangedEvent event) {
87100
// }
88101
// ]
89102
JavaDataModelChangeEvent mpEvent = new JavaDataModelChangeEvent();
90-
mpEvent.setProjects(new HashSet(changedProjects.values()));
103+
mpEvent.setProjects(new HashSet<>(changedProjects.values()));
91104
fireAsyncEvent(mpEvent);
92105
}
93106

@@ -192,22 +205,88 @@ private boolean isClasspathChanged(int flags) {
192205
}
193206

194207
private void fireAsyncEvent(JavaDataModelChangeEvent event) {
195-
// IMPORTANT: The LSP notification 'qute/javaDataModelChanged' must be
196-
// executed
197-
// in background otherwise it breaks everything (JDT LS for Java completion,
198-
// hover, etc are broken)
199-
CompletableFuture.runAsync(() -> {
200-
for (IJavaDataModelChangedListener listener : listeners) {
201-
try {
202-
listener.dataModelChanged(event);
203-
} catch (Exception e) {
204-
if (LOGGER.isLoggable(Level.SEVERE)) {
205-
LOGGER.log(Level.SEVERE, "Error while sending LSP 'qute/javaDataModelChanged' notification",
206-
e);
208+
synchronized (eventLock) {
209+
// Merge with pending event if one exists
210+
if (pendingEvent == null) {
211+
pendingEvent = event;
212+
} else {
213+
mergeEvents(pendingEvent, event);
214+
}
215+
216+
// Cancel previous timer if it exists
217+
if (scheduledNotification != null && !scheduledNotification.isDone()) {
218+
scheduledNotification.cancel(false);
219+
}
220+
221+
// Schedule notification after debounce delay
222+
scheduledNotification = scheduler.schedule(() -> {
223+
JavaDataModelChangeEvent eventToFire;
224+
synchronized (eventLock) {
225+
eventToFire = pendingEvent;
226+
pendingEvent = null;
227+
scheduledNotification = null;
228+
}
229+
230+
if (eventToFire != null) {
231+
notifyListeners(eventToFire);
232+
}
233+
}, DEBOUNCE_DELAY_MS, TimeUnit.MILLISECONDS);
234+
}
235+
}
236+
237+
/**
238+
* Merges two events by combining their project information. For each project,
239+
* combines the source class names from both events.
240+
*/
241+
private void mergeEvents(JavaDataModelChangeEvent target, JavaDataModelChangeEvent source) {
242+
if (source.getProjects() == null) {
243+
return;
244+
}
245+
246+
if (target.getProjects() == null) {
247+
target.setProjects(new HashSet<>());
248+
}
249+
250+
// Create a map for quick lookup of existing project info by URI
251+
Map<String, JavaDataModelChangeEvent.ProjectChangeInfo> targetProjectMap = new HashMap<>();
252+
for (JavaDataModelChangeEvent.ProjectChangeInfo projectInfo : target.getProjects()) {
253+
targetProjectMap.put(projectInfo.getUri(), projectInfo);
254+
}
255+
256+
// Merge source projects into target
257+
for (JavaDataModelChangeEvent.ProjectChangeInfo sourceProject : source.getProjects()) {
258+
String uri = sourceProject.getUri();
259+
JavaDataModelChangeEvent.ProjectChangeInfo targetProject = targetProjectMap.get(uri);
260+
261+
if (targetProject == null) {
262+
// Project doesn't exist in target, add it
263+
target.getProjects().add(sourceProject);
264+
targetProjectMap.put(uri, sourceProject);
265+
} else {
266+
// Project exists, merge the sources
267+
if (sourceProject.getSources() != null) {
268+
if (targetProject.getSources() == null) {
269+
targetProject.setSources(new HashSet<>());
207270
}
271+
targetProject.getSources().addAll(sourceProject.getSources());
208272
}
209273
}
210-
});
274+
}
275+
}
276+
277+
/**
278+
* Notifies all registered listeners about the java data model change event.
279+
*/
280+
private void notifyListeners(JavaDataModelChangeEvent event) {
281+
for (IJavaDataModelChangedListener listener : listeners) {
282+
try {
283+
listener.dataModelChanged(event);
284+
} catch (Exception e) {
285+
if (LOGGER.isLoggable(Level.SEVERE)) {
286+
LOGGER.log(Level.SEVERE, "Error while sending LSP 'qute/dataModelChanged' notification", e);
287+
}
288+
}
289+
}
211290
}
212291

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

217296
private final Set<IJavaDataModelChangedListener> listeners;
218297

298+
private ScheduledExecutorService scheduler;
299+
219300
private JavaDataModelListenerManager() {
220301
listeners = new HashSet<>();
221302
}
222303

223304
/**
224-
* Add the given MicroProfile properties changed listener.
305+
* Add the given Java data model changed listener.
225306
*
226307
* @param listener the listener to add
227308
*/
@@ -232,7 +313,7 @@ public void addJavaDataModelChangedListener(IJavaDataModelChangedListener listen
232313
}
233314

234315
/**
235-
* Remove the given MicroProfile properties changed listener.
316+
* Remove the given Java data model changed listener.
236317
*
237318
* @param listener the listener to remove
238319
*/
@@ -249,6 +330,11 @@ public synchronized void initialize() {
249330
if (quteListener != null) {
250331
return;
251332
}
333+
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
334+
Thread t = new Thread(r, "Qute_JavaDataModel-Debouncer");
335+
t.setDaemon(true);
336+
return t;
337+
});
252338
this.quteListener = new QuteListener();
253339
JavaCore.addElementChangedListener(quteListener);
254340
}
@@ -261,6 +347,18 @@ public synchronized void destroy() {
261347
JavaCore.removeElementChangedListener(quteListener);
262348
this.quteListener = null;
263349
}
350+
if (scheduler != null && !scheduler.isShutdown()) {
351+
scheduler.shutdown();
352+
try {
353+
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
354+
scheduler.shutdownNow();
355+
}
356+
} catch (InterruptedException e) {
357+
scheduler.shutdownNow();
358+
Thread.currentThread().interrupt();
359+
}
360+
scheduler = null;
361+
}
264362
}
265363

266-
}
364+
}

qute.jdt/com.redhat.qute.jdt/src/main/java/com/redhat/qute/jdt/internal/ls/AbstractQuteDelegateCommandHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ private static synchronized void initialize() {
6262
return;
6363
}
6464
// Add a classpath changed listener to execute client command
65-
// "qute/javaDataModelChanged"
65+
// "qute/dataModelChanged"
6666
JavaDataModelListenerManager.getInstance().addJavaDataModelChangedListener(LISTENER);
6767
initialized = true;
6868
}

0 commit comments

Comments
 (0)