From 79a9dcee7295b7f0ef5f1d7459aaca1fb8a739ae Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 13 May 2026 07:54:21 +0200 Subject: [PATCH] Surface embedded portable PDB metadata as top-level sibling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Browsing portable PDB metadata required drilling three levels deep: Assembly → Metadata → Debug Directory → Debug Metadata (Embedded). The PDB's tables are now exposed as a top-level MetadataTreeNode alongside the host module's Metadata folder, so PDB browsing is one click away. The PdbProvider already cached for decompilation is reused — no second parse. Assisted-by: Claude:claude-opus-4-7:Claude Code --- ILSpy.Tests/Metadata/EmbeddedPdbTreeTests.cs | 47 ++++++++++++++++++- .../MetadataTabSessionRestoreTests.cs | 6 ++- ILSpy/SettingsService.cs | 5 +- ILSpy/TreeNodes/AssemblyTreeNode.cs | 13 +++++ 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/ILSpy.Tests/Metadata/EmbeddedPdbTreeTests.cs b/ILSpy.Tests/Metadata/EmbeddedPdbTreeTests.cs index f0ce6bfd3..4d70196d3 100644 --- a/ILSpy.Tests/Metadata/EmbeddedPdbTreeTests.cs +++ b/ILSpy.Tests/Metadata/EmbeddedPdbTreeTests.cs @@ -40,6 +40,8 @@ namespace ICSharpCode.ILSpy.Tests.Metadata; [TestFixture] public class EmbeddedPdbTreeTests { + // audit (2026-05-12): catches changing the "Debug Metadata (Embedded)" label string in + // DebugDirectoryTreeNode.cs (~line 90) — the Text equality assertion in the test fails. [AvaloniaTest] public async Task Expanding_DebugDirectory_Surfaces_Embedded_PDB_Metadata_Subtree() { @@ -73,7 +75,11 @@ public class EmbeddedPdbTreeTests var assemblyNode = vm.AssemblyTreeModel.FindNode(loaded.ShortName); assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); + // AssemblyTreeNode now surfaces two MetadataTreeNode children for assemblies with an + // embedded PDB (host metadata + a top-level PDB sibling). This test cares about the + // nested-under-DebugDirectory path, so pick the host metadata explicitly by label. + var metadataNode = assemblyNode.Children.OfType() + .Single(n => (string?)n.Text == Resources.Metadata); metadataNode.EnsureLazyChildren(); var debugDirectoryNode = metadataNode.Children.OfType().Single(); @@ -92,4 +98,43 @@ public class EmbeddedPdbTreeTests pdbTables.Children.OfType().Should().NotBeEmpty( "the embedded PDB's Tables subtree must expose the Document table — the lead debug-only table that always has rows in a populated PDB"); } + + [AvaloniaTest] + public async Task Embedded_PDB_Metadata_Is_Exposed_As_A_Top_Level_Sibling_Of_The_Host_Metadata_Node() + { + // Sibling-discoverability path: the embedded PDB's metadata should also be reachable + // directly under the AssemblyTreeNode (as a sibling of the host module's "Metadata" + // folder), not only nested under DebugDirectory. Without this, users have to drill + // three levels deep to reach PDB metadata — same content, harder to find. + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var testDllPath = typeof(ICSharpCode.Decompiler.Metadata.MetadataFile).Assembly.Location; + var registry = AppComposition.Current.GetExport(); + var openCommand = registry.Commands + .Single(c => c.Metadata.Header == nameof(Resources._Open)) + .CreateExport().Value; + openCommand.Execute(testDllPath); + + await Waiters.WaitForAsync(() => + vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => + string.Equals(a.FileName, testDllPath, System.StringComparison.OrdinalIgnoreCase))); + var loaded = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() + .First(a => string.Equals(a.FileName, testDllPath, System.StringComparison.OrdinalIgnoreCase)); + await loaded.GetLoadResultAsync(); + + var assemblyNode = vm.AssemblyTreeModel.FindNode(loaded.ShortName); + assemblyNode.EnsureLazyChildren(); + + var topLevelMetadataNodes = assemblyNode.Children.OfType().ToList(); + topLevelMetadataNodes.Should().HaveCount(2, + "the assembly node must expose two MetadataTreeNode children: the host's PE metadata " + + "and the embedded portable PDB's metadata as a sibling"); + topLevelMetadataNodes.Select(n => n.Text.ToString()) + .Should().Contain("Debug Metadata (Embedded)", + "the embedded PDB sibling must use the same label as the nested one for consistency"); + } } diff --git a/ILSpy.Tests/Metadata/MetadataTabSessionRestoreTests.cs b/ILSpy.Tests/Metadata/MetadataTabSessionRestoreTests.cs index 989e506ef..69ec2961f 100644 --- a/ILSpy.Tests/Metadata/MetadataTabSessionRestoreTests.cs +++ b/ILSpy.Tests/Metadata/MetadataTabSessionRestoreTests.cs @@ -103,7 +103,11 @@ public class MetadataTabSessionRestoreTests var assemblyNode = vm.AssemblyTreeModel.FindNode(loaded.ShortName); assemblyNode.EnsureLazyChildren(); - var metadataNode = assemblyNode.Children.OfType().Single(); + // AssemblyTreeNode surfaces the embedded PDB's metadata as a second top-level + // MetadataTreeNode sibling, so disambiguate by label — this test exercises the + // nested-under-DebugDirectory path. + var metadataNode = assemblyNode.Children.OfType() + .Single(n => (string?)n.Text == ICSharpCode.ILSpy.Properties.Resources.Metadata); metadataNode.EnsureLazyChildren(); var debugDirectoryNode = metadataNode.Children.OfType().Single(); debugDirectoryNode.EnsureLazyChildren(); diff --git a/ILSpy/SettingsService.cs b/ILSpy/SettingsService.cs index 64cd3b73f..3ec4d6584 100644 --- a/ILSpy/SettingsService.cs +++ b/ILSpy/SettingsService.cs @@ -42,7 +42,10 @@ namespace ILSpy public MiscSettings MiscSettings => GetSettings(); AssemblyListManager? assemblyListManager; - public AssemblyListManager AssemblyListManager => assemblyListManager ??= new(SpySettings); + public AssemblyListManager AssemblyListManager => assemblyListManager ??= new(SpySettings) { + ApplyWinRTProjections = DecompilerSettings.ApplyWindowsRuntimeProjections, + UseDebugSymbols = DecompilerSettings.UseDebugSymbols, + }; /// /// Persists every materialised section to disk. Options panels bind two-way to the diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index 4beea52af..82682e3dc 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -31,6 +31,7 @@ using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.ILSpyX; using ICSharpCode.ILSpyX.FileLoaders; +using ICSharpCode.ILSpyX.PdbProvider; using ILSpy; using ILSpy.Languages; @@ -250,6 +251,18 @@ namespace ILSpy.TreeNodes Children.Add(new ILSpy.Metadata.MetadataTreeNode(module, ICSharpCode.ILSpy.Properties.Resources.Metadata)); + // Surface portable PDB metadata (embedded or side-by-side) as a top-level sibling + // of the host Metadata folder so PDB browsing is one click away instead of buried + // under Metadata → Debug Directory. Reuses the cached PdbProvider that the rest of + // the app already opened for decompilation — no second parse of the PDB blob. + var debugInfo = assembly.GetDebugInfoOrNull(); + if (debugInfo is PortableDebugInfoProvider ppdb + && ppdb.GetMetadataReader() is not null) + { + var label = $"Debug Metadata ({(ppdb.IsEmbedded ? "Embedded" : "From portable PDB")})"; + Children.Add(new ILSpy.Metadata.MetadataTreeNode(ppdb.ToMetadataFile(), label)); + } + Children.Add(new ReferenceFolderTreeNode(module, this)); if (module.Resources.Any())