IJPL-171417 Go to target (choose implementation) freezes if there are many results

- Support FilteringModels in the cache
- Added tests

GitOrigin-RevId: 6791e4418c8481a0d6a2ea2b0341323d2a1e2da5
This commit is contained in:
Pavel Porvatov
2025-08-08 18:09:40 +00:00
committed by intellij-monorepo-bot
parent 1f9763a7b3
commit a60b64fe8a
5 changed files with 225 additions and 58 deletions
-1
View File
@@ -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
@@ -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<Any, Dimension> = 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<Any?, Boolean>())
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
}
}
}
@@ -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<Object> dataModel,
@NotNull ListCellRenderer<Object> 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
@@ -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<Int>
val list: JBList<Int> = JBList(DefaultListModel())
return panel {
row {
list = scrollCell(JBList(DefaultListModel<Int>()))
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<Int>
val model = list.model as NameFilteringListModel<Int>
val count = tfCount.text.toIntOrNull() ?: return@button
val size = model.size
model.addAll(size, (size..<size + count).toList())
model.addAll((size..<size + count).toList())
list.invalidate()
}
}
@@ -62,6 +64,14 @@ internal class LcrPerformanceTestPanel : UISandboxPanel {
}
}
row {
checkBox("ImmutableModelAndRenderer")
.comment("Turn on performance optimizations")
.onChanged {
list.putClientProperty(JBList.IMMUTABLE_MODEL_AND_RENDERER, it.isSelected)
}
}
row {
lateinit var label: JLabel
button("updateLayoutState") {
@@ -83,7 +93,9 @@ internal class LcrPerformanceTestPanel : UISandboxPanel {
list.cellRenderer = when (renderer) {
RendererType.KOTLIN_UI_DSL -> listCellRenderer("") {
icon(getMainIcon(value))
text(getMainText(value))
text(getMainText(value)) {
speedSearch { }
}
text(getSecondaryText(value)) {
foreground = greyForeground
}
@@ -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<Int>
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<Int>
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<Int>()
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<Int>.allItems: Set<Int>
get() = checkedToSet((0..<size).map { getElementAt(it) })
private val JBList<Int>.cachedItems: Set<Int>
get() = checkedToSet((ui as WideSelectionListUI).cacheSupport.preferredSizeCache.keys)
private fun checkedToSet(list: Collection<Any>): Set<Int> {
val result = list.filterIsInstance<Int>().toSet()
assertEquals(result.size, list.size)
return result
}
private fun updateAndCheckCache(list: JBList<Int>, items: Set<Any>) {
val ui = list.ui as WideSelectionListUI
ui.updateLayoutState()
assertEquals(list.cachedItems, items.toSet())
}
private fun updateAndCheckCacheEqualsModel(list: JBList<Int>) {
updateAndCheckCache(list, list.model.allItems)
}
}