feat(diff): sync vertical scrolling across the split panes

This commit is contained in:
mika kuns
2026-08-07 09:37:38 +02:00
parent 861ba12f7f
commit 14e0cffa25
@@ -5,6 +5,8 @@ using System.IO;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.VisualTree;
using AvaloniaEdit;
using AvaloniaEdit.Document;
using AvaloniaEdit.Editing;
@@ -64,6 +66,10 @@ public partial class DiffTextView : UserControl
private DiffLineNumberMargin? _leftMargin, _rightMargin;
private ScrollViewer? _leftScroll, _rightScroll;
private bool _scrollHooked;
private bool _syncing;
public DiffTextView()
{
InitializeComponent();
@@ -71,6 +77,7 @@ public partial class DiffTextView : UserControl
_rightTm = RightEditor.InstallTextMate(Registry);
InstallRenderers();
Rebuild();
HookScrollSync();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@@ -199,6 +206,49 @@ public partial class DiffTextView : UserControl
RightEditor.TextArea.TextView.InvalidateVisual();
}
/// The editors' ScrollViewers only exist once the template has been applied.
private void HookScrollSync()
{
if (_scrollHooked) return;
_scrollHooked = true;
Dispatcher.UIThread.Post(() =>
{
_leftScroll = LeftEditor.FindDescendantOfType<ScrollViewer>();
_rightScroll = RightEditor.FindDescendantOfType<ScrollViewer>();
if (_leftScroll is not null) _leftScroll.ScrollChanged += OnPaneScroll;
if (_rightScroll is not null) _rightScroll.ScrollChanged += OnPaneScroll;
}, DispatcherPriority.Loaded);
}
private void OnPaneScroll(object? sender, ScrollChangedEventArgs e)
{
if (_syncing || !IsSplit || _leftScroll is null || _rightScroll is null) return;
var fromLeft = ReferenceEquals(sender, _leftScroll);
var source = fromLeft ? _leftScroll : _rightScroll;
var target = fromLeft ? _rightScroll : _leftScroll;
var targetEditor = fromLeft ? RightEditor : LeftEditor;
var sourceEditor = fromLeft ? LeftEditor : RightEditor;
_syncing = true;
try
{
if (WrapLines)
{
// Line heights differ once lines wrap, so anchor on the top visible line.
var topLine = sourceEditor.TextArea.TextView.VisualLines is { Count: > 0 } visualLines
? visualLines[0].FirstDocumentLine.LineNumber
: (int?)null;
if (topLine is { } line && line >= 1 && line <= targetEditor.Document.LineCount)
targetEditor.ScrollToLine(line);
}
else if (Math.Abs(target.Offset.Y - source.Offset.Y) > 0.5)
{
target.Offset = new Vector(target.Offset.X, source.Offset.Y);
}
}
finally { _syncing = false; }
}
/// <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);