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