mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
The metadata-grid hover infrastructure shipped but no table entry fed it, so every cell tooltip the previous version had was silently gone: the heap-offset/value hints on string and blob columns, the entity description on token columns, and the per-bit breakdown on flag columns. Reinstate all of them. FlagsTooltip renders the rich per-bit view (a checkbox per flag plus the selected member of each mutually exclusive sub-range) instead of a flat string; GenerateTooltip on the table base describes what a token column points at; the remaining columns expose their heap-offset and value hints. MetadataCellTooltip now hands a FlagsTooltip back as a built control and stringifies everything else. Assisted-by: Claude:claude-opus-4-8:Claude Codepull/3755/head
47 changed files with 984 additions and 35 deletions
@ -0,0 +1,81 @@
@@ -0,0 +1,81 @@
|
||||
// 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 System.Reflection; |
||||
|
||||
using Avalonia.Controls; |
||||
using Avalonia.Headless.NUnit; |
||||
|
||||
using AwesomeAssertions; |
||||
|
||||
using ILSpy.Metadata; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests.Metadata; |
||||
|
||||
/// <summary>
|
||||
/// The rich flags tooltip breaks an enum value into per-bit groups: multiple-choice groups render a
|
||||
/// checkbox per flag with the set bits checked, single-choice groups render the selected member of a
|
||||
/// mutually exclusive sub-range. (Regression: the Avalonia port had only a plain-text fallback.)
|
||||
/// </summary>
|
||||
[TestFixture] |
||||
public class FlagsTooltipTests |
||||
{ |
||||
[AvaloniaTest] |
||||
public void MultipleChoiceGroup_Checks_The_Set_Bits() |
||||
{ |
||||
var group = FlagGroup.CreateMultipleChoiceGroup(typeof(MethodAttributes), "Flags:", |
||||
(int)MethodAttributes.Static, (int)MethodAttributes.Static, includeAll: false); |
||||
|
||||
var view = (StackPanel)group.Build(); |
||||
var statik = view.Children.OfType<CheckBox>() |
||||
.Single(c => ((string?)c.Content)!.StartsWith("Static")); |
||||
statik.IsChecked.Should().BeTrue("the Static bit is set in the selected value"); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public void SingleChoiceGroup_Shows_The_Selected_Member() |
||||
{ |
||||
var group = FlagGroup.CreateSingleChoiceGroup(typeof(MethodAttributes), "Accessibility: ", |
||||
(int)MethodAttributes.MemberAccessMask, (int)MethodAttributes.Public, includeAny: false); |
||||
|
||||
group.SelectedFlag.Name.Should().StartWith("Public"); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public void MetadataCellTooltip_Renders_A_FlagsTooltip_As_A_Control() |
||||
{ |
||||
var entry = new EntryWithFlags(); |
||||
var resolved = MetadataCellTooltip.Resolve(entry, "Attributes"); |
||||
resolved.Should().BeAssignableTo<Control>("a FlagsTooltip value renders as the rich control"); |
||||
} |
||||
|
||||
sealed class EntryWithFlags |
||||
{ |
||||
public MethodAttributes Attributes => MethodAttributes.Public | MethodAttributes.Static; |
||||
|
||||
public object AttributesTooltip => new FlagsTooltip { |
||||
FlagGroup.CreateSingleChoiceGroup(typeof(MethodAttributes), "Accessibility: ", |
||||
(int)MethodAttributes.MemberAccessMask, (int)(Attributes & MethodAttributes.MemberAccessMask), includeAny: false), |
||||
FlagGroup.CreateMultipleChoiceGroup(typeof(MethodAttributes), "Flags:", |
||||
~(int)MethodAttributes.MemberAccessMask, (int)Attributes, includeAll: false), |
||||
}; |
||||
} |
||||
} |
||||
@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
|
||||
// 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 System.Threading.Tasks; |
||||
|
||||
using Avalonia.Controls; |
||||
using Avalonia.Headless.NUnit; |
||||
|
||||
using AwesomeAssertions; |
||||
|
||||
using ILSpy.Metadata; |
||||
using ILSpy.Metadata.CorTables; |
||||
using ILSpy.TreeNodes; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests.Metadata; |
||||
|
||||
/// <summary>
|
||||
/// End-to-end check that the per-cell metadata tooltips restored across the table entries actually
|
||||
/// resolve against a real assembly: a heap-offset string tooltip, a token tooltip routed through
|
||||
/// <c>GenerateTooltip</c>, and a rich per-bit <see cref="FlagsTooltip"/> rendered as a control.
|
||||
/// </summary>
|
||||
[TestFixture] |
||||
public class MetadataTooltipParityTests |
||||
{ |
||||
[AvaloniaTest] |
||||
public async Task TypeDef_Row_Tooltips_Resolve_For_System_Object() |
||||
{ |
||||
var (_, vm) = await TestHarness.BootAsync(); |
||||
|
||||
var typeDefNode = vm.AssemblyTreeModel.FindCoreLib() |
||||
.GetChild<MetadataTreeNode>() |
||||
.GetChild<MetadataTablesTreeNode>() |
||||
.GetChild<TypeDefTableTreeNode>(); |
||||
|
||||
vm.AssemblyTreeModel.SelectNode(typeDefNode); |
||||
var tab = await vm.DockWorkspace.WaitForMetadataTabAsync(); |
||||
|
||||
var objectRow = tab.Items.Cast<TypeDefTableTreeNode.TypeDefEntry>() |
||||
.Single(e => e.Name == "Object" && e.Namespace == "System"); |
||||
|
||||
// Heap-offset string tooltip on the Name column.
|
||||
MetadataCellTooltip.Resolve(objectRow, "Name").Should().BeOfType<string>() |
||||
.Which.Should().Contain("Object"); |
||||
|
||||
// Token tooltip routed through GenerateTooltip (System.Object's first method).
|
||||
MetadataCellTooltip.Resolve(objectRow, "MethodList").Should().BeOfType<string>() |
||||
.Which.Should().NotBeNullOrWhiteSpace(); |
||||
|
||||
// Rich per-bit flags breakdown renders as a control, not a plain string.
|
||||
MetadataCellTooltip.Resolve(objectRow, "Attributes").Should().BeAssignableTo<Control>(); |
||||
} |
||||
} |
||||
@ -0,0 +1,184 @@
@@ -0,0 +1,184 @@
|
||||
// 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 System.Collections; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Reflection; |
||||
|
||||
using Avalonia; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Layout; |
||||
using Avalonia.Media; |
||||
|
||||
using ICSharpCode.Decompiler.Util; |
||||
|
||||
namespace ILSpy.Metadata |
||||
{ |
||||
/// <summary>
|
||||
/// Rich cell tooltip for an enum-valued metadata column: it breaks a flags value down into
|
||||
/// per-bit groups, showing each named flag with a checkbox reflecting whether the bit is set
|
||||
/// (multiple-choice groups) or the single selected member of a mutually exclusive sub-range
|
||||
/// (single-choice groups). Entries build one with a collection initializer of
|
||||
/// <see cref="FlagGroup"/>s; <see cref="MetadataCellTooltip"/> renders it via <see cref="Build"/>.
|
||||
/// </summary>
|
||||
public sealed class FlagsTooltip : IEnumerable<FlagGroup> |
||||
{ |
||||
// The (value, flagsType) ctor parameters are unused but kept so entry call sites read the
|
||||
// same as the value they describe; the groups carry the actual selection state.
|
||||
public FlagsTooltip(int value = 0, Type? flagsType = null) |
||||
{ |
||||
} |
||||
|
||||
public List<FlagGroup> Groups { get; } = new List<FlagGroup>(); |
||||
|
||||
public void Add(FlagGroup group) => Groups.Add(group); |
||||
|
||||
public IEnumerator<FlagGroup> GetEnumerator() => Groups.GetEnumerator(); |
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => Groups.GetEnumerator(); |
||||
|
||||
/// <summary>
|
||||
/// Materialises the tooltip into an Avalonia control: a vertical stack of group views.
|
||||
/// </summary>
|
||||
public Control Build() |
||||
{ |
||||
var panel = new StackPanel { Orientation = Orientation.Vertical }; |
||||
foreach (var group in Groups) |
||||
panel.Children.Add(group.Build()); |
||||
return panel; |
||||
} |
||||
} |
||||
|
||||
public readonly struct Flag |
||||
{ |
||||
public string Name { get; } |
||||
public int Value { get; } |
||||
public bool IsSelected { get; } |
||||
|
||||
public Flag(string name, int value, bool isSelected) |
||||
{ |
||||
this.Name = name; |
||||
this.Value = value; |
||||
this.IsSelected = isSelected; |
||||
} |
||||
} |
||||
|
||||
public abstract class FlagGroup |
||||
{ |
||||
public static MultipleChoiceGroup CreateMultipleChoiceGroup(Type flagsType, string? header = null, int mask = -1, int selectedValue = 0, bool includeAll = true) |
||||
{ |
||||
return new MultipleChoiceGroup(GetFlags(flagsType, mask, selectedValue, includeAll ? "<All>" : null)) { |
||||
Header = header, |
||||
SelectedFlags = selectedValue, |
||||
}; |
||||
} |
||||
|
||||
public static SingleChoiceGroup CreateSingleChoiceGroup(Type flagsType, string? header = null, int mask = -1, int selectedValue = 0, Flag defaultFlag = default, bool includeAny = true) |
||||
{ |
||||
var group = new SingleChoiceGroup(GetFlags(flagsType, mask, selectedValue, includeAny ? "<Any>" : null)) { |
||||
Header = header, |
||||
}; |
||||
group.SelectedFlag = group.Flags.SingleOrDefault(f => f.Value == selectedValue); |
||||
if (group.SelectedFlag.Name == null) |
||||
group.SelectedFlag = defaultFlag; |
||||
return group; |
||||
} |
||||
|
||||
public static IEnumerable<Flag> GetFlags(Type flagsType, int mask = -1, int selectedValues = 0, string? neutralItem = null) |
||||
{ |
||||
if (neutralItem != null) |
||||
yield return new Flag(neutralItem, -1, false); |
||||
|
||||
foreach (var item in flagsType.GetFields(BindingFlags.Static | BindingFlags.Public)) |
||||
{ |
||||
if (item.Name.EndsWith("Mask", StringComparison.Ordinal)) |
||||
continue; |
||||
int value = (int)CSharpPrimitiveCast.Cast(TypeCode.Int32, item.GetRawConstantValue(), false); |
||||
if ((value & mask) == 0) |
||||
continue; |
||||
yield return new Flag($"{item.Name} ({value:X4})", value, (selectedValues & value) != 0); |
||||
} |
||||
} |
||||
|
||||
public string? Header { get; set; } |
||||
|
||||
public IList<Flag> Flags { get; protected set; } = Array.Empty<Flag>(); |
||||
|
||||
public abstract Control Build(); |
||||
|
||||
private protected static TextBlock? BuildHeader(string? header) |
||||
{ |
||||
if (string.IsNullOrEmpty(header)) |
||||
return null; |
||||
return new TextBlock { Text = header, FontWeight = FontWeight.Bold }; |
||||
} |
||||
} |
||||
|
||||
public sealed class MultipleChoiceGroup : FlagGroup |
||||
{ |
||||
public MultipleChoiceGroup(IEnumerable<Flag> flags) |
||||
{ |
||||
this.Flags = flags.ToList(); |
||||
} |
||||
|
||||
public int SelectedFlags { get; set; } |
||||
|
||||
public override Control Build() |
||||
{ |
||||
var panel = new StackPanel { Orientation = Orientation.Vertical, Margin = new Thickness(3) }; |
||||
var header = BuildHeader(Header); |
||||
if (header != null) |
||||
{ |
||||
header.Margin = new Thickness(0, 0, 0, 3); |
||||
panel.Children.Add(header); |
||||
} |
||||
foreach (var flag in Flags) |
||||
{ |
||||
panel.Children.Add(new CheckBox { |
||||
Content = flag.Name, |
||||
IsChecked = flag.IsSelected, |
||||
IsHitTestVisible = false, |
||||
Margin = new Thickness(3, 1), |
||||
}); |
||||
} |
||||
return panel; |
||||
} |
||||
} |
||||
|
||||
public sealed class SingleChoiceGroup : FlagGroup |
||||
{ |
||||
public SingleChoiceGroup(IEnumerable<Flag> flags) |
||||
{ |
||||
this.Flags = flags.ToList(); |
||||
} |
||||
|
||||
public Flag SelectedFlag { get; set; } |
||||
|
||||
public override Control Build() |
||||
{ |
||||
var panel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(3), Spacing = 3 }; |
||||
var header = BuildHeader(Header); |
||||
if (header != null) |
||||
panel.Children.Add(header); |
||||
panel.Children.Add(new TextBlock { Text = SelectedFlag.Name }); |
||||
return panel; |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue