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();