3131import java .util .Map .Entry ;
3232import java .util .Objects ;
3333import java .util .concurrent .atomic .AtomicReference ;
34+ import java .util .function .Consumer ;
3435import java .util .function .Function ;
3536import java .util .regex .Matcher ;
3637import java .util .regex .Pattern ;
@@ -104,6 +105,12 @@ public abstract class QuickAccessContents {
104105 /** Trailing-edge debounce window collapsing a burst of shell resizes. */
105106 private static final int RESIZE_DEBOUNCE_MS = 100 ;
106107
108+ /**
109+ * Family of the background job that computes the matching entries. Lets tests
110+ * join on the streaming compute deterministically instead of polling the table.
111+ */
112+ public static final Object COMPUTE_JOB_FAMILY = new Object ();
113+
107114 protected Text filterText ;
108115
109116 private final QuickAccessProvider [] providers ;
@@ -181,15 +188,49 @@ public void updateProposals(String filterInput) {
181188 lastComputedItemCount = maxNumberOfItemsInTable ;
182189 // Captured to detect a show-all toggle while this compute runs.
183190 final boolean requestShowAllMatches = showAllMatches ;
184- AtomicReference <List <QuickAccessEntry >[]> entries = new AtomicReference <>();
185- final Job currentComputeEntriesJob = Job .create (computingMessage , theMonitor -> {
186- entries .set (
187- computeMatchingEntries (filter , perfectMatch , maxNumberOfItemsInTable , theMonitor ));
188- if (Policy .DEBUG_QUICK_ACCESS ) {
189- trace ("Computed entries: " + toIds (entries )); //$NON-NLS-1$
191+ // Set once the first results are rendered, so the "computing" feedback never
192+ // flashes over content that has already streamed in.
193+ boolean [] rendered = { false };
194+ AtomicReference <UIJob > feedbackJobRef = new AtomicReference <>();
195+ final Job currentComputeEntriesJob = new Job (computingMessage ) {
196+ @ Override
197+ protected IStatus run (IProgressMonitor theMonitor ) {
198+ // Query providers one at a time and re-render as each returns, so a slow
199+ // provider neither holds the UI thread for the whole batch nor delays the
200+ // results that are already available.
201+ computeMatchingEntries (filter , perfectMatch , maxNumberOfItemsInTable , theMonitor , entries -> {
202+ if (table .isDisposed ()) {
203+ return ;
204+ }
205+ display .asyncExec (() -> {
206+ if (table .isDisposed () || filterText == null || filterText .isDisposed ()) {
207+ return ;
208+ }
209+ // Apply only while the results still match the current filter and
210+ // show-all state, and this compute has not been superseded.
211+ if (theMonitor .isCanceled () || !filter .equals (filterText .getText ().toLowerCase ())
212+ || requestShowAllMatches != showAllMatches ) {
213+ return ;
214+ }
215+ if (Policy .DEBUG_QUICK_ACCESS ) {
216+ trace ("Setting quick access contents: " + toIds (entries )); //$NON-NLS-1$
217+ }
218+ rendered [0 ] = true ;
219+ UIJob feedbackJob = feedbackJobRef .get ();
220+ if (feedbackJob != null ) {
221+ feedbackJob .cancel ();
222+ }
223+ refreshTable (perfectMatch , entries , filter );
224+ });
225+ });
226+ return theMonitor .isCanceled () ? Status .CANCEL_STATUS : Status .OK_STATUS ;
190227 }
191- return theMonitor .isCanceled () ? Status .CANCEL_STATUS : Status .OK_STATUS ;
192- });
228+
229+ @ Override
230+ public boolean belongsTo (Object family ) {
231+ return family == COMPUTE_JOB_FAMILY ;
232+ }
233+ };
193234 currentComputeEntriesJob .setPriority (Job .INTERACTIVE );
194235 if (Policy .DEBUG_QUICK_ACCESS ) {
195236 trace ("Will compute proposals with Job: " + currentComputeEntriesJob ); //$NON-NLS-1$
@@ -199,41 +240,24 @@ public void updateProposals(String filterInput) {
199240 UIJob computingFeedbackJob = new UIJob (table .getDisplay (), QuickAccessMessages .QuickAccessContents_computeMatchingEntries_displayFeedback_jobName ) {
200241 @ Override
201242 public IStatus runInUIThread (IProgressMonitor monitor ) {
202- if (currentComputeEntriesJob .getResult () == null && !monitor .isCanceled () && !table .isDisposed ()) {
243+ if (!rendered [0 ] && currentComputeEntriesJob .getResult () == null && !monitor .isCanceled ()
244+ && !table .isDisposed ()) {
203245 showHintText (computingMessage , grayColor );
204246 return Status .OK_STATUS ;
205247 }
206248 return Status .CANCEL_STATUS ;
207249 }
208250 };
251+ feedbackJobRef .set (computingFeedbackJob );
209252 currentComputeEntriesJob .addJobChangeListener (new JobChangeAdapter () {
210253 @ Override
211254 public void done (IJobChangeEvent event ) {
212255 if (Policy .DEBUG_QUICK_ACCESS ) {
213256 trace ("Compute entries Job result: " + event .getResult () + //$NON-NLS-1$
214257 ", Job: " + currentComputeEntriesJob + //$NON-NLS-1$
215- ", last proposals Job: " + computeProposalsJob + //$NON-NLS-1$
216- ", entries: " + toIds (entries )); //$NON-NLS-1$
258+ ", last proposals Job: " + computeProposalsJob ); //$NON-NLS-1$
217259 }
218260 computingFeedbackJob .cancel ();
219- if (event .getResult ().isOK () && !table .isDisposed ()) {
220- display .asyncExec (() -> {
221- if (table .isDisposed () || filterText == null || filterText .isDisposed ()) {
222- return ;
223- }
224- // Apply if the results still match the current filter and show-all,
225- // not only when this is the last scheduled job.
226- if (!filter .equals (filterText .getText ().toLowerCase ())
227- || requestShowAllMatches != showAllMatches ) {
228- return ;
229- }
230- if (Policy .DEBUG_QUICK_ACCESS ) {
231- trace ("Setting quick access contents: " + toIds (entries )); //$NON-NLS-1$
232- }
233- computingFeedbackJob .cancel ();
234- refreshTable (perfectMatch , entries .get (), filter );
235- });
236- }
237261 }
238262 });
239263 this .computeProposalsJob = currentComputeEntriesJob ;
@@ -399,59 +423,46 @@ protected int getNumberOfFilteredResults() {
399423 }
400424
401425 /**
402- * Queries every provider that requires UI access in a single UI-thread pass and
403- * returns their sorted elements keyed by provider. Batching the queries avoids a
404- * blocking worker-to-UI round-trip per provider on each keystroke .
426+ * Collects one provider's matching elements. Providers that require UI access are
427+ * queried on the display thread one at a time, so the UI thread is released
428+ * between providers instead of being held for the whole batch .
405429 */
406- private Map < QuickAccessProvider , List <QuickAccessElement >> computeUiProviderElements ( String filter , String category ,
430+ private List <QuickAccessElement > collectProviderElements ( QuickAccessProvider provider , String filter ,
407431 IProgressMonitor monitor ) {
408- List <QuickAccessProvider > uiProviders = new ArrayList <>();
409- for (QuickAccessProvider provider : providers ) {
410- if (!provider .requiresUiAccess ()) {
411- continue ;
412- }
413- boolean isPreviousPickProvider = provider instanceof PreviousPicksProvider ;
414- if (category != null && !category .equalsIgnoreCase (provider .getName ()) && !isPreviousPickProvider ) {
415- continue ;
416- }
417- if (!filter .isEmpty () || isPreviousPickProvider || showAllMatches ) {
418- uiProviders .add (provider );
419- }
432+ if (!provider .requiresUiAccess ()) {
433+ return Arrays .asList (provider .getElementsSorted (filter , monitor ));
420434 }
421- if (uiProviders . isEmpty () || monitor .isCanceled () || table == null || table .isDisposed ()) {
422- return Collections .emptyMap ();
435+ if (monitor .isCanceled () || table == null || table .isDisposed ()) {
436+ return Collections .emptyList ();
423437 }
424- Map < QuickAccessProvider , List <QuickAccessElement >> result = new HashMap <>();
438+ AtomicReference < List <QuickAccessElement >> result = new AtomicReference <>(Collections . emptyList () );
425439 table .getDisplay ().syncExec (() -> {
426- for (QuickAccessProvider provider : uiProviders ) {
427- if (monitor .isCanceled ()) {
428- return ;
429- }
430- try {
431- result .put (provider , Arrays .asList (provider .getElementsSorted (filter , monitor )));
432- } catch (RuntimeException e ) {
433- WorkbenchPlugin .log (e );
434- }
440+ if (monitor .isCanceled () || table .isDisposed ()) {
441+ return ;
442+ }
443+ try {
444+ result .set (Arrays .asList (provider .getElementsSorted (filter , monitor )));
445+ } catch (RuntimeException e ) {
446+ WorkbenchPlugin .log (e );
435447 }
436448 });
437- return result ;
449+ return result . get () ;
438450 }
439451
440452 /**
441- * Returns a list per provider containing matching {@link QuickAccessEntry} that
442- * should be displayed in the table given a text filter and a perfect match
443- * entry that should be given priority. The number of items returned is affected
444- * by {@link #getShowAllMatches()} and the size of the table's composite.
453+ * Queries each provider in turn and streams the matching entries to {@code render}
454+ * after every provider that contributes, so results appear as they are computed
455+ * rather than only once the slowest provider has finished. The number of entries
456+ * is affected by {@link #getShowAllMatches()} and the size of the table's
457+ * composite.
445458 *
446459 * @param filter the string text filter to apply, possibly empty
447460 * @param perfectMatch a quick access element that should be given priority or
448461 * <code>null</code>
449- *
450- * @return the array of lists (one per provider) contains the quick access
451- * entries that should be added to the table, possibly empty
462+ * @param render receives one snapshot (a list per provider) per re-render
452463 */
453- private List < QuickAccessEntry >[] computeMatchingEntries (String filter , QuickAccessElement perfectMatch ,
454- int maxNumberOfItemsInTable , IProgressMonitor aMonitor ) {
464+ private void computeMatchingEntries (String filter , QuickAccessElement perfectMatch , int maxNumberOfItemsInTable ,
465+ IProgressMonitor aMonitor , Consumer < List < QuickAccessEntry >[]> render ) {
455466 if (aMonitor == null ) {
456467 aMonitor = new NullProgressMonitor ();
457468 }
@@ -464,41 +475,58 @@ private List<QuickAccessEntry>[] computeMatchingEntries(String filter, QuickAcce
464475 }
465476 final String finalFilter = filter ;
466477
467- // Query providers that must run on the UI thread once, in a single UI pass,
468- // rather than a separate blocking worker-to-UI round-trip per provider.
469- Map <QuickAccessProvider , List <QuickAccessElement >> uiProviderElements = computeUiProviderElements (finalFilter ,
470- category , aMonitor );
471-
472- // collect matching elements
478+ // Collect elements provider by provider and re-render after each, so results
479+ // stream into the table.
473480 LinkedHashMap <QuickAccessProvider , List <QuickAccessElement >> elementsForProviders = new LinkedHashMap <>(
474481 providers .length );
482+ boolean anyRendered = false ;
475483 for (QuickAccessProvider provider : providers ) {
476484 if (aMonitor .isCanceled ()) {
477- break ;
485+ return ;
478486 }
479487 boolean isPreviousPickProvider = provider instanceof PreviousPicksProvider ;
480488 // skip if filter contains a category, and current provider isn't this category
481489 if (category != null && !category .equalsIgnoreCase (provider .getName ()) && !isPreviousPickProvider ) {
482490 continue ;
483491 }
484- if (!filter .isEmpty () || isPreviousPickProvider || showAllMatches ) {
485- List <QuickAccessElement > sortedElements ;
486- if (provider .requiresUiAccess ()) {
487- sortedElements = uiProviderElements .get (provider );
488- } else {
489- sortedElements = Arrays .asList (provider .getElementsSorted (filter , aMonitor ));
490- }
491- if (sortedElements == null ) {
492- sortedElements = Collections .emptyList ();
493- }
494- if (!(provider instanceof PreviousPicksProvider )) {
495- for (QuickAccessElement element : sortedElements ) {
496- elementsToProviders .put (element , provider );
497- }
492+ if (finalFilter .isEmpty () && !isPreviousPickProvider && !showAllMatches ) {
493+ continue ;
494+ }
495+ List <QuickAccessElement > sortedElements = collectProviderElements (provider , finalFilter , aMonitor );
496+ if (sortedElements == null ) {
497+ sortedElements = Collections .emptyList ();
498+ }
499+ if (!isPreviousPickProvider ) {
500+ for (QuickAccessElement element : sortedElements ) {
501+ elementsToProviders .put (element , provider );
498502 }
499- elementsForProviders .put (provider , new ArrayList <>(sortedElements ));
500503 }
504+ elementsForProviders .put (provider , new ArrayList <>(sortedElements ));
505+ if (sortedElements .isEmpty ()) {
506+ continue ;
507+ }
508+ render .accept (assembleEntries (elementsForProviders , finalFilter , perfectMatch , maxNumberOfItemsInTable ,
509+ aMonitor ));
510+ anyRendered = true ;
501511 }
512+ // Nothing matched (or only a perfect match exists): render once so the table
513+ // reflects the final result rather than stale content.
514+ if (!anyRendered && !aMonitor .isCanceled ()) {
515+ render .accept (assembleEntries (elementsForProviders , finalFilter , perfectMatch , maxNumberOfItemsInTable ,
516+ aMonitor ));
517+ }
518+ }
519+
520+ /**
521+ * Builds the table snapshot (one list per provider, perfect match first) from the
522+ * elements gathered so far. Pure computation, safe to call repeatedly as more
523+ * providers report.
524+ */
525+ private List <QuickAccessEntry >[] assembleEntries (
526+ LinkedHashMap <QuickAccessProvider , List <QuickAccessElement >> collected , String finalFilter ,
527+ QuickAccessElement perfectMatch , int maxNumberOfItemsInTable , IProgressMonitor aMonitor ) {
528+ LinkedHashMap <QuickAccessProvider , List <QuickAccessElement >> elementsForProviders = new LinkedHashMap <>(
529+ collected );
502530
503531 // Sort out the Previous Pick
504532 List <String > prevPickIds = new ArrayList <>();
@@ -966,8 +994,8 @@ public Table getTable() {
966994 return table ;
967995 }
968996
969- private static List <String > toIds (AtomicReference < List <QuickAccessEntry >[]> entries ) {
970- return Stream .of (entries . get () ).flatMap (List ::stream ).map (e -> e .element .getId ()).toList ();
997+ private static List <String > toIds (List <QuickAccessEntry >[] entries ) {
998+ return Stream .of (entries ). filter ( Objects :: nonNull ).flatMap (List ::stream ).map (e -> e .element .getId ()).toList ();
971999 }
9721000
9731001 private static void trace (String message ) {
0 commit comments