feat(diff): draw old and new line numbers in a custom margin

This commit is contained in:
mika kuns
2026-08-07 09:35:21 +02:00
parent ffc60c4101
commit e299421f76
@@ -1,9 +1,13 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using AvaloniaEdit;
using AvaloniaEdit.Editing;
using AvaloniaEdit.Rendering;
using AvaloniaEdit.TextMate;
using ClaudeDo.Ui.ViewModels.Modals;
using TextMateSharp.Grammars;
@@ -57,6 +61,8 @@ public partial class DiffTextView : UserControl
private AlignedDiff _aligned = AlignedDiff.Empty;
private DiffLineNumberMargin? _leftMargin, _rightMargin;
public DiffTextView()
{
InitializeComponent();
@@ -104,6 +110,7 @@ public partial class DiffTextView : UserControl
_rightRows = Array.Empty<RowInfo?>();
}
RebuildMargins();
ApplyWrap();
ApplyGrammar(File?.Path);
}
@@ -144,7 +151,98 @@ public partial class DiffTextView : UserControl
_rightTm?.SetGrammar(scope);
}
/// The margin's column layout depends on split vs unified, so it is rebuilt rather than
/// reconfigured whenever the layout changes.
private void RebuildMargins()
{
if (_leftMargin is not null) LeftEditor.TextArea.LeftMargins.Remove(_leftMargin);
if (_rightMargin is not null) RightEditor.TextArea.LeftMargins.Remove(_rightMargin);
var foreground = Brush("TextFaintBrush", Color.Parse("#80FFFFFF"));
var typeface = new Typeface(LeftEditor.FontFamily);
_leftMargin = new DiffLineNumberMargin(LeftRow, showOld: true, showNew: !IsSplit,
foreground, typeface, LeftEditor.FontSize);
LeftEditor.TextArea.LeftMargins.Insert(0, _leftMargin);
_rightMargin = new DiffLineNumberMargin(RightRow, showOld: false, showNew: true,
foreground, typeface, RightEditor.FontSize);
RightEditor.TextArea.LeftMargins.Insert(0, _rightMargin);
}
private IBrush Brush(string key, Color fallback) =>
this.TryGetResource(key, ActualThemeVariant, out var value) && value is IBrush brush
? brush
: new SolidColorBrush(fallback);
/// <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);
/// <summary>Draws one or two gutter columns of diff line numbers from the row lookup.</summary>
private sealed class DiffLineNumberMargin : AbstractMargin
{
private const double ColumnWidth = 44;
private const double Gap = 6;
private readonly Func<int, RowInfo?> _rows;
private readonly bool _showOld;
private readonly bool _showNew;
private readonly IBrush _foreground;
private readonly Typeface _typeface;
private readonly double _fontSize;
public DiffLineNumberMargin(Func<int, RowInfo?> rows, bool showOld, bool showNew,
IBrush foreground, Typeface typeface, double fontSize)
{
_rows = rows;
_showOld = showOld;
_showNew = showNew;
_foreground = foreground;
_typeface = typeface;
_fontSize = fontSize;
}
private double Columns => (_showOld ? 1 : 0) + (_showNew ? 1 : 0);
protected override Size MeasureOverride(Size availableSize) =>
new(Columns * ColumnWidth + Gap, 0);
protected override void OnTextViewChanged(TextView? oldTextView, TextView? newTextView)
{
if (oldTextView is not null) oldTextView.VisualLinesChanged -= OnVisualLinesChanged;
base.OnTextViewChanged(oldTextView, newTextView);
if (newTextView is not null) newTextView.VisualLinesChanged += OnVisualLinesChanged;
InvalidateVisual();
}
private void OnVisualLinesChanged(object? sender, EventArgs e) => InvalidateVisual();
public override void Render(DrawingContext context)
{
var textView = TextView;
if (textView is null || !textView.VisualLinesValid) return;
foreach (var visualLine in textView.VisualLines)
{
var lineNumber = visualLine.FirstDocumentLine.LineNumber;
if (_rows(lineNumber) is not { } row) continue;
var y = visualLine.VisualTop - textView.ScrollOffset.Y;
var column = 0;
if (_showOld) DrawNumber(context, row.OldNo, column++, y);
if (_showNew) DrawNumber(context, row.NewNo, column, y);
}
}
private void DrawNumber(DrawingContext context, int? value, int column, double y)
{
if (value is null) return;
var text = new FormattedText(value.Value.ToString(CultureInfo.InvariantCulture),
CultureInfo.InvariantCulture, FlowDirection.LeftToRight, _typeface, _fontSize, _foreground);
// Right-align inside the column so the digits line up across rows.
var x = (column + 1) * ColumnWidth - text.Width - Gap;
context.DrawText(text, new Point(x, y));
}
}
}