Skip to content

Commit 4ff11dd

Browse files
Add a hover-only reference mode, distinct from local highlighting
References had two behaviors encoded in one bool: navigable links, and "local" references that are non-navigable but highlight all occurrences on click. Synthesized dynamic members were routed through the latter, so they picked up the occurrence highlight even though each use is a distinct synthetic member with nothing to group. Model the three modes explicitly with a ReferenceMode enum on ReferenceSegment (Link / LocalHighlight / HoverOnly). Dynamic members, the dynamic constructor and the dynamic keyword now use HoverOnly: they still show a hover tooltip (BuildHoverContent resolves any IEntity reference regardless of mode) but get the arrow cursor, do not navigate, and do not highlight. Local variables keep LocalHighlight. Plumbed via a new isHoverOnly flag on ITextOutput.WriteLocalReference. Assisted-by: Claude:claude-fable-5:Claude Code
1 parent 3254945 commit 4ff11dd

13 files changed

Lines changed: 129 additions & 28 deletions

ICSharpCode.Decompiler.Tests/Output/TextTokenWriterTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public void WriteReference(IMember member, string text, bool isDefinition = fals
102102
MemberReferences.Add((text, member));
103103
}
104104

105-
public void WriteLocalReference(string text, object reference, bool isDefinition = false)
105+
public void WriteLocalReference(string text, object reference, bool isDefinition = false, bool isHoverOnly = false)
106106
{
107107
LocalReferences.Add((text, reference, isDefinition));
108108
}

ICSharpCode.Decompiler/Output/ITextOutput.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public interface ITextOutput
3636
void WriteReference(MetadataFile metadata, Handle handle, string text, string protocol = "decompile", bool isDefinition = false);
3737
void WriteReference(IType type, string text, bool isDefinition = false);
3838
void WriteReference(IMember member, string text, bool isDefinition = false);
39-
void WriteLocalReference(string text, object reference, bool isDefinition = false);
39+
void WriteLocalReference(string text, object reference, bool isDefinition = false, bool isHoverOnly = false);
4040

4141
void MarkFoldStart(string collapsedText = "...", bool defaultCollapsed = false, bool isDefinition = false);
4242
void MarkFoldEnd();

ICSharpCode.Decompiler/Output/PlainTextOutput.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ public void WriteReference(IMember member, string text, bool isDefinition = fals
147147
Write(text);
148148
}
149149

150-
public void WriteLocalReference(string text, object reference, bool isDefinition = false)
150+
public void WriteLocalReference(string text, object reference, bool isDefinition = false, bool isHoverOnly = false)
151151
{
152152
Write(text);
153153
}
@@ -224,7 +224,7 @@ public void WriteLine()
224224
actions.Add(target => target.WriteLine());
225225
}
226226

227-
public void WriteLocalReference(string text, object reference, bool isDefinition = false)
227+
public void WriteLocalReference(string text, object reference, bool isDefinition = false, bool isHoverOnly = false)
228228
{
229229
actions.Add(target => target.WriteLocalReference(text, reference, isDefinition));
230230
}

ICSharpCode.Decompiler/Output/TextTokenWriter.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@ public override void WriteIdentifier(Identifier identifier)
8181
if (IsDynamicMemberReference(nodeStack.Peek()))
8282
{
8383
// A member synthesized for a dynamic access: show its signature on hover, but do not
84-
// make it a navigation target (there is no real member to jump to) - like a local.
85-
output.WriteLocalReference(name, m);
84+
// make it a navigation target (there is no real member to jump to), and no occurrence
85+
// highlight either - it is a distinct synthetic member at every use.
86+
output.WriteLocalReference(name, m, isHoverOnly: true);
8687
}
8788
else
8889
{
@@ -350,7 +351,7 @@ public override void WriteToken(string token)
350351
return;
351352
case IMember m:
352353
if (IsDynamicMemberReference(node))
353-
output.WriteLocalReference(token, m);
354+
output.WriteLocalReference(token, m, isHoverOnly: true);
354355
else
355356
output.WriteReference(m, token, false);
356357
return;
@@ -457,8 +458,8 @@ public override void WritePrimitiveType(string type)
457458
break;
458459
case "dynamic":
459460
// dynamic has no metadata type definition; emit it as a hover-only reference (no navigation
460-
// target) so it can carry a tooltip, like a local.
461-
output.WriteLocalReference(type, SpecialType.Dynamic);
461+
// target, no occurrence highlight) so it can still carry a tooltip.
462+
output.WriteLocalReference(type, SpecialType.Dynamic, isHoverOnly: true);
462463
return;
463464
case "bool":
464465
case "byte":

ILSpy.ReadyToRun/ReadyToRunLanguage.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public void WriteLine()
8282
{
8383
}
8484

85-
public void WriteLocalReference(string text, object reference, bool isDefinition = false)
85+
public void WriteLocalReference(string text, object reference, bool isDefinition = false, bool isHoverOnly = false)
8686
{
8787
}
8888

ILSpy.Tests/Editor/CaretHighlightAdornerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public async Task Clicking_A_Member_Definition_In_The_Decompiled_Output_Triggers
9898
generator.References.Should().NotBeNull(
9999
"DecompilerTextView assigns the live References collection on every ApplyDocument");
100100
var memberDef = generator.References!
101-
.FirstOrDefault(s => s.IsDefinition && !s.IsLocal && s.Reference != null);
101+
.FirstOrDefault(s => s.IsDefinition && s.Kind == ReferenceMode.Link && s.Reference != null);
102102
((object?)memberDef).Should().NotBeNull(
103103
"the decompiled Enumerable class contains at least one member-definition reference");
104104

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) 2026 Siegfried Pammer
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
4+
// software and associated documentation files (the "Software"), to deal in the Software
5+
// without restriction, including without limitation the rights to use, copy, modify, merge,
6+
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
7+
// to whom the Software is furnished to do so, subject to the following conditions:
8+
//
9+
// The above copyright notice and this permission notice shall be included in all copies or
10+
// substantial portions of the Software.
11+
//
12+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13+
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14+
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
15+
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
17+
// DEALINGS IN THE SOFTWARE.
18+
19+
using System.Linq;
20+
using System.Threading.Tasks;
21+
22+
using Avalonia.Headless.NUnit;
23+
using Avalonia.VisualTree;
24+
25+
using AwesomeAssertions;
26+
27+
using ICSharpCode.ILSpy.TextView;
28+
using ICSharpCode.ILSpy.TreeNodes;
29+
30+
using NUnit.Framework;
31+
32+
namespace ICSharpCode.ILSpy.Tests.TextView;
33+
34+
/// <summary>
35+
/// Sample type decompiled by <see cref="HoverOnlyReferenceTests"/>: a dynamic member access, whose
36+
/// synthesized member the decompiler renders as a hover-only reference.
37+
/// </summary>
38+
public class DynamicMemberSample
39+
{
40+
public object Get(dynamic d)
41+
{
42+
return d.Property;
43+
}
44+
}
45+
46+
/// <summary>
47+
/// Pins the third reference mode. A member synthesized for a dynamic access shows a hover tooltip but
48+
/// is not a navigation target and, unlike a local variable, clicking it does not highlight occurrences
49+
/// (it is a distinct synthetic member at every use).
50+
/// </summary>
51+
[TestFixture]
52+
public class HoverOnlyReferenceTests
53+
{
54+
[AvaloniaTest]
55+
public async Task Dynamic_Member_Is_HoverOnly_And_A_Click_Neither_Navigates_Nor_Highlights()
56+
{
57+
var (window, vm) = await TestHarness.BootAsync();
58+
await vm.OpenAssemblyAsync(typeof(DynamicMemberSample).Assembly.Location);
59+
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
60+
"ILSpy.Tests",
61+
"ICSharpCode.ILSpy.Tests.TextView",
62+
"ICSharpCode.ILSpy.Tests.TextView.DynamicMemberSample");
63+
vm.AssemblyTreeModel.SelectNode(typeNode);
64+
var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync();
65+
var view = window.GetVisualDescendants().OfType<DecompilerTextView>().First();
66+
67+
var hoverOnly = tab.References!
68+
.Where(r => r.Kind == ReferenceMode.HoverOnly)
69+
.ToList();
70+
hoverOnly.Should().NotBeEmpty("the dynamic member access 'd.Property' is a hover-only reference");
71+
72+
var navigated = false;
73+
tab.NavigateRequested += _ => navigated = true;
74+
view.OnReferenceClicked(hoverOnly.First());
75+
76+
navigated.Should().BeFalse("clicking a hover-only reference must not navigate");
77+
view.LocalReferenceMarks.Should().BeEmpty("a hover-only reference must not highlight occurrences");
78+
}
79+
}

ILSpy.Tests/Editor/LocalReferenceHighlightTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public async Task Clicking_A_Generic_Local_Function_Use_Highlights_Definition_An
6868
var view = window.GetVisualDescendants().OfType<DecompilerTextView>().First();
6969

7070
var localFunctionSegments = tab.References!
71-
.Where(r => r.IsLocal && r.Reference is IMethod { IsLocalFunction: true })
71+
.Where(r => r.Kind == ReferenceMode.LocalHighlight && r.Reference is IMethod { IsLocalFunction: true })
7272
.ToList();
7373
localFunctionSegments.Should().HaveCount(3, "the local function has one definition and two calls");
7474
localFunctionSegments.Count(r => r.IsDefinition).Should().Be(1);

ILSpy.Tests/Editor/ReferenceClickTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public class ReferenceClickTests
7272
{
7373
var textView = view.Editor.TextArea.TextView;
7474
var segment = tab.References!
75-
.First(r => r.Reference != null && !r.IsLocal && !r.IsDefinition);
75+
.First(r => r.Reference != null && r.Kind == ReferenceMode.Link && !r.IsDefinition);
7676
var line = view.Editor.Document.GetLineByOffset(segment.StartOffset);
7777
view.Editor.ScrollTo(line.LineNumber, segment.StartOffset - line.Offset + 1);
7878
window.UpdateLayout();

ILSpy/TextView/AvaloniaEditTextOutput.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -226,18 +226,18 @@ public void WriteReference(OpCodeInfo opCode, bool omitSuffix = false)
226226
}
227227

228228
public void WriteReference(MetadataFile metadata, Handle handle, string text, string protocol = "decompile", bool isDefinition = false)
229-
=> AddReference(text, new EntityReference(metadata, handle, protocol), local: false, isDefinition);
229+
=> AddReference(text, new EntityReference(metadata, handle, protocol), ReferenceMode.Link, isDefinition);
230230

231231
public void WriteReference(IType type, string text, bool isDefinition = false)
232-
=> AddReference(text, type, local: false, isDefinition);
232+
=> AddReference(text, type, ReferenceMode.Link, isDefinition);
233233

234234
public void WriteReference(IMember member, string text, bool isDefinition = false)
235-
=> AddReference(text, member, local: false, isDefinition);
235+
=> AddReference(text, member, ReferenceMode.Link, isDefinition);
236236

237-
public void WriteLocalReference(string text, object reference, bool isDefinition = false)
238-
=> AddReference(text, reference, local: true, isDefinition);
237+
public void WriteLocalReference(string text, object reference, bool isDefinition = false, bool isHoverOnly = false)
238+
=> AddReference(text, reference, isHoverOnly ? ReferenceMode.HoverOnly : ReferenceMode.LocalHighlight, isDefinition);
239239

240-
void AddReference(string text, object reference, bool local, bool isDefinition)
240+
void AddReference(string text, object reference, ReferenceMode kind, bool isDefinition)
241241
{
242242
WriteIndentIfNeeded();
243243
int start = builder.Length;
@@ -249,7 +249,7 @@ void AddReference(string text, object reference, bool local, bool isDefinition)
249249
StartOffset = start,
250250
EndOffset = end,
251251
Reference = reference,
252-
IsLocal = local,
252+
Kind = kind,
253253
IsDefinition = isDefinition,
254254
});
255255
}

0 commit comments

Comments
 (0)