diff --git a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs index 77b9965bd..9a5e56632 100644 --- a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs +++ b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs @@ -44,6 +44,27 @@ namespace ICSharpCode.ILSpy.Tests; [TestFixture] public class AssemblyTreeTests { + [AvaloniaTest] + public async Task Assembly_Node_Has_Resources_Folder_When_Module_Has_Resources() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + // Pick a loaded assembly that ships embedded resources. CoreLib always has at least + // the localised error-message tables. + var coreLibName = typeof(object).Assembly.GetName().Name!; + var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + assemblyNode.EnsureLazyChildren(); + + var resources = assemblyNode.Children.OfType().Single(); + resources.Text.Should().Be(Resources._Resources); + resources.EnsureLazyChildren(); + resources.Children.Should().NotBeEmpty(); + resources.Children.Should().AllBeAssignableTo(); + } + [AvaloniaTest] public async Task Assembly_Node_Has_References_Folder_Child() { diff --git a/ILSpy/Assets/Icons/FolderClosed.svg b/ILSpy/Assets/Icons/FolderClosed.svg new file mode 100644 index 000000000..481bb01ac --- /dev/null +++ b/ILSpy/Assets/Icons/FolderClosed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/FolderOpen.svg b/ILSpy/Assets/Icons/FolderOpen.svg new file mode 100644 index 000000000..4c8a03f5b --- /dev/null +++ b/ILSpy/Assets/Icons/FolderOpen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/Resource.svg b/ILSpy/Assets/Icons/Resource.svg new file mode 100644 index 000000000..7b36178ab --- /dev/null +++ b/ILSpy/Assets/Icons/Resource.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Images.cs b/ILSpy/Images.cs index 17eda5a63..2a754ae7f 100644 --- a/ILSpy/Images.cs +++ b/ILSpy/Images.cs @@ -77,6 +77,9 @@ namespace ILSpy.Images public static readonly IImage Namespace = LoadSvg(nameof(Namespace)); public static readonly IImage ReferenceFolder = LoadSvg(nameof(ReferenceFolder)); public static readonly IImage MetadataTable = LoadSvg(nameof(MetadataTable)); + public static readonly IImage FolderClosed = LoadSvg(nameof(FolderClosed)); + public static readonly IImage FolderOpen = LoadSvg(nameof(FolderOpen)); + public static readonly IImage Resource = LoadSvg(nameof(Resource)); // Types public static readonly IImage Class = LoadSvg(nameof(Class)); diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index d5560d3fd..82d0ac586 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -205,6 +205,9 @@ namespace ILSpy.TreeNodes Children.Add(new ReferenceFolderTreeNode(module, this)); + if (module.Resources.Any()) + Children.Add(new ResourceListTreeNode(module)); + var metadata = module.Metadata; var namespaces = metadata.TypeDefinitions .Where(t => metadata.GetTypeDefinition(t).GetDeclaringType().IsNil) diff --git a/ILSpy/TreeNodes/ResourceListTreeNode.cs b/ILSpy/TreeNodes/ResourceListTreeNode.cs new file mode 100644 index 000000000..7d70afa32 --- /dev/null +++ b/ILSpy/TreeNodes/ResourceListTreeNode.cs @@ -0,0 +1,77 @@ +// 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.Linq; + +using Avalonia.Threading; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.ILSpy.Properties; + +using ILSpy.Languages; + +namespace ILSpy.TreeNodes +{ + /// + /// Lists the embedded resources of an assembly. Children are + /// instances created via the factory. + /// + sealed class ResourceListTreeNode : ILSpyTreeNode + { + readonly MetadataFile module; + + public ResourceListTreeNode(MetadataFile module) + { + this.module = module; + LazyLoading = true; + } + + public override object Text => Resources._Resources; + + public override object Icon => IsExpanded ? Images.Images.FolderOpen : Images.Images.FolderClosed; + + protected override void OnExpanding() + { + base.OnExpanding(); + RaisePropertyChanged(nameof(Icon)); + } + + protected override void OnCollapsing() + { + base.OnCollapsing(); + RaisePropertyChanged(nameof(Icon)); + } + + protected override void LoadChildren() + { + foreach (var r in module.Resources.OrderBy(m => m.Name, NaturalStringComparer.Instance)) + Children.Add(ResourceTreeNode.Create(r)); + } + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + Dispatcher.UIThread.Invoke(EnsureLazyChildren); + foreach (var child in Children.OfType()) + { + child.Decompile(language, output, options); + output.WriteLine(); + } + } + } +} diff --git a/ILSpy/TreeNodes/ResourceTreeNode.cs b/ILSpy/TreeNodes/ResourceTreeNode.cs new file mode 100644 index 000000000..03f1ccab0 --- /dev/null +++ b/ILSpy/TreeNodes/ResourceTreeNode.cs @@ -0,0 +1,56 @@ +// 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 ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.IL; +using ICSharpCode.Decompiler.Metadata; + +using ILSpy.Languages; + +namespace ILSpy.TreeNodes +{ + /// + /// Default resource entry node — used when no specialised handler (image, XAML, .resources + /// file, …) is registered for a given resource. The Avalonia port currently always falls + /// back to this plain node since the typed handlers are not yet ported. + /// + public class ResourceTreeNode : ILSpyTreeNode + { + public ResourceTreeNode(Resource r) + { + Resource = r ?? throw new ArgumentNullException(nameof(r)); + } + + public Resource Resource { get; } + + public override object Text => ILAmbience.EscapeName(Resource.Name); + + public override object Icon => Images.Images.Resource; + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + var sizeInBytes = Resource.TryGetLength(); + var sizeInBytesText = sizeInBytes == null ? "" : ", " + sizeInBytes + " bytes"; + language.WriteCommentLine(output, $"{Resource.Name} ({Resource.ResourceType}, {Resource.Attributes}{sizeInBytesText})"); + } + + public static ILSpyTreeNode Create(Resource resource) => new ResourceTreeNode(resource); + } +}