Browse Source

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
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
a67783e540
  1. 45
      ILSpy.Tests/Metadata/MetadataTokenNavigationTests.cs
  2. 35
      ILSpy/Views/MetadataTablePage.axaml.cs

45
ILSpy.Tests/Metadata/MetadataTokenNavigationTests.cs

@ -86,6 +86,51 @@ public class MetadataTokenNavigationTests @@ -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<MainWindow>();
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<AssemblyTreeNode>(coreLibName);
assemblyNode.EnsureLazyChildren();
var metadataNode = assemblyNode.Children.OfType<MetadataTreeNode>().Single();
metadataNode.EnsureLazyChildren();
var tablesNode = metadataNode.Children.OfType<MetadataTablesTreeNode>().Single();
tablesNode.EnsureLazyChildren();
var typeDefNode = tablesNode.Children.OfType<TypeDefTableTreeNode>().Single();
vm.AssemblyTreeModel.SelectNode(typeDefNode);
var tab = await vm.DockWorkspace.WaitForMetadataTabAsync();
var rowWithBase = tab.Items.Cast<TypeDefTableTreeNode.TypeDefEntry>()
.First(e => e.BaseType != 0);
var expectedTableIndex = (System.Reflection.Metadata.Ecma335.TableIndex)
(int)MetadataTokens.EntityHandle(rowWithBase.BaseType).Kind;
var metadataPage = await window.WaitForComponent<MetadataTablePage>();
// 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

35
ILSpy/Views/MetadataTablePage.axaml.cs

@ -29,6 +29,7 @@ using Avalonia.Markup.Xaml; @@ -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 @@ -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<DataGridCell>();
var cell = focusedCell ?? hoveredCell;
if (cell is not null && TryNavigateToTokenInCell(cell))
e.Handled = true;
}
/// <summary>
/// Dispatches the "Go to token" action on <paramref name="cell"/>. Returns
/// <see langword="true"/> if the cell was a token-kind column on a metadata page,
/// matching the context-menu entry's visibility test.
/// </summary>
internal bool TryNavigateToTokenInCell(DataGridCell cell)
{
ArgumentNullException.ThrowIfNull(cell);
var grid = this.FindControl<DataGrid>("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<IContextMenuEntryExport> TryGetContextMenuEntries()
{
try

Loading…
Cancel
Save