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.Controls;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using AvaloniaEdit;
|
using AvaloniaEdit;
|
||||||
|
using AvaloniaEdit.Document;
|
||||||
using AvaloniaEdit.Editing;
|
using AvaloniaEdit.Editing;
|
||||||
using AvaloniaEdit.Rendering;
|
using AvaloniaEdit.Rendering;
|
||||||
using AvaloniaEdit.TextMate;
|
using AvaloniaEdit.TextMate;
|
||||||
@@ -68,6 +69,7 @@ public partial class DiffTextView : UserControl
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_leftTm = LeftEditor.InstallTextMate(Registry);
|
_leftTm = LeftEditor.InstallTextMate(Registry);
|
||||||
_rightTm = RightEditor.InstallTextMate(Registry);
|
_rightTm = RightEditor.InstallTextMate(Registry);
|
||||||
|
InstallRenderers();
|
||||||
Rebuild();
|
Rebuild();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +115,7 @@ public partial class DiffTextView : UserControl
|
|||||||
RebuildMargins();
|
RebuildMargins();
|
||||||
ApplyWrap();
|
ApplyWrap();
|
||||||
ApplyGrammar(File?.Path);
|
ApplyGrammar(File?.Path);
|
||||||
|
InvalidateRenderers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Row index i is document line i + 1, so slot 0 stays null and lookups can pass the
|
/// 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
|
? brush
|
||||||
: new SolidColorBrush(fallback);
|
: 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>
|
/// <summary>What one document line represents, for the margin and the background renderers.</summary>
|
||||||
internal sealed record RowInfo(
|
internal sealed record RowInfo(
|
||||||
AlignedSide Kind, int? OldNo, int? NewNo, IReadOnlyList<TextSpan> Spans);
|
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));
|
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