From e299421f76915d1d9f8121a30b61e9ceee69e93b Mon Sep 17 00:00:00 2001 From: mika kuns Date: Fri, 7 Aug 2026 09:35:21 +0200 Subject: [PATCH] feat(diff): draw old and new line numbers in a custom margin --- .../Views/Controls/DiffTextView.axaml.cs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml.cs b/src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml.cs index e706bca8..7553daf9 100644 --- a/src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml.cs +++ b/src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml.cs @@ -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(); } + 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); + /// What one document line represents, for the margin and the background renderers. internal sealed record RowInfo( AlignedSide Kind, int? OldNo, int? NewNo, IReadOnlyList Spans); + + /// Draws one or two gutter columns of diff line numbers from the row lookup. + private sealed class DiffLineNumberMargin : AbstractMargin + { + private const double ColumnWidth = 44; + private const double Gap = 6; + + private readonly Func _rows; + private readonly bool _showOld; + private readonly bool _showNew; + private readonly IBrush _foreground; + private readonly Typeface _typeface; + private readonly double _fontSize; + + public DiffLineNumberMargin(Func 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)); + } + } }