feat(merge): additive conflict accept — stack ours/theirs in click order
Replace the single-side replace (and the short-lived accept-both button) with additive accepts: each result conflict region starts EMPTY (thin marker bar), and the gutter controls append a side in click order — > adds ours, < adds theirs (first pick on top, next below), x clears. Controls stay visible after the first pick so both sides can be stacked; empty/unresolved regions render a marker so they stay visible. en/de keys updated; Ui 128 + Localization 16 green.
This commit is contained in:
@@ -126,7 +126,8 @@ public partial class ConflictResolverView : Window
|
||||
|
||||
var (oursText, oursSpans) = BuildSide(file, b => b.Ours);
|
||||
var (theirsText, theirsSpans) = BuildSide(file, b => b.Theirs);
|
||||
var (resultText, resultSpans) = BuildSide(file, b => b.Resolution ?? b.Ours);
|
||||
// Unresolved conflicts start EMPTY — the user builds the result by appending sides.
|
||||
var (resultText, resultSpans) = BuildSide(file, b => b.Resolution ?? "");
|
||||
_oursSpans = oursSpans;
|
||||
_theirsSpans = theirsSpans;
|
||||
|
||||
@@ -214,21 +215,32 @@ public partial class ConflictResolverView : Window
|
||||
|
||||
// ── Accept a side into the result ────────────────────────────────────────
|
||||
|
||||
private void AcceptOurs(MergeConflictBlock block) => AcceptInto(block, block.Ours);
|
||||
private void AcceptTheirs(MergeConflictBlock block) => AcceptInto(block, block.Theirs);
|
||||
private void AcceptBoth(MergeConflictBlock block) => AcceptInto(block, block.Ours + block.Theirs);
|
||||
private void AppendOurs(MergeConflictBlock block) => AppendSide(block, block.Ours);
|
||||
private void AppendTheirs(MergeConflictBlock block) => AppendSide(block, block.Theirs);
|
||||
|
||||
private void AcceptInto(MergeConflictBlock block, string text)
|
||||
// Accept APPENDS a side to the result region in click order (first pick on top, the
|
||||
// next below), so a conflict can take ours, theirs, or both — and stay editable.
|
||||
private void AppendSide(MergeConflictBlock block, string text)
|
||||
{
|
||||
var region = _resultRegions.FirstOrDefault(r => ReferenceEquals(r.Block, block));
|
||||
if (region.Block is null) return;
|
||||
_applyingAccept = true;
|
||||
try
|
||||
{
|
||||
ResultEditor.Document.Replace(region.Start.Offset, region.End.Offset - region.Start.Offset, text);
|
||||
}
|
||||
try { ResultEditor.Document.Insert(region.End.Offset, text); }
|
||||
finally { _applyingAccept = false; }
|
||||
block.Resolution = text;
|
||||
block.Resolution = ResultEditor.Document.GetText(region.Start.Offset, Math.Max(0, region.End.Offset - region.Start.Offset));
|
||||
InvalidateRenderers();
|
||||
PositionGutters();
|
||||
}
|
||||
|
||||
// Reset a conflict back to empty/unresolved (start over).
|
||||
private void ClearRegion(MergeConflictBlock block)
|
||||
{
|
||||
var region = _resultRegions.FirstOrDefault(r => ReferenceEquals(r.Block, block));
|
||||
if (region.Block is null) return;
|
||||
_applyingAccept = true;
|
||||
try { ResultEditor.Document.Replace(region.Start.Offset, region.End.Offset - region.Start.Offset, ""); }
|
||||
finally { _applyingAccept = false; }
|
||||
block.Resolution = null;
|
||||
InvalidateRenderers();
|
||||
PositionGutters();
|
||||
}
|
||||
@@ -263,7 +275,7 @@ public partial class ConflictResolverView : Window
|
||||
var doc = ResultEditor.Document;
|
||||
foreach (var (block, start, end) in _resultRegions)
|
||||
{
|
||||
if (block.IsResolved) continue;
|
||||
// Controls stay visible even once resolved, so you can append the other side too.
|
||||
var len = end.Offset - start.Offset;
|
||||
ISegment probe = len > 0
|
||||
? new Seg(start.Offset, len)
|
||||
@@ -276,16 +288,16 @@ public partial class ConflictResolverView : Window
|
||||
if (tv.TranslatePoint(new Point(0, y), LeftGutter) is { } pl &&
|
||||
pl.Y > -24 && pl.Y < LeftGutter.Bounds.Height + 24)
|
||||
{
|
||||
AddAcceptButton(LeftGutter, pl.Y, "›", () => AcceptOurs(capturedBlock),
|
||||
AddAcceptButton(LeftGutter, pl.Y, "›", () => AppendOurs(capturedBlock),
|
||||
Tr("conflictResolver.acceptOurs"));
|
||||
// "Accept both" sits just under the ours chevron: ours text then theirs text.
|
||||
AddAcceptButton(LeftGutter, pl.Y + 21, "⊕", () => AcceptBoth(capturedBlock),
|
||||
Tr("conflictResolver.acceptBoth"));
|
||||
// ✕ resets the conflict to empty so you can start the stack over.
|
||||
AddAcceptButton(LeftGutter, pl.Y + 21, "✕", () => ClearRegion(capturedBlock),
|
||||
Tr("conflictResolver.clearConflict"));
|
||||
}
|
||||
|
||||
if (tv.TranslatePoint(new Point(0, y), RightGutter) is { } pr &&
|
||||
pr.Y > -24 && pr.Y < RightGutter.Bounds.Height + 24)
|
||||
AddAcceptButton(RightGutter, pr.Y, "‹", () => AcceptTheirs(capturedBlock),
|
||||
AddAcceptButton(RightGutter, pr.Y, "‹", () => AppendTheirs(capturedBlock),
|
||||
Tr("conflictResolver.acceptTheirs"));
|
||||
}
|
||||
}
|
||||
@@ -388,12 +400,22 @@ public partial class ConflictResolverView : Window
|
||||
if (!textView.VisualLinesValid) return;
|
||||
foreach (var (offset, length, resolved) in _spans())
|
||||
{
|
||||
ISegment seg = new Seg(offset, Math.Max(length, 0));
|
||||
var builder = new BackgroundGeometryBuilder { AlignToWholePixels = true, CornerRadius = 2 };
|
||||
builder.AddSegment(textView, seg);
|
||||
var geo = builder.CreateGeometry();
|
||||
if (geo is not null)
|
||||
drawingContext.DrawGeometry(resolved ? _resolved : _conflict, null, geo);
|
||||
var brush = resolved ? _resolved : _conflict;
|
||||
if (length > 0)
|
||||
{
|
||||
var builder = new BackgroundGeometryBuilder { AlignToWholePixels = true, CornerRadius = 2 };
|
||||
builder.AddSegment(textView, new Seg(offset, length));
|
||||
var geo = builder.CreateGeometry();
|
||||
if (geo is not null) drawingContext.DrawGeometry(brush, null, geo);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Empty region (nothing accepted yet): a thin marker bar marks the spot.
|
||||
var at = offset < textView.Document.TextLength ? offset : Math.Max(0, offset - 1);
|
||||
var rects = BackgroundGeometryBuilder.GetRectsForSegment(textView, new Seg(at, 1)).ToList();
|
||||
if (rects.Count > 0)
|
||||
drawingContext.FillRectangle(brush, new Rect(0, rects[0].Top, textView.Bounds.Width, 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user