From b0ae2e409fa0749672aafcc6dfe2092820912ed5 Mon Sep 17 00:00:00 2001 From: Piotr Tomiak Date: Fri, 4 Jul 2025 14:27:45 +0200 Subject: [PATCH] [ui] IJPL-195407 Reduce flickering of the completion popup by rendering more items in advance. GitOrigin-RevId: 38d14378affb5237cbec40a6b5dc2366324e4da8 --- .../lookup/impl/LookupCellRenderer.kt | 28 ++++++++++++++----- .../codeInsight/lookup/impl/LookupImpl.java | 21 +++++++++++++- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupCellRenderer.kt b/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupCellRenderer.kt index 5743d3b1120c..f983aface839 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupCellRenderer.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupCellRenderer.kt @@ -52,6 +52,7 @@ import org.jetbrains.annotations.Nls import org.jetbrains.annotations.VisibleForTesting import java.awt.* import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger import java.util.function.Supplier import javax.swing.* import javax.swing.border.EmptyBorder @@ -91,6 +92,7 @@ class LookupCellRenderer(lookup: LookupImpl, editorComponent: JComponent) : List private val presentationUpdateRequests = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) private val forceRefreshUi = AtomicBoolean(false) private val isUnitTestMode = ApplicationManager.getApplication().isUnitTestMode + private val itemAddedCount = AtomicInteger() init { val scheme = lookup.topLevelEditor.colorsScheme @@ -166,8 +168,10 @@ class LookupCellRenderer(lookup: LookupImpl, editorComponent: JComponent) : List @JvmField val MATCHED_FOREGROUND_COLOR: Color = JBColor.namedColor("CompletionPopup.matchForeground", JBUI.CurrentTheme.Link.Foreground.ENABLED) + @JvmField val SELECTED_BACKGROUND_COLOR: Color = JBColor.namedColor("CompletionPopup.selectionBackground", JBColor(0xc5dffc, 0x113a5c)) + @JvmField val SELECTED_NON_FOCUSED_BACKGROUND_COLOR: Color = JBColor.namedColor("CompletionPopup.selectionInactiveBackground", JBColor(0xE0E0E0, 0x515457)) @@ -500,17 +504,16 @@ class LookupCellRenderer(lookup: LookupImpl, editorComponent: JComponent) : List } return null } - /** * Update lookup width due to visible in lookup items */ private fun updateLookupWidthFromVisibleItems() { + if (lookup.isLookupDisposed) return + scheduleVisibleItemsExpensiveRendering() val visibleItems = lookup.visibleItems var maxWidth = if (shrinkLookup) 0 else lookupTextWidth for (item in visibleItems) { - scheduleForRenderingIfNeeded(item) - val presentation = asyncRendering.getLastComputed(item) item.putUserData(CUSTOM_NAME_FONT, getFontAbleToDisplay(presentation.itemText)) @@ -541,10 +544,15 @@ class LookupCellRenderer(lookup: LookupImpl, editorComponent: JComponent) : List } } - private fun scheduleForRenderingIfNeeded(element: LookupElement) { - if (element.getUserData(SCHEDULED_FOR_RENDERING) != true) { - element.putUserData(SCHEDULED_FOR_RENDERING, true) - updateItemPresentation(element) + @ApiStatus.Internal + fun scheduleVisibleItemsExpensiveRendering() { + // Ensure that all visible items plus a range of invisible items have been + // scheduled for async rendering. + for (item in lookup.getItemsForAsyncRendering()) { + if (item.getUserData(SCHEDULED_FOR_RENDERING) != true) { + item.putUserData(SCHEDULED_FOR_RENDERING, true) + updateItemPresentation(item) + } } } @@ -566,6 +574,12 @@ class LookupCellRenderer(lookup: LookupImpl, editorComponent: JComponent) : List updateIconWidth(fastPresentation.icon) scheduleUpdateLookupWidthFromVisibleItems() AsyncRendering.rememberPresentation(element, fastPresentation) + + // Fast path for the first 20 matched items to + // avoid initial lookup flickering as much as possible + if (itemAddedCount.incrementAndGet() < 20 && lookup.arranger.matchingItems.contains(element)) { + updateItemPresentation(element) + } } fun updateItemPresentation(element: LookupElement) { diff --git a/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupImpl.java b/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupImpl.java index 8abb1f7f52ba..0b781e5a8e3b 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupImpl.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupImpl.java @@ -311,10 +311,10 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable, return false; } - cellRenderer.itemAdded(item, presentation); LookupArranger arranger = myArranger; arranger.registerMatcher(item, matcher); arranger.addElement(item, presentation); + cellRenderer.itemAdded(item, presentation); return true; } @@ -522,6 +522,8 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable, } }); + cellRenderer.scheduleVisibleItemsExpensiveRendering(); + updateListHeight(listModel); list.setSelectedIndex(toSelect); @@ -1206,6 +1208,23 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable, } } + @ApiStatus.Internal + public List getItemsForAsyncRendering() { + ThreadingAssertions.assertEventDispatchThread(); + + var itemsCount = list.getItemsCount(); + if (itemsCount == 0) return Collections.emptyList(); + + synchronized (uiLock) { + int lowerItemIndex = list.getFirstVisibleIndex(); + int higherItemIndex = list.getLastVisibleIndex(); + if (lowerItemIndex < 0 || higherItemIndex < 0) return Collections.emptyList(); + + int delta = 15; + var items = getListModel().toList(); + return items.subList(Math.max(lowerItemIndex - delta, 0), Math.min(higherItemIndex + 1 + delta, itemsCount)); + } + } @Override public @Unmodifiable List getAdvertisements() {