diff --git a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs index ded727b45..eb9907d3c 100644 --- a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs +++ b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs @@ -856,6 +856,44 @@ public class AssemblyTreeTests description: "a second Delete must unload another assembly; the selection didn't desync"); } + [AvaloniaTest] + public async Task Left_And_Right_Keys_Collapse_Expand_And_Navigate_The_Tree() + { + // Standard tree keyboard nav: Right expands a collapsed node then steps into its first + // child; Left collapses an expanded node, or moves to the parent when already collapsed. + var (window, vm) = await TestHarness.BootAsync(3); + var model = vm.AssemblyTreeModel; + var pane = await window.WaitForComponent(); + var grid = await pane.WaitForComponent(); + grid.Focus(); + Dispatcher.UIThread.RunJobs(); + + var assembly = model.FindNode("System.Linq"); + assembly.EnsureLazyChildren(); + model.SelectNode(assembly); + await Waiters.WaitForAsync(() => ReferenceEquals(model.SelectedItem, assembly)); + assembly.IsExpanded.Should().BeFalse("precondition: the assembly starts collapsed"); + + // Right expands. + window.KeyPress(Key.Right, RawInputModifiers.None, PhysicalKey.ArrowRight, null); + await Waiters.WaitForAsync(() => assembly.IsExpanded, description: "Right expands the collapsed node"); + + // Right again steps into the first child. + window.KeyPress(Key.Right, RawInputModifiers.None, PhysicalKey.ArrowRight, null); + await Waiters.WaitForAsync( + () => model.SelectedItem is { } s && !ReferenceEquals(s, assembly) && ReferenceEquals(s.Parent, assembly), + description: "Right on an expanded node selects its first child"); + + // Left on the (collapsed/leaf) child moves selection back to the parent. + window.KeyPress(Key.Left, RawInputModifiers.None, PhysicalKey.ArrowLeft, null); + await Waiters.WaitForAsync(() => ReferenceEquals(model.SelectedItem, assembly), + description: "Left on a collapsed child selects the parent"); + + // Left on the expanded parent collapses it. + window.KeyPress(Key.Left, RawInputModifiers.None, PhysicalKey.ArrowLeft, null); + await Waiters.WaitForAsync(() => !assembly.IsExpanded, description: "Left collapses the expanded node"); + } + [AvaloniaTest] public async Task Clear_Assembly_List_Command_Empties_The_Active_List() { diff --git a/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs b/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs index bcb5d280c..2cc030af2 100644 --- a/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs +++ b/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs @@ -310,6 +310,34 @@ namespace ILSpy.AssemblyTree Dispatcher.UIThread.Post(() => ReselectAfterDelete(reselectIndex), DispatcherPriority.Background); return; } + if (e.Key is Key.Left or Key.Right && e.KeyModifiers == KeyModifiers.None + && model.SelectedItem is { } current + && TreeGrid.HierarchicalModel is IHierarchicalModel hmNav + && hmNav.FindNode(current) is { } currentNode) + { + if (e.Key == Key.Left) + { + // Collapse if open; otherwise step out to the parent (unless it's the hidden root). + if (currentNode.IsExpanded) + hmNav.Collapse(currentNode); + else if (current.Parent is { } parent && hmNav.FindNode(parent) is not null) + model.SelectNode(parent); + else + return; + } + else + { + // Expand if closed and has children; otherwise step into the first child. + if (!currentNode.IsExpanded && !currentNode.IsLeaf) + hmNav.Expand(currentNode); + else if (currentNode.IsExpanded && currentNode.Children.Count > 0) + model.SelectNode(currentNode.Children[0].Item as SharpTreeNode); + else + return; + } + e.Handled = true; + return; + } if (e.Key == Key.R && e.KeyModifiers == KeyModifiers.Control) { var members = model.SelectedItems.OfType()