mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
Doc-comment crefs in hover tooltips rendered as link-styled but inert text, and cref resolution was never wired up. Crefs now resolve against the current tab's assembly list and navigate through the same NavigateToReferenceEventArgs channel the analyzer uses; href links open via the TopLevel launcher. Avalonia inlines are not input elements, so links are hit-testable TextBlocks embedded via InlineUIContainer. Interactive content also requires the popup to survive the pointer travelling onto it: the editor's exit handler now tolerates the overlay-popup enter/exit ordering via the distance corridor, and the popup closes when the pointer leaves its own content instead. Assisted-by: Claude:claude-fable-5:Claude Codepull/3776/head
4 changed files with 277 additions and 34 deletions
@ -0,0 +1,140 @@ |
|||||||
|
// Copyright (c) 2026 Siegfried Pammer
|
||||||
|
//
|
||||||
|
// 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; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
using Avalonia; |
||||||
|
using Avalonia.Controls; |
||||||
|
using Avalonia.Headless; |
||||||
|
using Avalonia.Headless.NUnit; |
||||||
|
using Avalonia.Input; |
||||||
|
using Avalonia.Media; |
||||||
|
using Avalonia.VisualTree; |
||||||
|
|
||||||
|
using AwesomeAssertions; |
||||||
|
|
||||||
|
using ICSharpCode.Decompiler.CSharp.OutputVisitor; |
||||||
|
using ICSharpCode.Decompiler.TypeSystem; |
||||||
|
|
||||||
|
using ILSpy.AssemblyTree; |
||||||
|
using ILSpy.TextView; |
||||||
|
using ILSpy.TreeNodes; |
||||||
|
using ILSpy.Util; |
||||||
|
|
||||||
|
using NUnit.Framework; |
||||||
|
|
||||||
|
namespace ICSharpCode.ILSpy.Tests.TextView; |
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pins the hyperlink behaviour of documentation tooltips: a resolvable
|
||||||
|
/// <c><see cref="..."/></c> must render as a clickable link that requests
|
||||||
|
/// navigation to the referenced entity (via <see cref="MessageBus{T}"/> with
|
||||||
|
/// <see cref="NavigateToReferenceEventArgs"/>, the same channel the analyzer uses) and
|
||||||
|
/// must notify the hosting popup through <see cref="DocumentationRenderer.LinkClicked"/>
|
||||||
|
/// so it can close.
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture] |
||||||
|
public class DocumentationLinkTests |
||||||
|
{ |
||||||
|
[AvaloniaTest] |
||||||
|
public async Task SeeCref_Renders_As_A_Link_That_Navigates_On_Click() |
||||||
|
{ |
||||||
|
var (_, vm) = await TestHarness.BootAsync(); |
||||||
|
var coreLibName = typeof(object).Assembly.GetName().Name!; |
||||||
|
var stringType = (IEntity)vm.AssemblyTreeModel |
||||||
|
.FindNode<TypeTreeNode>(coreLibName, "System", "System.String").Member!; |
||||||
|
|
||||||
|
var renderer = new DocumentationRenderer( |
||||||
|
new CSharpAmbience(), |
||||||
|
new FontFamily("Consolas, Menlo, Monospace"), |
||||||
|
12); |
||||||
|
renderer.AddXmlDocumentation( |
||||||
|
"""<summary>Implemented by <see cref="T:System.String"/> instances.</summary>""", |
||||||
|
declaringEntity: null, |
||||||
|
resolver: id => id == "T:System.String" ? stringType : null); |
||||||
|
|
||||||
|
var window = new Window { Content = renderer.CreateView() }; |
||||||
|
window.Show(); |
||||||
|
AvaloniaHeadlessPlatform.ForceRenderTimerTick(); |
||||||
|
Avalonia.Threading.Dispatcher.UIThread.RunJobs(); |
||||||
|
window.UpdateLayout(); |
||||||
|
|
||||||
|
var link = window.GetVisualDescendants().OfType<TextBlock>() |
||||||
|
.FirstOrDefault(tb => tb.Classes.Contains("doc-link")); |
||||||
|
link.Should().NotBeNull("a resolvable cref must render as a clickable link"); |
||||||
|
link!.Cursor.Should().NotBeNull("links show the hand cursor as a click affordance"); |
||||||
|
|
||||||
|
object? navigated = null; |
||||||
|
var linkClickedRaised = false; |
||||||
|
renderer.LinkClicked += (_, _) => linkClickedRaised = true; |
||||||
|
EventHandler<NavigateToReferenceEventArgs> capture = (_, e) => navigated = e.Reference; |
||||||
|
MessageBus<NavigateToReferenceEventArgs>.Subscribers += capture; |
||||||
|
try |
||||||
|
{ |
||||||
|
var centre = link.TranslatePoint( |
||||||
|
new Point(link.Bounds.Width / 2, link.Bounds.Height / 2), window)!.Value; |
||||||
|
window.MouseDown(centre, MouseButton.Left); |
||||||
|
window.MouseUp(centre, MouseButton.Left); |
||||||
|
} |
||||||
|
finally |
||||||
|
{ |
||||||
|
MessageBus<NavigateToReferenceEventArgs>.Subscribers -= capture; |
||||||
|
} |
||||||
|
|
||||||
|
navigated.Should().BeSameAs(stringType, |
||||||
|
"clicking the link must request navigation to the referenced entity"); |
||||||
|
linkClickedRaised.Should().BeTrue( |
||||||
|
"the hosting popup closes itself when a link is followed"); |
||||||
|
} |
||||||
|
|
||||||
|
[AvaloniaTest] |
||||||
|
public void Unresolvable_Cref_Falls_Back_To_Plain_Text() |
||||||
|
{ |
||||||
|
var renderer = new DocumentationRenderer( |
||||||
|
new CSharpAmbience(), |
||||||
|
new FontFamily("Consolas, Menlo, Monospace"), |
||||||
|
12); |
||||||
|
renderer.AddXmlDocumentation( |
||||||
|
"""<summary>See <see cref="T:Does.Not.Exist"/>.</summary>""", |
||||||
|
declaringEntity: null, |
||||||
|
resolver: _ => null); |
||||||
|
|
||||||
|
var window = new Window { Content = renderer.CreateView() }; |
||||||
|
window.Show(); |
||||||
|
window.UpdateLayout(); |
||||||
|
|
||||||
|
window.GetVisualDescendants().OfType<TextBlock>() |
||||||
|
.Should().NotContain(tb => tb.Classes.Contains("doc-link"), |
||||||
|
"an unresolvable cref renders as plain text, not a dead link"); |
||||||
|
} |
||||||
|
|
||||||
|
[AvaloniaTest] |
||||||
|
public async Task FindEntityInRelevantAssemblies_Resolves_A_Type_IdString() |
||||||
|
{ |
||||||
|
var (_, vm) = await TestHarness.BootAsync(); |
||||||
|
var list = vm.AssemblyTreeModel.AssemblyList!; |
||||||
|
|
||||||
|
var entity = AssemblyTreeModel.FindEntityInRelevantAssemblies( |
||||||
|
"T:System.String", list.GetAssemblies()); |
||||||
|
|
||||||
|
entity.Should().NotBeNull("the doc-comment cref resolver feeds on id strings"); |
||||||
|
entity!.FullName.Should().Be("System.String"); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue