1

I'm currently using the following code as a LineTransformer with an AvalonEdit TextEditor. I want to be able to highlight the current single search result with a selection, however the selection is barely visible because the formatting of the DocumentColorizingTransformer has precedence over showing highlighted text. How do I get the highlighted selection to show instead of or before the formatting?

public class ColorizeSearchResults : DocumentColorizingTransformer {

    public ColorizeSearchResults() : base() {
        SearchTerm = "";
        MatchCase = false;
    }

    public string SearchTerm { get; set; }
    public bool MatchCase { get; set; }

    protected override void ColorizeLine(DocumentLine line) {
        if (SearchTerm.Length == 0)
            return;

        int lineStartOffset = line.Offset;
        string text = CurrentContext.Document.GetText(line);
        int count = 0;
        int start = 0;
        int index;
        while ((index = text.IndexOf(SearchTerm, start, MatchCase ? StringComparison.CurrentCulture : StringComparison.CurrentCultureIgnoreCase)) >= 0) {
            base.ChangeLinePart(
                lineStartOffset + index,
                lineStartOffset + index + SearchTerm.Length,
                (VisualLineElement element) => {
                    element.TextRunProperties.SetForegroundBrush(Brushes.White);
                    element.TextRunProperties.SetBackgroundBrush(Brushes.Magenta);
                });
            start = index + 1;
            count++;
        }
    }
}

Example of formatting showing over selection

trigger_segfault
  • 554
  • 1
  • 6
  • 23

3 Answers3

3

See http://avalonedit.net/documentation/html/c06e9832-9ef0-4d65-ac2e-11f7ce9c7774.htm for AvalonEdit render flow.

The selection layer is rendered before text. So if the text has background, it overrides the selection background. Fortunately we can set the background to Brush.Transparent (or a mix of the Selection.Brush and your own color).

Solution: I've modified the SelectionColorizer code to reset selection background to transparent:

class SelectionColorizerWithBackground : ColorizingTransformer
{
    ICSharpCode.AvalonEdit.Editing.TextArea _textArea;

    public SelectionColorizerWithBackground(
        ICSharpCode.AvalonEdit.Editing.TextArea textArea)
    {
        if (textArea == null)
            throw new ArgumentNullException("textArea");
        this._textArea = textArea;
    }

    protected override void Colorize(ITextRunConstructionContext context)
    {
        int lineStartOffset = context.VisualLine.FirstDocumentLine.Offset;

        int lineEndOffset = context.VisualLine.LastDocumentLine.Offset +
            context.VisualLine.LastDocumentLine.TotalLength;

        foreach (var segment in _textArea.Selection.Segments)
        {
            int segmentStart = segment.StartOffset;
            if (segmentStart >= lineEndOffset)
                continue;

            int segmentEnd = segment.EndOffset;
            if (segmentEnd <= lineStartOffset)
                continue;

            int startColumn;
            if (segmentStart < lineStartOffset)
                startColumn = 0;
            else
                startColumn = context.VisualLine.ValidateVisualColumn(
                    segment.StartOffset, segment.StartVisualColumn,
                    _textArea.Selection.EnableVirtualSpace);

            int endColumn;
            if (segmentEnd > lineEndOffset)
                endColumn =
                    _textArea.Selection.EnableVirtualSpace
                        ? int.MaxValue
                        : context.VisualLine
                                 .VisualLengthWithEndOfLineMarker;
            else
                endColumn = context.VisualLine.ValidateVisualColumn(
                    segment.EndOffset, segment.EndVisualColumn,
                    _textArea.Selection.EnableVirtualSpace);

            ChangeVisualElements(
                startColumn, endColumn,
                element => {
                    element.TextRunProperties.SetBackgroundBrush(
                        System.Windows.Media.Brushes.Transparent);
                    if (_textArea.SelectionForeground != null)
                    {
                        element.TextRunProperties.SetForegroundBrush(
                            _textArea.SelectionForeground);
                    }
                });
        }
    }
}

To use the code you are supposed to do the following:

var lineTransformers = textEditor.TextArea.TextView.LineTransformers;

// Remove the original SelectionColorizer.
// Note: if you have syntax highlighting you need to do something else
// to avoid clearing other colorizers. If too complicated you can skip
// this step but to suffer a 2x performance penalty.
lineTransformers.Clear();

lineTransformers.Add(new ColorizeSearchResults());
lineTransformers.Add(
    new SelectionColorizerWithBackground(textEditor.TextArea));
lwchkg
  • 106
  • 4
2

After I've tried my solutions extensively, I'd like to add a few points:

  • While my other solution above appears to work, you'll have some subpixel artefacts when the rectangles are supposed to be tiled. If that is unacceptable you can implement an IBackgroundRenderer. (That happens to be my chosen solution.) If you want some code you may request here, but I doubt whether it will be useful.

  • BTW, since your question is about search result, most likely you can use https://github.com/icsharpcode/AvalonEdit/blob/697ff0d38c95c9e5a536fbc05ae2307ec9ef2a63/ICSharpCode.AvalonEdit/Search/SearchResultBackgroundRenderer.cs unmodified (or modify it if you don't want the rounded borders).

  • You may use element.BackgroundBrush = Brushes.Magenta; instead of element.TextRunProperties.SetBackgroundBrush(Brushes.Magenta);. AvalonEdit appears to draw the background with a rectangle with 3px radius.

  • There is also a RichTextColorizer starting from AvalonEdit 5.01. I don't know how to use it though because it is not referenced in other files. And the (most likely unwanted) rounded rectangles in the previous paragraph are likely to be present.

lwchkg
  • 106
  • 4
  • I came back to this issue a year later with a different program and tried the `IBackgroundRenderer`. It worked perfectly other than the fact that the drawn rectangles were slightly larger than the selection rectangles but not a big deal. – trigger_segfault Dec 23 '17 at 17:59
  • Apparently removing the pen brush from the highlighter even fixed that minor issue. – trigger_segfault Dec 23 '17 at 18:16
2

So here's my final product based almost entirely off of the existing AvalonEdit SearchResultBackgroundRenderer.

This works a little different than my post's colorizer as you have to modify the search results manually instead of it doing it for you. But that may also save some computation time.

If your search doesn't use Regex, then you can easily modify SearchResult to instead just pass in a start offset and length for the constructor.

/// <summary>A search result storing a match and text segment.</summary>
public class SearchResult : TextSegment {
    /// <summary>The regex match for the search result.</summary>
    public Match Match { get; }

    /// <summary>Constructs the search result from the match.</summary>
    public SearchResult(Match match) {
        this.StartOffset = match.Index;
        this.Length = match.Length;
        this.Match = match;
    }
}

/// <summary>Colorizes search results behind the selection.</summary>
public class ColorizeSearchResultsBackgroundRenderer : IBackgroundRenderer {

    /// <summary>The search results to be modified.</summary>
    TextSegmentCollection<SearchResult> currentResults = new TextSegmentCollection<SearchResult>();

    /// <summary>Constructs the search result colorizer.</summary>
    public ColorizeSearchResultsBackgroundRenderer() {
        Background = new SolidColorBrush(Color.FromRgb(246, 185, 77));
        Background.Freeze();
    }

    /// <summary>Gets the layer on which this background renderer should draw.</summary>
    public KnownLayer Layer {
        get {
            // draw behind selection
            return KnownLayer.Selection;
        }
    }

    /// <summary>Causes the background renderer to draw.</summary>
    public void Draw(TextView textView, DrawingContext drawingContext) {
        if (textView == null)
            throw new ArgumentNullException("textView");
        if (drawingContext == null)
            throw new ArgumentNullException("drawingContext");

        if (currentResults == 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 (SearchResult result in currentResults.FindOverlappingSegments(viewStart, viewEnd - viewStart)) {
            BackgroundGeometryBuilder geoBuilder = new BackgroundGeometryBuilder();
            geoBuilder.AlignToWholePixels = true;
            geoBuilder.BorderThickness = 0;
            geoBuilder.CornerRadius = 0;
            geoBuilder.AddSegment(textView, result);
            Geometry geometry = geoBuilder.CreateGeometry();
            if (geometry != null) {
                drawingContext.DrawGeometry(Background, null, geometry);
            }
        }
    }

    /// <summary>Gets the search results for modification.</summary>
    public TextSegmentCollection<SearchResult> CurrentResults {
        get { return currentResults; }
    }

    /// <summary>Gets or sets the background brush for the search results.</summary>
    public Brush Background { get; set; }
}

In order to make use of the background renderer:

var searchColorizor = new ColorizeSearchResultsBackgroundRenderer();
textEditor.TextArea.TextView.BackgroundRenderers.Add(searchColorizor);
trigger_segfault
  • 554
  • 1
  • 6
  • 23
  • I am new to C# and trying to do something similar to OP. Instead I am implementing a `IBackgroundRenderer` to highlight all occurrences of a selected word rather than search results. This is where I'm at now: https://pastebin.com/MGeHrCf0. In your example here, how is `TextSegmentCollection` getting populated with search results? I only see a getter and don't quite understand the flow of data. At what point (and how) does `currentResults` get populated with all of the actual matches? Thank you for any guidance! – fmotion1 Aug 03 '22 at 05:44