mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
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 Codepull/3816/head
16 changed files with 371 additions and 26 deletions
@ -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"); |
||||
} |
||||
} |
||||
@ -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(); |
||||
} |
||||
} |
||||
@ -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(); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue