feat(merge): toggle add/remove per side, MAIN/INCOMING labels, files readout
- Conflict accept is now a per-side toggle: > adds MAIN (ours), < adds INCOMING (theirs) in click order (first on top); clicking again removes that side, so each side is included at most once. Region content is rebuilt from the included set. - Drop the separate reset (x) control — toggling both off clears the region. - Relabel the panes/tooltips Ours/Theirs -> MAIN/INCOMING (merge target vs task). - Add a cross-file 'N of M files unresolved' readout (FilesSummary) so you can see how many more files still have conflicts. en/de updated; Ui 128 + Loc 16 green.
This commit is contained in:
@@ -104,7 +104,7 @@
|
||||
</Border>
|
||||
|
||||
<!-- Toolbar: change nav · file switcher · readout -->
|
||||
<Grid Grid.Row="2" ColumnDefinitions="Auto,Auto,Auto,*,Auto" Margin="0,0,0,8"
|
||||
<Grid Grid.Row="2" ColumnDefinitions="Auto,Auto,Auto,*,Auto,Auto" Margin="0,0,0,8"
|
||||
IsVisible="{Binding HasCurrent}">
|
||||
<Button Grid.Column="0" Classes="btn" Content="↑" Margin="0,0,4,0" Padding="10,4"
|
||||
ToolTip.Tip="{loc:Tr conflictResolver.prevConflict}"
|
||||
@@ -121,7 +121,11 @@
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<TextBlock Grid.Column="4" Classes="meta" VerticalAlignment="Center"
|
||||
<TextBlock Grid.Column="4" Classes="meta" VerticalAlignment="Center" Margin="0,0,14,0"
|
||||
Foreground="{DynamicResource AmberBrush}"
|
||||
IsVisible="{Binding HasMultipleFiles}"
|
||||
Text="{Binding FilesSummary}"/>
|
||||
<TextBlock Grid.Column="5" Classes="meta" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource TextDimBrush}"
|
||||
Text="{Binding PositionText}"/>
|
||||
</Grid>
|
||||
|
||||
@@ -30,7 +30,7 @@ public partial class ConflictResolverView : Window
|
||||
private List<(int Offset, int Length, MergeConflictBlock Block)> _theirsSpans = new();
|
||||
|
||||
// Live, edit-tracked conflict regions in the editable result document.
|
||||
private readonly List<(MergeConflictBlock Block, TextAnchor Start, TextAnchor End)> _resultRegions = new();
|
||||
private readonly List<ResultRegion> _resultRegions = new();
|
||||
private readonly List<MergeConflictBlock> _hookedBlocks = new();
|
||||
|
||||
private ScrollViewer?[] _scrollViewers = Array.Empty<ScrollViewer?>();
|
||||
@@ -143,7 +143,7 @@ public partial class ConflictResolverView : Window
|
||||
start.MovementType = AnchorMovementType.BeforeInsertion;
|
||||
var end = doc.CreateAnchor(offset + length);
|
||||
end.MovementType = AnchorMovementType.AfterInsertion;
|
||||
_resultRegions.Add((block, start, end));
|
||||
_resultRegions.Add(new ResultRegion(block, start, end));
|
||||
block.PropertyChanged += OnBlockChanged;
|
||||
_hookedBlocks.Add(block);
|
||||
}
|
||||
@@ -202,45 +202,32 @@ public partial class ConflictResolverView : Window
|
||||
private void OnResultDocumentChanged(object? sender, DocumentChangeEventArgs e)
|
||||
{
|
||||
if (_rebuilding || _applyingAccept) return;
|
||||
foreach (var (block, start, end) in _resultRegions)
|
||||
foreach (var r in _resultRegions)
|
||||
{
|
||||
if (e.Offset >= start.Offset && e.Offset <= end.Offset)
|
||||
if (e.Offset >= r.Start.Offset && e.Offset <= r.End.Offset)
|
||||
{
|
||||
block.Resolution = ResultEditor.Document.GetText(start.Offset, Math.Max(0, end.Offset - start.Offset));
|
||||
r.Block.Resolution = ResultEditor.Document.GetText(r.Start.Offset, Math.Max(0, r.End.Offset - r.Start.Offset));
|
||||
break;
|
||||
}
|
||||
}
|
||||
QueueGutters();
|
||||
}
|
||||
|
||||
// ── Accept a side into the result ────────────────────────────────────────
|
||||
// ── Toggle a side in/out of the result region ────────────────────────────
|
||||
|
||||
private void AppendOurs(MergeConflictBlock block) => AppendSide(block, block.Ours);
|
||||
private void AppendTheirs(MergeConflictBlock block) => AppendSide(block, block.Theirs);
|
||||
|
||||
// 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)
|
||||
// Each side can be included at most once. Clicking adds it (in click order, first on
|
||||
// top); clicking again removes it. The region content is rebuilt from the included set.
|
||||
private void ToggleSide(ResultRegion region, char side)
|
||||
{
|
||||
var region = _resultRegions.FirstOrDefault(r => ReferenceEquals(r.Block, block));
|
||||
if (region.Block is null) return;
|
||||
_applyingAccept = true;
|
||||
try { ResultEditor.Document.Insert(region.End.Offset, text); }
|
||||
finally { _applyingAccept = false; }
|
||||
block.Resolution = ResultEditor.Document.GetText(region.Start.Offset, Math.Max(0, region.End.Offset - region.Start.Offset));
|
||||
InvalidateRenderers();
|
||||
PositionGutters();
|
||||
}
|
||||
if (region.Order.Contains(side)) region.Order.Remove(side);
|
||||
else region.Order.Add(side);
|
||||
|
||||
// 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;
|
||||
var text = string.Concat(region.Order.Select(c => c == 'o' ? region.Block.Ours : region.Block.Theirs));
|
||||
_applyingAccept = true;
|
||||
try { ResultEditor.Document.Replace(region.Start.Offset, region.End.Offset - region.Start.Offset, ""); }
|
||||
try { ResultEditor.Document.Replace(region.Start.Offset, region.End.Offset - region.Start.Offset, text); }
|
||||
finally { _applyingAccept = false; }
|
||||
block.Resolution = null;
|
||||
|
||||
region.Block.Resolution = region.Order.Count == 0 ? null : text;
|
||||
InvalidateRenderers();
|
||||
PositionGutters();
|
||||
}
|
||||
@@ -273,32 +260,30 @@ public partial class ConflictResolverView : Window
|
||||
}
|
||||
|
||||
var doc = ResultEditor.Document;
|
||||
foreach (var (block, start, end) in _resultRegions)
|
||||
foreach (var region in _resultRegions)
|
||||
{
|
||||
// Controls stay visible even once resolved, so you can append the other side too.
|
||||
var len = end.Offset - start.Offset;
|
||||
// Controls stay visible whether or not a side is included, so either can be toggled.
|
||||
var len = region.End.Offset - region.Start.Offset;
|
||||
ISegment probe = len > 0
|
||||
? new Seg(start.Offset, len)
|
||||
: new Seg(start.Offset, start.Offset < doc.TextLength ? 1 : 0);
|
||||
? new Seg(region.Start.Offset, len)
|
||||
: new Seg(region.Start.Offset, region.Start.Offset < doc.TextLength ? 1 : 0);
|
||||
var rects = BackgroundGeometryBuilder.GetRectsForSegment(tv, probe).ToList();
|
||||
if (rects.Count == 0) continue;
|
||||
var y = rects[0].Top;
|
||||
|
||||
var capturedBlock = block;
|
||||
var r = region;
|
||||
var oursIn = region.Order.Contains('o');
|
||||
var theirsIn = region.Order.Contains('t');
|
||||
|
||||
if (tv.TranslatePoint(new Point(0, y), LeftGutter) is { } pl &&
|
||||
pl.Y > -24 && pl.Y < LeftGutter.Bounds.Height + 24)
|
||||
{
|
||||
AddAcceptButton(LeftGutter, pl.Y, "›", () => AppendOurs(capturedBlock),
|
||||
Tr("conflictResolver.acceptOurs"));
|
||||
// ✕ resets the conflict to empty so you can start the stack over.
|
||||
AddAcceptButton(LeftGutter, pl.Y + 21, "✕", () => ClearRegion(capturedBlock),
|
||||
Tr("conflictResolver.clearConflict"));
|
||||
}
|
||||
AddAcceptButton(LeftGutter, pl.Y, oursIn ? "−" : "›", () => ToggleSide(r, 'o'),
|
||||
Tr(oursIn ? "conflictResolver.removeOurs" : "conflictResolver.acceptOurs"));
|
||||
|
||||
if (tv.TranslatePoint(new Point(0, y), RightGutter) is { } pr &&
|
||||
pr.Y > -24 && pr.Y < RightGutter.Bounds.Height + 24)
|
||||
AddAcceptButton(RightGutter, pr.Y, "‹", () => AppendTheirs(capturedBlock),
|
||||
Tr("conflictResolver.acceptTheirs"));
|
||||
AddAcceptButton(RightGutter, pr.Y, theirsIn ? "−" : "‹", () => ToggleSide(r, 't'),
|
||||
Tr(theirsIn ? "conflictResolver.removeTheirs" : "conflictResolver.acceptTheirs"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,7 +329,7 @@ public partial class ConflictResolverView : Window
|
||||
{
|
||||
if (_vm?.Current is not { } block) return;
|
||||
var region = _resultRegions.FirstOrDefault(r => ReferenceEquals(r.Block, block));
|
||||
if (region.Block is null) return;
|
||||
if (region is null) return;
|
||||
var line = ResultEditor.Document.GetLineByOffset(region.Start.Offset).LineNumber;
|
||||
ResultEditor.ScrollToLine(line);
|
||||
QueueGutters();
|
||||
@@ -381,6 +366,20 @@ public partial class ConflictResolverView : Window
|
||||
public int EndOffset => Offset + Length;
|
||||
}
|
||||
|
||||
/// <summary>An editable conflict region in the result document, tracking which sides are
|
||||
/// currently included (in click order — <c>'o'</c> = ours/main, <c>'t'</c> = theirs/incoming).</summary>
|
||||
private sealed class ResultRegion
|
||||
{
|
||||
public ResultRegion(MergeConflictBlock block, TextAnchor start, TextAnchor end)
|
||||
{
|
||||
Block = block; Start = start; End = end;
|
||||
}
|
||||
public MergeConflictBlock Block { get; }
|
||||
public TextAnchor Start { get; }
|
||||
public TextAnchor End { get; }
|
||||
public List<char> Order { get; } = new();
|
||||
}
|
||||
|
||||
/// <summary>Paints each conflict block with the unresolved/resolved tint across a pane.</summary>
|
||||
private sealed class MergeBlockRenderer : IBackgroundRenderer
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user