From a60b64fe8a416b34d4e905c216b9935d60962607 Mon Sep 17 00:00:00 2001 From: Pavel Porvatov Date: Fri, 8 Aug 2025 11:08:39 +0200 Subject: [PATCH] IJPL-171417 Go to target (choose implementation) freezes if there are many results - Support FilteringModels in the cache - Added tests GitOrigin-RevId: 6791e4418c8481a0d6a2ea2b0341323d2a1e2da5 --- platform/platform-api/api-dump.txt | 1 - .../ui/components/WideSelectionListCache.kt | 94 +++++++++++++++++++ .../ui/components/WideSelectionListUI.java | 74 +++++---------- .../LcrPerformanceTestPanel.kt | 24 +++-- .../components/WideSelectionListCacheTest.kt | 90 ++++++++++++++++++ 5 files changed, 225 insertions(+), 58 deletions(-) create mode 100644 platform/platform-api/src/com/intellij/ui/components/WideSelectionListCache.kt create mode 100644 platform/platform-tests/testSrc/com/intellij/ui/components/WideSelectionListCacheTest.kt diff --git a/platform/platform-api/api-dump.txt b/platform/platform-api/api-dump.txt index cf617f715866..488f7e5096ec 100644 --- a/platform/platform-api/api-dump.txt +++ b/platform/platform-api/api-dump.txt @@ -825,7 +825,6 @@ com.intellij.ui.components.Magnificator - sf:CLIENT_PROPERTY_KEY:com.intellij.openapi.util.Key - a:magnify(D,java.awt.Point):java.awt.Point f:com.intellij.ui.components.WideSelectionListUI -- uninstallUI(javax.swing.JComponent):V com.intellij.ui.components.ZoomableViewport - a:getMagnificator():com.intellij.ui.components.Magnificator - a:magnificationFinished(D):V diff --git a/platform/platform-api/src/com/intellij/ui/components/WideSelectionListCache.kt b/platform/platform-api/src/com/intellij/ui/components/WideSelectionListCache.kt new file mode 100644 index 000000000000..328db7865ee1 --- /dev/null +++ b/platform/platform-api/src/com/intellij/ui/components/WideSelectionListCache.kt @@ -0,0 +1,94 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.ui.components + +import com.intellij.ui.speedSearch.FilteringListModel +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.TestOnly +import java.awt.Dimension +import java.beans.PropertyChangeListener +import java.util.* +import javax.swing.JList +import javax.swing.ListModel +import javax.swing.event.ListDataEvent +import javax.swing.event.ListDataListener + +@ApiStatus.Internal +@TestOnly +class WideSelectionListCache(private val list: JList<*>) { + + /** + * Key is a value from the [list]. + * Don't cache `null` value because of using [MutableMap.getOrPut] + */ + @ApiStatus.Internal + @TestOnly + val preferredSizeCache: MutableMap = IdentityHashMap() + + private val listDataListener = object : ListDataListener { + override fun intervalAdded(e: ListDataEvent?) {} + + override fun intervalRemoved(e: ListDataEvent) { + purge() + } + + override fun contentsChanged(e: ListDataEvent) { + purge() + } + } + + private val modelChangeListener = PropertyChangeListener { evt -> + uninstallListDataListener(evt.oldValue as ListModel<*>?) + installListDataListener(evt.newValue as ListModel<*>?) + preferredSizeCache.clear() + } + + fun installListeners() { + list.addPropertyChangeListener("model", modelChangeListener) + installListDataListener(list.model) + } + + fun uninstallListeners() { + list.removePropertyChangeListener("model", modelChangeListener) + uninstallListDataListener(list.model) + preferredSizeCache.clear() + } + + fun getCachedPreferredSizeOrCalculate(value: Any?, useCache: Boolean, supplier: () -> Dimension): Dimension { + if (!useCache || value == null) { + return supplier() + } + return preferredSizeCache.getOrPut(value, supplier) + } + + private fun purge() { + val model = list.model.originalModel + + if (model == null) { + preferredSizeCache.clear() + return + } + + val existingValues = Collections.newSetFromMap(IdentityHashMap()) + for (i in 0 until model.size) { + existingValues.add(model.getElementAt(i)) + } + preferredSizeCache.keys.retainAll(existingValues) + } + + private fun installListDataListener(model: ListModel<*>?) { + model.originalModel?.addListDataListener(listDataListener) + } + + private fun uninstallListDataListener(model: ListModel<*>?) { + model.originalModel?.removeListDataListener(listDataListener) + } + + private val ListModel<*>?.originalModel: ListModel<*>? + get() { + return when (this) { + null -> null + is FilteringListModel<*> -> originalModel + else -> this + } + } +} \ No newline at end of file diff --git a/platform/platform-api/src/com/intellij/ui/components/WideSelectionListUI.java b/platform/platform-api/src/com/intellij/ui/components/WideSelectionListUI.java index 511715337743..ac63f2cbdb65 100644 --- a/platform/platform-api/src/com/intellij/ui/components/WideSelectionListUI.java +++ b/platform/platform-api/src/com/intellij/ui/components/WideSelectionListUI.java @@ -10,18 +10,17 @@ import com.intellij.ui.hover.ListHoverListener; import com.intellij.ui.list.ListCellBackgroundSupplier; import com.intellij.ui.render.RenderingUtil; import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; import javax.swing.*; -import javax.swing.event.ListDataEvent; -import javax.swing.event.ListDataListener; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicListUI; import java.awt.*; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; -import java.util.HashMap; import static com.intellij.openapi.util.SystemInfo.isMac; import static com.intellij.ui.paint.RectanglePainter.DRAW; @@ -32,7 +31,10 @@ import static com.intellij.ui.paint.RectanglePainter.DRAW; public final class WideSelectionListUI extends BasicListUI { private static final Logger LOG = Logger.getInstance(WideSelectionListUI.class); private Rectangle myPaintBounds; - private HashMap<@NotNull Integer, @NotNull Dimension> preferredSizeCache = new HashMap<>(); + + @ApiStatus.Internal + @TestOnly + public WideSelectionListCache cacheSupport; @Override public void paint(Graphics g, JComponent c) { @@ -176,39 +178,17 @@ public final class WideSelectionListUI extends BasicListUI { } @Override - public void uninstallUI(JComponent c) { - preferredSizeCache.clear(); - super.uninstallUI(c); + protected void installListeners() { + super.installListeners(); + cacheSupport = new WideSelectionListCache(list); + cacheSupport.installListeners(); } @Override - protected ListDataListener createListDataListener() { - ListDataListener superListener = super.createListDataListener(); - - return new ListDataListener() { - @Override - public void intervalAdded(ListDataEvent e) { - superListener.intervalAdded(e); - } - - @Override - public void intervalRemoved(ListDataEvent e) { - removeFromCache(e); - superListener.intervalRemoved(e); - } - - @Override - public void contentsChanged(ListDataEvent e) { - removeFromCache(e); - superListener.contentsChanged(e); - } - }; - } - - private void removeFromCache(ListDataEvent e) { - for (int i = e.getIndex0(); i <= e.getIndex1(); i++) { - preferredSizeCache.remove(i); - } + protected void uninstallListeners() { + cacheSupport.uninstallListeners(); + cacheSupport = null; + super.uninstallListeners(); } @Override @@ -264,25 +244,17 @@ public final class WideSelectionListUI extends BasicListUI { @NotNull ListModel dataModel, @NotNull ListCellRenderer renderer, boolean immutableRenderer) { - if (immutableRenderer) { - Dimension result = preferredSizeCache.get(index); - if (result != null) { - return result; - } - } - Object value = dataModel.getElementAt(index); - Component c = renderer.getListCellRendererComponent(list, value, index, false, false); - rendererPane.add(c); - var result = c.getPreferredSize(); - if (ClientProperty.get(c, JBList.IGNORE_LIST_ROW_HEIGHT) == null) { - result = UIUtil.updateListRowHeight(result); - } - if (immutableRenderer) { - preferredSizeCache.put(index, result); - } - return result; + return cacheSupport.getCachedPreferredSizeOrCalculate(value, immutableRenderer, () -> { + Component c = renderer.getListCellRendererComponent(list, value, index, false, false); + rendererPane.add(c); + var result = c.getPreferredSize(); + if (ClientProperty.get(c, JBList.IGNORE_LIST_ROW_HEIGHT) == null) { + result = UIUtil.updateListRowHeight(result); + } + return result; + }); } @Override diff --git a/platform/platform-impl/internal/src/com/intellij/internal/ui/sandbox/tests/dsl/listCellRenderer/LcrPerformanceTestPanel.kt b/platform/platform-impl/internal/src/com/intellij/internal/ui/sandbox/tests/dsl/listCellRenderer/LcrPerformanceTestPanel.kt index f3b66e94a751..de0e5d9ab538 100644 --- a/platform/platform-impl/internal/src/com/intellij/internal/ui/sandbox/tests/dsl/listCellRenderer/LcrPerformanceTestPanel.kt +++ b/platform/platform-impl/internal/src/com/intellij/internal/ui/sandbox/tests/dsl/listCellRenderer/LcrPerformanceTestPanel.kt @@ -9,12 +9,15 @@ import com.intellij.openapi.Disposable import com.intellij.ui.ColoredListCellRenderer import com.intellij.ui.SimpleTextAttributes import com.intellij.ui.components.JBList +import com.intellij.ui.components.JBScrollPane import com.intellij.ui.components.WideSelectionListUI import com.intellij.ui.dsl.builder.Align import com.intellij.ui.dsl.builder.panel import com.intellij.ui.dsl.builder.text import com.intellij.ui.dsl.listCellRenderer.LcrInitParams import com.intellij.ui.dsl.listCellRenderer.listCellRenderer +import com.intellij.ui.speedSearch.ListWithFilter +import com.intellij.ui.speedSearch.NameFilteringListModel import com.intellij.util.ReflectionUtil import org.jetbrains.annotations.Nls import javax.swing.* @@ -30,13 +33,12 @@ internal class LcrPerformanceTestPanel : UISandboxPanel { override val title: String = "Performance" override fun createContent(disposable: Disposable): JComponent { - lateinit var list: JBList + val list: JBList = JBList(DefaultListModel()) return panel { row { - list = scrollCell(JBList(DefaultListModel())) + cell(ListWithFilter.wrap(list, JBScrollPane(list)) { getMainText(it) }) .align(Align.FILL) .resizableColumn() - .component applyRenderer(list, RendererType.KOTLIN_UI_DSL) panel { @@ -45,10 +47,10 @@ internal class LcrPerformanceTestPanel : UISandboxPanel { .text("10000") .component button("Generate") { - val model = list.model as DefaultListModel + val model = list.model as NameFilteringListModel val count = tfCount.text.toIntOrNull() ?: return@button val size = model.size - model.addAll(size, (size.. listCellRenderer("") { icon(getMainIcon(value)) - text(getMainText(value)) + text(getMainText(value)) { + speedSearch { } + } text(getSecondaryText(value)) { foreground = greyForeground } diff --git a/platform/platform-tests/testSrc/com/intellij/ui/components/WideSelectionListCacheTest.kt b/platform/platform-tests/testSrc/com/intellij/ui/components/WideSelectionListCacheTest.kt new file mode 100644 index 000000000000..29e395d0289b --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/ui/components/WideSelectionListCacheTest.kt @@ -0,0 +1,90 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.ui.components + +import com.intellij.ui.speedSearch.FilteringListModel +import org.junit.Test +import javax.swing.DefaultListModel +import javax.swing.ListModel +import kotlin.test.assertEquals + +class WideSelectionListCacheTest { + + @Test + fun testNoOptimizationDefaultModel() { + val list = JBList((0..100).toList()) + val model = list.model as DefaultListModel + + updateAndCheckCache(list, emptySet()) + model.remove(10) + updateAndCheckCache(list, emptySet()) + model.addElement(101) + updateAndCheckCache(list, emptySet()) + } + + @Test + fun testOptimizationDefaultModel() { + val list = JBList((0..100).toList()) + val model = list.model as DefaultListModel + + list.putClientProperty(JBList.IMMUTABLE_MODEL_AND_RENDERER, true) + + updateAndCheckCacheEqualsModel(list) + model.remove(10) + updateAndCheckCacheEqualsModel(list) + model.addElement(101) + updateAndCheckCacheEqualsModel(list) + + list.model = DefaultListModel() + assertEquals(0, list.cachedItems.size) + } + + @Test + fun testFilteringModel() { + val defaultModel = DefaultListModel() + defaultModel.addAll((0..100).toList()) + val filteringModel = FilteringListModel(defaultModel) + val list = JBList(filteringModel) + + list.putClientProperty(JBList.IMMUTABLE_MODEL_AND_RENDERER, true) + + filteringModel.setFilter { it <= 50 } + updateAndCheckCache(list, (0..50).toSet()) + filteringModel.setFilter { it <= 60 } + updateAndCheckCache(list, (0..60).toSet()) + filteringModel.setFilter(null) + updateAndCheckCache(list, (0..100).toSet()) + filteringModel.setFilter { it <= 50 } + updateAndCheckCache(list, (0..100).toSet()) + + filteringModel.setFilter(null) + defaultModel.remove(50) + val no50 = defaultModel.allItems + assertEquals(no50, list.cachedItems) + defaultModel.addElement(999) + assertEquals(no50, list.cachedItems) + updateAndCheckCacheEqualsModel(list) + } + + private val ListModel.allItems: Set + get() = checkedToSet((0...cachedItems: Set + get() = checkedToSet((ui as WideSelectionListUI).cacheSupport.preferredSizeCache.keys) + + private fun checkedToSet(list: Collection): Set { + val result = list.filterIsInstance().toSet() + assertEquals(result.size, list.size) + return result + } + + private fun updateAndCheckCache(list: JBList, items: Set) { + val ui = list.ui as WideSelectionListUI + ui.updateLayoutState() + + assertEquals(list.cachedItems, items.toSet()) + } + + private fun updateAndCheckCacheEqualsModel(list: JBList) { + updateAndCheckCache(list, list.model.allItems) + } +}