fix(jetbrains): invalidate diff tree layout cache on folder toggle

A folder row hides its rolled-up badge while expanded, so its preferred
width now depends on expansion state. JTree only invalidates cached path
bounds on model changes, not on expand/collapse, so a collapsed folder could
keep its narrower expanded-state bounds and let the re-shown badge squeeze
the file name until an unrelated re-measure. Add a TreeExpansionListener that
invalidates the layout cache on toggle so the row re-measures immediately.
This commit is contained in:
kirillk
2026-07-31 16:50:18 -04:00
parent 820d5d5f01
commit 87b00a9b19
2 changed files with 35 additions and 0 deletions
@@ -49,6 +49,7 @@ import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.treeStructure.Tree
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.tree.TreeUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -67,6 +68,8 @@ import javax.swing.JPanel
import javax.swing.ScrollPaneConstants
import javax.swing.JViewport
import javax.swing.JTree
import javax.swing.event.TreeExpansionEvent
import javax.swing.event.TreeExpansionListener
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.TreeCellRenderer
@@ -439,6 +442,15 @@ private fun buildFileTree(files: List<DiffFileDto>): Tree {
val node = path.lastPathComponent as? DefaultMutableTreeNode
(node?.userObject as? Node)?.name.orEmpty()
}
// A folder row hides its rolled-up badge while expanded, so its preferred width depends on
// expansion state. JTree only invalidates cached path bounds on model changes, not on
// expand/collapse, so a collapsed folder would keep its narrower expanded-state bounds and the
// re-shown badge would squeeze the name until an unrelated re-measure. Invalidate the layout
// cache on toggle so the row re-measures immediately.
tree.addTreeExpansionListener(object : TreeExpansionListener {
override fun treeExpanded(event: TreeExpansionEvent) = TreeUtil.invalidateCacheAndRepaint(tree.ui)
override fun treeCollapsed(event: TreeExpansionEvent) = TreeUtil.invalidateCacheAndRepaint(tree.ui)
})
expandAll(tree)
return tree
}
@@ -144,6 +144,29 @@ class KiloDiffEditorContentTest : BasePlatformTestCase() {
}
}
fun `test folder row width tracks its badge visibility`() {
val parent = Disposer.newDisposable()
try {
val view = view(files(), parent)
val tree = components(view).filterIsInstance<Tree>().single()
val path = TreePath(folder(tree).path)
tree.expandPath(path)
val expanded = tree.getPathBounds(path)!!.width
// Collapsing re-shows the rolled-up badge, so the folder row's measured width must grow.
// This expansion-dependent width is why buildFileTree invalidates JTree's layout cache on
// toggle: a displayed tree caches path bounds across expand/collapse and would otherwise
// paint the row at its stale narrower width, squeezing the name.
tree.collapsePath(path)
val collapsed = tree.getPathBounds(path)!!.width
assertTrue("collapsed folder row must be wider to fit its badge", collapsed > expanded)
} finally {
Disposer.dispose(parent)
}
}
fun `test leaf badge stays visible while its folder is expanded`() {
val parent = Disposer.newDisposable()
try {