Browse Source

Metadata:// handler — IProtocolHandler + MEF

Lets non-decompile-protocol EntityReference hyperlinks route through plug-in
resolvers instead of always falling into FindTreeNode. The dispatcher in
DockWorkspace.OnNavigateRequested checks `entity.Protocol != "decompile"`,
walks every IProtocolHandler MEF export, and lands the user on the first
non-null tree node returned.

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
a4acfa0390
  1. 43
      ILSpy/Commands/IProtocolHandler.cs
  2. 32
      ILSpy/Docking/DockWorkspace.cs
  3. 69
      ILSpy/Metadata/MetadataProtocolHandler.cs

43
ILSpy/Commands/IProtocolHandler.cs

@ -0,0 +1,43 @@
// 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.Reflection.Metadata;
using ICSharpCode.Decompiler.Metadata;
using ILSpy.TreeNodes;
namespace ILSpy.Commands
{
/// <summary>
/// Plug-in contract for resolving non-default reference protocols (e.g.
/// <c>metadata://</c>) to tree nodes. Implementations are MEF-discovered via
/// <c>[Export(typeof(IProtocolHandler))]</c> and consulted by
/// <see cref="Docking.DockWorkspace"/>'s navigation router; the first handler to
/// return a non-<c>null</c> node wins.
/// </summary>
public interface IProtocolHandler
{
/// <summary>
/// Resolves a reference to a tree node, or returns <c>null</c> to signal "not my
/// protocol". <paramref name="newTabPage"/> is set to <c>true</c> when the handler
/// wants the host to carve out a new tab instead of reusing the active one.
/// </summary>
ILSpyTreeNode? Resolve(string protocol, MetadataFile module, Handle handle, out bool newTabPage);
}
}

32
ILSpy/Docking/DockWorkspace.cs

@ -340,11 +340,43 @@ namespace ILSpy.Docking
// reference (only types/members/EntityReferences are supported today). // reference (only types/members/EntityReferences are supported today).
if (segment.Reference == null) if (segment.Reference == null)
return; return;
// EntityReferences with a non-"decompile" protocol (e.g. metadata://) get a first
// pass through registered IProtocolHandler exports. The first handler returning a
// non-null node wins; if none match we fall through to the default resolver.
if (segment.Reference is global::ILSpy.EntityReference entity
&& entity.Protocol != "decompile")
{
var module = entity.ResolveAssembly(assemblyTreeModel.AssemblyList!);
if (module != null)
{
foreach (var handler in TryGetProtocolHandlers())
{
var resolved = handler.Resolve(entity.Protocol, module, entity.Handle, out _);
if (resolved != null)
{
assemblyTreeModel.SelectedItem = resolved;
return;
}
}
}
}
var node = assemblyTreeModel.FindTreeNode(segment.Reference); var node = assemblyTreeModel.FindTreeNode(segment.Reference);
if (node != null) if (node != null)
assemblyTreeModel.SelectedItem = node; assemblyTreeModel.SelectedItem = node;
} }
static IEnumerable<Commands.IProtocolHandler> TryGetProtocolHandlers()
{
try
{
return AppEnv.AppComposition.Current.GetExports<Commands.IProtocolHandler>();
}
catch
{
return System.Array.Empty<Commands.IProtocolHandler>();
}
}
// Long-lived decompiler viewmodel — kept alive across metadata interludes so going // Long-lived decompiler viewmodel — kept alive across metadata interludes so going
// back to text doesn't lose the previous decompile until a fresh one supersedes it. // back to text doesn't lose the previous decompile until a fresh one supersedes it.
// Created lazily on first need. // Created lazily on first need.

69
ILSpy/Metadata/MetadataProtocolHandler.cs

@ -0,0 +1,69 @@
// 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;
using System.Composition;
using System.Linq;
using System.Reflection.Metadata;
using ICSharpCode.Decompiler.Metadata;
using ILSpy.AssemblyTree;
using ILSpy.Commands;
using ILSpy.TreeNodes;
namespace ILSpy.Metadata
{
/// <summary>
/// Resolves <c>metadata://</c> hyperlinks in the decompiler output to a metadata-tree
/// node under the containing assembly. The reference's <see cref="Handle"/> picks
/// which metadata table to surface; the user lands on the table itself and can
/// scroll/select the specific row.
/// </summary>
[Export(typeof(IProtocolHandler))]
[Shared]
public sealed class MetadataProtocolHandler : IProtocolHandler
{
readonly AssemblyTreeModel assemblyTreeModel;
[ImportingConstructor]
public MetadataProtocolHandler(AssemblyTreeModel assemblyTreeModel)
{
this.assemblyTreeModel = assemblyTreeModel;
}
public ILSpyTreeNode? Resolve(string protocol, MetadataFile module, Handle handle, out bool newTabPage)
{
newTabPage = true;
if (protocol != "metadata")
return null;
var assemblyNode = assemblyTreeModel.FindTreeNode(module) as AssemblyTreeNode;
if (assemblyNode == null)
return null;
assemblyNode.EnsureLazyChildren();
var metadataNode = assemblyNode.Children.OfType<MetadataTreeNode>().FirstOrDefault();
if (metadataNode == null)
return null;
// WPF additionally drills into the matching table for the handle kind. The
// Avalonia MetadataTreeNode doesn't yet expose a FindNodeByHandleKind helper;
// surfacing the MetadataTreeNode itself is enough to land the user inside the
// per-assembly metadata view. Specific-table drill-down is a follow-up.
return metadataNode;
}
}
}
Loading…
Cancel
Save