From b2b0daf83edaf4f068fc7938ebc6fcdff785dacd Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 28 Apr 2026 00:10:18 +0200 Subject: [PATCH] Highlight local-reference matches on click MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2.2 of the references/tooltips work — clicking a local reference (parameter, variable, label, ...) highlights every occurrence in the current document with a colored background. Cross-document references clear any existing marks before navigating. Assisted-by: Claude:claude-opus-4-7:Claude Code --- ILSpy/TextView/DecompilerTextView.axaml.cs | 47 +++++++ ILSpy/TextView/TextMarkerService.cs | 141 +++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 ILSpy/TextView/TextMarkerService.cs diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs index 54ff4d7d2..130230769 100644 --- a/ILSpy/TextView/DecompilerTextView.axaml.cs +++ b/ILSpy/TextView/DecompilerTextView.axaml.cs @@ -16,11 +16,13 @@ // 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.Controls; using Avalonia.Input; +using Avalonia.Media; using Avalonia.Threading; using AvaloniaEdit.Folding; @@ -30,7 +32,14 @@ namespace ILSpy.TextView { public partial class DecompilerTextView : UserControl { + // Local-reference highlight colours, mirroring ILSpy/Themes/generic.xaml. Use the same + // 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); + readonly ReferenceElementGenerator referenceElementGenerator; + readonly TextMarkerService textMarkerService; + readonly List localReferenceMarks = new(); RichTextColorizer? activeColorizer; FoldingManager? activeFoldingManager; @@ -44,6 +53,11 @@ namespace ILSpy.TextView referenceElementGenerator = new ReferenceElementGenerator(static segment => segment.Reference != null); referenceElementGenerator.ReferenceClicked += OnReferenceClicked; Editor.TextArea.TextView.ElementGenerators.Add(referenceElementGenerator); + + // Background renderer that paints local-reference highlights. Lives once for the + // lifetime of the view; the marks themselves are cleared and rebuilt per click. + textMarkerService = new TextMarkerService(Editor.TextArea.TextView); + Editor.TextArea.TextView.BackgroundRenderers.Add(textMarkerService); } protected override void OnDataContextChanged(System.EventArgs e) @@ -72,6 +86,7 @@ namespace ILSpy.TextView void ApplyDocument(DecompilerTabPageModel model) { + ClearLocalReferenceMarks(); Editor.SyntaxHighlighting = HighlightingService.GetByExtension(model.SyntaxExtension); Editor.Document.Text = model.Text; @@ -113,6 +128,16 @@ namespace ILSpy.TextView if (DataContext is not DecompilerTabPageModel model || segment.Reference == null) 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.IsLocal) + { + HighlightLocalReferences(model, segment.Reference); + return; + } + ClearLocalReferenceMarks(); + // In-document jumps win when the definition is in this same view. if (model.DefinitionLookup is { } lookup) { @@ -129,5 +154,27 @@ namespace ILSpy.TextView // Otherwise let the host route the reference (assembly-tree navigation, new tab, ...). model.RaiseNavigateRequested(segment); } + + void HighlightLocalReferences(DecompilerTabPageModel model, object reference) + { + ClearLocalReferenceMarks(); + if (model.References == null) + return; + foreach (var r in model.References) + { + if (!ReferenceEquals(reference, r.Reference) && !reference.Equals(r.Reference)) + continue; + var mark = textMarkerService.Create(r.StartOffset, r.Length); + mark.BackgroundColor = r.IsDefinition ? LocalDefinitionBackground : LocalMatchBackground; + localReferenceMarks.Add(mark); + } + } + + void ClearLocalReferenceMarks() + { + foreach (var mark in localReferenceMarks) + textMarkerService.Remove(mark); + localReferenceMarks.Clear(); + } } } diff --git a/ILSpy/TextView/TextMarkerService.cs b/ILSpy/TextView/TextMarkerService.cs new file mode 100644 index 000000000..6ab56e05b --- /dev/null +++ b/ILSpy/TextView/TextMarkerService.cs @@ -0,0 +1,141 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// 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; +using System.Collections.Generic; +using System.Linq; + +using Avalonia; +using Avalonia.Media; +using Avalonia.Threading; + +using AvaloniaEdit.Document; +using AvaloniaEdit.Rendering; + +namespace ILSpy.TextView +{ + /// + /// Manages background-coloured text marks over the editor — used for local-reference + /// highlighting today, and a useful primitive for future features (errors, search hits, ...). + /// Slimmed down from the WPF host's TextMarkerService: only the colored background + /// path is implemented; underline marker types (squiggly / dotted / solid) and per-marker + /// foreground / typeface tweaks are deliberately omitted until something actually needs them. + /// + sealed class TextMarkerService : IBackgroundRenderer + { + readonly AvaloniaEdit.Rendering.TextView textView; + TextSegmentCollection? markers; + + public TextMarkerService(AvaloniaEdit.Rendering.TextView textView) + { + this.textView = textView ?? throw new ArgumentNullException(nameof(textView)); + textView.DocumentChanged += OnDocumentChanged; + OnDocumentChanged(null, EventArgs.Empty); + } + + void OnDocumentChanged(object? sender, EventArgs e) + { + markers = textView.Document != null ? new TextSegmentCollection(textView.Document) : null; + } + + public TextMarker Create(int startOffset, int length) + { + if (markers == null) + throw new InvalidOperationException("Cannot create a marker when not attached to a document"); + var textLength = textView.Document.TextLength; + if (startOffset < 0 || startOffset > textLength) + throw new ArgumentOutOfRangeException(nameof(startOffset)); + if (length < 0 || startOffset + length > textLength) + throw new ArgumentOutOfRangeException(nameof(length)); + + var marker = new TextMarker(this, startOffset, length); + markers.Add(marker); + return marker; + } + + public void Remove(TextMarker marker) + { + if (markers != null && markers.Remove(marker)) + Redraw(marker); + } + + public void RemoveAll(Predicate predicate) + { + if (markers == null) + return; + foreach (var m in markers.ToArray()) + if (predicate(m)) + Remove(m); + } + + internal void Redraw(ISegment segment) => textView.Redraw(segment); + + // IBackgroundRenderer + + public KnownLayer Layer => KnownLayer.Selection; // draw behind selection + + public void Draw(AvaloniaEdit.Rendering.TextView textView, DrawingContext drawingContext) + { + if (markers == null || !textView.VisualLinesValid) + return; + var visualLines = textView.VisualLines; + if (visualLines.Count == 0) + return; + + int viewStart = visualLines.First().FirstDocumentLine.Offset; + int viewEnd = visualLines.Last().LastDocumentLine.EndOffset; + foreach (var marker in markers.FindOverlappingSegments(viewStart, viewEnd - viewStart)) + { + if (marker.BackgroundColor is not { } bg) + 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); + } + } + } + + /// + /// A single highlighted span. Setting triggers a redraw of + /// just that segment. + /// + sealed class TextMarker : TextSegment + { + readonly TextMarkerService service; + Color? backgroundColor; + + public TextMarker(TextMarkerService service, int startOffset, int length) + { + this.service = service; + StartOffset = startOffset; + Length = length; + } + + public Color? BackgroundColor { + get => backgroundColor; + set { + if (backgroundColor == value) + return; + backgroundColor = value; + service.Redraw(this); + } + } + } +}