From 28b682262c10b413b417d46ca7202ecdbbc1c59c Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 24 Jun 2026 07:06:00 +0200 Subject: [PATCH] Syntax-colour analyzer signatures with bold type names The Analyze panel showed each signature as a flat string, so the type names that carry the key information were hard to spot in long Used-By / Uses lists. Render analyzer entity rows as syntax-highlighted rich text with the type-name spans emboldened (issue #2164). Replace Language.GetRichTextTooltip(entity) with a general GetRichText(entity, conversionFlags, boldTypeNames): the hover tooltip is one caller, the analyzer pane another. C# colours the signature via CSharpHighlightingTokenWriter and bolds the type-name colour spans; IL renders its disassembled header (its signature form), already lexically highlighted; the base produces plain text. The IL method/type/field header is semantically the same artifact as the C# signature, so it shares the one method. Rendering is opt-in: nodes that want rich labels implement IRichTextNode, and the single shared SharpTreeView cell renders its runs via the new RichNodeText attached behaviour, falling back to the plain Text otherwise. SharpTreeNode.Text stays the authoritative plain string, so search, copy and keyboard navigation are unchanged; analyzer entity nodes derive Text from the same RichText so the two never diverge. Fixes #2164 Assisted-by: Claude:claude-opus-4-8:Claude Code --- ILSpy.ReadyToRun/ReadyToRunLanguage.cs | 4 +- .../Analyzers/AnalyzerRichTextTests.cs | 89 +++++++++++++++++ ILSpy.Tests/Editor/ILLanguageTooltipTests.cs | 8 +- ILSpy/Analyzers/AnalyzedEventTreeNode.cs | 9 +- ILSpy/Analyzers/AnalyzedFieldTreeNode.cs | 9 +- ILSpy/Analyzers/AnalyzedMethodTreeNode.cs | 8 +- ILSpy/Analyzers/AnalyzedPropertyTreeNode.cs | 9 +- ILSpy/Analyzers/AnalyzedTypeTreeNode.cs | 6 +- ILSpy/Analyzers/AnalyzerEntityTreeNode.cs | 55 ++++++++++- ILSpy/Controls/TreeView/IRichTextNode.cs | 40 ++++++++ ILSpy/Controls/TreeView/RichNodeText.cs | 96 +++++++++++++++++++ ILSpy/Controls/TreeView/SharpTreeView.axaml | 4 +- ILSpy/Languages/CSharpLanguage.cs | 37 ++++++- ILSpy/Languages/ILLanguage.cs | 7 +- ILSpy/Languages/Language.cs | 10 +- ILSpy/TextView/DecompilerTextView.axaml.cs | 6 +- 16 files changed, 371 insertions(+), 26 deletions(-) create mode 100644 ILSpy.Tests/Analyzers/AnalyzerRichTextTests.cs create mode 100644 ILSpy/Controls/TreeView/IRichTextNode.cs create mode 100644 ILSpy/Controls/TreeView/RichNodeText.cs diff --git a/ILSpy.ReadyToRun/ReadyToRunLanguage.cs b/ILSpy.ReadyToRun/ReadyToRunLanguage.cs index 47a26fe75..18172e447 100644 --- a/ILSpy.ReadyToRun/ReadyToRunLanguage.cs +++ b/ILSpy.ReadyToRun/ReadyToRunLanguage.cs @@ -232,9 +232,9 @@ namespace ICSharpCode.ILSpy.ReadyToRun } } - public override RichText GetRichTextTooltip(IEntity entity) + public override RichText GetRichText(IEntity entity, ConversionFlags conversionFlags, bool boldTypeNames = false) { - return languageService.Value.GetLanguage("IL").GetRichTextTooltip(entity); + return languageService.Value.GetLanguage("IL").GetRichText(entity, conversionFlags, boldTypeNames); } private ReadyToRunReaderCacheEntry GetReader(LoadedAssembly assembly, MetadataFile file) diff --git a/ILSpy.Tests/Analyzers/AnalyzerRichTextTests.cs b/ILSpy.Tests/Analyzers/AnalyzerRichTextTests.cs new file mode 100644 index 000000000..c9152e9ab --- /dev/null +++ b/ILSpy.Tests/Analyzers/AnalyzerRichTextTests.cs @@ -0,0 +1,89 @@ +// 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.Headless.NUnit; +using Avalonia.Media; + +using AwesomeAssertions; + +using ICSharpCode.Decompiler.TypeSystem; + +using ICSharpCode.ILSpy.Analyzers; +using ICSharpCode.ILSpy.Analyzers.TreeNodes; +using ICSharpCode.ILSpy.AssemblyTree; +using ICSharpCode.ILSpy.Controls.TreeView; +using ICSharpCode.ILSpy.TreeNodes; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests.Analyzers; + +[TestFixture] +public class AnalyzerRichTextTests +{ + static IMethod GetEnumerableMethod(AssemblyTreeModel model, string name) + { + var typeNode = model.FindNode("System.Linq", "System.Linq", "System.Linq.Enumerable"); + typeNode.EnsureLazyChildren(); + return (IMethod)typeNode.Children.OfType() + .First(m => m.MethodDefinition.Name == name).Member!; + } + + [AvaloniaTest] + public async Task AnalyzerEntityNode_Produces_RichText_With_Bold_Type_Names() + { + // Issue #2164: analyzer-pane signatures colour the C# syntax and render type-name spans + // bold so the key info stands out in long lists. The node exposes this via IRichTextNode. + var (_, vm) = await TestHarness.BootAsync(); + var method = GetEnumerableMethod(vm.AssemblyTreeModel, "Empty"); + + var node = new AnalyzedMethodTreeNode(method, source: null); + + var richNode = node as IRichTextNode; + richNode.Should().NotBeNull("analyzer entity nodes opt into rich rendering via IRichTextNode"); + var rich = richNode!.CreateRichText(); + rich.Should().NotBeNull(); + + // The colored signature text and the plain Text used for search/copy stay identical. + rich!.Text.Should().Be(node.Text.ToString()); + + // At least one span (the declaring/return type, e.g. Enumerable / IEnumerable) is bold. + var hasBoldSpan = rich.ToRichTextModel() + .GetHighlightedSections(0, rich.Length) + .Any(s => s.Color?.FontWeight == FontWeight.Bold); + hasBoldSpan.Should().BeTrue("type names in the analyzer signature must be emboldened (#2164)"); + } + + [AvaloniaTest] + public async Task RichText_Does_Not_Change_The_Plain_Text_Contract() + { + // The string Text (and ToString) must stay the plain signature so search, copy and + // keyboard navigation keep working unchanged. + var (_, vm) = await TestHarness.BootAsync(); + var method = GetEnumerableMethod(vm.AssemblyTreeModel, "Empty"); + + var node = new AnalyzedMethodTreeNode(method, source: null); + + node.Text.Should().BeOfType(); + node.ToString().Should().Be(node.Text.ToString()); + node.ToString().Should().Contain("Empty").And.Contain("Enumerable"); + } +} diff --git a/ILSpy.Tests/Editor/ILLanguageTooltipTests.cs b/ILSpy.Tests/Editor/ILLanguageTooltipTests.cs index 555f66b6e..45114a106 100644 --- a/ILSpy.Tests/Editor/ILLanguageTooltipTests.cs +++ b/ILSpy.Tests/Editor/ILLanguageTooltipTests.cs @@ -46,7 +46,7 @@ namespace ICSharpCode.ILSpy.Tests.TextView; /// Pins the IL-specific halves of the decompiler-view hover tooltip. In the IL language /// view, hovering a member reference must show the member's disassembled IL header /// (".method ...", ".class ...", ".field ..."), not a C#-style one-liner — that is what -/// produces. Hovering an opcode must show the +/// produces. Hovering an opcode must show the /// opcode's XML documentation, which comes from /// via the /// "F:System.Reflection.Emit.OpCodes.*" doc ids. @@ -78,7 +78,7 @@ public class ILLanguageTooltipTests .First(m => m.MethodDefinition.Name == "Concat" && m.MethodDefinition.Parameters.Count == 2) .MethodDefinition; - var tooltip = GetILLanguage().GetRichTextTooltip(concat); + var tooltip = GetILLanguage().GetRichText(concat, default); tooltip.Should().NotBeNull(); tooltip!.Text.Should().StartWith(".method", @@ -94,7 +94,7 @@ public class ILLanguageTooltipTests var stringNode = await LoadCoreLibStringNodeAsync(); var stringType = (ITypeDefinition)stringNode.Member!; - var tooltip = GetILLanguage().GetRichTextTooltip(stringType); + var tooltip = GetILLanguage().GetRichText(stringType, default); tooltip.Should().NotBeNull(); tooltip!.Text.Should().StartWith(".class"); @@ -110,7 +110,7 @@ public class ILLanguageTooltipTests .First(f => f.FieldDefinition.Name == "Empty") .FieldDefinition; - var tooltip = GetILLanguage().GetRichTextTooltip(empty); + var tooltip = GetILLanguage().GetRichText(empty, default); tooltip.Should().NotBeNull(); tooltip!.Text.Should().StartWith(".field"); diff --git a/ILSpy/Analyzers/AnalyzedEventTreeNode.cs b/ILSpy/Analyzers/AnalyzedEventTreeNode.cs index 0471921bc..1a3044bd6 100644 --- a/ILSpy/Analyzers/AnalyzedEventTreeNode.cs +++ b/ILSpy/Analyzers/AnalyzedEventTreeNode.cs @@ -19,6 +19,8 @@ using System; using System.Diagnostics.CodeAnalysis; +using AvaloniaEdit.Highlighting; + using ICSharpCode.Decompiler.Output; using ICSharpCode.Decompiler.TypeSystem; @@ -39,8 +41,11 @@ namespace ICSharpCode.ILSpy.Analyzers.TreeNodes public override IEntity Member => analyzedEvent; - public override object Text => prefix + Language.EntityToString(analyzedEvent, - ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames); + public override object Text => CreateRichText()?.Text + ?? prefix + Language.EntityToString(analyzedEvent, + ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames); + + protected override RichText? BuildRichText() => CreateMemberRichText(prefix, MemberSignatureFlags); public override object Icon => Images.GetIcon(Images.Event, Images.GetOverlay(analyzedEvent.Accessibility), analyzedEvent.IsStatic); diff --git a/ILSpy/Analyzers/AnalyzedFieldTreeNode.cs b/ILSpy/Analyzers/AnalyzedFieldTreeNode.cs index 4538ad9be..da64e7e44 100644 --- a/ILSpy/Analyzers/AnalyzedFieldTreeNode.cs +++ b/ILSpy/Analyzers/AnalyzedFieldTreeNode.cs @@ -18,6 +18,8 @@ using System; +using AvaloniaEdit.Highlighting; + using ICSharpCode.Decompiler.Output; using ICSharpCode.Decompiler.TypeSystem; @@ -36,8 +38,11 @@ namespace ICSharpCode.ILSpy.Analyzers.TreeNodes public override IEntity Member => analyzedField; - public override object Text => Language.EntityToString(analyzedField, - ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames); + public override object Text => CreateRichText()?.Text + ?? Language.EntityToString(analyzedField, + ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames); + + protected override RichText? BuildRichText() => CreateMemberRichText("", MemberSignatureFlags); public override object Icon => Images.GetIcon(Images.Field, Images.GetOverlay(analyzedField.Accessibility), analyzedField.IsStatic); diff --git a/ILSpy/Analyzers/AnalyzedMethodTreeNode.cs b/ILSpy/Analyzers/AnalyzedMethodTreeNode.cs index 6f6685960..5b6278d9a 100644 --- a/ILSpy/Analyzers/AnalyzedMethodTreeNode.cs +++ b/ILSpy/Analyzers/AnalyzedMethodTreeNode.cs @@ -18,6 +18,8 @@ using System; +using AvaloniaEdit.Highlighting; + using ICSharpCode.Decompiler.Output; using ICSharpCode.Decompiler.TypeSystem; @@ -38,10 +40,12 @@ namespace ICSharpCode.ILSpy.Analyzers.TreeNodes public override IEntity Member => analyzedMethod; - public override object Text - => prefix + Language.EntityToString(analyzedMethod, + public override object Text => CreateRichText()?.Text + ?? prefix + Language.EntityToString(analyzedMethod, ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames); + protected override RichText? BuildRichText() => CreateMemberRichText(prefix, MemberSignatureFlags); + public override object Icon => ResolveIcon(analyzedMethod); internal static object ResolveIcon(IMethod method) diff --git a/ILSpy/Analyzers/AnalyzedPropertyTreeNode.cs b/ILSpy/Analyzers/AnalyzedPropertyTreeNode.cs index 9071a1fca..0ee416226 100644 --- a/ILSpy/Analyzers/AnalyzedPropertyTreeNode.cs +++ b/ILSpy/Analyzers/AnalyzedPropertyTreeNode.cs @@ -18,6 +18,8 @@ using System; +using AvaloniaEdit.Highlighting; + using ICSharpCode.Decompiler.Output; using ICSharpCode.Decompiler.TypeSystem; @@ -38,8 +40,11 @@ namespace ICSharpCode.ILSpy.Analyzers.TreeNodes public override IEntity Member => analyzedProperty; - public override object Text => prefix + Language.EntityToString(analyzedProperty, - ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames); + public override object Text => CreateRichText()?.Text + ?? prefix + Language.EntityToString(analyzedProperty, + ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames); + + protected override RichText? BuildRichText() => CreateMemberRichText(prefix, MemberSignatureFlags); public override object Icon => Images.GetIcon(Images.Property, Images.GetOverlay(analyzedProperty.Accessibility), analyzedProperty.IsStatic); diff --git a/ILSpy/Analyzers/AnalyzedTypeTreeNode.cs b/ILSpy/Analyzers/AnalyzedTypeTreeNode.cs index 2588a1ce1..2160f3410 100644 --- a/ILSpy/Analyzers/AnalyzedTypeTreeNode.cs +++ b/ILSpy/Analyzers/AnalyzedTypeTreeNode.cs @@ -18,6 +18,8 @@ using System; +using AvaloniaEdit.Highlighting; + using ICSharpCode.Decompiler.TypeSystem; namespace ICSharpCode.ILSpy.Analyzers.TreeNodes @@ -35,7 +37,9 @@ namespace ICSharpCode.ILSpy.Analyzers.TreeNodes public override IEntity Member => analyzedType; - public override object Text => Language.TypeToString(analyzedType); + public override object Text => CreateRichText()?.Text ?? Language.TypeToString(analyzedType); + + protected override RichText? BuildRichText() => CreateMemberRichText("", TypeSignatureFlags); public override object Icon => ResolveIcon(analyzedType); diff --git a/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs b/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs index 3984a1a97..7e1bf5fdf 100644 --- a/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs +++ b/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs @@ -18,6 +18,9 @@ using System.Collections.Generic; +using AvaloniaEdit.Highlighting; + +using ICSharpCode.Decompiler.Output; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.ILSpyX; using ICSharpCode.ILSpyX.TreeView; @@ -25,6 +28,7 @@ using ICSharpCode.ILSpyX.TreeView.PlatformAbstractions; using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.AssemblyTree; +using ICSharpCode.ILSpy.Controls.TreeView; using ICSharpCode.ILSpy.Util; namespace ICSharpCode.ILSpy.Analyzers @@ -36,8 +40,57 @@ namespace ICSharpCode.ILSpy.Analyzers /// icon, and its text; this base owns the navigation hook and the assembly-change /// pruning logic. /// - public abstract class AnalyzerEntityTreeNode : AnalyzerTreeNode + public abstract class AnalyzerEntityTreeNode : AnalyzerTreeNode, IRichTextNode { + // Flags reproducing the plain signature the pane used before highlighting: the member's + // declaring type, fully-qualified names, and the usual return-type/parameter detail. + protected const ConversionFlags MemberSignatureFlags = + ConversionFlags.ShowDeclaringType + | ConversionFlags.UseFullyQualifiedEntityNames + | ConversionFlags.ShowReturnType + | ConversionFlags.ShowParameterList + | ConversionFlags.ShowParameterModifiers + | ConversionFlags.ShowTypeParameterList + | ConversionFlags.PlaceReturnTypeAfterParameterList; + + // Type rows show a fully-qualified type signature with its type parameters. + protected const ConversionFlags TypeSignatureFlags = + ConversionFlags.UseFullyQualifiedEntityNames + | ConversionFlags.UseFullyQualifiedTypeNames + | ConversionFlags.ShowTypeParameterList; + + RichText? richText; + bool richTextComputed; + + /// + /// The highlighted, type-name-emboldened signature for this row, computed once and cached. + /// Falls back to (plain Text) for non-entity rows. + /// + public RichText? CreateRichText() + { + if (!richTextComputed) + { + richText = BuildRichText(); + richTextComputed = true; + } + return richText; + } + + protected virtual RichText? BuildRichText() => null; + + /// + /// Builds the coloured signature for with the given flags, bolding + /// type names, and prepends as plain (uncoloured) text. + /// + protected RichText? CreateMemberRichText(string prefix, ConversionFlags flags) + { + var member = Member; + if (member == null) + return null; + var body = Language.GetRichText(member, flags, boldTypeNames: true); + return prefix.Length == 0 ? body : new RichText(prefix) + body; + } + public override void ActivateItem(IPlatformRoutedEventArgs e) { e.Handled = true; diff --git a/ILSpy/Controls/TreeView/IRichTextNode.cs b/ILSpy/Controls/TreeView/IRichTextNode.cs new file mode 100644 index 000000000..95ebfefc7 --- /dev/null +++ b/ILSpy/Controls/TreeView/IRichTextNode.cs @@ -0,0 +1,40 @@ +// 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 AvaloniaEdit.Highlighting; + +namespace ICSharpCode.ILSpy.Controls.TreeView +{ + /// + /// Implemented by tree nodes whose label should render as syntax-highlighted rich text + /// (coloured runs, bold type names) instead of the plain string from + /// SharpTreeNode.Text. The cell template renders when + /// it returns a value and falls back to the plain Text otherwise, so a node's + /// Text stays the authoritative plain-string representation for search, copy and + /// keyboard navigation. + /// + public interface IRichTextNode + { + /// + /// The highlighted label, or to fall back to the plain + /// Text (e.g. when no entity is available or the active language has no + /// highlighting). The returned text must equal the plain Text. + /// + RichText? CreateRichText(); + } +} diff --git a/ILSpy/Controls/TreeView/RichNodeText.cs b/ILSpy/Controls/TreeView/RichNodeText.cs new file mode 100644 index 000000000..3cb2d3bcc --- /dev/null +++ b/ILSpy/Controls/TreeView/RichNodeText.cs @@ -0,0 +1,96 @@ +// 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.ComponentModel; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Documents; + +using ICSharpCode.ILSpy.TextView; +using ICSharpCode.ILSpyX.TreeView; + +namespace ICSharpCode.ILSpy.Controls.TreeView +{ + /// + /// Attached behaviour for the tree's label : when the bound node is an + /// that yields rich text, it renders coloured / bold inlines; + /// otherwise it shows the node's plain Text. Keeping this in one place lets the single + /// shared SharpTreeView cell template support rich labels without every node becoming rich, and + /// without changing the SharpTreeNode.Text string used for search, copy and navigation. + /// + public static class RichNodeText + { + public static readonly AttachedProperty NodeProperty = + AvaloniaProperty.RegisterAttached("Node", typeof(RichNodeText)); + + // Holds the live PropertyChanged subscription so a recycled TextBlock detaches cleanly. + static readonly AttachedProperty HandlerProperty = + AvaloniaProperty.RegisterAttached("Handler", typeof(RichNodeText)); + + static RichNodeText() + { + NodeProperty.Changed.AddClassHandler(OnNodeChanged); + } + + public static void SetNode(TextBlock element, SharpTreeNode? value) => element.SetValue(NodeProperty, value); + public static SharpTreeNode? GetNode(TextBlock element) => element.GetValue(NodeProperty); + + static void OnNodeChanged(TextBlock textBlock, AvaloniaPropertyChangedEventArgs e) + { + if (e.OldValue is INotifyPropertyChanged oldNode + && textBlock.GetValue(HandlerProperty) is { } oldHandler) + { + oldNode.PropertyChanged -= oldHandler; + textBlock.SetValue(HandlerProperty, null); + } + + var node = e.NewValue as SharpTreeNode; + if (node is INotifyPropertyChanged newNode) + { + // Mirror the {Binding Text} the cell used to have: refresh when Text changes + // (e.g. a lazily-loaded node swapping its placeholder for the real label). + void Handler(object? sender, PropertyChangedEventArgs args) + { + if (args.PropertyName is nameof(SharpTreeNode.Text) or null or "") + Render(textBlock, node); + } + + newNode.PropertyChanged += Handler; + textBlock.SetValue(HandlerProperty, Handler); + } + + Render(textBlock, node); + } + + static void Render(TextBlock textBlock, SharpTreeNode? node) + { + if (node is IRichTextNode richNode && richNode.CreateRichText() is { } rich) + { + var inlines = new InlineCollection(); + DocumentationRenderer.AppendRichText(inlines, rich); + textBlock.Inlines = inlines; + } + else + { + textBlock.Inlines = null; + textBlock.Text = node?.Text?.ToString(); + } + } + } +} diff --git a/ILSpy/Controls/TreeView/SharpTreeView.axaml b/ILSpy/Controls/TreeView/SharpTreeView.axaml index fc1a3771d..692b4c2fb 100644 --- a/ILSpy/Controls/TreeView/SharpTreeView.axaml +++ b/ILSpy/Controls/TreeView/SharpTreeView.axaml @@ -108,7 +108,9 @@ - + diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index e31fadc04..b777e3182 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -265,17 +265,46 @@ namespace ICSharpCode.ILSpy.Languages return !CSharpDecompiler.MemberIsHidden(assembly, member.MetadataToken, settings); } - public override RichText GetRichTextTooltip(IEntity entity) + // Type-name highlighting colours, matching the spans CSharpHighlightingTokenWriter assigns + // to the various kinds of type reference. Used to embolden type names (issue #2164). + static readonly string[] typeHighlightingColorNames = { + "ReferenceTypes", "ValueTypes", "InterfaceTypes", "EnumTypes", "TypeParameters", "DelegateTypes", + }; + + public override RichText GetRichText(IEntity entity, ConversionFlags conversionFlags, bool boldTypeNames = false) { ArgumentNullException.ThrowIfNull(entity); - var flags = ConversionFlags.All & ~(ConversionFlags.ShowBody | ConversionFlags.PlaceReturnTypeAfterParameterList); var output = new StringWriter(); var decoratedWriter = new TextWriterTokenWriter(output); var writer = new CSharpHighlightingTokenWriter(TokenWriter.InsertRequiredSpaces(decoratedWriter), locatable: decoratedWriter); if (entity is IMethod m && m.IsLocalFunction) writer.WriteIdentifier(Identifier.Create("(local)")); - new CSharpAmbience { ConversionFlags = flags }.ConvertSymbol(entity, writer, FormattingOptionsFactory.CreateAllman()); - return new RichText(output.ToString(), writer.HighlightingModel); + new CSharpAmbience { ConversionFlags = conversionFlags }.ConvertSymbol(entity, writer, FormattingOptionsFactory.CreateAllman()); + var text = output.ToString(); + var model = writer.HighlightingModel; + if (boldTypeNames) + EmboldenTypeNames(text, model); + return new RichText(text, model); + } + + static void EmboldenTypeNames(string text, RichTextModel model) + { + var highlighting = HighlightingManager.Instance.GetDefinition("C#"); + if (highlighting == null) + return; + var typeColors = new HashSet(); + foreach (var name in typeHighlightingColorNames) + { + if (highlighting.GetNamedColor(name) is { } color) + typeColors.Add(color); + } + if (typeColors.Count == 0) + return; + foreach (var section in model.GetHighlightedSections(0, text.Length).ToList()) + { + if (section.Color is { } sectionColor && typeColors.Contains(sectionColor)) + model.SetFontWeight(section.Offset, section.Length, Avalonia.Media.FontWeight.Bold); + } } CSharpDecompiler CreateDecompiler(MetadataFile module, DecompilationOptions options) diff --git a/ILSpy/Languages/ILLanguage.cs b/ILSpy/Languages/ILLanguage.cs index 60b081652..df36c2f15 100644 --- a/ILSpy/Languages/ILLanguage.cs +++ b/ILSpy/Languages/ILLanguage.cs @@ -29,6 +29,7 @@ using AvaloniaEdit.Highlighting; using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.Disassembler; using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.Output; using ICSharpCode.Decompiler.Solution; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.ILSpyX; @@ -84,12 +85,12 @@ namespace ICSharpCode.ILSpy.Languages /// The hover tooltip for an entity in IL view is its disassembled IL header (signature /// only, no body), collapsed to a single line and highlighted with the IL definition. /// - public override RichText GetRichTextTooltip(IEntity entity) + public override RichText GetRichText(IEntity entity, ConversionFlags conversionFlags, bool boldTypeNames = false) { ArgumentNullException.ThrowIfNull(entity); var module = entity.ParentModule?.MetadataFile; if (module == null) - return base.GetRichTextTooltip(entity); + return base.GetRichText(entity, conversionFlags, boldTypeNames); var output = new AvaloniaEditTextOutput { IgnoreNewLineAndIndent = true }; // A header never reaches the settings the full Decompile* paths feed into @@ -121,7 +122,7 @@ namespace ICSharpCode.ILSpy.Languages disasm.DisassembleMethodHeader(module, (MethodDefinitionHandle)entity.MetadataToken); break; default: - return base.GetRichTextTooltip(entity); + return base.GetRichText(entity, conversionFlags, boldTypeNames); } var text = output.GetText().TrimEnd(); diff --git a/ILSpy/Languages/Language.cs b/ILSpy/Languages/Language.cs index 5572f895e..3befc61f7 100644 --- a/ILSpy/Languages/Language.cs +++ b/ILSpy/Languages/Language.cs @@ -178,7 +178,15 @@ namespace ICSharpCode.ILSpy.Languages return EntityToString(entity, ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames | ConversionFlags.ShowDeclaringType); } - public virtual RichText GetRichTextTooltip(IEntity entity) => new(GetTooltip(entity)); + /// + /// Produces a syntax-highlighted signature for using the given + /// conversion flags. The base implementation carries no highlighting; language subclasses + /// colour the signature (C#) or render their own equivalent (the IL method/type header). + /// When is set, type-name spans are rendered bold so they + /// stand out in dense lists such as the analyzer pane (issue #2164). + /// + public virtual RichText GetRichText(IEntity entity, ConversionFlags conversionFlags, bool boldTypeNames = false) + => new(EntityToString(entity, conversionFlags)); public virtual void DecompileType(ITypeDefinition type, ITextOutput output, DecompilationOptions options) { diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs index a55cfe04a..04ce0accc 100644 --- a/ILSpy/TextView/DecompilerTextView.axaml.cs +++ b/ILSpy/TextView/DecompilerTextView.axaml.cs @@ -40,6 +40,7 @@ using ICSharpCode.Decompiler.CSharp.OutputVisitor; using ICSharpCode.Decompiler.Disassembler; using ICSharpCode.Decompiler.Documentation; using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.Output; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.ILSpyX; @@ -1081,7 +1082,10 @@ namespace ICSharpCode.ILSpy.TextView switch (resolved) { case IEntity entity when language != null: - var rich = language.GetRichTextTooltip(entity); + // The hover tooltip is the full signature (return type before the name, no body); + // IL renders its own header form. No bold emphasis here, unlike the analyzer pane. + var rich = language.GetRichText(entity, + ConversionFlags.All & ~(ConversionFlags.ShowBody | ConversionFlags.PlaceReturnTypeAfterParameterList)); if (rich == null || string.IsNullOrEmpty(rich.Text)) return null; var renderer = CreateTooltipRenderer();