From 2af0289ce48f97402b2e75f34a8affa10de092fb Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 9 Jun 2026 09:11:54 +0200 Subject: [PATCH] Stop the tree auto-scrolling to the selection on in-tree gestures SharpTreeView is a ListBox with AutoScrollToSelectedItem left at its default (true), so it chased the selected row whenever its index shifted: expanding an unrelated node pushed the selection off-screen and the ListBox yanked the viewport back to it, fighting the expand's own reveal -- the "weird scrolling". The rule is that a user mutating a control directly should not have the app mutate the view too; only navigation from a *different* control (search results, code/metadata links, analyzer nodes, Back/forward) may sync the tree. The app already does that sync explicitly through the model (TreeSelectionBinder -> CenterNodeInView), so the ListBox's own auto-scroll is redundant and wrong for in-tree actions. Disable it. Assisted-by: Claude:claude-opus-4-8:Claude Code --- ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs | 56 +++++++++++++++++++ ILSpy/Controls/TreeView/SharpTreeView.cs | 6 ++ 2 files changed, 62 insertions(+) diff --git a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs index 6c75a7582..3991f5cc7 100644 --- a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs +++ b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs @@ -752,6 +752,62 @@ public class AssemblyTreeTests "selecting an already-visible row via the model (e.g. Decompile to new tab) must not move the viewport"); } + [AvaloniaTest] + public async Task Expanding_A_Node_Does_Not_Scroll_The_Selection_Back_Into_View() + { + // Rule: when the user mutates the tree directly -- here, expanding an unrelated node -- the + // app must not chase the selection; the viewport follows the user's action, not the selected + // row. Regression: the ListBox's AutoScrollToSelectedItem yanked the (now off-screen) + // selection back into view on every expand, fighting the expand's own reveal ("weird + // scrolling"). Cross-control navigation still reveals (that path goes through the model). + + var (window, vm) = await TestHarness.BootAsync(3); + + // Tall tree: fully expand System.Linq so the list is taller than the viewport. + var enumerable = vm.AssemblyTreeModel.FindNode( + "System.Linq", "System.Linq", "System.Linq.Enumerable"); + enumerable.Expand(); + var ns = (NamespaceTreeNode)enumerable.Parent!; + ns.Expand(); + ((AssemblyTreeNode)ns.Parent!).IsExpanded = true; + foreach (var t in ns.Children.OfType()) + t.Expand(); + + var pane = await window.WaitForComponent(); + var grid = await pane.WaitForComponent(); + var scrollViewer = await grid.WaitForComponent(); + + // Select + reveal the type, then let it settle on screen. + vm.AssemblyTreeModel.SelectNode(enumerable); + await Waiters.WaitForAsync(() => ReferenceEquals(vm.AssemblyTreeModel.SelectedItem, enumerable)); + for (int i = 0; i < 8; i++) + { + Dispatcher.UIThread.RunJobs(); + await Task.Delay(25); + } + grid.UpdateLayout(); + + (scrollViewer.Extent.Height - scrollViewer.Viewport.Height).Should().BeGreaterThan(50, + "the tree must be taller than the viewport for this test to be meaningful"); + grid.IsNodeFullyVisible(enumerable).Should().BeTrue("the selected type is revealed before the expand"); + + // Act: the user expands an unrelated, earlier node (CoreLib, at the top), inserting many + // rows above the selection and pushing it off-screen. + var coreLib = vm.AssemblyTreeModel.FindNode(typeof(object).Assembly.GetName().Name!); + coreLib.IsExpanded = true; + for (int i = 0; i < 8; i++) + { + Dispatcher.UIThread.RunJobs(); + await Task.Delay(25); + } + grid.UpdateLayout(); + + // Assert: the app did not chase the selection. The expand reveals the opened node's children; + // the off-screen selection stays off-screen. + grid.IsNodeFullyVisible(enumerable).Should().BeFalse( + "expanding a node the user opened must not auto-scroll the off-screen selection back into view"); + } + [AvaloniaTest] public async Task Save_Code_Command_Dispatches_Single_Selected_Node_Save_Override() { diff --git a/ILSpy/Controls/TreeView/SharpTreeView.cs b/ILSpy/Controls/TreeView/SharpTreeView.cs index f4960d995..28bf21587 100644 --- a/ILSpy/Controls/TreeView/SharpTreeView.cs +++ b/ILSpy/Controls/TreeView/SharpTreeView.cs @@ -75,6 +75,12 @@ namespace ILSpy.Controls.TreeView // Take keyboard focus directly so gestures like Ctrl+A work the moment the user tabs // or clicks into the pane, before any row becomes the current item. Focusable = true; + // The app reveals the selection explicitly on cross-control navigation (search results, + // code/metadata links, analyzer nodes) via TreeSelectionBinder -> CenterNodeInView. The + // ListBox's own AutoScrollToSelectedItem additionally chases the selected row whenever its + // index shifts -- e.g. when the user expands an unrelated node -- yanking the viewport away + // from what the user is doing. Disable it so in-tree gestures never move the view. + AutoScrollToSelectedItem = false; SelectionChanged += OnSelectionChanged; DoubleTapped += OnDoubleTapped; // Drag-drop is handled generically here and delegated to SharpTreeNode.CanDrop/Drop.