From 2d4722805ba8d6aae77698bad7e3f03cc8d15659 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Thu, 21 May 2026 07:25:45 +0200 Subject: [PATCH] Hover tooltip signature wraps instead of clipping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-reported: editor hover tooltips were not wide enough to show full method signatures. Root cause was a three-way layout interaction — DocumentationRenderer.AddSignatureBlock created the signature SelectableTextBlock with TextWrapping.NoWrap, the outer Border capped MaxWidth at 600px, and the wrapping ScrollViewer disabled horizontal scrolling. So anything past 600px was simply clipped, with no way to reveal the rest. Assisted-by: Claude:claude-opus-4-7:Claude Code --- .../DocumentationRendererTooltipWidthTests.cs | 97 +++++++++++++++++++ ILSpy/TextView/DocumentationRenderer.cs | 9 +- 2 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 ILSpy.Tests/Editor/DocumentationRendererTooltipWidthTests.cs diff --git a/ILSpy.Tests/Editor/DocumentationRendererTooltipWidthTests.cs b/ILSpy.Tests/Editor/DocumentationRendererTooltipWidthTests.cs new file mode 100644 index 000000000..1906006dd --- /dev/null +++ b/ILSpy.Tests/Editor/DocumentationRendererTooltipWidthTests.cs @@ -0,0 +1,97 @@ +// 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 Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Headless.NUnit; +using Avalonia.LogicalTree; +using Avalonia.Media; + +using AvaloniaEdit.Highlighting; + +using AwesomeAssertions; + +using ICSharpCode.Decompiler.CSharp.OutputVisitor; + +using ILSpy.TextView; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests.TextView; + +/// +/// Pins the rendering shape of method-signature tooltips so a long signature isn't +/// clipped horizontally. Two failure modes the original implementation suffered from: +/// (1) the signature was created with +/// , so text past the popup's MaxWidth was simply cut +/// off (no horizontal scrolling because the outer ScrollViewer disables it); +/// (2) the popup MaxWidth defaulted to a snug 600px, narrower than most real C# +/// generic-method signatures. WPF parity is wrap-friendly, so we mirror that — these +/// tests fail if either property regresses. +/// +[TestFixture] +public class DocumentationRendererTooltipWidthTests +{ + [AvaloniaTest] + public void Signature_Block_Wraps_So_Long_Method_Signatures_Are_Not_Clipped() + { + var renderer = new DocumentationRenderer( + new CSharpAmbience(), + new FontFamily("Consolas, Menlo, Monospace"), + 12); + + // A real C# Zip overload — long enough that it exceeds the previous 600px cap at + // the popup's monospace 12pt size. The bug shows up on any method whose rendered + // signature is wider than the popup's outer MaxWidth. + var longSignature = new RichText( + "public static IEnumerable Zip(" + + "this IEnumerable first, IEnumerable second, " + + "IEnumerable third, " + + "Func resultSelector)"); + + renderer.AddSignatureBlock(longSignature); + + var view = renderer.CreateView(); + var signature = view.GetLogicalDescendants() + .OfType() + .FirstOrDefault(); + signature.Should().NotBeNull( + "DocumentationRenderer must materialise the signature as a SelectableTextBlock"); + signature!.TextWrapping.Should().Be(TextWrapping.Wrap, + "the signature must wrap inside the popup — without wrapping a long signature is " + + "clipped at the outer MaxWidth because the ScrollViewer disables horizontal scrolling"); + } + + [AvaloniaTest] + public void CreateView_Default_MaxWidth_Is_Generous_Enough_For_Typical_Signatures() + { + // 600 was too narrow — generic methods + ref-struct parameters routinely exceed it. + // Pin the bumped default so a future tweak doesn't silently shrink it back. + var renderer = new DocumentationRenderer( + new CSharpAmbience(), + new FontFamily("Consolas, Menlo, Monospace"), + 12); + + var view = (Border)renderer.CreateView(); + view.MaxWidth.Should().BeGreaterThanOrEqualTo(900, + "the popup's outer MaxWidth must be wide enough that most realistic method " + + "signatures fit without aggressive wrapping"); + } +} diff --git a/ILSpy/TextView/DocumentationRenderer.cs b/ILSpy/TextView/DocumentationRenderer.cs index aca728228..c38d4caab 100644 --- a/ILSpy/TextView/DocumentationRenderer.cs +++ b/ILSpy/TextView/DocumentationRenderer.cs @@ -101,7 +101,7 @@ namespace ILSpy.TextView /// Wraps the accumulated content in a chrome border + scroll viewer and returns the /// final control tree for use in a popup. /// - public Control CreateView(double maxWidth = 600, double maxHeight = 400) + public Control CreateView(double maxWidth = 900, double maxHeight = 400) { FlushAddedText(true); @@ -127,10 +127,15 @@ namespace ILSpy.TextView FlushAddedText(true); inlineCollection = null; + // Wrap rather than NoWrap so a signature wider than the popup's MaxWidth folds + // to multiple lines instead of being clipped at the right edge — the outer + // ScrollViewer disables horizontal scrolling, so NoWrap meant "the right half + // of long generic method signatures is invisible". WPF parity: the equivalent + // FlowDocument Paragraph wraps by default (Paragraph doesn't even support NoWrap). var block = new SelectableTextBlock { FontFamily = codeFont, FontSize = fontSize, - TextWrapping = TextWrapping.NoWrap, + TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 0, 0, 6), Inlines = new InlineCollection(), };