diff --git a/ICSharpCode.ILSpyX/LoadedAssemblyExtensions.cs b/ICSharpCode.ILSpyX/LoadedAssemblyExtensions.cs
index 7db035305..a02bd333f 100644
--- a/ICSharpCode.ILSpyX/LoadedAssemblyExtensions.cs
+++ b/ICSharpCode.ILSpyX/LoadedAssemblyExtensions.cs
@@ -70,6 +70,20 @@ namespace ICSharpCode.ILSpyX
return GetLoadedAssembly(file).GetTypeSystemOrNull(DecompilerTypeSystem.GetOptions(settings));
}
+ ///
+ /// Like , but returns null for a file that was built outside
+ /// the machinery instead of throwing.
+ ///
+ public static LoadedAssembly? GetLoadedAssemblyOrNull(this MetadataFile file)
+ {
+ if (file == null)
+ throw new ArgumentNullException(nameof(file));
+ lock (LoadedAssembly.loadedAssemblies)
+ {
+ return LoadedAssembly.loadedAssemblies.TryGetValue(file, out var loadedAssembly) ? loadedAssembly : null;
+ }
+ }
+
public static LoadedAssembly GetLoadedAssembly(this MetadataFile file)
{
if (file == null)
diff --git a/ILSpy.Tests/AssemblyTree/AssemblyTreeModelTests.cs b/ILSpy.Tests/AssemblyTree/AssemblyTreeModelTests.cs
index d0a44536d..ceda979af 100644
--- a/ILSpy.Tests/AssemblyTree/AssemblyTreeModelTests.cs
+++ b/ILSpy.Tests/AssemblyTree/AssemblyTreeModelTests.cs
@@ -16,6 +16,12 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Threading.Tasks;
+
using Avalonia.Headless.NUnit;
using AwesomeAssertions;
@@ -24,6 +30,7 @@ using ICSharpCode.ILSpyX;
using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.AssemblyTree;
+using ICSharpCode.ILSpy.TreeNodes;
using NUnit.Framework;
@@ -63,4 +70,31 @@ public class AssemblyTreeModelTests
model.ActiveListName.Should().Be(AssemblyListManager.DefaultListName,
"Initialize selects the (Default) list so the tree has something to render at startup.");
}
+
+ [AvaloniaTest]
+ public async Task FindTreeNode_resolves_a_type_inside_an_unexpanded_package()
+ {
+ // Search enumerates the contents of packages whether or not the user ever opened them in
+ // the tree, so activating such a result has to reach a node that does not exist yet.
+ var (_, vm) = await TestHarness.BootAsync();
+
+ var tempDir = Path.Combine(Path.GetTempPath(), "ILSpy.Tests", Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempDir);
+ var zipPath = Path.Combine(tempDir, "package.zip");
+ using (var zip = ZipFile.Open(zipPath, ZipArchiveMode.Create))
+ zip.CreateEntryFromFile(FixtureAssembly.Emit("Nested"), "lib/net10.0/Nested.dll");
+
+ await vm.OpenAssemblyAsync(zipPath);
+
+ var nested = (await vm.AssemblyTreeModel.AssemblyList!.GetAllAssemblies())
+ .Single(a => a.ParentBundle != null);
+ var type = nested.GetTypeSystemOrNull()!.MainModule.TypeDefinitions
+ .Single(t => t.Name == FixtureAssembly.TypeName);
+
+ var node = vm.AssemblyTreeModel.FindTreeNode(type);
+
+ // Cast through object so the generic Should() resolves, not the SharpTreeNode shadow.
+ ((object?)node).Should().BeOfType(
+ "the lookup must descend into the package's folders, expanding them on the way");
+ }
}
diff --git a/ILSpy/AssemblyTree/TreeNodeLocator.cs b/ILSpy/AssemblyTree/TreeNodeLocator.cs
index 693fe68ba..3b2fc49a0 100644
--- a/ILSpy/AssemblyTree/TreeNodeLocator.cs
+++ b/ILSpy/AssemblyTree/TreeNodeLocator.cs
@@ -100,8 +100,7 @@ namespace ICSharpCode.ILSpy.AssemblyTree
return root.FindAssemblyNode(lasm);
case MetadataFile metadataFile:
- return root.Children.OfType()
- .FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == metadataFile);
+ return FindAssemblyNode(root, metadataFile);
case Resource resource:
return FindResourceNode(root, resource, null);
@@ -117,6 +116,47 @@ namespace ICSharpCode.ILSpy.AssemblyTree
}
}
+ ///
+ /// Finds the node for the assembly was loaded from, including
+ /// assemblies nested inside a package or bundle. Package folders are expanded on the way
+ /// down, so this resolves even when the user has never opened the package in the tree.
+ ///
+ public static AssemblyTreeNode? FindAssemblyNode(AssemblyListTreeNode root, MetadataFile? module)
+ {
+ // A package child records the bundle it came from, so walking up that chain leads
+ // straight to the one top-level node worth descending into. Searching the tree for a
+ // matching module instead would have to visit every namespace and type node already
+ // built, and would still miss nested assemblies that are not loaded yet.
+ var nesting = new Stack();
+ for (var current = module?.GetLoadedAssemblyOrNull(); current != null; current = current.ParentBundle)
+ nesting.Push(current);
+ if (nesting.Count == 0)
+ return null;
+
+ var node = root.FindAssemblyNode(nesting.Pop());
+ while (node != null && nesting.Count > 0)
+ node = FindNestedAssemblyNode(node, nesting.Pop());
+ return node;
+ }
+
+ // Depth-first search for one assembly within a package node's folder structure. Only
+ // package folders are descended into, so the walk stays inside the package.
+ static AssemblyTreeNode? FindNestedAssemblyNode(SharpTreeNode packageNode, LoadedAssembly assembly)
+ {
+ packageNode.EnsureLazyChildren();
+ foreach (var child in packageNode.Children)
+ {
+ switch (child)
+ {
+ case AssemblyTreeNode nested when nested.LoadedAssembly == assembly:
+ return nested;
+ case PackageFolderTreeNode folder when FindNestedAssemblyNode(folder, assembly) is { } found:
+ return found;
+ }
+ }
+ 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)
@@ -152,8 +192,7 @@ namespace ICSharpCode.ILSpy.AssemblyTree
var module = ns.ContributingModules.FirstOrDefault();
if (module?.MetadataFile == null)
return null;
- var assembly = root.Children.OfType()
- .FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == module.MetadataFile);
+ var assembly = FindAssemblyNode(root, module.MetadataFile);
if (assembly == null)
return null;
assembly.EnsureLazyChildren();
@@ -166,8 +205,7 @@ namespace ICSharpCode.ILSpy.AssemblyTree
var module = type.ParentModule?.MetadataFile;
if (module == null)
return null;
- var assembly = root.Children.OfType()
- .FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == module);
+ var assembly = FindAssemblyNode(root, module);
if (assembly == null)
return null;