diff --git a/src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml.cs b/src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml.cs
index 9149201c..914cb3d7 100644
--- a/src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml.cs
+++ b/src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml.cs
@@ -76,23 +76,34 @@ public partial class DiffTextView : UserControl
_leftTm = LeftEditor.InstallTextMate(Registry);
_rightTm = RightEditor.InstallTextMate(Registry);
InstallRenderers();
- Rebuild();
+ ReloadFile();
HookScrollSync();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
- if (change.Property == FileProperty || change.Property == IsSplitProperty)
- Rebuild();
+ if (change.Property == FileProperty)
+ ReloadFile();
+ else if (change.Property == IsSplitProperty)
+ RefreshLayout();
else if (change.Property == WrapLinesProperty)
ApplyWrap();
}
- private void Rebuild()
+ /// Recomputes the alignment (word-diff LCS) for the current and then
+ /// refreshes everything downstream of it. Only ever needed when the file actually changes.
+ private void ReloadFile()
{
_aligned = DiffAlignment.Build(File?.Lines);
+ RefreshLayout();
+ }
+ /// Re-applies the current to the editors for the current layout
+ /// (split vs unified). A single already carries both row sets,
+ /// so toggling never needs to re-run the alignment.
+ private void RefreshLayout()
+ {
var split = IsSplit;
RightEditor.IsVisible = split;
PaneDivider.IsVisible = split;
@@ -181,7 +192,7 @@ public partial class DiffTextView : UserControl
}
private IBrush Brush(string key, Color fallback) =>
- this.TryGetResource(key, ActualThemeVariant, out var value) && value is IBrush brush
+ this.TryFindResource(key, out var value) && value is IBrush brush
? brush
: new SolidColorBrush(fallback);
@@ -235,11 +246,20 @@ public partial class DiffTextView : UserControl
if (WrapLines)
{
// Line heights differ once lines wrap, so anchor on the top visible line.
+ // ScrollToLine/ScrollTo are "bring into view" primitives (they park the line
+ // near mid-viewport past a hysteresis threshold) — top-align by computing the
+ // target's own vertical offset for that document line and assigning it
+ // directly, the same way the non-wrap branch assigns Offset below. That is
+ // synchronous, so the same offset-comparison guard makes the echo self-terminate.
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);
+ {
+ var targetTop = targetEditor.TextArea.TextView.GetVisualTopByDocumentLine(line);
+ if (Math.Abs(target.Offset.Y - targetTop) > 0.5)
+ target.Offset = new Vector(target.Offset.X, targetTop);
+ }
}
else if (Math.Abs(target.Offset.Y - source.Offset.Y) > 0.5)
{