mirror of
https://github.com/icsharpcode/ILSpy.git
synced 2026-10-12 02:15:58 +00:00
Refine caret symbol highlighting
Fold the caret highlight implementation into the text view, remove click-to-highlight behavior, and make the symbol highlight option/theme colors clearer. Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
This commit is contained in:
@@ -38,9 +38,8 @@ using NUnit.Framework;
|
||||
namespace ICSharpCode.ILSpy.Tests.TextView;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// (<see cref="MemberReferenceHighlightTests"/>), 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.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class CaretUsageHighlightTests
|
||||
@@ -49,7 +48,7 @@ public class CaretUsageHighlightTests
|
||||
{
|
||||
var (window, vm) = await TestHarness.BootAsync();
|
||||
var settings = AppComposition.Current.GetExport<SettingsService>().DisplaySettings;
|
||||
settings.HighlightUsagesAtCaret = enabled;
|
||||
settings.HighlightSymbolsUnderCaret = enabled;
|
||||
await vm.OpenAssemblyAsync(typeof(MemberHighlightSample).Assembly.Location);
|
||||
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
|
||||
"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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,14 +48,14 @@ public class GenericLocalFunctionSample
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,18 +67,16 @@ public class MemberHighlightSample
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<SettingsService>().DisplaySettings.HighlightMemberReferences = highlightMemberReferences;
|
||||
AppComposition.Current.GetExport<SettingsService>().DisplaySettings.NavigateOnCtrlClick = navigateOnCtrlClick;
|
||||
await vm.OpenAssemblyAsync(typeof(MemberHighlightSample).Assembly.Location);
|
||||
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
|
||||
"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<T> and List<int> 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<SettingsService>().DisplaySettings.HighlightMemberReferences = true;
|
||||
AppComposition.Current.GetExport<SettingsService>().DisplaySettings.NavigateOnCtrlClick = true;
|
||||
var coreLibName = typeof(object).Assembly.GetName().Name!;
|
||||
var stringNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(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<SettingsService>().DisplaySettings.HighlightMemberReferences = true;
|
||||
AppComposition.Current.GetExport<SettingsService>().DisplaySettings.NavigateOnCtrlClick = true;
|
||||
var coreLibName = typeof(object).Assembly.GetName().Name!;
|
||||
var stringNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
TextArea style leaves SelectionForeground unset). -->
|
||||
<SolidColorBrush x:Key="ILSpy.EditorSelectionBrush" Color="#007ACC" Opacity="0.3" />
|
||||
<SolidColorBrush x:Key="ILSpy.EditorSearchResultsBrush" Color="#FFD54F" />
|
||||
<SolidColorBrush x:Key="ILSpy.SymbolHighlightBrush" Color="#87CEFA" Opacity="0.31" />
|
||||
<SolidColorBrush x:Key="ILSpy.SymbolDefinitionHighlightBrush" Color="#87CEFA" Opacity="0.50" />
|
||||
<SolidColorBrush x:Key="ILSpy.EditorWaitAdornerBackground" Color="#80FFFFFF" />
|
||||
<!-- Clickable text: About-page and decompiler-view hyperlinks (AvaloniaEdit's
|
||||
TextView.LinkTextForegroundBrush, see the style below) and the metadata
|
||||
@@ -118,6 +120,8 @@
|
||||
dark editor background. -->
|
||||
<SolidColorBrush x:Key="ILSpy.EditorSelectionBrush" Color="#3794FF" Opacity="0.35" />
|
||||
<SolidColorBrush x:Key="ILSpy.EditorSearchResultsBrush" Color="#665500" />
|
||||
<SolidColorBrush x:Key="ILSpy.SymbolHighlightBrush" Color="#2F6F9F" Opacity="0.50" />
|
||||
<SolidColorBrush x:Key="ILSpy.SymbolDefinitionHighlightBrush" Color="#3794FF" Opacity="0.45" />
|
||||
<SolidColorBrush x:Key="ILSpy.EditorWaitAdornerBackground" Color="#A0000000" />
|
||||
<SolidColorBrush x:Key="ILSpy.LinkForeground" Color="#4DA6FF" />
|
||||
<SolidColorBrush x:Key="ILSpy.ToolbarBackground" Color="#2D2D30" />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,10 +77,10 @@
|
||||
Content="{x:Static res:Resources.HighlightMatchingBraces}" />
|
||||
<CheckBox IsChecked="{Binding Settings.HighlightCurrentLine, Mode=TwoWay}"
|
||||
Content="{x:Static res:Resources.HighlightCurrentLine}" />
|
||||
<CheckBox IsChecked="{Binding Settings.HighlightMemberReferences, Mode=TwoWay}"
|
||||
Content="{x:Static res:Resources.HighlightMemberReferences}" />
|
||||
<CheckBox IsChecked="{Binding Settings.HighlightUsagesAtCaret, Mode=TwoWay}"
|
||||
Content="{x:Static res:Resources.HighlightUsagesAtCaret}" />
|
||||
<CheckBox IsChecked="{Binding Settings.NavigateOnCtrlClick, Mode=TwoWay}"
|
||||
Content="{x:Static res:Resources.NavigateOnCtrlClick}" />
|
||||
<CheckBox IsChecked="{Binding Settings.HighlightSymbolsUnderCaret, Mode=TwoWay}"
|
||||
Content="{x:Static res:Resources.HighlightSymbolsUnderCaret}" />
|
||||
<CheckBox IsChecked="{Binding Settings.ExpandXmlDocumentationComments, Mode=TwoWay}"
|
||||
Content="{x:Static res:Resources.ExpandXmlDocumentationCommentsAfterDecompilation}" />
|
||||
<CheckBox IsChecked="{Binding Settings.ExpandMemberDefinitions, Mode=TwoWay}"
|
||||
|
||||
Generated
+13
-13
@@ -2218,23 +2218,14 @@ namespace ICSharpCode.ILSpy.Properties {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static string HighlightMemberReferences {
|
||||
public static string HighlightSymbolsUnderCaret {
|
||||
get {
|
||||
return ResourceManager.GetString("HighlightMemberReferences", resourceCulture);
|
||||
return ResourceManager.GetString("HighlightSymbolsUnderCaret", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Highlight usages of the symbol under the caret.
|
||||
/// </summary>
|
||||
public static string HighlightUsagesAtCaret {
|
||||
get {
|
||||
return ResourceManager.GetString("HighlightUsagesAtCaret", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to ILSpyAboutPage.txt.
|
||||
/// </summary>
|
||||
@@ -2397,6 +2388,15 @@ namespace ICSharpCode.ILSpy.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Navigate on Ctrl+Click.
|
||||
/// </summary>
|
||||
public static string NavigateOnCtrlClick {
|
||||
get {
|
||||
return ResourceManager.GetString("NavigateOnCtrlClick", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Navigation.
|
||||
/// </summary>
|
||||
|
||||
@@ -768,11 +768,8 @@ Are you sure you want to continue?</value>
|
||||
<data name="HighlightMatchingBraces" xml:space="preserve">
|
||||
<value>Highlight matching braces</value>
|
||||
</data>
|
||||
<data name="HighlightMemberReferences" xml:space="preserve">
|
||||
<value>Single-click highlights member references (Ctrl+Click navigates)</value>
|
||||
</data>
|
||||
<data name="HighlightUsagesAtCaret" xml:space="preserve">
|
||||
<value>Highlight usages of the symbol under the caret</value>
|
||||
<data name="HighlightSymbolsUnderCaret" xml:space="preserve">
|
||||
<value>Highlight symbols under caret</value>
|
||||
</data>
|
||||
<data name="ILSpyAboutPageTxt" xml:space="preserve">
|
||||
<value>ILSpyAboutPage.txt</value>
|
||||
@@ -834,6 +831,9 @@ Are you sure you want to continue?</value>
|
||||
<data name="Name" xml:space="preserve">
|
||||
<value>Name</value>
|
||||
</data>
|
||||
<data name="NavigateOnCtrlClick" xml:space="preserve">
|
||||
<value>Navigate on Ctrl+Click</value>
|
||||
</data>
|
||||
<data name="Navigation" xml:space="preserve">
|
||||
<value>Navigation</value>
|
||||
</data>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<TextMarker> 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;
|
||||
|
||||
/// <summary>Marks painted by the highlight-usages-at-caret feature (exposed for tests).</summary>
|
||||
internal IReadOnlyList<TextMarker> CaretUsageMarks => caretUsageMarks;
|
||||
|
||||
void SetupCaretUsageHighlight()
|
||||
{
|
||||
Editor.TextArea.Caret.PositionChanged += (_, _) => UpdateCaretUsageHighlight();
|
||||
if (currentDisplaySettings is INotifyPropertyChanged settings)
|
||||
{
|
||||
settings.PropertyChanged += (_, e) => {
|
||||
if (e.PropertyName == nameof(DisplaySettings.HighlightUsagesAtCaret))
|
||||
UpdateCaretUsageHighlight();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<ReferenceSegment> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<TextMarker> localReferenceMarks = new();
|
||||
readonly List<TextMarker> caretUsageMarks = new();
|
||||
readonly List<TextMarker> 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<AvaloniaEdit.Rendering.VisualLineElementGenerator> 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
|
||||
/// <summary>The currently painted local-reference highlight marks (test observability).</summary>
|
||||
internal IReadOnlyList<TextMarker> LocalReferenceMarks => localReferenceMarks;
|
||||
|
||||
/// <summary>The currently painted symbol-under-caret highlight marks (test observability).</summary>
|
||||
internal IReadOnlyList<TextMarker> CaretUsageMarks => caretUsageMarks;
|
||||
|
||||
void SetupCaretUsageHighlight()
|
||||
{
|
||||
Editor.TextArea.Caret.PositionChanged += (_, _) => UpdateCaretUsageHighlight();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<ReferenceSegment> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when a plain click on <paramref name="segment"/> 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TextMarker>? 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user