mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
AssemblyTreeModel carried a ~180-line node-finding subsystem -- resolve a reference (entity / namespace / resource / assembly / metadata file) or a saved path to its tree node -- that has nothing to do with the class's selection and list-lifecycle responsibilities. Lift the implementation into a TreeNodeLocator, with the locator taking the tree root and assembly list as parameters so it stays stateless. The model keeps its public FindTreeNode / FindNodeByPath / GetPathForNode as thin delegators (they're called from DockWorkspace, SearchPaneModel, AssemblyTreeNode and tests), so no external call site changes -- the god class just sheds the implementation. Assisted-by: Claude:claude-opus-4-8:Claude Codepull/3755/head
2 changed files with 241 additions and 180 deletions
@ -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 |
||||||
|
{ |
||||||
|
/// <summary>
|
||||||
|
/// Resolves a metadata reference (entity, namespace, resource, assembly, ...) or a saved tree
|
||||||
|
/// path to the matching <see cref="SharpTreeNode"/>, expanding lazy children along the way. This
|
||||||
|
/// is the node-finding subsystem split out of <see cref="AssemblyTreeModel"/>; the model keeps
|
||||||
|
/// thin instance/static wrappers that pass in its current tree root and assembly list.
|
||||||
|
/// </summary>
|
||||||
|
internal static class TreeNodeLocator |
||||||
|
{ |
||||||
|
/// <summary>
|
||||||
|
/// Walks down from <paramref name="root"/> matching each path segment against
|
||||||
|
/// <see cref="object.ToString"/>, expanding lazy children along the way.
|
||||||
|
/// </summary>
|
||||||
|
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; |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The path of <paramref name="node"/>'s ancestors (root excluded), in root-first order.
|
||||||
|
/// </summary>
|
||||||
|
public static string[]? GetPathForNode(SharpTreeNode? node) |
||||||
|
{ |
||||||
|
if (node == null) |
||||||
|
return null; |
||||||
|
var path = new List<string>(); |
||||||
|
while (node.Parent != null) |
||||||
|
{ |
||||||
|
path.Add(node.ToString()!); |
||||||
|
node = node.Parent; |
||||||
|
} |
||||||
|
path.Reverse(); |
||||||
|
return path.ToArray(); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds the tree node corresponding to <paramref name="reference"/> -- 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.
|
||||||
|
/// </summary>
|
||||||
|
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<AssemblyTreeNode>() |
||||||
|
.FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == metadataFile); |
||||||
|
|
||||||
|
case Resource resource: |
||||||
|
return FindResourceNode(root, resource, null); |
||||||
|
|
||||||
|
case ValueTuple<Resource, string> 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<AssemblyTreeNode>()) |
||||||
|
{ |
||||||
|
if (!node.LoadedAssembly.IsLoaded) |
||||||
|
continue; |
||||||
|
node.EnsureLazyChildren(); |
||||||
|
foreach (var list in node.Children.OfType<ResourceListTreeNode>()) |
||||||
|
{ |
||||||
|
resourceNode = list.Children.OfType<ResourceTreeNode>().FirstOrDefault(x => x.Resource == resource) |
||||||
|
?? (ILSpyTreeNode?)list.Children.OfType<ResourceEntryNode>().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<ILSpyTreeNode>().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<AssemblyTreeNode>() |
||||||
|
.FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == module.MetadataFile); |
||||||
|
if (assembly == null) |
||||||
|
return null; |
||||||
|
assembly.EnsureLazyChildren(); |
||||||
|
return assembly.Children.OfType<NamespaceTreeNode>() |
||||||
|
.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<AssemblyTreeNode>() |
||||||
|
.FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == module); |
||||||
|
if (assembly == null) |
||||||
|
return null; |
||||||
|
assembly.EnsureLazyChildren(); |
||||||
|
|
||||||
|
var nesting = new Stack<ITypeDefinition>(); |
||||||
|
for (var current = type; current != null; current = current.DeclaringTypeDefinition) |
||||||
|
nesting.Push(current); |
||||||
|
|
||||||
|
var top = nesting.Pop(); |
||||||
|
var ns = assembly.Children.OfType<NamespaceTreeNode>() |
||||||
|
.FirstOrDefault(n => n.Name == (top.Namespace ?? string.Empty)); |
||||||
|
if (ns == null) |
||||||
|
return null; |
||||||
|
ns.EnsureLazyChildren(); |
||||||
|
var typeNode = ns.Children.OfType<TypeTreeNode>() |
||||||
|
.FirstOrDefault(t => t.Handle == top.MetadataToken); |
||||||
|
while (typeNode != null && nesting.Count > 0) |
||||||
|
{ |
||||||
|
typeNode.EnsureLazyChildren(); |
||||||
|
var nested = nesting.Pop(); |
||||||
|
typeNode = typeNode.Children.OfType<TypeTreeNode>() |
||||||
|
.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<FieldTreeNode>().FirstOrDefault(n => n.FieldDefinition.MetadataToken == f.MetadataToken), |
||||||
|
IMethod m => FindMethodNode(typeNode, m), |
||||||
|
IProperty p => typeNode.Children.OfType<PropertyTreeNode>().FirstOrDefault(n => n.PropertyDefinition.MetadataToken == p.MetadataToken), |
||||||
|
IEvent e => typeNode.Children.OfType<EventTreeNode>().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<PropertyTreeNode>() |
||||||
|
.FirstOrDefault(n => n.PropertyDefinition.MetadataToken == owningProperty.MetadataToken); |
||||||
|
if (propNode != null) |
||||||
|
return propNode.Children.OfType<MethodTreeNode>() |
||||||
|
.FirstOrDefault(n => n.MethodDefinition.MetadataToken == method.MetadataToken); |
||||||
|
} |
||||||
|
if (method.AccessorOwner is IEvent owningEvent) |
||||||
|
{ |
||||||
|
var eventNode = typeNode.Children.OfType<EventTreeNode>() |
||||||
|
.FirstOrDefault(n => n.EventDefinition.MetadataToken == owningEvent.MetadataToken); |
||||||
|
if (eventNode != null) |
||||||
|
return eventNode.Children.OfType<MethodTreeNode>() |
||||||
|
.FirstOrDefault(n => n.MethodDefinition.MetadataToken == method.MetadataToken); |
||||||
|
} |
||||||
|
return typeNode.Children.OfType<MethodTreeNode>() |
||||||
|
.FirstOrDefault(n => n.MethodDefinition.MetadataToken == method.MetadataToken); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue