mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
Right-click on a [Kind=Token] cell now exposes a "Go to token" menu item that reuses the same NavigateToCellRequested path as the hyperlink-style left-click. Visibility is gated on the click landing on a token-kind column, so plain columns and non-metadata grids stay clean. Assisted-by: Claude:claude-opus-4-7:Claude Codepull/3755/head
3 changed files with 245 additions and 0 deletions
@ -0,0 +1,119 @@
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using Avalonia.Controls; |
||||
using Avalonia.Headless.NUnit; |
||||
|
||||
using AwesomeAssertions; |
||||
|
||||
using ILSpy; |
||||
using ILSpy.Commands; |
||||
using ILSpy.Metadata; |
||||
using ILSpy.ViewModels; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests.Metadata; |
||||
|
||||
[TestFixture] |
||||
public class GoToTokenContextMenuTests |
||||
{ |
||||
sealed class Row |
||||
{ |
||||
public int RID { get; set; } |
||||
|
||||
[ColumnInfo("X8", Kind = ColumnKind.Token)] |
||||
public int Method { get; set; } |
||||
|
||||
public string Name { get; set; } = ""; |
||||
} |
||||
|
||||
static DataGridCell MakeCell(DataGridColumn column, object dataContext) |
||||
{ |
||||
// DataGridCell.OwningColumn has an internal setter; the registered DirectProperty is
|
||||
// public, so we route through SetValue to wire the synthetic cell up the same way the
|
||||
// DataGrid does at runtime.
|
||||
var cell = new DataGridCell { DataContext = dataContext }; |
||||
cell.SetValue(DataGridCell.OwningColumnProperty, column); |
||||
return cell; |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public void IsVisible_True_When_Right_Click_Lands_On_A_Token_Kind_Cell() |
||||
{ |
||||
// The entry should only surface when the user clicked a hyperlink-style cell —
|
||||
// otherwise "Go to token" would be a no-op or, worse, dispatch to the wrong target.
|
||||
var entry = new GoToTokenContextMenuEntry(); |
||||
var col = new DataGridTemplateColumn { Header = "Method" }; |
||||
var cell = MakeCell(col, new Row { Method = 0x06000001 }); |
||||
var ctx = new TextViewContext { DataGrid = new DataGrid(), OriginalSource = cell }; |
||||
|
||||
entry.IsVisible(ctx).Should().BeTrue(); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public void IsVisible_False_When_Cell_Is_On_A_Plain_Column() |
||||
{ |
||||
// Name is annotated-without-Kind; right-clicking it must not show the entry.
|
||||
var entry = new GoToTokenContextMenuEntry(); |
||||
var col = new DataGridTextColumn { Header = "Name" }; |
||||
var cell = MakeCell(col, new Row { Name = "X" }); |
||||
var ctx = new TextViewContext { DataGrid = new DataGrid(), OriginalSource = cell }; |
||||
|
||||
entry.IsVisible(ctx).Should().BeFalse(); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public void IsVisible_False_When_Context_Has_No_DataGrid() |
||||
{ |
||||
// On the assembly tree (TreeGrid), the metadata-only entry must stay hidden — the
|
||||
// DataGrid field is the discriminator between "right-click on a metadata cell" and
|
||||
// "right-click anywhere else".
|
||||
var entry = new GoToTokenContextMenuEntry(); |
||||
var col = new DataGridTemplateColumn { Header = "Method" }; |
||||
var cell = MakeCell(col, new Row { Method = 1 }); |
||||
var ctx = new TextViewContext { OriginalSource = cell }; |
||||
|
||||
entry.IsVisible(ctx).Should().BeFalse(); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public void Execute_Raises_NavigateToCellRequested_On_The_Page_Model() |
||||
{ |
||||
// Reuses the existing token-cell navigation path: the same code path that fires when
|
||||
// the user left-clicks a hyperlink cell, just triggered through the menu instead.
|
||||
var page = new MetadataTablePageModel(); |
||||
object? capturedRow = null; |
||||
string? capturedColumn = null; |
||||
page.NavigateToCellRequested += args => { |
||||
capturedRow = args.Row; |
||||
capturedColumn = args.ColumnName; |
||||
}; |
||||
|
||||
var grid = new DataGrid { DataContext = page }; |
||||
var col = new DataGridTemplateColumn { Header = "Method" }; |
||||
var row = new Row { Method = 0x06000001 }; |
||||
var cell = MakeCell(col, row); |
||||
var ctx = new TextViewContext { DataGrid = grid, OriginalSource = cell }; |
||||
|
||||
new GoToTokenContextMenuEntry().Execute(ctx); |
||||
|
||||
capturedRow.Should().BeSameAs(row); |
||||
capturedColumn.Should().Be("Method"); |
||||
} |
||||
} |
||||
@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Composition; |
||||
using System.Reflection; |
||||
|
||||
using Avalonia.Controls; |
||||
|
||||
using ICSharpCode.ILSpy.Properties; |
||||
|
||||
using ILSpy.Metadata; |
||||
using ILSpy.ViewModels; |
||||
|
||||
namespace ILSpy.Commands |
||||
{ |
||||
/// <summary>
|
||||
/// Right-click → "Go to token" on a metadata-table cell. Visible only when the click
|
||||
/// landed on a column annotated <c>[ColumnInfo(Kind = Token)]</c>; firing it routes
|
||||
/// through the page model's <c>NavigateToCellRequested</c> event so the dock workspace
|
||||
/// resolves the (row, column) pair to the target table row — the same path a left-click
|
||||
/// on the hyperlink-styled cell takes.
|
||||
/// </summary>
|
||||
[ExportContextMenuEntry(Header = nameof(Resources.GoToToken), Order = 100, InputGestureText = "Ctrl+G")] |
||||
[Shared] |
||||
public sealed class GoToTokenContextMenuEntry : IContextMenuEntry |
||||
{ |
||||
public bool IsVisible(TextViewContext context) => ResolveCell(context) is not null; |
||||
|
||||
public bool IsEnabled(TextViewContext context) => true; |
||||
|
||||
public void Execute(TextViewContext context) |
||||
{ |
||||
if (ResolveCell(context) is not (DataGridCell cell, string columnName)) |
||||
return; |
||||
if (context.DataGrid?.DataContext is not MetadataTablePageModel page) |
||||
return; |
||||
page.RaiseNavigateToCell(cell.DataContext!, columnName); |
||||
} |
||||
|
||||
static (DataGridCell Cell, string ColumnName)? ResolveCell(TextViewContext context) |
||||
{ |
||||
if (context.DataGrid is null) |
||||
return null; |
||||
if (context.OriginalSource is not DataGridCell cell) |
||||
return null; |
||||
if (cell.OwningColumn is null) |
||||
return null; |
||||
var columnName = cell.OwningColumn.Header?.ToString(); |
||||
if (string.IsNullOrEmpty(columnName)) |
||||
return null; |
||||
var row = cell.DataContext; |
||||
if (row is null) |
||||
return null; |
||||
var prop = row.GetType().GetProperty(columnName, BindingFlags.Public | BindingFlags.Instance); |
||||
if (prop?.GetCustomAttribute<ColumnInfoAttribute>()?.Kind != ColumnKind.Token) |
||||
return null; |
||||
return (cell, columnName); |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue