Browse Source

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
pull/3816/head
Siegfried Pammer 2 weeks ago committed by Siegfried Pammer
parent
commit
28b682262c
  1. 4
      ILSpy.ReadyToRun/ReadyToRunLanguage.cs
  2. 89
      ILSpy.Tests/Analyzers/AnalyzerRichTextTests.cs
  3. 8
      ILSpy.Tests/Editor/ILLanguageTooltipTests.cs
  4. 9
      ILSpy/Analyzers/AnalyzedEventTreeNode.cs
  5. 9
      ILSpy/Analyzers/AnalyzedFieldTreeNode.cs
  6. 8
      ILSpy/Analyzers/AnalyzedMethodTreeNode.cs
  7. 9
      ILSpy/Analyzers/AnalyzedPropertyTreeNode.cs
  8. 6
      ILSpy/Analyzers/AnalyzedTypeTreeNode.cs
  9. 55
      ILSpy/Analyzers/AnalyzerEntityTreeNode.cs
  10. 40
      ILSpy/Controls/TreeView/IRichTextNode.cs
  11. 96
      ILSpy/Controls/TreeView/RichNodeText.cs
  12. 4
      ILSpy/Controls/TreeView/SharpTreeView.axaml
  13. 37
      ILSpy/Languages/CSharpLanguage.cs
  14. 7
      ILSpy/Languages/ILLanguage.cs
  15. 10
      ILSpy/Languages/Language.cs
  16. 6
      ILSpy/TextView/DecompilerTextView.axaml.cs

4
ILSpy.ReadyToRun/ReadyToRunLanguage.cs

@ -232,9 +232,9 @@ namespace ICSharpCode.ILSpy.ReadyToRun @@ -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)

89
ILSpy.Tests/Analyzers/AnalyzerRichTextTests.cs

@ -0,0 +1,89 @@ @@ -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<TypeTreeNode>("System.Linq", "System.Linq", "System.Linq.Enumerable");
typeNode.EnsureLazyChildren();
return (IMethod)typeNode.Children.OfType<MethodTreeNode>()
.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<string>();
node.ToString().Should().Be(node.Text.ToString());
node.ToString().Should().Contain("Empty").And.Contain("Enumerable");
}
}

8
ILSpy.Tests/Editor/ILLanguageTooltipTests.cs

@ -46,7 +46,7 @@ namespace ICSharpCode.ILSpy.Tests.TextView; @@ -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
/// <see cref="ILLanguage.GetRichTextTooltip"/> produces. Hovering an opcode must show the
/// <see cref="ILLanguage.GetRichText"/> produces. Hovering an opcode must show the
/// opcode's XML documentation, which comes from
/// <see cref="XmlDocLoader.MscorlibDocumentation"/> via the
/// "F:System.Reflection.Emit.OpCodes.*" doc ids.
@ -78,7 +78,7 @@ public class ILLanguageTooltipTests @@ -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 @@ -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 @@ -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");

9
ILSpy/Analyzers/AnalyzedEventTreeNode.cs

@ -19,6 +19,8 @@ @@ -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 @@ -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);

9
ILSpy/Analyzers/AnalyzedFieldTreeNode.cs

@ -18,6 +18,8 @@ @@ -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 @@ -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);

8
ILSpy/Analyzers/AnalyzedMethodTreeNode.cs

@ -18,6 +18,8 @@ @@ -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 @@ -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)

9
ILSpy/Analyzers/AnalyzedPropertyTreeNode.cs

@ -18,6 +18,8 @@ @@ -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 @@ -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);

6
ILSpy/Analyzers/AnalyzedTypeTreeNode.cs

@ -18,6 +18,8 @@ @@ -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 @@ -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);

55
ILSpy/Analyzers/AnalyzerEntityTreeNode.cs

@ -18,6 +18,9 @@ @@ -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; @@ -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 @@ -36,8 +40,57 @@ namespace ICSharpCode.ILSpy.Analyzers
/// icon, and its text; this base owns the navigation hook and the assembly-change
/// pruning logic.
/// </summary>
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;
/// <summary>
/// The highlighted, type-name-emboldened signature for this row, computed once and cached.
/// Falls back to <see langword="null"/> (plain <c>Text</c>) for non-entity rows.
/// </summary>
public RichText? CreateRichText()
{
if (!richTextComputed)
{
richText = BuildRichText();
richTextComputed = true;
}
return richText;
}
protected virtual RichText? BuildRichText() => null;
/// <summary>
/// Builds the coloured signature for <see cref="Member"/> with the given flags, bolding
/// type names, and prepends <paramref name="prefix"/> as plain (uncoloured) text.
/// </summary>
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;

40
ILSpy/Controls/TreeView/IRichTextNode.cs

@ -0,0 +1,40 @@ @@ -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
{
/// <summary>
/// Implemented by tree nodes whose label should render as syntax-highlighted rich text
/// (coloured runs, bold type names) instead of the plain string from
/// <c>SharpTreeNode.Text</c>. The cell template renders <see cref="CreateRichText"/> when
/// it returns a value and falls back to the plain <c>Text</c> otherwise, so a node's
/// <c>Text</c> stays the authoritative plain-string representation for search, copy and
/// keyboard navigation.
/// </summary>
public interface IRichTextNode
{
/// <summary>
/// The highlighted label, or <see langword="null"/> to fall back to the plain
/// <c>Text</c> (e.g. when no entity is available or the active language has no
/// highlighting). The returned text must equal the plain <c>Text</c>.
/// </summary>
RichText? CreateRichText();
}
}

96
ILSpy/Controls/TreeView/RichNodeText.cs

@ -0,0 +1,96 @@ @@ -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
{
/// <summary>
/// Attached behaviour for the tree's label <see cref="TextBlock"/>: when the bound node is an
/// <see cref="IRichTextNode"/> that yields rich text, it renders coloured / bold inlines;
/// otherwise it shows the node's plain <c>Text</c>. Keeping this in one place lets the single
/// shared SharpTreeView cell template support rich labels without every node becoming rich, and
/// without changing the <c>SharpTreeNode.Text</c> string used for search, copy and navigation.
/// </summary>
public static class RichNodeText
{
public static readonly AttachedProperty<SharpTreeNode?> NodeProperty =
AvaloniaProperty.RegisterAttached<TextBlock, SharpTreeNode?>("Node", typeof(RichNodeText));
// Holds the live PropertyChanged subscription so a recycled TextBlock detaches cleanly.
static readonly AttachedProperty<PropertyChangedEventHandler?> HandlerProperty =
AvaloniaProperty.RegisterAttached<TextBlock, PropertyChangedEventHandler?>("Handler", typeof(RichNodeText));
static RichNodeText()
{
NodeProperty.Changed.AddClassHandler<TextBlock>(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();
}
}
}
}

4
ILSpy/Controls/TreeView/SharpTreeView.axaml

@ -108,7 +108,9 @@ @@ -108,7 +108,9 @@
<Border Width="13" IsVisible="{Binding !ShowExpander}" />
<Image Source="{Binding Icon}" Width="16" Height="16"
Margin="4,0,6,0" VerticalAlignment="Center" />
<TextBlock Text="{Binding Text}" VerticalAlignment="Center"
<!-- RichNodeText renders coloured/bold inlines for IRichTextNode rows (the analyzer
pane, issue #2164) and falls back to the node's plain Text otherwise. -->
<TextBlock tv:RichNodeText.Node="{Binding}" VerticalAlignment="Center"
Classes.autoloaded="{Binding IsAutoLoaded}"
Classes.nonPublicAPI="{Binding IsNonPublicAPI}" />
</StackPanel>

37
ILSpy/Languages/CSharpLanguage.cs

@ -265,17 +265,46 @@ namespace ICSharpCode.ILSpy.Languages @@ -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<HighlightingColor>();
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)

7
ILSpy/Languages/ILLanguage.cs

@ -29,6 +29,7 @@ using AvaloniaEdit.Highlighting; @@ -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 @@ -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.
/// </summary>
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 @@ -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();

10
ILSpy/Languages/Language.cs

@ -178,7 +178,15 @@ namespace ICSharpCode.ILSpy.Languages @@ -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));
/// <summary>
/// Produces a syntax-highlighted signature for <paramref name="entity"/> 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 <paramref name="boldTypeNames"/> is set, type-name spans are rendered bold so they
/// stand out in dense lists such as the analyzer pane (issue #2164).
/// </summary>
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)
{

6
ILSpy/TextView/DecompilerTextView.axaml.cs

@ -40,6 +40,7 @@ using ICSharpCode.Decompiler.CSharp.OutputVisitor; @@ -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 @@ -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();

Loading…
Cancel
Save