mirror of
https://github.com/icsharpcode/ILSpy.git
synced 2026-10-12 02:15:58 +00:00
Merge pull request #4216 from pieroviano/nav-caret-usage-highlight
Add option to highlight usages of the symbol at the caret
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
{
|
||||
static async Task<(DecompilerTextView View, DecompilerTabPageModel Tab, DisplaySettings Settings)> SetupAsync(bool enabled = true)
|
||||
{
|
||||
var (window, vm) = await TestHarness.BootAsync();
|
||||
var settings = AppComposition.Current.GetExport<SettingsService>().DisplaySettings;
|
||||
settings.HighlightSymbolsUnderCaret = enabled;
|
||||
await vm.OpenAssemblyAsync(typeof(MemberHighlightSample).Assembly.Location);
|
||||
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
|
||||
"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<DecompilerTextView>().First();
|
||||
return (view, tab, settings);
|
||||
}
|
||||
|
||||
static List<ReferenceSegment> 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_On_And_Round_Trips()
|
||||
{
|
||||
new DisplaySettings().HighlightSymbolsUnderCaret.Should().BeTrue();
|
||||
|
||||
var loaded = new DisplaySettings();
|
||||
loaded.LoadFromXml(new XElement("DisplaySettings"));
|
||||
loaded.HighlightSymbolsUnderCaret.Should().BeTrue("a missing attribute keeps the default");
|
||||
|
||||
var on = new DisplaySettings { HighlightSymbolsUnderCaret = false };
|
||||
var reloaded = new DisplaySettings();
|
||||
reloaded.LoadFromXml(on.SaveToXml());
|
||||
reloaded.HighlightSymbolsUnderCaret.Should().BeFalse();
|
||||
}
|
||||
|
||||
[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 external reference-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.HighlightSymbolsUnderCaret = true;
|
||||
view.CaretUsageMarks.Should().NotBeEmpty("turning the option on highlights the symbol already under the caret");
|
||||
|
||||
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,7 +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.HighlightSymbolsUnderCaret)] = DisplaySettingReaction.EditorLive,
|
||||
|
||||
// No model-side reaction.
|
||||
[nameof(DisplaySettings.StyleWindowTitleBar)] = DisplaySettingReaction.None,
|
||||
|
||||
@@ -109,7 +109,10 @@ namespace ICSharpCode.ILSpy.Options
|
||||
bool enableOmnibar;
|
||||
|
||||
[ObservableProperty]
|
||||
bool highlightMemberReferences;
|
||||
bool navigateOnCtrlClick;
|
||||
|
||||
[ObservableProperty]
|
||||
bool highlightSymbolsUnderCaret = true;
|
||||
|
||||
public XName SectionName => "DisplaySettings";
|
||||
|
||||
@@ -139,7 +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;
|
||||
NavigateOnCtrlClick = (bool?)section.Attribute(nameof(NavigateOnCtrlClick)) ?? false;
|
||||
HighlightSymbolsUnderCaret = (bool?)section.Attribute(nameof(HighlightSymbolsUnderCaret)) ?? true;
|
||||
}
|
||||
|
||||
public XElement SaveToXml()
|
||||
@@ -169,7 +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(NavigateOnCtrlClick), NavigateOnCtrlClick);
|
||||
section.SetAttributeValue(nameof(HighlightSymbolsUnderCaret), HighlightSymbolsUnderCaret);
|
||||
return section;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,8 +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.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
-4
@@ -2218,14 +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 ILSpyAboutPage.txt.
|
||||
/// </summary>
|
||||
@@ -2388,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,8 +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 name="HighlightSymbolsUnderCaret" xml:space="preserve">
|
||||
<value>Highlight symbols under caret</value>
|
||||
</data>
|
||||
<data name="ILSpyAboutPageTxt" xml:space="preserve">
|
||||
<value>ILSpyAboutPage.txt</value>
|
||||
@@ -831,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>
|
||||
|
||||
@@ -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
|
||||
@@ -155,6 +157,7 @@ namespace ICSharpCode.ILSpy.TextView
|
||||
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.
|
||||
@@ -311,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
|
||||
@@ -329,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);
|
||||
}
|
||||
@@ -424,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
|
||||
@@ -731,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1175,6 +1246,7 @@ namespace ICSharpCode.ILSpy.TextView
|
||||
// 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;
|
||||
@@ -1323,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;
|
||||
|
||||
@@ -1354,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);
|
||||
}
|
||||
|
||||
@@ -1380,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.
|
||||
@@ -1430,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