From a67783e540183e44f423bde08fec305b4878e938 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 8 May 2026 14:02:17 +0200 Subject: [PATCH] Ctrl+G keyboard shortcut for go-to-token The metadata page's KeyDown handler now mirrors the "Go to token" context-menu entry: when Ctrl+G fires while focus is on a token-kind DataGridCell (or, as a fallback, while one is hovered), dispatch the same NavigateToCellRequested path as the hyperlink-style click. The focus-resolution + dispatch logic lives behind an internal TryNavigateToTokenInCell hook so tests can drive it without simulating the keyboard event end-to-end. Assisted-by: Claude:claude-opus-4-7:Claude Code --- .../Metadata/MetadataTokenNavigationTests.cs | 45 +++++++++++++++++++ ILSpy/Views/MetadataTablePage.axaml.cs | 35 +++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/ILSpy.Tests/Metadata/MetadataTokenNavigationTests.cs b/ILSpy.Tests/Metadata/MetadataTokenNavigationTests.cs index b9df2a0fc..65ebcae70 100644 --- a/ILSpy.Tests/Metadata/MetadataTokenNavigationTests.cs +++ b/ILSpy.Tests/Metadata/MetadataTokenNavigationTests.cs @@ -86,6 +86,51 @@ public class MetadataTokenNavigationTests landed.Items.Should().HaveCountGreaterThan((int)(expectedRowNumber - 1), "the target row index must be in range for the destination table"); } + + [AvaloniaTest] + public async Task Pressing_Ctrl_G_On_A_Token_Cell_Navigates_The_Same_Way_As_Clicking_The_Hyperlink() + { + // Ctrl+G is the keyboard shortcut for "Go to token", advertised on the context-menu + // entry. It should fire the same NavigateToCellRequested path as the hyperlink-style + // click, with the focused (or last-clicked) token cell as the target. + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var coreLibName = typeof(object).Assembly.GetName().Name!; + var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + assemblyNode.EnsureLazyChildren(); + var metadataNode = assemblyNode.Children.OfType().Single(); + metadataNode.EnsureLazyChildren(); + var tablesNode = metadataNode.Children.OfType().Single(); + tablesNode.EnsureLazyChildren(); + var typeDefNode = tablesNode.Children.OfType().Single(); + + vm.AssemblyTreeModel.SelectNode(typeDefNode); + var tab = await vm.DockWorkspace.WaitForMetadataTabAsync(); + + var rowWithBase = tab.Items.Cast() + .First(e => e.BaseType != 0); + var expectedTableIndex = (System.Reflection.Metadata.Ecma335.TableIndex) + (int)MetadataTokens.EntityHandle(rowWithBase.BaseType).Kind; + + var metadataPage = await window.WaitForComponent(); + + // Synthesize a DataGridCell for the row's BaseType (token-kind) column. The Ctrl+G + // handler doesn't care how the cell got its OwningColumn / DataContext — it just + // dispatches whatever is given. + var baseTypeColumn = tab.Columns.Single(c => (string?)c.Tag == "BaseType"); + var cell = new global::Avalonia.Controls.DataGridCell { DataContext = rowWithBase }; + cell.SetValue(global::Avalonia.Controls.DataGridCell.OwningColumnProperty, baseTypeColumn); + + metadataPage.TryNavigateToTokenInCell(cell); + + await Waiters.WaitForAsync( + () => vm.AssemblyTreeModel.SelectedItem is MetadataTableTreeNode m + && m.Kind == expectedTableIndex); + } } internal static class MetadataTokenNavigationTestExtensions diff --git a/ILSpy/Views/MetadataTablePage.axaml.cs b/ILSpy/Views/MetadataTablePage.axaml.cs index 643ccbae3..1b3c994d8 100644 --- a/ILSpy/Views/MetadataTablePage.axaml.cs +++ b/ILSpy/Views/MetadataTablePage.axaml.cs @@ -29,6 +29,7 @@ using Avalonia.Markup.Xaml; using Avalonia.VisualTree; using ILSpy.AppEnv; +using ILSpy.Commands; using ILSpy.Metadata; using ILSpy.ViewModels; @@ -51,9 +52,43 @@ namespace ILSpy.Views InitializeComponent(); DataContextChanged += (_, _) => RebindModel(); AddHandler(PointerMovedEvent, OnPointerMovedOverGrid); + AddHandler(KeyDownEvent, OnKeyDown); AttachContextMenu(TryGetContextMenuEntries()); } + void OnKeyDown(object? sender, KeyEventArgs e) + { + // Ctrl+G mirrors the "Go to token" context-menu entry: dispatch through the + // focused cell (or last-hovered cell as a fallback when focus hasn't landed on + // the grid, e.g. after the user clicks a column header). + if (e.Key != Key.G || (e.KeyModifiers & KeyModifiers.Control) == 0) + return; + var focusedCell = (TopLevel.GetTopLevel(this)?.FocusManager?.GetFocusedElement() as Visual) + ?.FindAncestorOfType(); + var cell = focusedCell ?? hoveredCell; + if (cell is not null && TryNavigateToTokenInCell(cell)) + e.Handled = true; + } + + /// + /// Dispatches the "Go to token" action on . Returns + /// if the cell was a token-kind column on a metadata page, + /// matching the context-menu entry's visibility test. + /// + internal bool TryNavigateToTokenInCell(DataGridCell cell) + { + ArgumentNullException.ThrowIfNull(cell); + var grid = this.FindControl("Grid"); + if (grid is null) + return false; + var ctx = new TextViewContext { DataGrid = grid, OriginalSource = cell }; + var entry = new GoToTokenContextMenuEntry(); + if (!entry.IsVisible(ctx)) + return false; + entry.Execute(ctx); + return true; + } + static IReadOnlyList TryGetContextMenuEntries() { try