diff --git a/ILSpy.Tests/Editor/CaretUsageHighlightTests.cs b/ILSpy.Tests/Editor/CaretUsageHighlightTests.cs
index f0d1430ed..f2990f0b5 100644
--- a/ILSpy.Tests/Editor/CaretUsageHighlightTests.cs
+++ b/ILSpy.Tests/Editor/CaretUsageHighlightTests.cs
@@ -38,9 +38,8 @@ using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.TextView;
///
-/// Highlight usages at caret (ReSharper-style): while the caret rests on a symbol, every
-/// occurrence of that symbol in the document is marked. Independent of the click highlight
-/// (), which keeps its own marks.
+/// Highlight symbols under caret (ReSharper-style): while the caret rests on a symbol, every
+/// occurrence of that symbol in the document is marked.
///
[TestFixture]
public class CaretUsageHighlightTests
@@ -49,7 +48,7 @@ public class CaretUsageHighlightTests
{
var (window, vm) = await TestHarness.BootAsync();
var settings = AppComposition.Current.GetExport().DisplaySettings;
- settings.HighlightUsagesAtCaret = enabled;
+ settings.HighlightSymbolsUnderCaret = enabled;
await vm.OpenAssemblyAsync(typeof(MemberHighlightSample).Assembly.Location);
var typeNode = vm.AssemblyTreeModel.FindNode(
"ILSpy.Tests",
@@ -67,18 +66,18 @@ public class CaretUsageHighlightTests
.ToList();
[Test]
- public void Setting_Defaults_To_Off_And_Round_Trips()
+ public void Setting_Defaults_To_On_And_Round_Trips()
{
- new DisplaySettings().HighlightUsagesAtCaret.Should().BeFalse();
+ new DisplaySettings().HighlightSymbolsUnderCaret.Should().BeTrue();
var loaded = new DisplaySettings();
loaded.LoadFromXml(new XElement("DisplaySettings"));
- loaded.HighlightUsagesAtCaret.Should().BeFalse("a missing attribute keeps the default");
+ loaded.HighlightSymbolsUnderCaret.Should().BeTrue("a missing attribute keeps the default");
- var on = new DisplaySettings { HighlightUsagesAtCaret = true };
+ var on = new DisplaySettings { HighlightSymbolsUnderCaret = false };
var reloaded = new DisplaySettings();
reloaded.LoadFromXml(on.SaveToXml());
- reloaded.HighlightUsagesAtCaret.Should().BeTrue();
+ reloaded.HighlightSymbolsUnderCaret.Should().BeFalse();
}
[AvaloniaTest]
@@ -93,7 +92,7 @@ public class CaretUsageHighlightTests
view.CaretUsageMarks.Select(m => m.StartOffset).Should().BeEquivalentTo(
fieldSegments.Select(s => s.StartOffset),
"the definition and every use of the field under the caret are marked");
- view.LocalReferenceMarks.Should().BeEmpty("the caret highlight must not touch the click-highlight marks");
+ view.LocalReferenceMarks.Should().BeEmpty("the caret highlight must not touch external reference-highlight marks");
}
[AvaloniaTest]
@@ -147,10 +146,22 @@ public class CaretUsageHighlightTests
view.Editor.TextArea.Caret.Offset = use.StartOffset;
view.CaretUsageMarks.Should().BeEmpty("the option is off");
- settings.HighlightUsagesAtCaret = true;
+ settings.HighlightSymbolsUnderCaret = true;
view.CaretUsageMarks.Should().NotBeEmpty("turning the option on highlights the symbol already under the caret");
- settings.HighlightUsagesAtCaret = false;
+ settings.HighlightSymbolsUnderCaret = false;
view.CaretUsageMarks.Should().BeEmpty("turning the option off clears the marks");
}
+
+ [AvaloniaTest]
+ public async Task Highlight_Uses_Theme_Resource_Brushes()
+ {
+ var (view, tab, _) = await SetupAsync();
+ var definition = MemberSegments(tab, nameof(MemberHighlightSample.Field)).First(r => r.IsDefinition);
+
+ view.Editor.TextArea.Caret.Offset = definition.StartOffset + 1;
+
+ view.CaretUsageMarks.Should().Contain(m => m.BackgroundResourceKey == "ILSpy.SymbolDefinitionHighlightBrush");
+ view.CaretUsageMarks.Should().Contain(m => m.BackgroundResourceKey == "ILSpy.SymbolHighlightBrush");
+ }
}
diff --git a/ILSpy.Tests/Editor/LocalReferenceHighlightTests.cs b/ILSpy.Tests/Editor/LocalReferenceHighlightTests.cs
index 83fc8af6d..491d6a0cb 100644
--- a/ILSpy.Tests/Editor/LocalReferenceHighlightTests.cs
+++ b/ILSpy.Tests/Editor/LocalReferenceHighlightTests.cs
@@ -48,14 +48,14 @@ public class GenericLocalFunctionSample
}
///
-/// Pins the local-reference highlighting for local functions: clicking any occurrence of a
-/// generic local function must paint the definition and every use site as one group.
+/// Pins the local-reference highlighting for local functions: placing the caret on any occurrence
+/// of a generic local function must paint the definition and every use site as one group.
///
[TestFixture]
public class LocalReferenceHighlightTests
{
[AvaloniaTest]
- public async Task Clicking_A_Generic_Local_Function_Use_Highlights_Definition_And_All_Uses()
+ public async Task Caret_On_A_Generic_Local_Function_Use_Highlights_Definition_And_All_Uses()
{
var (window, vm) = await TestHarness.BootAsync();
await vm.OpenAssemblyAsync(typeof(GenericLocalFunctionSample).Assembly.Location);
@@ -74,10 +74,11 @@ public class LocalReferenceHighlightTests
localFunctionSegments.Count(r => r.IsDefinition).Should().Be(1);
var use = localFunctionSegments.First(r => !r.IsDefinition);
- view.OnReferenceClicked(use);
+ view.Editor.TextArea.Caret.Offset = use.StartOffset + 1;
- view.LocalReferenceMarks.Select(m => m.StartOffset).Should().BeEquivalentTo(
+ view.CaretUsageMarks.Select(m => m.StartOffset).Should().BeEquivalentTo(
localFunctionSegments.Select(s => s.StartOffset),
- "clicking a use must highlight the definition and every use");
+ "placing the caret on a use must highlight the definition and every use");
+ view.LocalReferenceMarks.Should().BeEmpty("local symbol highlights are now driven by the caret");
}
}
diff --git a/ILSpy.Tests/Editor/MemberReferenceHighlightTests.cs b/ILSpy.Tests/Editor/MemberReferenceHighlightTests.cs
index dd922adf3..2a06cc4cd 100644
--- a/ILSpy.Tests/Editor/MemberReferenceHighlightTests.cs
+++ b/ILSpy.Tests/Editor/MemberReferenceHighlightTests.cs
@@ -67,18 +67,16 @@ public class MemberHighlightSample
}
///
-/// Pins the member click-highlight behavior (issue #781): with the setting enabled, a
-/// single click on a member or type reference highlights all its occurrences in the view
-/// and Ctrl+Click navigates; with the setting disabled (the default), plain click keeps
-/// navigating.
+/// Pins reference click navigation: plain clicks navigate by default, and the display setting
+/// can require Ctrl+Click for navigation.
///
[TestFixture]
public class MemberReferenceHighlightTests
{
- static async Task<(MainWindow Window, DecompilerTextView View, DecompilerTabPageModel Tab)> SetupAsync(bool highlightMemberReferences)
+ static async Task<(MainWindow Window, DecompilerTextView View, DecompilerTabPageModel Tab)> SetupAsync(bool navigateOnCtrlClick)
{
var (window, vm) = await TestHarness.BootAsync();
- AppComposition.Current.GetExport().DisplaySettings.HighlightMemberReferences = highlightMemberReferences;
+ AppComposition.Current.GetExport().DisplaySettings.NavigateOnCtrlClick = navigateOnCtrlClick;
await vm.OpenAssemblyAsync(typeof(MemberHighlightSample).Assembly.Location);
var typeNode = vm.AssemblyTreeModel.FindNode(
"ILSpy.Tests",
@@ -98,99 +96,58 @@ public class MemberReferenceHighlightTests
}
[AvaloniaTest]
- public async Task Clicking_A_Member_Navigates_When_The_Setting_Is_Off()
+ public async Task Clicking_A_Member_Navigates_By_Default()
{
- var (_, view, tab) = await SetupAsync(highlightMemberReferences: false);
+ var (_, view, tab) = await SetupAsync(navigateOnCtrlClick: false);
var fieldSegments = SegmentsOf(tab, nameof(MemberHighlightSample.Field));
+ var definition = fieldSegments.First(r => r.IsDefinition);
var use = fieldSegments.First(r => !r.IsDefinition);
view.OnReferenceClicked(use);
view.LocalReferenceMarks.Should().BeEmpty(
- "with the setting off, a plain click must keep navigating instead of highlighting");
+ "plain clicks no longer create member reference highlights");
+ view.Editor.TextArea.Caret.Offset.Should().BeInRange(definition.StartOffset, definition.EndOffset,
+ "plain clicks navigate to an in-document definition by default");
}
[AvaloniaTest]
- public async Task Clicking_A_Member_Highlights_All_Occurrences_When_Enabled()
+ public async Task Plain_Click_Does_Not_Navigate_When_Ctrl_Click_Is_Required()
{
- var (_, view, tab) = await SetupAsync(highlightMemberReferences: true);
+ var (_, view, tab) = await SetupAsync(navigateOnCtrlClick: true);
var fieldSegments = SegmentsOf(tab, nameof(MemberHighlightSample.Field));
- // Besides the definition and the two uses, punctuation tokens of the declaration
- // carry the member reference as well; all of them belong to the highlight group.
- fieldSegments.Count(r => r.IsDefinition).Should().Be(1);
- fieldSegments.Should().HaveCountGreaterThanOrEqualTo(3, "the field has one definition and two uses");
- bool navigated = false;
- tab.NavigateRequested += (_, _) => navigated = true;
-
+ var originalOffset = view.Editor.TextArea.Caret.Offset;
var use = fieldSegments.First(r => !r.IsDefinition);
view.OnReferenceClicked(use);
- view.LocalReferenceMarks.Select(m => m.StartOffset).Should().BeEquivalentTo(
- fieldSegments.Select(s => s.StartOffset),
- "clicking a use must highlight the definition and every use");
- navigated.Should().BeFalse("a highlighting click must not navigate");
+ view.Editor.TextArea.Caret.Offset.Should().Be(originalOffset);
+ view.LocalReferenceMarks.Should().BeEmpty("plain clicks do not create member reference highlights");
}
[AvaloniaTest]
- public async Task Highlight_Matches_Specialized_Member_Uses()
+ public async Task Ctrl_Click_Navigates_When_Ctrl_Click_Is_Required()
{
- var (_, view, tab) = await SetupAsync(highlightMemberReferences: true);
-
- var makeSegments = SegmentsOf(tab, nameof(MemberHighlightSample.Make));
- // The identifier segments plus the call parentheses, which carry the member
- // reference as well.
- makeSegments.Count(r => r.IsDefinition).Should().Be(1);
- makeSegments.Should().HaveCountGreaterThanOrEqualTo(2, "the generic method has one definition and one call");
- // The call site carries a specialized method instance, the declaration the definition.
- var use = makeSegments.First(r => !r.IsDefinition && r.Length > 1);
- view.OnReferenceClicked(use);
-
- view.LocalReferenceMarks.Select(m => m.StartOffset).Should().BeEquivalentTo(
- makeSegments.Select(s => s.StartOffset),
- "clicking the specialized call site must also highlight the definition");
- }
-
- [AvaloniaTest]
- public async Task Clicking_A_Type_Highlights_All_Its_Parameterizations()
- {
- var (_, view, tab) = await SetupAsync(highlightMemberReferences: true);
-
- var listTypeSegments = tab.References!
- .Where(r => r.Kind == ReferenceMode.Link && r.Reference is IType { Name: "List" })
- .ToList();
- listTypeSegments.Should().HaveCountGreaterThanOrEqualTo(2,
- "List and List both occur in the view");
-
- view.OnReferenceClicked(listTypeSegments[0]);
-
- view.LocalReferenceMarks.Select(m => m.StartOffset).Should().BeEquivalentTo(
- listTypeSegments.Select(s => s.StartOffset),
- "clicking a type reference must highlight all its occurrences regardless of type arguments");
- }
-
- [AvaloniaTest]
- public async Task Ctrl_Click_Navigates_When_Enabled()
- {
- var (_, view, tab) = await SetupAsync(highlightMemberReferences: true);
+ var (_, view, tab) = await SetupAsync(navigateOnCtrlClick: true);
var fieldSegments = SegmentsOf(tab, nameof(MemberHighlightSample.Field));
+ var definition = fieldSegments.First(r => r.IsDefinition);
var use = fieldSegments.First(r => !r.IsDefinition);
view.OnReferenceClicked(use, DecompilerTextView.ReferenceClickKind.Navigate);
view.LocalReferenceMarks.Should().BeEmpty(
- "Ctrl+Click must navigate even with the setting enabled");
+ "Ctrl+Click navigates instead of highlighting");
+ view.Editor.TextArea.Caret.Offset.Should().BeInRange(definition.StartOffset, definition.EndOffset);
}
[AvaloniaTest]
- public async Task Pointer_Click_Highlights_And_Ctrl_Click_Navigates()
+ public async Task Pointer_Click_Respects_Ctrl_Click_Requirement()
{
// Mirrors ReferenceClickTests.SetupAsync/FindVisibleReference: the System.String
// view and its first visible link are the proven coordinate path for pointer
- // gestures; Stationary_Click_On_A_Link_Navigates pins that this very click
- // navigates when the setting is off.
+ // gestures.
var (window, vm) = await TestHarness.BootAsync();
- AppComposition.Current.GetExport().DisplaySettings.HighlightMemberReferences = true;
+ AppComposition.Current.GetExport().DisplaySettings.NavigateOnCtrlClick = true;
var coreLibName = typeof(object).Assembly.GetName().Name!;
var stringNode = vm.AssemblyTreeModel.FindNode(coreLibName, "System", "System.String");
vm.AssemblyTreeModel.SelectNode(stringNode);
@@ -217,20 +174,20 @@ public class MemberReferenceHighlightTests
window.MouseDown(point, MouseButton.Left);
window.MouseUp(point, MouseButton.Left);
- view.LocalReferenceMarks.Should().NotBeEmpty("a plain click must highlight, not navigate");
- navigated.Should().BeFalse();
+ view.LocalReferenceMarks.Should().BeEmpty("plain clicks do not create member reference highlights");
+ navigated.Should().BeFalse("plain clicks do not navigate when Ctrl+Click is required");
window.MouseDown(point, MouseButton.Left, RawInputModifiers.Control);
window.MouseUp(point, MouseButton.Left, RawInputModifiers.Control);
- navigated.Should().BeTrue("Ctrl+Click must navigate to the definition");
+ navigated.Should().BeTrue("Ctrl+Click must navigate");
}
[AvaloniaTest]
- public async Task Cursor_Shows_Hand_Only_When_A_Click_Would_Navigate()
+ public async Task Cursor_Shows_Hand_Only_When_Click_Would_Navigate()
{
var (window, vm) = await TestHarness.BootAsync();
- AppComposition.Current.GetExport().DisplaySettings.HighlightMemberReferences = true;
+ AppComposition.Current.GetExport().DisplaySettings.NavigateOnCtrlClick = true;
var coreLibName = typeof(object).Assembly.GetName().Name!;
var stringNode = vm.AssemblyTreeModel.FindNode(coreLibName, "System", "System.String");
vm.AssemblyTreeModel.SelectNode(stringNode);
@@ -251,27 +208,13 @@ public class MemberReferenceHighlightTests
var visual = textView.GetVisualPosition(position, VisualYPosition.LineMiddle) - textView.ScrollOffset;
var point = textView.TranslatePoint(visual, window)!.Value;
- // Without Ctrl a click highlights, so no hand cursor; with Ctrl it navigates.
+ // Without Ctrl a click does not navigate, so no hand cursor; with Ctrl it navigates.
window.MouseMove(point);
textView.Cursor?.ToString().Should().NotBe("Hand",
- "with the setting enabled, a plain click highlights instead of navigating");
+ "with the setting enabled, a plain click does not navigate");
window.MouseMove(point, RawInputModifiers.Control);
textView.Cursor?.ToString().Should().Be("Hand",
"holding Ctrl makes the click navigate, so the link affordance must show");
}
-
- [AvaloniaTest]
- public async Task Clicking_Empty_Space_Clears_The_Member_Highlight()
- {
- var (_, view, tab) = await SetupAsync(highlightMemberReferences: true);
-
- var use = SegmentsOf(tab, nameof(MemberHighlightSample.Field)).First(r => !r.IsDefinition);
- view.OnReferenceClicked(use);
- view.LocalReferenceMarks.Should().NotBeEmpty();
-
- // A subsequent navigating click (Ctrl) on another member clears the previous marks.
- view.OnReferenceClicked(use, DecompilerTextView.ReferenceClickKind.Navigate);
- view.LocalReferenceMarks.Should().BeEmpty();
- }
}
diff --git a/ILSpy/App.axaml b/ILSpy/App.axaml
index f457d07a9..5b28e3bb5 100644
--- a/ILSpy/App.axaml
+++ b/ILSpy/App.axaml
@@ -44,6 +44,8 @@
TextArea style leaves SelectionForeground unset). -->
+
+
+
+
diff --git a/ILSpy/Options/DisplaySettingReactions.cs b/ILSpy/Options/DisplaySettingReactions.cs
index d08bfb7fa..531754b66 100644
--- a/ILSpy/Options/DisplaySettingReactions.cs
+++ b/ILSpy/Options/DisplaySettingReactions.cs
@@ -85,9 +85,9 @@ namespace ICSharpCode.ILSpy.Options
// The text view shows/hides its own omnibar from this; tree and output are unaffected.
[nameof(DisplaySettings.EnableOmnibar)] = DisplaySettingReaction.EditorLive,
// Read at click time inside the text view; nothing needs re-decompilation.
- [nameof(DisplaySettings.HighlightMemberReferences)] = DisplaySettingReaction.EditorLive,
+ [nameof(DisplaySettings.NavigateOnCtrlClick)] = DisplaySettingReaction.EditorLive,
// The text view re-evaluates the caret highlight itself; nothing needs re-decompilation.
- [nameof(DisplaySettings.HighlightUsagesAtCaret)] = DisplaySettingReaction.EditorLive,
+ [nameof(DisplaySettings.HighlightSymbolsUnderCaret)] = DisplaySettingReaction.EditorLive,
// No model-side reaction.
[nameof(DisplaySettings.StyleWindowTitleBar)] = DisplaySettingReaction.None,
diff --git a/ILSpy/Options/DisplaySettings.cs b/ILSpy/Options/DisplaySettings.cs
index ac3e9a842..6f0ee5471 100644
--- a/ILSpy/Options/DisplaySettings.cs
+++ b/ILSpy/Options/DisplaySettings.cs
@@ -109,10 +109,10 @@ namespace ICSharpCode.ILSpy.Options
bool enableOmnibar;
[ObservableProperty]
- bool highlightMemberReferences;
+ bool navigateOnCtrlClick;
[ObservableProperty]
- bool highlightUsagesAtCaret;
+ bool highlightSymbolsUnderCaret = true;
public XName SectionName => "DisplaySettings";
@@ -142,8 +142,8 @@ namespace ICSharpCode.ILSpy.Options
StyleWindowTitleBar = (bool?)section.Attribute(nameof(StyleWindowTitleBar)) ?? false;
DecodeCustomAttributeBlobs = (bool?)section.Attribute(nameof(DecodeCustomAttributeBlobs)) ?? false;
EnableOmnibar = (bool?)section.Attribute(nameof(EnableOmnibar)) ?? false;
- HighlightMemberReferences = (bool?)section.Attribute(nameof(HighlightMemberReferences)) ?? false;
- HighlightUsagesAtCaret = (bool?)section.Attribute(nameof(HighlightUsagesAtCaret)) ?? false;
+ NavigateOnCtrlClick = (bool?)section.Attribute(nameof(NavigateOnCtrlClick)) ?? false;
+ HighlightSymbolsUnderCaret = (bool?)section.Attribute(nameof(HighlightSymbolsUnderCaret)) ?? true;
}
public XElement SaveToXml()
@@ -173,8 +173,8 @@ namespace ICSharpCode.ILSpy.Options
section.SetAttributeValue(nameof(StyleWindowTitleBar), StyleWindowTitleBar);
section.SetAttributeValue(nameof(DecodeCustomAttributeBlobs), DecodeCustomAttributeBlobs);
section.SetAttributeValue(nameof(EnableOmnibar), EnableOmnibar);
- section.SetAttributeValue(nameof(HighlightMemberReferences), HighlightMemberReferences);
- section.SetAttributeValue(nameof(HighlightUsagesAtCaret), HighlightUsagesAtCaret);
+ section.SetAttributeValue(nameof(NavigateOnCtrlClick), NavigateOnCtrlClick);
+ section.SetAttributeValue(nameof(HighlightSymbolsUnderCaret), HighlightSymbolsUnderCaret);
return section;
}
}
diff --git a/ILSpy/Options/DisplaySettingsPanel.axaml b/ILSpy/Options/DisplaySettingsPanel.axaml
index 2a4fdc01d..89d5f08a1 100644
--- a/ILSpy/Options/DisplaySettingsPanel.axaml
+++ b/ILSpy/Options/DisplaySettingsPanel.axaml
@@ -77,10 +77,10 @@
Content="{x:Static res:Resources.HighlightMatchingBraces}" />
-
-
+
+
- /// Looks up a localized string similar to Single-click highlights member references (Ctrl+Click navigates).
+ /// Looks up a localized string similar to Highlight symbols under caret.
///
- public static string HighlightMemberReferences {
+ public static string HighlightSymbolsUnderCaret {
get {
- return ResourceManager.GetString("HighlightMemberReferences", resourceCulture);
+ return ResourceManager.GetString("HighlightSymbolsUnderCaret", resourceCulture);
}
}
-
- ///
- /// Looks up a localized string similar to Highlight usages of the symbol under the caret.
- ///
- public static string HighlightUsagesAtCaret {
- get {
- return ResourceManager.GetString("HighlightUsagesAtCaret", resourceCulture);
- }
- }
-
+
///
/// Looks up a localized string similar to ILSpyAboutPage.txt.
///
@@ -2397,6 +2388,15 @@ namespace ICSharpCode.ILSpy.Properties {
}
}
+ ///
+ /// Looks up a localized string similar to Navigate on Ctrl+Click.
+ ///
+ public static string NavigateOnCtrlClick {
+ get {
+ return ResourceManager.GetString("NavigateOnCtrlClick", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Navigation.
///
diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx
index 8e1c735fb..30fcbb113 100644
--- a/ILSpy/Properties/Resources.resx
+++ b/ILSpy/Properties/Resources.resx
@@ -768,11 +768,8 @@ Are you sure you want to continue?
Highlight matching braces
-
- Single-click highlights member references (Ctrl+Click navigates)
-
-
- Highlight usages of the symbol under the caret
+
+ Highlight symbols under caret
ILSpyAboutPage.txt
@@ -834,6 +831,9 @@ Are you sure you want to continue?
Name
+
+ Navigate on Ctrl+Click
+
Navigation
diff --git a/ILSpy/TextView/DecompilerTextView.CaretUsageHighlight.cs b/ILSpy/TextView/DecompilerTextView.CaretUsageHighlight.cs
deleted file mode 100644
index 2802deaad..000000000
--- a/ILSpy/TextView/DecompilerTextView.CaretUsageHighlight.cs
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright (c) 2026 Piero Viano
-//
-// 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.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-
-using Avalonia.Media;
-
-using ICSharpCode.Decompiler.TypeSystem;
-
-using ICSharpCode.ILSpy.Options;
-
-namespace ICSharpCode.ILSpy.TextView
-{
- ///
- /// Highlight usages at caret: while the caret rests on a symbol, every occurrence of that
- /// symbol in the document is marked. Independent of the click highlight, which keeps its own
- /// marks.
- ///
- public partial class DecompilerTextView
- {
- // Translucent so the caret-usage marks stay readable on top of the click-highlight marks.
- static readonly Color CaretUsageBackground = Color.FromArgb(0x50, 0x87, 0xCE, 0xFA);
- static readonly Color CaretUsageDefinitionBackground = Color.FromArgb(0x80, 0x87, 0xCE, 0xFA);
-
- readonly List caretUsageMarks = new();
- // The segment the current caret-usage marks were computed for; re-computing is skipped while
- // the caret moves within that same segment.
- ReferenceSegment? caretUsageSegment;
-
- /// Marks painted by the highlight-usages-at-caret feature (exposed for tests).
- internal IReadOnlyList CaretUsageMarks => caretUsageMarks;
-
- void SetupCaretUsageHighlight()
- {
- Editor.TextArea.Caret.PositionChanged += (_, _) => UpdateCaretUsageHighlight();
- if (currentDisplaySettings is INotifyPropertyChanged settings)
- {
- settings.PropertyChanged += (_, e) => {
- if (e.PropertyName == nameof(DisplaySettings.HighlightUsagesAtCaret))
- UpdateCaretUsageHighlight();
- };
- }
- }
-
- ///
- /// The reference segment under the caret: one containing the caret offset, or ending right
- /// before it (caret just after an identifier). Hover-only segments are skipped.
- ///
- ReferenceSegment? GetReferenceSegmentAtCaret()
- {
- if (DataContext is not DecompilerTabPageModel { References: { } references })
- return null;
- int offset = Editor.TextArea.Caret.Offset;
- return Pick(references.FindSegmentsContaining(offset))
- ?? (offset > 0 ? Pick(references.FindSegmentsContaining(offset - 1)) : null);
-
- static ReferenceSegment? Pick(IEnumerable segments)
- => segments.FirstOrDefault(s => s.Reference != null && s.Kind != ReferenceMode.HoverOnly);
- }
-
- void UpdateCaretUsageHighlight()
- {
- if (currentDisplaySettings is not { HighlightUsagesAtCaret: true }
- || DataContext is not DecompilerTabPageModel { References: { } references }
- || GetReferenceSegmentAtCaret() is not { } segment
- || !IsCaretHighlightable(segment))
- {
- ClearCaretUsageMarks();
- return;
- }
- if (ReferenceEquals(segment, caretUsageSegment) && caretUsageMarks.Count > 0)
- return;
-
- ClearCaretUsageMarks();
- caretUsageSegment = segment;
- int textLength = Editor.Document.TextLength;
- foreach (var r in references)
- {
- if (r.Kind == ReferenceMode.HoverOnly || !AreSameReference(segment.Reference!, r.Reference))
- continue;
- if (r.StartOffset < 0 || r.EndOffset > textLength)
- continue;
- var mark = textMarkerService.Create(r.StartOffset, r.Length);
- mark.BackgroundColor = r.IsDefinition ? CaretUsageDefinitionBackground : CaretUsageBackground;
- caretUsageMarks.Add(mark);
- }
- }
-
- // Locals and parameters, members, types and unresolved entity references. Opcodes and other
- // link targets are excluded: marking every occurrence of an IL opcode would be noise.
- static bool IsCaretHighlightable(ReferenceSegment segment)
- => segment.Kind == ReferenceMode.LocalHighlight
- || segment.Reference is IMember or IType or EntityReference;
-
- void ClearCaretUsageMarks()
- {
- caretUsageSegment = null;
- foreach (var mark in caretUsageMarks)
- textMarkerService.Remove(mark);
- caretUsageMarks.Clear();
- }
- }
-}
diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs
index fad56c392..aa71fddca 100644
--- a/ILSpy/TextView/DecompilerTextView.axaml.cs
+++ b/ILSpy/TextView/DecompilerTextView.axaml.cs
@@ -68,11 +68,9 @@ namespace ICSharpCode.ILSpy.TextView
// pointer-distance corridor; plain content uses Avalonia's ToolTip attached property.
internal readonly record struct HoverContent(Control Control, bool IsRich);
- // Local-reference highlight colours: light-yellow "GreenYellow" for matches and a
- // softer green for the actual definition.
- static readonly Color LocalMatchBackground = Colors.GreenYellow;
- static readonly Color LocalDefinitionBackground = Color.FromArgb(0x80, 0xA0, 0xFF, 0xA0);
static readonly Color DebugStepBackground = Color.FromArgb(0x80, 0xFF, 0xD7, 0x66);
+ const string SymbolHighlightBrushKey = "ILSpy.SymbolHighlightBrush";
+ const string SymbolDefinitionHighlightBrushKey = "ILSpy.SymbolDefinitionHighlightBrush";
// Stay-open corridor for the rich popup: as long as the pointer is closer than
// `distanceToPopupLimit` to the popup edges, the popup stays. The limit shrinks toward
@@ -87,7 +85,11 @@ namespace ICSharpCode.ILSpy.TextView
TextMarkerService textMarkerService = null!;
BracketHighlightRenderer bracketHighlightRenderer = null!;
readonly List localReferenceMarks = new();
+ readonly List caretUsageMarks = new();
readonly List debugStepMarks = new();
+ // The segment the current caret-usage marks were computed for; re-computing is skipped while
+ // the caret moves within that same segment.
+ ReferenceSegment? caretUsageSegment;
int debugStepHighlightVersion;
readonly List activeCustomGenerators = new();
RichTextColorizer? activeColorizer;
@@ -132,8 +134,8 @@ namespace ICSharpCode.ILSpy.TextView
// AvaloniaEdit defaults to "Ctrl+Click to follow hyperlink" on its built-in
// LinkElementGenerator and propagates that flag onto every VisualLineLinkText it
// constructs (including those produced by user-added LinkElementGenerator subclasses
- // like the About-page resource generator). Decompiler output uses hyperlinks
- // extensively for in-app navigation — a plain click is the expected affordance.
+ // like the About-page resource generator). Decompiler references are handled below
+ // so NavigateOnCtrlClick can be applied consistently.
Editor.Options.RequireControlModifierForHyperlinkClick = false;
// The view is read-only, so Tab has nothing to indent; with AcceptsTab the text area marks
// the key handled and the focus could never leave the editor. Leaving it unhandled lets
@@ -312,11 +314,7 @@ namespace ICSharpCode.ILSpy.TextView
var segment = GetReferenceSegmentAtPointer(e);
if (segment?.Reference == null)
- {
- // A click on empty space dismisses the local-reference highlight.
- ClearLocalReferenceMarks();
return;
- }
// AvaloniaEdit's own release handler has already reset its selection mode and released
// the pointer capture (it runs first in the bubble phase). Clear the empty caret-click
// selection it leaves behind, then navigate; marking the release handled stops it from
@@ -330,7 +328,7 @@ namespace ICSharpCode.ILSpy.TextView
// cleared/rebuilt per click) and the bracket-pair outline (repainted on every caret move).
void SetupBackgroundRenderers()
{
- textMarkerService = new TextMarkerService(Editor.TextArea.TextView);
+ textMarkerService = new TextMarkerService(Editor.TextArea.TextView, this);
Editor.TextArea.TextView.BackgroundRenderers.Add(textMarkerService);
bracketHighlightRenderer = new BracketHighlightRenderer(Editor.TextArea.TextView);
}
@@ -425,6 +423,72 @@ namespace ICSharpCode.ILSpy.TextView
/// The currently painted local-reference highlight marks (test observability).
internal IReadOnlyList LocalReferenceMarks => localReferenceMarks;
+ /// The currently painted symbol-under-caret highlight marks (test observability).
+ internal IReadOnlyList CaretUsageMarks => caretUsageMarks;
+
+ void SetupCaretUsageHighlight()
+ {
+ Editor.TextArea.Caret.PositionChanged += (_, _) => UpdateCaretUsageHighlight();
+ }
+
+ ///
+ /// The reference segment under the caret: one containing the caret offset, or ending right
+ /// before it (caret just after an identifier). Hover-only segments are skipped.
+ ///
+ ReferenceSegment? GetReferenceSegmentAtCaret()
+ {
+ if (DataContext is not DecompilerTabPageModel { References: { } references })
+ return null;
+ int offset = Editor.TextArea.Caret.Offset;
+ return Pick(references.FindSegmentsContaining(offset))
+ ?? (offset > 0 ? Pick(references.FindSegmentsContaining(offset - 1)) : null);
+
+ static ReferenceSegment? Pick(IEnumerable segments)
+ => segments.FirstOrDefault(s => s.Reference != null && s.Kind != ReferenceMode.HoverOnly);
+ }
+
+ void UpdateCaretUsageHighlight()
+ {
+ if (currentDisplaySettings is not { HighlightSymbolsUnderCaret: true }
+ || DataContext is not DecompilerTabPageModel { References: { } references }
+ || GetReferenceSegmentAtCaret() is not { } segment
+ || !IsCaretHighlightable(segment))
+ {
+ ClearCaretUsageMarks();
+ return;
+ }
+ if (ReferenceEquals(segment, caretUsageSegment) && caretUsageMarks.Count > 0)
+ return;
+
+ ClearCaretUsageMarks();
+ caretUsageSegment = segment;
+ int textLength = Editor.Document.TextLength;
+ foreach (var r in references)
+ {
+ if (r.Kind == ReferenceMode.HoverOnly || !AreSameReference(segment.Reference!, r.Reference))
+ continue;
+ if (r.StartOffset < 0 || r.EndOffset > textLength)
+ continue;
+ var mark = textMarkerService.Create(r.StartOffset, r.Length);
+ mark.BackgroundResourceKey = r.IsDefinition ? SymbolDefinitionHighlightBrushKey : SymbolHighlightBrushKey;
+ caretUsageMarks.Add(mark);
+ }
+ }
+
+ // Locals and parameters, members, types and unresolved entity references. Opcodes and other
+ // link targets are excluded: marking every occurrence of an IL opcode would be noise.
+ static bool IsCaretHighlightable(ReferenceSegment segment)
+ => segment.Kind == ReferenceMode.LocalHighlight
+ || segment.Reference is IMember or IType or EntityReference;
+
+ void ClearCaretUsageMarks()
+ {
+ caretUsageSegment = null;
+ foreach (var mark in caretUsageMarks)
+ textMarkerService.Remove(mark);
+ caretUsageMarks.Clear();
+ }
+
// ThemeManager.Current is a process-lived singleton, so subscribing to its ThemeChanged in
// the constructor and never detaching would root every DecompilerTextView for the lifetime of
// the process -- one leaked view per decompiler tab. Bind the handler to the visual-tree
@@ -732,6 +796,12 @@ namespace ICSharpCode.ILSpy.TextView
// live without a re-decompile, per text view.
Omnibar.IsVisible = s.EnableOmnibar;
break;
+ case nameof(DisplaySettings.NavigateOnCtrlClick):
+ ApplyReferenceCursor(forceNavigation: false);
+ break;
+ case nameof(DisplaySettings.HighlightSymbolsUnderCaret):
+ UpdateCaretUsageHighlight();
+ break;
}
}
@@ -1325,19 +1395,6 @@ namespace ICSharpCode.ILSpy.TextView
return model.References.FindSegmentsContaining(offset).FirstOrDefault();
}
- ///
- /// True when a plain click on paints the occurrence
- /// highlight instead of navigating: the member-highlight setting is enabled, Ctrl is
- /// not held, and the reference is a member, type or unresolved entity reference.
- ///
- bool ShouldHighlightInsteadOfNavigate(ReferenceSegment segment, bool forceNavigation)
- {
- return !forceNavigation
- && segment.Kind == ReferenceMode.Link
- && currentDisplaySettings is { HighlightMemberReferences: true }
- && segment.Reference is IMember or IType or EntityReference;
- }
-
InputElement? cursorQueryElement;
ReferenceSegment? cursorQuerySegment;
@@ -1356,7 +1413,7 @@ namespace ICSharpCode.ILSpy.TextView
if (cursorQueryElement == null || cursorQuerySegment == null)
return;
bool navigates = cursorQuerySegment.Kind == ReferenceMode.Link
- && !ShouldHighlightInsteadOfNavigate(cursorQuerySegment, forceNavigation);
+ && (forceNavigation || currentDisplaySettings is not { NavigateOnCtrlClick: true });
cursorQueryElement.Cursor = new Cursor(navigates ? StandardCursorType.Hand : StandardCursorType.Arrow);
}
@@ -1382,23 +1439,12 @@ namespace ICSharpCode.ILSpy.TextView
if (segment.Kind == ReferenceMode.HoverOnly)
return;
- // Local references stay inside this document — paint every match and let the user
- // scrub through them. Cross-document references clear any existing marks since the
- // view is about to refresh anyway.
- if (segment.Kind == ReferenceMode.LocalHighlight)
- {
- HighlightLocalReferences(model, segment.Reference);
+ if (segment.Kind != ReferenceMode.Link)
return;
- }
- // With the member-highlight setting enabled, a plain click on a member or type
- // reference paints all its occurrences in this view instead of navigating;
- // Ctrl+Click keeps the navigation behavior. Opcode references always navigate:
- // highlighting every occurrence of an IL opcode would be noise.
- if (clickKind == ReferenceClickKind.Default && ShouldHighlightInsteadOfNavigate(segment, forceNavigation: false))
- {
- HighlightLocalReferences(model, segment.Reference);
+
+ if (clickKind == ReferenceClickKind.Default && currentDisplaySettings is { NavigateOnCtrlClick: true })
return;
- }
+
ClearLocalReferenceMarks();
// In-document jumps win when the definition is in this same view.
@@ -1432,7 +1478,7 @@ namespace ICSharpCode.ILSpy.TextView
if (!AreSameReference(reference, r.Reference))
continue;
var mark = textMarkerService.Create(r.StartOffset, r.Length);
- mark.BackgroundColor = r.IsDefinition ? LocalDefinitionBackground : LocalMatchBackground;
+ mark.BackgroundResourceKey = r.IsDefinition ? SymbolDefinitionHighlightBrushKey : SymbolHighlightBrushKey;
localReferenceMarks.Add(mark);
}
}
diff --git a/ILSpy/TextView/TextMarkerService.cs b/ILSpy/TextView/TextMarkerService.cs
index 4f543dd00..cd30a2486 100644
--- a/ILSpy/TextView/TextMarkerService.cs
+++ b/ILSpy/TextView/TextMarkerService.cs
@@ -21,6 +21,7 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia;
+using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
@@ -39,11 +40,13 @@ namespace ICSharpCode.ILSpy.TextView
sealed class TextMarkerService : IBackgroundRenderer
{
readonly AvaloniaEdit.Rendering.TextView textView;
+ readonly Control resourceHost;
TextSegmentCollection? markers;
- public TextMarkerService(AvaloniaEdit.Rendering.TextView textView)
+ public TextMarkerService(AvaloniaEdit.Rendering.TextView textView, Control resourceHost)
{
this.textView = textView ?? throw new ArgumentNullException(nameof(textView));
+ this.resourceHost = resourceHost ?? throw new ArgumentNullException(nameof(resourceHost));
textView.DocumentChanged += OnDocumentChanged;
OnDocumentChanged(null, EventArgs.Empty);
}
@@ -101,15 +104,26 @@ namespace ICSharpCode.ILSpy.TextView
int viewEnd = visualLines.Last().LastDocumentLine.EndOffset;
foreach (var marker in markers.FindOverlappingSegments(viewStart, viewEnd - viewStart))
{
- if (marker.BackgroundColor is not { } bg)
+ if (GetBackgroundBrush(marker) is not { } brush)
continue;
var geo = new BackgroundGeometryBuilder { AlignToWholePixels = true, CornerRadius = 3 };
geo.AddSegment(textView, marker);
var geometry = geo.CreateGeometry();
if (geometry != null)
- drawingContext.DrawGeometry(new SolidColorBrush(bg), null, geometry);
+ drawingContext.DrawGeometry(brush, null, geometry);
}
}
+
+ IBrush? GetBackgroundBrush(TextMarker marker)
+ {
+ if (marker.BackgroundResourceKey != null
+ && resourceHost.TryFindResource(marker.BackgroundResourceKey, resourceHost.ActualThemeVariant, out var resource)
+ && resource is IBrush brush)
+ {
+ return brush;
+ }
+ return marker.BackgroundColor is { } bg ? new SolidColorBrush(bg) : null;
+ }
}
///
@@ -120,6 +134,7 @@ namespace ICSharpCode.ILSpy.TextView
{
readonly TextMarkerService service;
Color? backgroundColor;
+ string? backgroundResourceKey;
public TextMarker(TextMarkerService service, int startOffset, int length)
{
@@ -137,5 +152,15 @@ namespace ICSharpCode.ILSpy.TextView
service.Redraw(this);
}
}
+
+ public string? BackgroundResourceKey {
+ get => backgroundResourceKey;
+ set {
+ if (backgroundResourceKey == value)
+ return;
+ backgroundResourceKey = value;
+ service.Redraw(this);
+ }
+ }
}
}