fix(merge): harden 3-pane editor + document the new conflict resolver

Review follow-ups: coalesce gutter re-layout posts (avoid dispatcher flooding when
visual lines aren't ready), drop the zero-length deletable segment (undo hygiene),
and clear stale scroll-sync hooks on DataContext swap. Update Ui/CLAUDE.md to the
3-pane editor and log visual-verification items (incl. empty-side + alignment edges)
in docs/open.md.
This commit is contained in:
Mika Kuns
2026-06-19 10:21:32 +02:00
parent c4d1acc75b
commit 869dd25a23
3 changed files with 22 additions and 7 deletions

View File

@@ -38,6 +38,7 @@ public partial class ConflictResolverView : Window
private bool _rebuilding;
private bool _applyingAccept;
private bool _syncing;
private bool _gutterPending;
public ConflictResolverView()
{
@@ -53,6 +54,12 @@ public partial class ConflictResolverView : Window
_vm.ActiveFileChanged -= Rebuild;
_vm.CurrentChanged -= ScrollToCurrent;
}
// The editors persist across a DataContext swap, so drop stale scroll-sync hooks first.
foreach (var sv in _scrollViewers)
if (sv is not null) sv.ScrollChanged -= OnPaneScroll;
_scrollViewers = Array.Empty<ScrollViewer?>();
_wired = false;
_vm = DataContext as ConflictResolverViewModel;
if (_vm is null) return;
@@ -150,7 +157,7 @@ public partial class ConflictResolverView : Window
_wired = true;
Dispatcher.UIThread.Post(HookScrollSync, DispatcherPriority.Loaded);
}
Dispatcher.UIThread.Post(PositionGutters, DispatcherPriority.Loaded);
QueueGutters();
}
private static (string Text, List<(int Offset, int Length, MergeConflictBlock Block)> Spans) BuildSide(
@@ -185,7 +192,7 @@ public partial class ConflictResolverView : Window
if (e.PropertyName is nameof(MergeConflictBlock.IsResolved) or nameof(MergeConflictBlock.Resolution))
{
InvalidateRenderers();
Dispatcher.UIThread.Post(PositionGutters, DispatcherPriority.Background);
QueueGutters();
}
}
@@ -202,7 +209,7 @@ public partial class ConflictResolverView : Window
break;
}
}
Dispatcher.UIThread.Post(PositionGutters, DispatcherPriority.Background);
QueueGutters();
}
// ── Accept a side into the result ────────────────────────────────────────
@@ -233,6 +240,14 @@ public partial class ConflictResolverView : Window
RightGutter.Children.Clear();
}
// Coalesce gutter re-layouts so repeated change/scroll events can't flood the dispatcher.
private void QueueGutters()
{
if (_gutterPending) return;
_gutterPending = true;
Dispatcher.UIThread.Post(() => { _gutterPending = false; PositionGutters(); }, DispatcherPriority.Background);
}
private void PositionGutters()
{
ClearGutters();
@@ -240,7 +255,7 @@ public partial class ConflictResolverView : Window
var tv = ResultEditor.TextArea.TextView;
if (!tv.VisualLinesValid)
{
Dispatcher.UIThread.Post(PositionGutters, DispatcherPriority.Background);
QueueGutters();
return;
}
@@ -314,7 +329,7 @@ public partial class ConflictResolverView : Window
if (region.Block is null) return;
var line = ResultEditor.Document.GetLineByOffset(region.Start.Offset).LineNumber;
ResultEditor.ScrollToLine(line);
Dispatcher.UIThread.Post(PositionGutters, DispatcherPriority.Background);
QueueGutters();
}
private void InvalidateRenderers()
@@ -392,7 +407,6 @@ public partial class ConflictResolverView : Window
var s = Math.Max(segment.Offset, start);
var e = Math.Min(segment.EndOffset, end);
if (e > s) yield return new Seg(s, e - s);
else if (e == s && segment.Length == 0 && s >= start && s <= end) yield return new Seg(s, 0);
}
}
}