1515import java .util .HashSet ;
1616import java .util .Map ;
1717import 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 ;
1922import java .util .logging .Level ;
2023import 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+ }
0 commit comments