mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
Right-clicking an entity hyperlink in the decompiler view now offers "Go to token" — when the click lands on a reference whose payload is an IEntity backed by a real metadata module, the entry routes through the dock workspace to the matching CLI metadata table at the entity's row. The decompiler text view also gets a context-menu host so other [ExportContextMenuEntry] contributors that target a TextView surface become reachable here too. Assisted-by: Claude:claude-opus-4-7:Claude Codepull/3755/head
4 changed files with 237 additions and 1 deletions
@ -0,0 +1,103 @@
@@ -0,0 +1,103 @@
|
||||
// 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.Linq; |
||||
using System.Reflection.Metadata.Ecma335; |
||||
using System.Threading.Tasks; |
||||
|
||||
using Avalonia.Headless.NUnit; |
||||
|
||||
using AwesomeAssertions; |
||||
|
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
|
||||
using ILSpy; |
||||
using ILSpy.AppEnv; |
||||
using ILSpy.Commands; |
||||
using ILSpy.Metadata; |
||||
using ILSpy.Metadata.CorTables; |
||||
using ILSpy.TextView; |
||||
using ILSpy.TreeNodes; |
||||
using ILSpy.ViewModels; |
||||
using ILSpy.Views; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests.Metadata; |
||||
|
||||
[TestFixture] |
||||
public class ShowInMetadataContextMenuTests |
||||
{ |
||||
[AvaloniaTest] |
||||
public async Task Right_Click_On_An_Entity_Reference_In_The_Decompiler_Routes_To_Its_Metadata_Row() |
||||
{ |
||||
// End-to-end: open a CoreLib type so the decompiler view fills with entity
|
||||
// hyperlinks; synthesize a TextViewContext pointing at one of those references;
|
||||
// verify the entry's IsVisible/Execute drives the dock workspace into the right
|
||||
// metadata table for the entity's MetadataToken.kind.
|
||||
|
||||
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); |
||||
|
||||
// Resolve System.Object's IEntity via the type system, since the entry only acts
|
||||
// on IEntity-bearing references.
|
||||
await assemblyNode.LoadedAssembly.GetLoadResultAsync(); |
||||
var ts = assemblyNode.LoadedAssembly.GetTypeSystemOrNull()!; |
||||
var entity = ts.MainModule.TypeDefinitions.Single(t => t.FullName == "System.Object"); |
||||
entity.MetadataToken.IsNil.Should().BeFalse( |
||||
"System.Object must have a real metadata token in CoreLib"); |
||||
|
||||
var ctx = new TextViewContext { |
||||
TextView = new DecompilerTextView(), |
||||
Reference = new ReferenceSegment { Reference = entity }, |
||||
}; |
||||
var entry = new ShowInMetadataContextMenuEntry(); |
||||
entry.IsVisible(ctx).Should().BeTrue( |
||||
"the entry must surface for entity references in the decompiler view"); |
||||
|
||||
entry.Execute(ctx); |
||||
|
||||
var expectedTable = (TableIndex)(int)entity.MetadataToken.Kind; |
||||
await Waiters.WaitForAsync( |
||||
() => vm.AssemblyTreeModel.SelectedItem is MetadataTableTreeNode m |
||||
&& m.Kind == expectedTable); |
||||
((MetadataTableTreeNode)vm.AssemblyTreeModel.SelectedItem!).Kind.Should().Be(expectedTable); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public void IsVisible_False_When_There_Is_No_TextView_Or_Entity_Reference() |
||||
{ |
||||
// The entry is decompiler-only. A right-click in a context that has no TextView,
|
||||
// or whose Reference is null / not an IEntity (e.g. an opcode link), must not
|
||||
// surface the entry — otherwise it would fire an unrelated NavigateToToken.
|
||||
var entry = new ShowInMetadataContextMenuEntry(); |
||||
|
||||
entry.IsVisible(new TextViewContext()).Should().BeFalse(); |
||||
entry.IsVisible(new TextViewContext { TextView = new DecompilerTextView() }) |
||||
.Should().BeFalse("a TextView without a Reference is just empty whitespace"); |
||||
entry.IsVisible(new TextViewContext { |
||||
TextView = new DecompilerTextView(), |
||||
Reference = new ReferenceSegment { Reference = "not-an-entity" }, |
||||
}).Should().BeFalse(); |
||||
} |
||||
} |
||||
@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
|
||||
// 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 ICSharpCode.Decompiler.TypeSystem; |
||||
using ICSharpCode.ILSpy.Properties; |
||||
|
||||
using ILSpy.AppEnv; |
||||
using ILSpy.Docking; |
||||
using ILSpy.Metadata; |
||||
|
||||
namespace ILSpy.Commands |
||||
{ |
||||
/// <summary>
|
||||
/// Right-click → "Show in metadata" on an entity hyperlink in the decompiler view.
|
||||
/// Visible whenever the click landed on a reference whose payload is an
|
||||
/// <see cref="IEntity"/> backed by a real metadata file; firing it routes through the
|
||||
/// dock workspace to open the matching CLI metadata table at the entity's row.
|
||||
/// </summary>
|
||||
[ExportContextMenuEntry(Header = nameof(Resources.GoToToken), Category = "Metadata", Order = 200)] |
||||
[Shared] |
||||
public sealed class ShowInMetadataContextMenuEntry : IContextMenuEntry |
||||
{ |
||||
public bool IsVisible(TextViewContext context) => Resolve(context) is not null; |
||||
|
||||
public bool IsEnabled(TextViewContext context) => true; |
||||
|
||||
public void Execute(TextViewContext context) |
||||
{ |
||||
if (Resolve(context) is not { } target) |
||||
return; |
||||
AppComposition.Current.GetExport<DockWorkspace>().NavigateToToken(target); |
||||
} |
||||
|
||||
static MetadataTokenReference? Resolve(TextViewContext context) |
||||
{ |
||||
if (context.TextView is null) |
||||
return null; |
||||
if (context.Reference?.Reference is not IEntity entity) |
||||
return null; |
||||
if (entity.ParentModule?.MetadataFile is not { } module) |
||||
return null; |
||||
if (entity.MetadataToken.IsNil) |
||||
return null; |
||||
return new MetadataTokenReference(module, entity.MetadataToken); |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue