feat(diff): tint diff rows and changed words via background renderers
This commit is contained in:
@@ -6,6 +6,7 @@ using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using AvaloniaEdit;
|
||||
using AvaloniaEdit.Document;
|
||||
using AvaloniaEdit.Editing;
|
||||
using AvaloniaEdit.Rendering;
|
||||
using AvaloniaEdit.TextMate;
|
||||
@@ -68,6 +69,7 @@ public partial class DiffTextView : UserControl
|
||||
InitializeComponent();
|
||||
_leftTm = LeftEditor.InstallTextMate(Registry);
|
||||
_rightTm = RightEditor.InstallTextMate(Registry);
|
||||
InstallRenderers();
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
@@ -113,6 +115,7 @@ public partial class DiffTextView : UserControl
|
||||
RebuildMargins();
|
||||
ApplyWrap();
|
||||
ApplyGrammar(File?.Path);
|
||||
InvalidateRenderers();
|
||||
}
|
||||
|
||||
/// Row index i is document line i + 1, so slot 0 stays null and lookups can pass the
|
||||
@@ -175,6 +178,27 @@ public partial class DiffTextView : UserControl
|
||||
? brush
|
||||
: new SolidColorBrush(fallback);
|
||||
|
||||
private void InstallRenderers()
|
||||
{
|
||||
var add = Brush("RunningTintBrush", Color.Parse("#1F7C9166"));
|
||||
var del = Brush("ErrorTintBrush", Color.Parse("#1FC87060"));
|
||||
var filler = Brush("DiffFillerBrush", Color.Parse("#0AFFFFFF"));
|
||||
var gap = Brush("DiffGapBrush", Color.Parse("#14FFFFFF"));
|
||||
var wordAdd = Brush("DiffWordAddBrush", Color.Parse("#556FA86B"));
|
||||
var wordDel = Brush("DiffWordDelBrush", Color.Parse("#55C87060"));
|
||||
|
||||
LeftEditor.TextArea.TextView.BackgroundRenderers.Add(new DiffLineRenderer(LeftRow, add, del, filler, gap));
|
||||
LeftEditor.TextArea.TextView.BackgroundRenderers.Add(new WordDiffRenderer(LeftRow, wordAdd, wordDel));
|
||||
RightEditor.TextArea.TextView.BackgroundRenderers.Add(new DiffLineRenderer(RightRow, add, del, filler, gap));
|
||||
RightEditor.TextArea.TextView.BackgroundRenderers.Add(new WordDiffRenderer(RightRow, wordAdd, wordDel));
|
||||
}
|
||||
|
||||
private void InvalidateRenderers()
|
||||
{
|
||||
LeftEditor.TextArea.TextView.InvalidateVisual();
|
||||
RightEditor.TextArea.TextView.InvalidateVisual();
|
||||
}
|
||||
|
||||
/// <summary>What one document line represents, for the margin and the background renderers.</summary>
|
||||
internal sealed record RowInfo(
|
||||
AlignedSide Kind, int? OldNo, int? NewNo, IReadOnlyList<TextSpan> Spans);
|
||||
@@ -245,4 +269,88 @@ public partial class DiffTextView : UserControl
|
||||
context.DrawText(text, new Point(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Tints whole rows by their diff role.</summary>
|
||||
private sealed class DiffLineRenderer : IBackgroundRenderer
|
||||
{
|
||||
private readonly Func<int, RowInfo?> _rows;
|
||||
private readonly IBrush _add, _del, _filler, _gap;
|
||||
|
||||
public DiffLineRenderer(Func<int, RowInfo?> rows, IBrush add, IBrush del, IBrush filler, IBrush gap)
|
||||
{
|
||||
_rows = rows; _add = add; _del = del; _filler = filler; _gap = gap;
|
||||
}
|
||||
|
||||
public KnownLayer Layer => KnownLayer.Background;
|
||||
|
||||
public void Draw(TextView textView, DrawingContext drawingContext)
|
||||
{
|
||||
if (!textView.VisualLinesValid) return;
|
||||
foreach (var visualLine in textView.VisualLines)
|
||||
{
|
||||
var row = _rows(visualLine.FirstDocumentLine.LineNumber);
|
||||
var brush = row?.Kind switch
|
||||
{
|
||||
AlignedSide.Add => _add,
|
||||
AlignedSide.Del => _del,
|
||||
AlignedSide.Filler => _filler,
|
||||
AlignedSide.Gap => _gap,
|
||||
_ => null,
|
||||
};
|
||||
if (brush is null) continue;
|
||||
|
||||
var top = visualLine.VisualTop - textView.ScrollOffset.Y;
|
||||
drawingContext.FillRectangle(brush, new Rect(0, top, textView.Bounds.Width, visualLine.Height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Tints the changed character ranges inside a row, on top of the row tint.</summary>
|
||||
private sealed class WordDiffRenderer : IBackgroundRenderer
|
||||
{
|
||||
private readonly Func<int, RowInfo?> _rows;
|
||||
private readonly IBrush _add, _del;
|
||||
|
||||
public WordDiffRenderer(Func<int, RowInfo?> rows, IBrush add, IBrush del)
|
||||
{
|
||||
_rows = rows; _add = add; _del = del;
|
||||
}
|
||||
|
||||
// Above the line tint but still behind the text.
|
||||
public KnownLayer Layer => KnownLayer.Selection;
|
||||
|
||||
public void Draw(TextView textView, DrawingContext drawingContext)
|
||||
{
|
||||
if (!textView.VisualLinesValid) return;
|
||||
foreach (var visualLine in textView.VisualLines)
|
||||
{
|
||||
var documentLine = visualLine.FirstDocumentLine;
|
||||
var row = _rows(documentLine.LineNumber);
|
||||
if (row is null || row.Spans.Count == 0) continue;
|
||||
|
||||
var brush = row.Kind == AlignedSide.Add ? _add : _del;
|
||||
foreach (var span in row.Spans)
|
||||
{
|
||||
var offset = documentLine.Offset + span.Start;
|
||||
// Spans are computed against the row text; clamp in case the document
|
||||
// and the row lookup ever disagree rather than drawing past the line.
|
||||
if (offset < documentLine.Offset || offset + span.Length > documentLine.EndOffset) continue;
|
||||
|
||||
var builder = new BackgroundGeometryBuilder { AlignToWholePixels = true, CornerRadius = 2 };
|
||||
builder.AddSegment(textView, new Seg(offset, span.Length));
|
||||
if (builder.CreateGeometry() is { } geometry)
|
||||
drawingContext.DrawGeometry(brush, null, geometry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A minimal <see cref="ISegment"/> for geometry queries.</summary>
|
||||
private readonly struct Seg : ISegment
|
||||
{
|
||||
public Seg(int offset, int length) { Offset = offset; Length = length; }
|
||||
public int Offset { get; }
|
||||
public int Length { get; }
|
||||
public int EndOffset => Offset + Length;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user