Skip to content

Commit fa1a384

Browse files
committed
Cap Quick Access providers at a fair share of the visible slots
Since the continuous relevance ranking (86a07a0) fills the limited table with the globally highest-scored entries, a single prolific provider can flood out all other categories: typing "Error" lists workspace files instead of the Error Log view, because short file names outscore the category-suffixed view label. Keep the relevance ranking, but cap each provider at its fair share of the visible slots and give slots a provider does not use to the best remaining entries, so strong matches from every category stay visible. Fixes #4155
1 parent 2029c9b commit fa1a384

3 files changed

Lines changed: 86 additions & 3 deletions

File tree

bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/quickaccess/QuickAccessContents.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -574,8 +574,7 @@ private List<QuickAccessEntry>[] assembleEntries(
574574
}
575575
} else {
576576
int numberOfSlotsLeft = perfectMatch != null ? maxNumberOfItemsInTable - 1 : maxNumberOfItemsInTable;
577-
// Score every candidate and keep the globally highest-ranked entries, so a
578-
// strong match wins a slot regardless of which provider produced it
577+
// Score every candidate so the most relevant entries compete for the slots
579578
List<QuickAccessEntry> matched = new ArrayList<>();
580579
for (Entry<QuickAccessProvider, List<QuickAccessElement>> elementsPerProvider : elementsForProviders
581580
.entrySet()) {
@@ -595,7 +594,8 @@ private List<QuickAccessEntry>[] assembleEntries(
595594
}
596595
matched.sort(BY_RELEVANCE);
597596
int slots = Math.max(0, numberOfSlotsLeft);
598-
List<QuickAccessEntry> winners = matched.subList(0, Math.min(slots, matched.size()));
597+
// a prolific provider (e.g. workspace files) must not flood out the others
598+
List<QuickAccessEntry> winners = QuickAccessMatching.pickFairly(matched, entry -> entry.provider, slots);
599599
// Group the winners back per provider for the table, keeping providers in
600600
// registration order and entries in relevance order within each provider
601601
for (QuickAccessProvider provider : elementsForProviders.keySet()) {

bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/quickaccess/QuickAccessMatching.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111

1212
package org.eclipse.ui.internal.quickaccess;
1313

14+
import java.util.ArrayList;
15+
import java.util.HashMap;
16+
import java.util.Iterator;
17+
import java.util.List;
18+
import java.util.Map;
19+
import java.util.function.Function;
1420
import java.util.regex.Pattern;
1521

1622
/**
@@ -178,6 +184,40 @@ public static int score(String text, String filter) {
178184
return score;
179185
}
180186

187+
/**
188+
* Picks up to {@code slots} items from a relevance-sorted list, capping each
189+
* group at its fair share of the slots so a single prolific group cannot flood
190+
* out all the others. Slots a group does not use go to the best remaining
191+
* items regardless of group.
192+
*/
193+
public static <T> List<T> pickFairly(List<T> sortedByRelevance, Function<T, Object> groupOf, int slots) {
194+
if (sortedByRelevance.size() <= slots) {
195+
return sortedByRelevance;
196+
}
197+
long groupCount = sortedByRelevance.stream().map(groupOf).distinct().count();
198+
int fairShare = Math.max(1, slots / (int) groupCount);
199+
List<T> winners = new ArrayList<>(Math.max(0, slots));
200+
List<T> overflow = new ArrayList<>();
201+
Map<Object, Integer> taken = new HashMap<>();
202+
for (T item : sortedByRelevance) {
203+
if (winners.size() >= slots) {
204+
return winners;
205+
}
206+
Object group = groupOf.apply(item);
207+
int alreadyTaken = taken.getOrDefault(group, 0);
208+
if (alreadyTaken < fairShare) {
209+
winners.add(item);
210+
taken.put(group, alreadyTaken + 1);
211+
} else {
212+
overflow.add(item);
213+
}
214+
}
215+
for (Iterator<T> it = overflow.iterator(); winners.size() < slots && it.hasNext();) {
216+
winners.add(it.next());
217+
}
218+
return winners;
219+
}
220+
181221
private static boolean isBoundary(String text, int i) {
182222
if (i == 0) {
183223
return true;

tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/quickaccess/QuickAccessMatchingTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import static org.junit.jupiter.api.Assertions.assertNotNull;
1717
import static org.junit.jupiter.api.Assertions.assertTrue;
1818

19+
import java.util.List;
20+
import java.util.function.Function;
1921
import java.util.regex.Pattern;
2022

2123
import org.eclipse.ui.internal.quickaccess.QuickAccessEntry;
@@ -157,4 +159,45 @@ public void scoreIgnoresWildcardChars() {
157159
public void scoreIsCaseInsensitive() {
158160
assertTrue(QuickAccessMatching.score("RENAME", "rename") > QuickAccessMatching.SCORE_NONE);
159161
}
162+
163+
/** Groups items by their prefix up to the first digit, e.g. "file1" -> "file". */
164+
private static final Function<String, Object> BY_PREFIX = s -> s.replaceAll("\\d", "");
165+
166+
@Test
167+
public void pickFairlyReturnsAllWhenEnoughSlots() {
168+
List<String> ranked = List.of("file1", "file2", "view1");
169+
assertEquals(ranked, QuickAccessMatching.pickFairly(ranked, BY_PREFIX, 5));
170+
}
171+
172+
@Test
173+
public void pickFairlyCapsFloodingGroup() {
174+
// files dominate the top of the relevance ranking, see issue #4155
175+
List<String> ranked = List.of("file1", "file2", "file3", "file4", "file5", "view1", "command1");
176+
List<String> winners = QuickAccessMatching.pickFairly(ranked, BY_PREFIX, 4);
177+
assertEquals(4, winners.size());
178+
assertTrue(winners.contains("view1"), "flooded-out view should win a slot: " + winners);
179+
assertTrue(winners.contains("command1"), "flooded-out command should win a slot: " + winners);
180+
}
181+
182+
@Test
183+
public void pickFairlyGivesUnusedShareToBestRemaining() {
184+
List<String> ranked = List.of("file1", "file2", "file3", "file4", "view1");
185+
// 2 groups, 4 slots -> fair share is 2 each; the view only needs 1,
186+
// so the leftover slot goes to the next best file
187+
assertEquals(List.of("file1", "file2", "view1", "file3"),
188+
QuickAccessMatching.pickFairly(ranked, BY_PREFIX, 4));
189+
}
190+
191+
@Test
192+
public void pickFairlyKeepsRelevanceOrderWithinTheCap() {
193+
List<String> ranked = List.of("view1", "file1", "file2", "file3", "command1");
194+
List<String> winners = QuickAccessMatching.pickFairly(ranked, BY_PREFIX, 3);
195+
assertEquals(List.of("view1", "file1", "command1"), winners);
196+
}
197+
198+
@Test
199+
public void pickFairlyHandlesZeroSlots() {
200+
List<String> ranked = List.of("file1", "file2");
201+
assertTrue(QuickAccessMatching.pickFairly(ranked, BY_PREFIX, 0).isEmpty());
202+
}
160203
}

0 commit comments

Comments
 (0)