From 28df83f9f674f6ffadb8f487bf0a2d73fbfd100f Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 25 Apr 2026 21:18:59 +0200 Subject: [PATCH] WPF-parity tree nodes, Field/Method/Property/Event glyphs Brings the assembly tree closer to the WPF impl: Assisted-by: Claude:claude-opus-4-7:Claude Code --- ILSpy/AssemblyTree/AssemblyListPane.axaml | 12 +- ILSpy/AssemblyTree/AssemblyListPane.axaml.cs | 9 +- ILSpy/AssemblyTree/AssemblyTreeModel.cs | 4 +- ILSpy/Assets/Icons/FindAssembly.svg | 93 +++++++++++ ILSpy/Assets/Icons/Library.svg | 1 + ILSpy/Assets/Icons/MetadataFile.svg | 73 +++++++++ ILSpy/Assets/Icons/NuGet.png | Bin 0 -> 13750 bytes ILSpy/Assets/Icons/ProgramDebugDatabase.svg | 1 + ILSpy/Assets/Icons/WebAssembly.svg | 8 + ILSpy/Images.cs | 61 +++++-- ILSpy/TreeNodes/AssemblyTreeNode.cs | 152 +++++++++++++----- .../{MemberTreeNode.cs => EventTreeNode.cs} | 18 +-- ILSpy/TreeNodes/FieldTreeNode.cs | 38 +++++ ILSpy/TreeNodes/MethodTreeNode.cs | 43 +++++ ILSpy/TreeNodes/NamespaceTreeNode.cs | 12 +- ILSpy/TreeNodes/NaturalStringComparer.cs | 32 ++++ ILSpy/TreeNodes/PropertyTreeNode.cs | 38 +++++ ILSpy/TreeNodes/TypeTreeNode.cs | 80 ++++----- 18 files changed, 554 insertions(+), 121 deletions(-) create mode 100644 ILSpy/Assets/Icons/FindAssembly.svg create mode 100644 ILSpy/Assets/Icons/Library.svg create mode 100644 ILSpy/Assets/Icons/MetadataFile.svg create mode 100644 ILSpy/Assets/Icons/NuGet.png create mode 100644 ILSpy/Assets/Icons/ProgramDebugDatabase.svg create mode 100644 ILSpy/Assets/Icons/WebAssembly.svg rename ILSpy/TreeNodes/{MemberTreeNode.cs => EventTreeNode.cs} (77%) create mode 100644 ILSpy/TreeNodes/FieldTreeNode.cs create mode 100644 ILSpy/TreeNodes/MethodTreeNode.cs create mode 100644 ILSpy/TreeNodes/NaturalStringComparer.cs create mode 100644 ILSpy/TreeNodes/PropertyTreeNode.cs diff --git a/ILSpy/AssemblyTree/AssemblyListPane.axaml b/ILSpy/AssemblyTree/AssemblyListPane.axaml index 5dba352e9..9bcf3dbfd 100644 --- a/ILSpy/AssemblyTree/AssemblyListPane.axaml +++ b/ILSpy/AssemblyTree/AssemblyListPane.axaml @@ -8,6 +8,8 @@ x:DataType="tree:AssemblyTreeModel"> - + - + - diff --git a/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs b/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs index a937e5a9c..97a20477c 100644 --- a/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs +++ b/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs @@ -54,19 +54,12 @@ namespace ILSpy.AssemblyTree void BindTree(SharpTreeNode root) { var options = new HierarchicalOptions { - // Force lazy children to load BEFORE returning the collection. ProDataGrid - // queries ChildrenSelector while expanding, before propagating IsExpanded to - // the source via IsExpandedSetter, so SharpTreeNode.LazyLoading wouldn't have - // triggered yet otherwise -- and an empty Children collection causes the grid - // to revert the expansion immediately. ChildrenSelector = node => { node.EnsureLazyChildren(); return node.Children; }, IsLeafSelector = node => !node.ShowExpander, - IsExpandedSelector = node => node.IsExpanded, - IsExpandedSetter = (node, val) => node.IsExpanded = val, - AutoExpandRoot = true, + VirtualizeChildren = false, }; var hierarchicalModel = new HierarchicalModel(options); diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs index 2bd329ce7..98643fc2c 100644 --- a/ILSpy/AssemblyTree/AssemblyTreeModel.cs +++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs @@ -102,9 +102,7 @@ namespace ILSpy.AssemblyTree if (AssemblyList.GetAssemblies().Length == 0 && value == AssemblyListManager.DefaultListName) LoadInitialAssemblies(AssemblyList); - var rootNode = new AssemblyListTreeNode(AssemblyList); - rootNode.IsExpanded = true; - Root = rootNode; + Root = new AssemblyListTreeNode(AssemblyList); settingsService.SessionSettings.ActiveAssemblyList = value; } diff --git a/ILSpy/Assets/Icons/FindAssembly.svg b/ILSpy/Assets/Icons/FindAssembly.svg new file mode 100644 index 000000000..ab9d7c6f2 --- /dev/null +++ b/ILSpy/Assets/Icons/FindAssembly.svg @@ -0,0 +1,93 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/ILSpy/Assets/Icons/Library.svg b/ILSpy/Assets/Icons/Library.svg new file mode 100644 index 000000000..0ffa85f60 --- /dev/null +++ b/ILSpy/Assets/Icons/Library.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/MetadataFile.svg b/ILSpy/Assets/Icons/MetadataFile.svg new file mode 100644 index 000000000..ce9cb449e --- /dev/null +++ b/ILSpy/Assets/Icons/MetadataFile.svg @@ -0,0 +1,73 @@ + + diff --git a/ILSpy/Assets/Icons/NuGet.png b/ILSpy/Assets/Icons/NuGet.png new file mode 100644 index 0000000000000000000000000000000000000000..0f7717cf585287b7076153919481e13c5f1b1d63 GIT binary patch literal 13750 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 \ No newline at end of file diff --git a/ILSpy/Assets/Icons/WebAssembly.svg b/ILSpy/Assets/Icons/WebAssembly.svg new file mode 100644 index 000000000..f2d67d77a --- /dev/null +++ b/ILSpy/Assets/Icons/WebAssembly.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/ILSpy/Images.cs b/ILSpy/Images.cs index 61d2e9d7f..f649bfa0b 100644 --- a/ILSpy/Images.cs +++ b/ILSpy/Images.cs @@ -16,34 +16,61 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System; + using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; using Avalonia.Svg.Skia; namespace ILSpy.Images { public static class Images { - static IImage Load(string name) + const string AssetBase = "avares://ILSpy/Assets/Icons/"; + + static IImage LoadSvg(string name) { return new SvgImage { - Source = SvgSource.Load($"avares://ILSpy/Assets/Icons/{name}.svg", null) + Source = SvgSource.Load(AssetBase + name + ".svg", null) }; } - public static readonly IImage Assembly = Load(nameof(Assembly)); - public static readonly IImage AssemblyLoading = Load(nameof(AssemblyLoading)); - public static readonly IImage AssemblyWarning = Load(nameof(AssemblyWarning)); - public static readonly IImage Namespace = Load(nameof(Namespace)); - public static readonly IImage Class = Load(nameof(Class)); - public static readonly IImage Interface = Load(nameof(Interface)); - public static readonly IImage Struct = Load(nameof(Struct)); - public static readonly IImage Enum = Load(nameof(Enum)); - public static readonly IImage Delegate = Load(nameof(Delegate)); - public static readonly IImage Method = Load(nameof(Method)); - public static readonly IImage Constructor = Load(nameof(Constructor)); - public static readonly IImage Operator = Load(nameof(Operator)); - public static readonly IImage Field = Load(nameof(Field)); - public static readonly IImage Property = Load(nameof(Property)); - public static readonly IImage Event = Load(nameof(Event)); + static IImage LoadPng(string name) + { + using var stream = AssetLoader.Open(new Uri(AssetBase + name + ".png")); + return new Bitmap(stream); + } + + // Assemblies / packages + public static readonly IImage Assembly = LoadSvg(nameof(Assembly)); + public static readonly IImage AssemblyLoading = LoadSvg(nameof(AssemblyLoading)); + public static readonly IImage AssemblyWarning = LoadSvg(nameof(AssemblyWarning)); + public static readonly IImage FindAssembly = LoadSvg(nameof(FindAssembly)); + public static readonly IImage Library = LoadSvg(nameof(Library)); + public static readonly IImage NuGet = LoadPng(nameof(NuGet)); + public static readonly IImage MetadataFile = LoadSvg(nameof(MetadataFile)); + public static readonly IImage WebAssemblyFile = LoadSvg("WebAssembly"); + public static readonly IImage ProgramDebugDatabase = LoadSvg(nameof(ProgramDebugDatabase)); + + // Containers + public static readonly IImage Namespace = LoadSvg(nameof(Namespace)); + + // Types + public static readonly IImage Class = LoadSvg(nameof(Class)); + public static readonly IImage Interface = LoadSvg(nameof(Interface)); + public static readonly IImage Struct = LoadSvg(nameof(Struct)); + public static readonly IImage Enum = LoadSvg(nameof(Enum)); + public static readonly IImage Delegate = LoadSvg(nameof(Delegate)); + + // Methods + public static readonly IImage Method = LoadSvg(nameof(Method)); + public static readonly IImage Constructor = LoadSvg(nameof(Constructor)); + public static readonly IImage Operator = LoadSvg(nameof(Operator)); + + // Fields / properties / events + public static readonly IImage Field = LoadSvg(nameof(Field)); + public static readonly IImage Property = LoadSvg(nameof(Property)); + public static readonly IImage Event = LoadSvg(nameof(Event)); } } diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index c794f76df..53f86bf7b 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -1,3 +1,4 @@ + // Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team // // Permission is hereby granted, free of charge, to any person obtaining a copy of this @@ -21,19 +22,18 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Documents; +using Avalonia.Media; + using ICSharpCode.Decompiler.Metadata; using ICSharpCode.ILSpyX; -using ILSpy.Images; - namespace ILSpy.TreeNodes { sealed class AssemblyTreeNode : ILSpyTreeNode { - enum LoadState { Loading, Loaded, Failed } - readonly LoadedAssembly assembly; - LoadState loadState = LoadState.Loading; string? loadError; MetadataFile? cachedModule; @@ -44,42 +44,24 @@ namespace ILSpy.TreeNodes ArgumentNullException.ThrowIfNull(assembly); this.assembly = assembly; LazyLoading = true; - _ = StartLoadingAsync(); + _ = InitAsync(); } - public override object Text => assembly.ShortName; - - public override object Icon => loadState switch { - LoadState.Failed => Images.Images.AssemblyWarning, - LoadState.Loading => Images.Images.AssemblyLoading, - _ => Images.Images.Assembly, - }; - - public override object? ToolTip => loadState == LoadState.Failed ? loadError : assembly.FileName; - - // Hide the chevron once a load has failed -- LoadChildren has nothing to show. - public override bool ShowExpander => loadState != LoadState.Failed && base.ShowExpander; - - async Task StartLoadingAsync() + async Task InitAsync() { try { - cachedModule = await assembly.GetMetadataFileOrNullAsync(); - if (cachedModule == null) + var loadResult = await assembly.GetLoadResultAsync(); + cachedModule = loadResult.MetadataFile; + if (cachedModule == null && loadResult.Package == null) { - loadState = LoadState.Failed; loadError = File.Exists(assembly.FileName) ? $"Failed to load '{assembly.FileName}'." : $"File not found:\n{assembly.FileName}"; } - else - { - loadState = LoadState.Loaded; - } } catch (Exception ex) { - loadState = LoadState.Failed; loadError = $"Failed to load '{assembly.FileName}':\n{ex.GetBaseException().Message}"; } RaisePropertyChanged(nameof(Icon)); @@ -87,33 +69,107 @@ namespace ILSpy.TreeNodes RaisePropertyChanged(nameof(ShowExpander)); } + public override object Text => assembly.ShortName; + + public override object Icon { + get { + if (assembly.HasLoadError || loadError != null) + return Images.Images.AssemblyWarning; + if (!assembly.IsLoaded) + return Images.Images.AssemblyLoading; + + var loadResult = assembly.GetLoadResultAsync().GetAwaiter().GetResult(); + if (loadResult.Package != null) + { + return loadResult.Package.Kind switch { + LoadedPackage.PackageKind.Zip => Images.Images.NuGet, + _ => Images.Images.Library, + }; + } + if (loadResult.MetadataFile != null) + { + return loadResult.MetadataFile.Kind switch { + MetadataFile.MetadataFileKind.PortableExecutable => Images.Images.Assembly, + MetadataFile.MetadataFileKind.ProgramDebugDatabase => Images.Images.ProgramDebugDatabase, + MetadataFile.MetadataFileKind.WebCIL => Images.Images.WebAssemblyFile, + _ => Images.Images.MetadataFile, + }; + } + return Images.Images.Assembly; + } + } + + public override object? ToolTip { + get { + if (assembly.HasLoadError || loadError != null) + return loadError ?? "Assembly could not be loaded."; + if (!assembly.IsLoaded || cachedModule == null) + return assembly.FileName; + + var tb = new TextBlock { TextWrapping = TextWrapping.Wrap }; + + var metadata = cachedModule.Metadata; + if (metadata?.IsAssembly == true && metadata.TryGetFullAssemblyName(out var assemblyName)) + { + tb.Inlines!.Add(BoldRun("Name: ")); + tb.Inlines.Add(new Run(assemblyName)); + tb.Inlines.Add(new LineBreak()); + } + + tb.Inlines!.Add(BoldRun("Location: ")); + tb.Inlines.Add(new Run(assembly.FileName)); + + if (cachedModule is PEFile peFile) + { + tb.Inlines.Add(new LineBreak()); + tb.Inlines.Add(BoldRun("Architecture: ")); + tb.Inlines.Add(new Run(GetPlatformDisplayName(peFile))); + } + + string? runtime = GetRuntimeDisplayName(cachedModule); + if (runtime != null) + { + tb.Inlines.Add(new LineBreak()); + tb.Inlines.Add(BoldRun("Runtime: ")); + tb.Inlines.Add(new Run(runtime)); + } + + return tb; + } + } + + public override bool ShowExpander => !assembly.HasLoadError && loadError == null && base.ShowExpander; + protected override void LoadChildren() { - // If the user expands before the background load finishes, fall back to a sync wait. - // Once StartLoadingAsync runs, cachedModule / loadState are authoritative. - MetadataFile? module = cachedModule; - if (module == null && loadState == LoadState.Loading) + var module = cachedModule; + if (module == null) { try { - module = assembly.GetMetadataFileOrNullAsync().Result; + module = assembly.GetLoadResultAsync().GetAwaiter().GetResult().MetadataFile; } catch { - module = null; + return; } } - if (module == null) return; + if (module.Kind != MetadataFile.MetadataFileKind.PortableExecutable + && module.Kind != MetadataFile.MetadataFileKind.WebCIL) + { + return; + } + var metadata = module.Metadata; var namespaces = metadata.TypeDefinitions .Where(t => metadata.GetTypeDefinition(t).GetDeclaringType().IsNil) .Select(t => metadata.GetString(metadata.GetTypeDefinition(t).Namespace)) .Where(ns => !string.IsNullOrEmpty(ns)) .Distinct() - .OrderBy(ns => ns); + .OrderBy(ns => ns, NaturalStringComparer.Instance); foreach (var ns in namespaces) Children.Add(new NamespaceTreeNode(ns, module)); @@ -128,5 +184,29 @@ namespace ILSpy.TreeNodes foreach (var t in globalTypes) Children.Add(new TypeTreeNode(t, module)); } + + static Run BoldRun(string text) => new(text) { FontWeight = FontWeight.Bold }; + + static string GetPlatformDisplayName(PEFile file) + { + return file.Reader.PEHeaders.CoffHeader.Machine switch { + System.Reflection.PortableExecutable.Machine.I386 => "x86", + System.Reflection.PortableExecutable.Machine.Amd64 => "x64", + System.Reflection.PortableExecutable.Machine.IA64 => "Itanium", + System.Reflection.PortableExecutable.Machine.Arm => "ARM", + System.Reflection.PortableExecutable.Machine.Arm64 => "ARM64", + _ => file.Reader.PEHeaders.CoffHeader.Machine.ToString(), + }; + } + + static string? GetRuntimeDisplayName(MetadataFile module) + { + return module.Metadata.MetadataKind switch { + System.Reflection.Metadata.MetadataKind.Ecma335 => module.Metadata.MetadataVersion, + System.Reflection.Metadata.MetadataKind.WindowsMetadata => "WinRT", + System.Reflection.Metadata.MetadataKind.ManagedWindowsMetadata => "Managed WinRT", + _ => null, + }; + } } } diff --git a/ILSpy/TreeNodes/MemberTreeNode.cs b/ILSpy/TreeNodes/EventTreeNode.cs similarity index 77% rename from ILSpy/TreeNodes/MemberTreeNode.cs rename to ILSpy/TreeNodes/EventTreeNode.cs index 13ed8d493..08ef94627 100644 --- a/ILSpy/TreeNodes/MemberTreeNode.cs +++ b/ILSpy/TreeNodes/EventTreeNode.cs @@ -16,23 +16,23 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -using Avalonia.Media; +using System; + +using ICSharpCode.Decompiler.TypeSystem; namespace ILSpy.TreeNodes { - sealed class MemberTreeNode : ILSpyTreeNode + sealed class EventTreeNode : ILSpyTreeNode { - readonly string name; - readonly IImage icon; + public IEvent EventDefinition { get; } - public MemberTreeNode(string name, IImage icon) + public EventTreeNode(IEvent ev) { - this.name = name; - this.icon = icon; + EventDefinition = ev ?? throw new ArgumentNullException(nameof(ev)); } - public override object Text => name; - public override object Icon => icon; + public override object Text => EventDefinition.Name; + public override object Icon => Images.Images.Event; public override bool ShowExpander => false; } } diff --git a/ILSpy/TreeNodes/FieldTreeNode.cs b/ILSpy/TreeNodes/FieldTreeNode.cs new file mode 100644 index 000000000..9488ee3e4 --- /dev/null +++ b/ILSpy/TreeNodes/FieldTreeNode.cs @@ -0,0 +1,38 @@ +// 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.TypeSystem; + +namespace ILSpy.TreeNodes +{ + sealed class FieldTreeNode : ILSpyTreeNode + { + public IField FieldDefinition { get; } + + public FieldTreeNode(IField field) + { + FieldDefinition = field ?? throw new ArgumentNullException(nameof(field)); + } + + public override object Text => FieldDefinition.Name; + public override object Icon => Images.Images.Field; + public override bool ShowExpander => false; + } +} diff --git a/ILSpy/TreeNodes/MethodTreeNode.cs b/ILSpy/TreeNodes/MethodTreeNode.cs new file mode 100644 index 000000000..956f1b81e --- /dev/null +++ b/ILSpy/TreeNodes/MethodTreeNode.cs @@ -0,0 +1,43 @@ +// 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.TypeSystem; + +namespace ILSpy.TreeNodes +{ + sealed class MethodTreeNode : ILSpyTreeNode + { + public IMethod MethodDefinition { get; } + + public MethodTreeNode(IMethod method) + { + MethodDefinition = method ?? throw new ArgumentNullException(nameof(method)); + } + + public override object Text => MethodDefinition.Name; + + public override object Icon => + MethodDefinition.IsConstructor ? Images.Images.Constructor : + MethodDefinition.IsOperator ? Images.Images.Operator : + Images.Images.Method; + + public override bool ShowExpander => false; + } +} diff --git a/ILSpy/TreeNodes/NamespaceTreeNode.cs b/ILSpy/TreeNodes/NamespaceTreeNode.cs index 725e28ec6..170d96045 100644 --- a/ILSpy/TreeNodes/NamespaceTreeNode.cs +++ b/ILSpy/TreeNodes/NamespaceTreeNode.cs @@ -16,13 +16,11 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System; using System.Linq; -using System.Reflection.Metadata; using ICSharpCode.Decompiler.Metadata; -using ILSpy.Images; - namespace ILSpy.TreeNodes { sealed class NamespaceTreeNode : ILSpyTreeNode @@ -34,12 +32,12 @@ namespace ILSpy.TreeNodes public NamespaceTreeNode(string name, MetadataFile module) { - this.name = name; - this.module = module; + this.name = name ?? throw new ArgumentNullException(nameof(name)); + this.module = module ?? throw new ArgumentNullException(nameof(module)); LazyLoading = true; } - public override object Text => name; + public override object Text => name.Length == 0 ? "-" : name; public override object Icon => Images.Images.Namespace; @@ -52,7 +50,7 @@ namespace ILSpy.TreeNodes return td.GetDeclaringType().IsNil && metadata.GetString(td.Namespace) == name; }) - .OrderBy(t => metadata.GetString(metadata.GetTypeDefinition(t).Name)); + .OrderBy(t => metadata.GetString(metadata.GetTypeDefinition(t).Name), NaturalStringComparer.Instance); foreach (var t in types) Children.Add(new TypeTreeNode(t, module)); diff --git a/ILSpy/TreeNodes/NaturalStringComparer.cs b/ILSpy/TreeNodes/NaturalStringComparer.cs new file mode 100644 index 000000000..eab8b8b34 --- /dev/null +++ b/ILSpy/TreeNodes/NaturalStringComparer.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2015 Siegfried Pammer +// +// 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.Collections.Generic; +using System.Globalization; + +namespace ILSpy.TreeNodes +{ + /// + /// .NET natural string comparison. + /// + internal static class NaturalStringComparer + { + public static readonly IComparer Instance = + System.StringComparer.Create(CultureInfo.CurrentCulture, CompareOptions.NumericOrdering); + } +} diff --git a/ILSpy/TreeNodes/PropertyTreeNode.cs b/ILSpy/TreeNodes/PropertyTreeNode.cs new file mode 100644 index 000000000..323374f46 --- /dev/null +++ b/ILSpy/TreeNodes/PropertyTreeNode.cs @@ -0,0 +1,38 @@ +// 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.TypeSystem; + +namespace ILSpy.TreeNodes +{ + sealed class PropertyTreeNode : ILSpyTreeNode + { + public IProperty PropertyDefinition { get; } + + public PropertyTreeNode(IProperty property) + { + PropertyDefinition = property ?? throw new ArgumentNullException(nameof(property)); + } + + public override object Text => PropertyDefinition.Name; + public override object Icon => Images.Images.Property; + public override bool ShowExpander => false; + } +} diff --git a/ILSpy/TreeNodes/TypeTreeNode.cs b/ILSpy/TreeNodes/TypeTreeNode.cs index e7a840320..e6253adc6 100644 --- a/ILSpy/TreeNodes/TypeTreeNode.cs +++ b/ILSpy/TreeNodes/TypeTreeNode.cs @@ -16,13 +16,13 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System; using System.Linq; -using System.Reflection; using System.Reflection.Metadata; using ICSharpCode.Decompiler.Metadata; - -using ILSpy.Images; +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.ILSpyX; namespace ILSpy.TreeNodes { @@ -37,7 +37,7 @@ namespace ILSpy.TreeNodes public TypeTreeNode(TypeDefinitionHandle handle, MetadataFile module) { this.handle = handle; - this.module = module; + this.module = module ?? throw new ArgumentNullException(nameof(module)); LazyLoading = true; } @@ -50,51 +50,57 @@ namespace ILSpy.TreeNodes public override object Icon { get { - var td = module.Metadata.GetTypeDefinition(handle); - var attrs = td.Attributes; - if ((attrs & TypeAttributes.ClassSemanticsMask) == TypeAttributes.Interface) - return Images.Images.Interface; - // crude detection: real distinction needs base-type lookups (System.Enum, ValueType, - // MulticastDelegate). Refining will land with the broader tree-node port. - return Images.Images.Class; + var typeDef = ResolveTypeDefinition(); + return typeDef?.Kind switch { + TypeKind.Interface => Images.Images.Interface, + TypeKind.Struct or TypeKind.Void => Images.Images.Struct, + TypeKind.Delegate => Images.Images.Delegate, + TypeKind.Enum => Images.Images.Enum, + _ => Images.Images.Class, + }; } } - protected override void LoadChildren() + public override bool CanExpandRecursively => true; + + ITypeDefinition? ResolveTypeDefinition() { - var td = module.Metadata.GetTypeDefinition(handle); + var typeSystem = module.GetTypeSystemOrNull(); + if (typeSystem == null) + return null; + return ((MetadataModule)typeSystem.MainModule).GetDefinition(handle); + } - foreach (var nestedType in td.GetNestedTypes()) - Children.Add(new TypeTreeNode(nestedType, module)); + protected override void LoadChildren() + { + var typeDef = ResolveTypeDefinition(); + if (typeDef == null) + return; - foreach (var method in td.GetMethods()) + foreach (var nestedType in typeDef.NestedTypes + .OrderBy(t => t.Name, NaturalStringComparer.Instance)) { - var md = module.Metadata.GetMethodDefinition(method); - var name = module.Metadata.GetString(md.Name); - var icon = name == ".ctor" || name == ".cctor" - ? Images.Images.Constructor - : ((md.Attributes & MethodAttributes.SpecialName) != 0 && name.StartsWith("op_")) - ? Images.Images.Operator - : Images.Images.Method; - Children.Add(new MemberTreeNode(name, icon)); + Children.Add(new TypeTreeNode((TypeDefinitionHandle)nestedType.MetadataToken, module)); } - foreach (var field in td.GetFields()) - { - var fd = module.Metadata.GetFieldDefinition(field); - Children.Add(new MemberTreeNode(module.Metadata.GetString(fd.Name), Images.Images.Field)); - } + // Enums look more useful in declaration order than alphabetical. + var fields = typeDef.Kind == TypeKind.Enum + ? typeDef.Fields + : typeDef.Fields.OrderBy(f => f.Name, NaturalStringComparer.Instance); + foreach (var field in fields) + Children.Add(new FieldTreeNode(field)); - foreach (var prop in td.GetProperties()) - { - var pd = module.Metadata.GetPropertyDefinition(prop); - Children.Add(new MemberTreeNode(module.Metadata.GetString(pd.Name), Images.Images.Property)); - } + foreach (var prop in typeDef.Properties.OrderBy(p => p.Name, NaturalStringComparer.Instance)) + Children.Add(new PropertyTreeNode(prop)); + + foreach (var ev in typeDef.Events.OrderBy(e => e.Name, NaturalStringComparer.Instance)) + Children.Add(new EventTreeNode(ev)); - foreach (var evt in td.GetEvents()) + foreach (var method in typeDef.Methods.OrderBy(m => m.Name, NaturalStringComparer.Instance)) { - var ed = module.Metadata.GetEventDefinition(evt); - Children.Add(new MemberTreeNode(module.Metadata.GetString(ed.Name), Images.Images.Event)); + if (method.MetadataToken.IsNil || method.IsAccessor) + continue; + Children.Add(new MethodTreeNode(method)); } } }