diff --git a/ILSpy.Tests/Editor/CaretUsageHighlightTests.cs b/ILSpy.Tests/Editor/CaretUsageHighlightTests.cs
new file mode 100644
index 00000000000..f0d1430eda4
--- /dev/null
+++ b/ILSpy.Tests/Editor/CaretUsageHighlightTests.cs
@@ -0,0 +1,156 @@
+// 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.Linq;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+
+using Avalonia.Headless.NUnit;
+using Avalonia.VisualTree;
+
+using AwesomeAssertions;
+
+using ICSharpCode.Decompiler.TypeSystem;
+using ICSharpCode.ILSpy.AppEnv;
+using ICSharpCode.ILSpy.Options;
+using ICSharpCode.ILSpy.TextView;
+using ICSharpCode.ILSpy.TreeNodes;
+
+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.
+///
+[TestFixture]
+public class CaretUsageHighlightTests
+{
+ static async Task<(DecompilerTextView View, DecompilerTabPageModel Tab, DisplaySettings Settings)> SetupAsync(bool enabled = true)
+ {
+ var (window, vm) = await TestHarness.BootAsync();
+ var settings = AppComposition.Current.GetExport().DisplaySettings;
+ settings.HighlightUsagesAtCaret = enabled;
+ await vm.OpenAssemblyAsync(typeof(MemberHighlightSample).Assembly.Location);
+ var typeNode = vm.AssemblyTreeModel.FindNode(
+ "ILSpy.Tests",
+ "ICSharpCode.ILSpy.Tests.TextView",
+ "ICSharpCode.ILSpy.Tests.TextView.MemberHighlightSample");
+ vm.AssemblyTreeModel.SelectNode(typeNode);
+ var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync();
+ var view = window.GetVisualDescendants().OfType().First();
+ return (view, tab, settings);
+ }
+
+ static List MemberSegments(DecompilerTabPageModel tab, string name)
+ => tab.References!
+ .Where(r => r.Kind == ReferenceMode.Link && r.Reference is IMember m && m.Name == name)
+ .ToList();
+
+ [Test]
+ public void Setting_Defaults_To_Off_And_Round_Trips()
+ {
+ new DisplaySettings().HighlightUsagesAtCaret.Should().BeFalse();
+
+ var loaded = new DisplaySettings();
+ loaded.LoadFromXml(new XElement("DisplaySettings"));
+ loaded.HighlightUsagesAtCaret.Should().BeFalse("a missing attribute keeps the default");
+
+ var on = new DisplaySettings { HighlightUsagesAtCaret = true };
+ var reloaded = new DisplaySettings();
+ reloaded.LoadFromXml(on.SaveToXml());
+ reloaded.HighlightUsagesAtCaret.Should().BeTrue();
+ }
+
+ [AvaloniaTest]
+ public async Task Caret_On_A_Member_Use_Marks_Every_Occurrence()
+ {
+ var (view, tab, _) = await SetupAsync();
+ var fieldSegments = MemberSegments(tab, nameof(MemberHighlightSample.Field));
+ var use = fieldSegments.First(r => !r.IsDefinition);
+
+ view.Editor.TextArea.Caret.Offset = use.StartOffset + 1;
+
+ 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");
+ }
+
+ [AvaloniaTest]
+ public async Task Caret_At_The_End_Of_An_Identifier_Still_Counts_As_On_It()
+ {
+ var (view, tab, _) = await SetupAsync();
+ var fieldSegments = MemberSegments(tab, nameof(MemberHighlightSample.Field));
+ var use = fieldSegments.First(r => !r.IsDefinition && r.Length > 1);
+
+ view.Editor.TextArea.Caret.Offset = use.EndOffset;
+
+ view.CaretUsageMarks.Should().NotBeEmpty();
+ }
+
+ [AvaloniaTest]
+ public async Task Caret_On_A_Parameter_Marks_Its_Local_Occurrences()
+ {
+ var (view, tab, _) = await SetupAsync();
+ var itemSegments = tab.References!
+ .Where(r => r.Kind == ReferenceMode.LocalHighlight && tab.Text.Substring(r.StartOffset, r.Length) == "item")
+ .ToList();
+ itemSegments.Should().HaveCountGreaterThanOrEqualTo(2, "the parameter is declared and used once");
+
+ view.Editor.TextArea.Caret.Offset = itemSegments[0].StartOffset;
+
+ view.CaretUsageMarks.Select(m => m.StartOffset).Should().BeEquivalentTo(
+ itemSegments.Select(s => s.StartOffset));
+ }
+
+ [AvaloniaTest]
+ public async Task Moving_The_Caret_Off_A_Symbol_Clears_The_Marks()
+ {
+ var (view, tab, _) = await SetupAsync();
+ var use = MemberSegments(tab, nameof(MemberHighlightSample.Field)).First(r => !r.IsDefinition);
+ view.Editor.TextArea.Caret.Offset = use.StartOffset;
+ view.CaretUsageMarks.Should().NotBeEmpty();
+
+ // Offset 0 is the start of the leading comment/using block, which carries no reference.
+ tab.References!.FindSegmentsContaining(0).Should().BeEmpty();
+ view.Editor.TextArea.Caret.Offset = 0;
+
+ view.CaretUsageMarks.Should().BeEmpty();
+ }
+
+ [AvaloniaTest]
+ public async Task Disabled_Setting_Marks_Nothing_And_Turning_It_Off_Clears_Marks()
+ {
+ var (view, tab, settings) = await SetupAsync(enabled: false);
+ var use = MemberSegments(tab, nameof(MemberHighlightSample.Field)).First(r => !r.IsDefinition);
+
+ view.Editor.TextArea.Caret.Offset = use.StartOffset;
+ view.CaretUsageMarks.Should().BeEmpty("the option is off");
+
+ settings.HighlightUsagesAtCaret = true;
+ view.CaretUsageMarks.Should().NotBeEmpty("turning the option on highlights the symbol already under the caret");
+
+ settings.HighlightUsagesAtCaret = false;
+ view.CaretUsageMarks.Should().BeEmpty("turning the option off clears the marks");
+ }
+}
diff --git a/ILSpy/Options/DisplaySettingReactions.cs b/ILSpy/Options/DisplaySettingReactions.cs
index b8a66f9a77b..d08bfb7fa16 100644
--- a/ILSpy/Options/DisplaySettingReactions.cs
+++ b/ILSpy/Options/DisplaySettingReactions.cs
@@ -86,6 +86,8 @@ public static class DisplaySettingReactions
[nameof(DisplaySettings.EnableOmnibar)] = DisplaySettingReaction.EditorLive,
// Read at click time inside the text view; nothing needs re-decompilation.
[nameof(DisplaySettings.HighlightMemberReferences)] = DisplaySettingReaction.EditorLive,
+ // The text view re-evaluates the caret highlight itself; nothing needs re-decompilation.
+ [nameof(DisplaySettings.HighlightUsagesAtCaret)] = DisplaySettingReaction.EditorLive,
// No model-side reaction.
[nameof(DisplaySettings.StyleWindowTitleBar)] = DisplaySettingReaction.None,
diff --git a/ILSpy/Options/DisplaySettings.cs b/ILSpy/Options/DisplaySettings.cs
index 932c15185dd..ac3e9a842e9 100644
--- a/ILSpy/Options/DisplaySettings.cs
+++ b/ILSpy/Options/DisplaySettings.cs
@@ -111,6 +111,9 @@ public sealed partial class DisplaySettings : ObservableObject, ISettingsSection
[ObservableProperty]
bool highlightMemberReferences;
+ [ObservableProperty]
+ bool highlightUsagesAtCaret;
+
public XName SectionName => "DisplaySettings";
public void LoadFromXml(XElement section)
@@ -140,6 +143,7 @@ public void LoadFromXml(XElement section)
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;
}
public XElement SaveToXml()
@@ -170,6 +174,7 @@ public XElement SaveToXml()
section.SetAttributeValue(nameof(DecodeCustomAttributeBlobs), DecodeCustomAttributeBlobs);
section.SetAttributeValue(nameof(EnableOmnibar), EnableOmnibar);
section.SetAttributeValue(nameof(HighlightMemberReferences), HighlightMemberReferences);
+ section.SetAttributeValue(nameof(HighlightUsagesAtCaret), HighlightUsagesAtCaret);
return section;
}
}
diff --git a/ILSpy/Options/DisplaySettingsPanel.axaml b/ILSpy/Options/DisplaySettingsPanel.axaml
index ecf99f963cd..2a4fdc01d71 100644
--- a/ILSpy/Options/DisplaySettingsPanel.axaml
+++ b/ILSpy/Options/DisplaySettingsPanel.axaml
@@ -79,6 +79,8 @@
Content="{x:Static res:Resources.HighlightCurrentLine}" />
+
+ /// 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.
///
diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx
index 94e510454b3..8e1c735fb4e 100644
--- a/ILSpy/Properties/Resources.resx
+++ b/ILSpy/Properties/Resources.resx
@@ -771,6 +771,9 @@ Are you sure you want to continue?
Single-click highlights member references (Ctrl+Click navigates)
+
+ Highlight usages of the symbol under the caret
+
ILSpyAboutPage.txt
diff --git a/ILSpy/TextView/DecompilerTextView.CaretUsageHighlight.cs b/ILSpy/TextView/DecompilerTextView.CaretUsageHighlight.cs
new file mode 100644
index 00000000000..2802deaad5d
--- /dev/null
+++ b/ILSpy/TextView/DecompilerTextView.CaretUsageHighlight.cs
@@ -0,0 +1,121 @@
+// 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 3293f14daca..bd86032f699 100644
--- a/ILSpy/TextView/DecompilerTextView.axaml.cs
+++ b/ILSpy/TextView/DecompilerTextView.axaml.cs
@@ -151,6 +151,7 @@ public DecompilerTextView()
Editor.TextArea.Caret.PositionChanged += OnCaretPositionChanged;
SetupZoomAndCopy();
+ SetupCaretUsageHighlight();
// Ctrl+L focuses the omnibar into search mode (browser address-bar gesture). Tunnel so
// it wins before AvaloniaEdit's own key handling while focus is anywhere in the editor.
@@ -1153,6 +1154,7 @@ void ApplyDocument(DecompilerTabPageModel model, bool restoreViewState = true)
// force-close even if the popup currently wants to stay (mouseClick: true).
TryCloseExistingPopup(mouseClick: true);
ClearLocalReferenceMarks();
+ ClearCaretUsageMarks();
ClearDebugStepMarks();
Editor.SyntaxHighlighting = HighlightingService.GetByExtension(model.SyntaxExtension);
Editor.Document.Text = model.Text;