diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs
index 5018781f9..6a296d08b 100644
--- a/ILSpy/AssemblyTree/AssemblyTreeModel.cs
+++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs
@@ -550,38 +550,13 @@ namespace ILSpy.AssemblyTree
/// , expanding lazy children along the way.
///
public SharpTreeNode? FindNodeByPath(string[]? path, bool returnBestMatch)
- {
- if (path == null || Root == null)
- return null;
- SharpTreeNode? node = Root;
- SharpTreeNode? bestMatch = node;
- foreach (var element in path)
- {
- if (node == null)
- break;
- bestMatch = node;
- node.EnsureLazyChildren();
- node = node.Children.FirstOrDefault(c => c.ToString() == element);
- }
- return returnBestMatch ? node ?? bestMatch : node;
- }
+ => TreeNodeLocator.FindNodeByPath(Root, path, returnBestMatch);
///
/// The path of 's ancestors (root excluded), in root-first order.
///
public static string[]? GetPathForNode(SharpTreeNode? node)
- {
- if (node == null)
- return null;
- var path = new List();
- while (node.Parent != null)
- {
- path.Add(node.ToString()!);
- node = node.Parent;
- }
- path.Reverse();
- return path.ToArray();
- }
+ => TreeNodeLocator.GetPathForNode(node);
internal AssemblyTreeNode? FindAssemblyNode(LoadedAssembly asm)
=> assemblyListTreeNode?.FindAssemblyNode(asm);
@@ -592,158 +567,7 @@ namespace ILSpy.AssemblyTree
/// only covers the reference kinds the tree knows how to model.
///
public ILSpyTreeNode? FindTreeNode(object? reference)
- {
- if (assemblyListTreeNode == null)
- return null;
-
- switch (reference)
- {
- case EntityReference unresolved:
- return FindTreeNode(unresolved.Resolve(AssemblyList!));
-
- case ITypeDefinition type:
- return FindTypeNode(assemblyListTreeNode, type);
-
- case IMember member:
- return FindMemberNode(assemblyListTreeNode, member);
-
- case LoadedAssembly lasm:
- return FindAssemblyNode(lasm);
-
- case MetadataFile metadataFile:
- return assemblyListTreeNode.Children.OfType()
- .FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == metadataFile);
-
- case Resource resource:
- return FindResourceNode(assemblyListTreeNode, resource, null);
-
- case ValueTuple resourceWithName:
- return FindResourceNode(assemblyListTreeNode, resourceWithName.Item1, resourceWithName.Item2);
-
- case INamespace ns:
- return FindNamespaceNode(assemblyListTreeNode, ns);
-
- default:
- return null;
- }
- }
-
- // Resolves a resource (optionally a named sub-entry) to its tree node. Mirrors the previous
- // version's AssemblyListTreeNode.FindResourceNode so resource search results / links navigate.
- static ILSpyTreeNode? FindResourceNode(AssemblyListTreeNode root, Resource resource, string? name)
- {
- if (resource == null)
- return null;
- ILSpyTreeNode? resourceNode = null;
- foreach (var node in root.Children.OfType())
- {
- if (!node.LoadedAssembly.IsLoaded)
- continue;
- node.EnsureLazyChildren();
- foreach (var list in node.Children.OfType())
- {
- resourceNode = list.Children.OfType().FirstOrDefault(x => x.Resource == resource)
- ?? (ILSpyTreeNode?)list.Children.OfType().FirstOrDefault(x => resource.Name.Equals(x.Text));
- if (resourceNode != null)
- break;
- }
- if (resourceNode != null)
- break;
- }
- if (resourceNode == null || name == null || name.Equals(resourceNode.Text))
- return resourceNode;
- resourceNode.EnsureLazyChildren();
- return resourceNode.Children.OfType().FirstOrDefault(x => name.Equals(x.Text)) ?? resourceNode;
- }
-
- // Resolves a namespace to its tree node within its contributing assembly. Mirrors the previous
- // version's AssemblyListTreeNode.FindNamespaceNode.
- static NamespaceTreeNode? FindNamespaceNode(AssemblyListTreeNode root, INamespace ns)
- {
- var module = ns.ContributingModules.FirstOrDefault();
- if (module?.MetadataFile == null)
- return null;
- var assembly = root.Children.OfType()
- .FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == module.MetadataFile);
- if (assembly == null)
- return null;
- assembly.EnsureLazyChildren();
- return assembly.Children.OfType()
- .FirstOrDefault(n => ns.FullName.Length == 0 || ns.FullName.Equals(n.Text));
- }
-
- static TypeTreeNode? FindTypeNode(AssemblyListTreeNode root, ITypeDefinition type)
- {
- var module = type.ParentModule?.MetadataFile;
- if (module == null)
- return null;
- var assembly = root.Children.OfType()
- .FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == module);
- if (assembly == null)
- return null;
- assembly.EnsureLazyChildren();
-
- var nesting = new Stack();
- for (var current = type; current != null; current = current.DeclaringTypeDefinition)
- nesting.Push(current);
-
- var top = nesting.Pop();
- var ns = assembly.Children.OfType()
- .FirstOrDefault(n => n.Name == (top.Namespace ?? string.Empty));
- if (ns == null)
- return null;
- ns.EnsureLazyChildren();
- var typeNode = ns.Children.OfType()
- .FirstOrDefault(t => t.Handle == top.MetadataToken);
- while (typeNode != null && nesting.Count > 0)
- {
- typeNode.EnsureLazyChildren();
- var nested = nesting.Pop();
- typeNode = typeNode.Children.OfType()
- .FirstOrDefault(t => t.Handle == nested.MetadataToken);
- }
- return typeNode;
- }
-
- static ILSpyTreeNode? FindMemberNode(AssemblyListTreeNode root, IMember member)
- {
- var typeNode = member.DeclaringTypeDefinition is { } declaring ? FindTypeNode(root, declaring) : null;
- if (typeNode == null)
- return null;
- typeNode.EnsureLazyChildren();
- return member switch {
- IField f => typeNode.Children.OfType().FirstOrDefault(n => n.FieldDefinition.MetadataToken == f.MetadataToken),
- IMethod m => FindMethodNode(typeNode, m),
- IProperty p => typeNode.Children.OfType().FirstOrDefault(n => n.PropertyDefinition.MetadataToken == p.MetadataToken),
- IEvent e => typeNode.Children.OfType().FirstOrDefault(n => n.EventDefinition.MetadataToken == e.MetadataToken),
- _ => null,
- };
- }
-
- static ILSpyTreeNode? FindMethodNode(TypeTreeNode typeNode, IMethod method)
- {
- // Accessor methods (get_X / set_X / add_X / remove_X / invoke_X) live as children
- // of their owning PropertyTreeNode / EventTreeNode, not directly under the type.
- // Route through the owner so MMB on a metadata-grid accessor row finds its node.
- if (method.AccessorOwner is IProperty owningProperty)
- {
- var propNode = typeNode.Children.OfType()
- .FirstOrDefault(n => n.PropertyDefinition.MetadataToken == owningProperty.MetadataToken);
- if (propNode != null)
- return propNode.Children.OfType()
- .FirstOrDefault(n => n.MethodDefinition.MetadataToken == method.MetadataToken);
- }
- if (method.AccessorOwner is IEvent owningEvent)
- {
- var eventNode = typeNode.Children.OfType()
- .FirstOrDefault(n => n.EventDefinition.MetadataToken == owningEvent.MetadataToken);
- if (eventNode != null)
- return eventNode.Children.OfType()
- .FirstOrDefault(n => n.MethodDefinition.MetadataToken == method.MetadataToken);
- }
- return typeNode.Children.OfType()
- .FirstOrDefault(n => n.MethodDefinition.MetadataToken == method.MetadataToken);
- }
+ => TreeNodeLocator.FindTreeNode(assemblyListTreeNode, AssemblyList, reference);
static void LoadInitialAssemblies(AssemblyList assemblyList, AssemblyListManager? manager)
{
@@ -796,7 +620,7 @@ namespace ILSpy.AssemblyTree
{
if (type == null || assemblyListTreeNode == null)
return false;
- var node = FindTypeNode(assemblyListTreeNode, type);
+ var node = TreeNodeLocator.FindTypeNode(assemblyListTreeNode, type);
if (node == null)
return false;
SelectNode(node);
diff --git a/ILSpy/AssemblyTree/TreeNodeLocator.cs b/ILSpy/AssemblyTree/TreeNodeLocator.cs
new file mode 100644
index 000000000..58853bdfa
--- /dev/null
+++ b/ILSpy/AssemblyTree/TreeNodeLocator.cs
@@ -0,0 +1,237 @@
+// 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.Collections.Generic;
+using System.Linq;
+
+using ICSharpCode.Decompiler.Metadata;
+using ICSharpCode.Decompiler.TypeSystem;
+using ICSharpCode.ILSpyX;
+using ICSharpCode.ILSpyX.TreeView;
+
+using ILSpy.TreeNodes;
+
+namespace ILSpy.AssemblyTree
+{
+ ///
+ /// Resolves a metadata reference (entity, namespace, resource, assembly, ...) or a saved tree
+ /// path to the matching , expanding lazy children along the way. This
+ /// is the node-finding subsystem split out of ; the model keeps
+ /// thin instance/static wrappers that pass in its current tree root and assembly list.
+ ///
+ internal static class TreeNodeLocator
+ {
+ ///
+ /// Walks down from matching each path segment against
+ /// , expanding lazy children along the way.
+ ///
+ public static SharpTreeNode? FindNodeByPath(SharpTreeNode? root, string[]? path, bool returnBestMatch)
+ {
+ if (path == null || root == null)
+ return null;
+ SharpTreeNode? node = root;
+ SharpTreeNode? bestMatch = node;
+ foreach (var element in path)
+ {
+ if (node == null)
+ break;
+ bestMatch = node;
+ node.EnsureLazyChildren();
+ node = node.Children.FirstOrDefault(c => c.ToString() == element);
+ }
+ return returnBestMatch ? node ?? bestMatch : node;
+ }
+
+ ///
+ /// The path of 's ancestors (root excluded), in root-first order.
+ ///
+ public static string[]? GetPathForNode(SharpTreeNode? node)
+ {
+ if (node == null)
+ return null;
+ var path = new List();
+ while (node.Parent != null)
+ {
+ path.Add(node.ToString()!);
+ node = node.Parent;
+ }
+ path.Reverse();
+ return path.ToArray();
+ }
+
+ ///
+ /// Finds the tree node corresponding to -- used by hyperlink
+ /// clicks in the decompiler view and search-result activation to route to the right entity.
+ /// Currently only covers the reference kinds the tree knows how to model.
+ ///
+ public static ILSpyTreeNode? FindTreeNode(AssemblyListTreeNode? root, AssemblyList? list, object? reference)
+ {
+ if (root == null)
+ return null;
+
+ switch (reference)
+ {
+ case EntityReference unresolved:
+ return FindTreeNode(root, list, unresolved.Resolve(list!));
+
+ case ITypeDefinition type:
+ return FindTypeNode(root, type);
+
+ case IMember member:
+ return FindMemberNode(root, member);
+
+ case LoadedAssembly lasm:
+ return root.FindAssemblyNode(lasm);
+
+ case MetadataFile metadataFile:
+ return root.Children.OfType()
+ .FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == metadataFile);
+
+ case Resource resource:
+ return FindResourceNode(root, resource, null);
+
+ case ValueTuple resourceWithName:
+ return FindResourceNode(root, resourceWithName.Item1, resourceWithName.Item2);
+
+ case INamespace ns:
+ return FindNamespaceNode(root, ns);
+
+ default:
+ return null;
+ }
+ }
+
+ // Resolves a resource (optionally a named sub-entry) to its tree node. Mirrors the previous
+ // version's AssemblyListTreeNode.FindResourceNode so resource search results / links navigate.
+ static ILSpyTreeNode? FindResourceNode(AssemblyListTreeNode root, Resource resource, string? name)
+ {
+ if (resource == null)
+ return null;
+ ILSpyTreeNode? resourceNode = null;
+ foreach (var node in root.Children.OfType())
+ {
+ if (!node.LoadedAssembly.IsLoaded)
+ continue;
+ node.EnsureLazyChildren();
+ foreach (var list in node.Children.OfType())
+ {
+ resourceNode = list.Children.OfType().FirstOrDefault(x => x.Resource == resource)
+ ?? (ILSpyTreeNode?)list.Children.OfType().FirstOrDefault(x => resource.Name.Equals(x.Text));
+ if (resourceNode != null)
+ break;
+ }
+ if (resourceNode != null)
+ break;
+ }
+ if (resourceNode == null || name == null || name.Equals(resourceNode.Text))
+ return resourceNode;
+ resourceNode.EnsureLazyChildren();
+ return resourceNode.Children.OfType().FirstOrDefault(x => name.Equals(x.Text)) ?? resourceNode;
+ }
+
+ // Resolves a namespace to its tree node within its contributing assembly. Mirrors the previous
+ // version's AssemblyListTreeNode.FindNamespaceNode.
+ static NamespaceTreeNode? FindNamespaceNode(AssemblyListTreeNode root, INamespace ns)
+ {
+ var module = ns.ContributingModules.FirstOrDefault();
+ if (module?.MetadataFile == null)
+ return null;
+ var assembly = root.Children.OfType()
+ .FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == module.MetadataFile);
+ if (assembly == null)
+ return null;
+ assembly.EnsureLazyChildren();
+ return assembly.Children.OfType()
+ .FirstOrDefault(n => ns.FullName.Length == 0 || ns.FullName.Equals(n.Text));
+ }
+
+ public static TypeTreeNode? FindTypeNode(AssemblyListTreeNode root, ITypeDefinition type)
+ {
+ var module = type.ParentModule?.MetadataFile;
+ if (module == null)
+ return null;
+ var assembly = root.Children.OfType()
+ .FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == module);
+ if (assembly == null)
+ return null;
+ assembly.EnsureLazyChildren();
+
+ var nesting = new Stack();
+ for (var current = type; current != null; current = current.DeclaringTypeDefinition)
+ nesting.Push(current);
+
+ var top = nesting.Pop();
+ var ns = assembly.Children.OfType()
+ .FirstOrDefault(n => n.Name == (top.Namespace ?? string.Empty));
+ if (ns == null)
+ return null;
+ ns.EnsureLazyChildren();
+ var typeNode = ns.Children.OfType()
+ .FirstOrDefault(t => t.Handle == top.MetadataToken);
+ while (typeNode != null && nesting.Count > 0)
+ {
+ typeNode.EnsureLazyChildren();
+ var nested = nesting.Pop();
+ typeNode = typeNode.Children.OfType()
+ .FirstOrDefault(t => t.Handle == nested.MetadataToken);
+ }
+ return typeNode;
+ }
+
+ static ILSpyTreeNode? FindMemberNode(AssemblyListTreeNode root, IMember member)
+ {
+ var typeNode = member.DeclaringTypeDefinition is { } declaring ? FindTypeNode(root, declaring) : null;
+ if (typeNode == null)
+ return null;
+ typeNode.EnsureLazyChildren();
+ return member switch {
+ IField f => typeNode.Children.OfType().FirstOrDefault(n => n.FieldDefinition.MetadataToken == f.MetadataToken),
+ IMethod m => FindMethodNode(typeNode, m),
+ IProperty p => typeNode.Children.OfType().FirstOrDefault(n => n.PropertyDefinition.MetadataToken == p.MetadataToken),
+ IEvent e => typeNode.Children.OfType().FirstOrDefault(n => n.EventDefinition.MetadataToken == e.MetadataToken),
+ _ => null,
+ };
+ }
+
+ static ILSpyTreeNode? FindMethodNode(TypeTreeNode typeNode, IMethod method)
+ {
+ // Accessor methods (get_X / set_X / add_X / remove_X / invoke_X) live as children
+ // of their owning PropertyTreeNode / EventTreeNode, not directly under the type.
+ // Route through the owner so MMB on a metadata-grid accessor row finds its node.
+ if (method.AccessorOwner is IProperty owningProperty)
+ {
+ var propNode = typeNode.Children.OfType()
+ .FirstOrDefault(n => n.PropertyDefinition.MetadataToken == owningProperty.MetadataToken);
+ if (propNode != null)
+ return propNode.Children.OfType()
+ .FirstOrDefault(n => n.MethodDefinition.MetadataToken == method.MetadataToken);
+ }
+ if (method.AccessorOwner is IEvent owningEvent)
+ {
+ var eventNode = typeNode.Children.OfType()
+ .FirstOrDefault(n => n.EventDefinition.MetadataToken == owningEvent.MetadataToken);
+ if (eventNode != null)
+ return eventNode.Children.OfType()
+ .FirstOrDefault(n => n.MethodDefinition.MetadataToken == method.MetadataToken);
+ }
+ return typeNode.Children.OfType()
+ .FirstOrDefault(n => n.MethodDefinition.MetadataToken == method.MetadataToken);
+ }
+ }
+}