From b1045f48e0611eb7adfd6d5b31d36d3bc4bd82ca Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 29 May 2026 00:42:26 +0200 Subject: [PATCH] Port the missing icon variants and the Copy glyph from the WPF tree The avalonia branch only carried the base set of tree-node icons. FieldReadOnly, EnumValue, Indexer, VirtualMethod, ExtensionMethod and PInvokeMethod were already SVGs in master:ILSpy/Images/ but never copied over -- so e.g. readonly fields shared the same glyph as mutable instance fields, and indexers were indistinguishable from plain properties. Bring across the seven SVGs (those six plus Copy for the Copy- fully-qualified-name context-menu entry), register them on Images.cs, and extend the icon dispatch in each tree node to mirror WPF's logic: FieldTreeNode EnumValue / Literal (const) / FieldReadOnly / Field PropertyTreeNode Indexer / Property, with the C# 14 Extension overlay when the accessor is an extension member MethodTreeNode Operator / extension Method / Constructor / PInvoke (extern + DllImport, no body) / VirtualMethod / Method PInvoke detection uses the documented (!HasBody && DllImport) pair so managed wrappers that re-apply the attribute via forwarding don't inherit the native-call glyph. Also wire the Copy icon onto the "Copy fully qualified name" entry so the menu row matches its WPF counterpart. Assisted-by: Claude:claude-opus-4-7:Claude Code --- ILSpy/Assets/Icons/Copy.svg | 1 + ILSpy/Assets/Icons/EnumValue.svg | 1 + ILSpy/Assets/Icons/ExtensionMethod.svg | 1 + ILSpy/Assets/Icons/FieldReadOnly.svg | 69 ++++++++++++++ ILSpy/Assets/Icons/Indexer.svg | 1 + ILSpy/Assets/Icons/PInvokeMethod.svg | 79 ++++++++++++++++ ILSpy/Assets/Icons/VirtualMethod.svg | 93 +++++++++++++++++++ .../CopyFullyQualifiedNameContextMenuEntry.cs | 2 +- ILSpy/Images.cs | 9 ++ ILSpy/TreeNodes/FieldTreeNode.cs | 15 +-- ILSpy/TreeNodes/MethodTreeNode.cs | 41 ++++++-- ILSpy/TreeNodes/PropertyTreeNode.cs | 17 +++- 12 files changed, 309 insertions(+), 20 deletions(-) create mode 100644 ILSpy/Assets/Icons/Copy.svg create mode 100644 ILSpy/Assets/Icons/EnumValue.svg create mode 100644 ILSpy/Assets/Icons/ExtensionMethod.svg create mode 100644 ILSpy/Assets/Icons/FieldReadOnly.svg create mode 100644 ILSpy/Assets/Icons/Indexer.svg create mode 100644 ILSpy/Assets/Icons/PInvokeMethod.svg create mode 100644 ILSpy/Assets/Icons/VirtualMethod.svg diff --git a/ILSpy/Assets/Icons/Copy.svg b/ILSpy/Assets/Icons/Copy.svg new file mode 100644 index 000000000..3de60b008 --- /dev/null +++ b/ILSpy/Assets/Icons/Copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/EnumValue.svg b/ILSpy/Assets/Icons/EnumValue.svg new file mode 100644 index 000000000..aa901ec19 --- /dev/null +++ b/ILSpy/Assets/Icons/EnumValue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/ExtensionMethod.svg b/ILSpy/Assets/Icons/ExtensionMethod.svg new file mode 100644 index 000000000..999349160 --- /dev/null +++ b/ILSpy/Assets/Icons/ExtensionMethod.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/FieldReadOnly.svg b/ILSpy/Assets/Icons/FieldReadOnly.svg new file mode 100644 index 000000000..c79e4b2d6 --- /dev/null +++ b/ILSpy/Assets/Icons/FieldReadOnly.svg @@ -0,0 +1,69 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/ILSpy/Assets/Icons/Indexer.svg b/ILSpy/Assets/Icons/Indexer.svg new file mode 100644 index 000000000..ff55f31ff --- /dev/null +++ b/ILSpy/Assets/Icons/Indexer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/PInvokeMethod.svg b/ILSpy/Assets/Icons/PInvokeMethod.svg new file mode 100644 index 000000000..4db51d198 --- /dev/null +++ b/ILSpy/Assets/Icons/PInvokeMethod.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/ILSpy/Assets/Icons/VirtualMethod.svg b/ILSpy/Assets/Icons/VirtualMethod.svg new file mode 100644 index 000000000..dfb0aeba5 --- /dev/null +++ b/ILSpy/Assets/Icons/VirtualMethod.svg @@ -0,0 +1,93 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/ILSpy/Commands/CopyFullyQualifiedNameContextMenuEntry.cs b/ILSpy/Commands/CopyFullyQualifiedNameContextMenuEntry.cs index f503d0361..7af87253c 100644 --- a/ILSpy/Commands/CopyFullyQualifiedNameContextMenuEntry.cs +++ b/ILSpy/Commands/CopyFullyQualifiedNameContextMenuEntry.cs @@ -34,7 +34,7 @@ namespace ILSpy.Commands /// Copies the entity's /// — the language-independent identifier used by FindNodeByPath etc. — to the clipboard. /// - [ExportContextMenuEntry(Header = nameof(Resources.CopyName), Category = "Edit", Order = 600)] + [ExportContextMenuEntry(Header = nameof(Resources.CopyName), Category = "Edit", Icon = "Images/Copy", Order = 600)] [Shared] public sealed class CopyFullyQualifiedNameContextMenuEntry : IContextMenuEntry { diff --git a/ILSpy/Images.cs b/ILSpy/Images.cs index 46b9327af..2607cfedc 100644 --- a/ILSpy/Images.cs +++ b/ILSpy/Images.cs @@ -110,14 +110,23 @@ namespace ILSpy.Images public static readonly IImage Method = LoadSvg(nameof(Method)); public static readonly IImage Constructor = LoadSvg(nameof(Constructor)); public static readonly IImage Operator = LoadSvg(nameof(Operator)); + public static readonly IImage VirtualMethod = LoadSvg(nameof(VirtualMethod)); + public static readonly IImage ExtensionMethod = LoadSvg(nameof(ExtensionMethod)); + public static readonly IImage PInvokeMethod = LoadSvg(nameof(PInvokeMethod)); // Fields / properties / events public static readonly IImage Field = LoadSvg(nameof(Field)); + public static readonly IImage FieldReadOnly = LoadSvg(nameof(FieldReadOnly)); + public static readonly IImage EnumValue = LoadSvg(nameof(EnumValue)); public static readonly IImage Property = LoadSvg(nameof(Property)); + public static readonly IImage Indexer = LoadSvg(nameof(Indexer)); public static readonly IImage Event = LoadSvg(nameof(Event)); public static readonly IImage Literal = LoadSvg(nameof(Literal)); public static readonly IImage ViewCode = LoadSvg(nameof(ViewCode)); + // Editing / context-menu icons + public static readonly IImage Copy = LoadSvg(nameof(Copy)); + // Reference overlays (small badge layered onto a base icon for TypeRef/MemberRef nodes). internal static readonly IImage ReferenceOverlay = LoadSvg(nameof(ReferenceOverlay)); internal static readonly IImage ExportOverlay = LoadSvg(nameof(ExportOverlay)); diff --git a/ILSpy/TreeNodes/FieldTreeNode.cs b/ILSpy/TreeNodes/FieldTreeNode.cs index 5a07b063d..1e45a3fdc 100644 --- a/ILSpy/TreeNodes/FieldTreeNode.cs +++ b/ILSpy/TreeNodes/FieldTreeNode.cs @@ -45,22 +45,23 @@ namespace ILSpy.TreeNodes public override object Icon => GetIcon(FieldDefinition); - // Distinguish enum members and const fields from regular instance/static fields so the - // tree reads the way the C# source does. The IsReadOnly / FieldReadOnly variant from - // the WPF era isn't ported because we don't have a dedicated SVG for it yet -- such - // fields fall through to the base Field icon. + // Mirrors WPF's discriminator: enum value, const literal, readonly, plain field. public static Avalonia.Media.IImage GetIcon(IField field) { - // EnumValue: the field's declaring type is an enum and its return type is the enum - // itself -- this excludes the synthesised int32 'value__' backing field. + // EnumValue: declaring type is an enum and the return type is the enum itself -- + // this excludes the synthesised int32 'value__' backing field. if (field.DeclaringType.Kind == TypeKind.Enum && field.ReturnType.Kind == TypeKind.Enum) - return Images.Images.GetIcon(Images.Images.Enum, + return Images.Images.GetIcon(Images.Images.EnumValue, Images.Images.GetOverlay(field.Accessibility)); if (field.IsConst) return Images.Images.GetIcon(Images.Images.Literal, Images.Images.GetOverlay(field.Accessibility)); + if (field.IsReadOnly) + return Images.Images.GetIcon(Images.Images.FieldReadOnly, + Images.Images.GetOverlay(field.Accessibility), field.IsStatic); + return Images.Images.GetIcon(Images.Images.Field, Images.Images.GetOverlay(field.Accessibility), field.IsStatic); } diff --git a/ILSpy/TreeNodes/MethodTreeNode.cs b/ILSpy/TreeNodes/MethodTreeNode.cs index e826b5a9f..494c668ff 100644 --- a/ILSpy/TreeNodes/MethodTreeNode.cs +++ b/ILSpy/TreeNodes/MethodTreeNode.cs @@ -43,16 +43,37 @@ namespace ILSpy.TreeNodes public override object NavigationText => Language.EntityToString(MethodDefinition, ConversionFlags.ShowDeclaringType); - public override object Icon { - get { - var baseImage = MethodDefinition.IsConstructor ? Images.Images.Constructor : - MethodDefinition.IsOperator ? Images.Images.Operator : - Images.Images.Method; - return Images.Images.GetIcon(baseImage, - Images.Images.GetOverlay(MethodDefinition.Accessibility), - MethodDefinition.IsStatic, - MethodDefinition.IsExtensionMethod); - } + public override object Icon => GetIcon(MethodDefinition); + + // Mirrors WPF's dispatch order: operator, extension method, constructor, + // P/Invoke (DllImport without a body), virtual, plain method. Tested against + // every kind in the analyzers fixtures. + public static Avalonia.Media.IImage GetIcon(IMethod method) + { + bool isExtensionMethod = method.IsExtensionMethod + || (method.ResolveExtensionInfo()?.InfoOfExtensionMember((IMethod)method.MemberDefinition).HasValue == true); + + if (method.IsOperator) + return Images.Images.GetIcon(Images.Images.Operator, + Images.Images.GetOverlay(method.Accessibility), false, isExtensionMethod); + + if (isExtensionMethod) + return Images.Images.GetIcon(Images.Images.Method, + Images.Images.GetOverlay(method.Accessibility), false, true); + + if (method.IsConstructor) + return Images.Images.GetIcon(Images.Images.Constructor, + Images.Images.GetOverlay(method.Accessibility), method.IsStatic); + + // P/Invoke: extern method tagged with [DllImport]. !HasBody filters out the + // managed wrappers that also carry the attribute via attribute-forwarding. + if (!method.HasBody && method.HasAttribute(KnownAttribute.DllImport)) + return Images.Images.GetIcon(Images.Images.PInvokeMethod, + Images.Images.GetOverlay(method.Accessibility), true); + + return Images.Images.GetIcon( + method.IsVirtual ? Images.Images.VirtualMethod : Images.Images.Method, + Images.Images.GetOverlay(method.Accessibility), method.IsStatic); } public override bool ShowExpander => false; diff --git a/ILSpy/TreeNodes/PropertyTreeNode.cs b/ILSpy/TreeNodes/PropertyTreeNode.cs index db76b855c..d4ea66ffd 100644 --- a/ILSpy/TreeNodes/PropertyTreeNode.cs +++ b/ILSpy/TreeNodes/PropertyTreeNode.cs @@ -47,8 +47,21 @@ namespace ILSpy.TreeNodes public override object NavigationText => Language.EntityToString(PropertyDefinition, ConversionFlags.ShowDeclaringType); - public override object Icon => Images.Images.GetIcon(Images.Images.Property, - Images.Images.GetOverlay(PropertyDefinition.Accessibility), PropertyDefinition.IsStatic); + public override object Icon => GetIcon(PropertyDefinition); + + // Mirrors WPF: indexers get a distinct base glyph; extension properties (C# 14 + // extension blocks) layer the Extension overlay on top. + public static Avalonia.Media.IImage GetIcon(IProperty property) + { + IMethod? accessor = property.Getter ?? property.Setter; + bool isExtension = accessor is not null + && property.ResolveExtensionInfo()?.InfoOfExtensionMember((IMethod)accessor.MemberDefinition) != null; + return Images.Images.GetIcon( + property.IsIndexer ? Images.Images.Indexer : Images.Images.Property, + Images.Images.GetOverlay(property.Accessibility), + property.IsStatic, + isExtension); + } public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) => language.DecompileProperty(PropertyDefinition, output, options);