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:
@@ -400,15 +400,16 @@
|
|||||||
"windowTitle": "Merge-Konflikte lösen",
|
"windowTitle": "Merge-Konflikte lösen",
|
||||||
"modalTitle": "KONFLIKTE LÖSEN",
|
"modalTitle": "KONFLIKTE LÖSEN",
|
||||||
"loading": "Konflikte werden geladen…",
|
"loading": "Konflikte werden geladen…",
|
||||||
"ours": "OURS · aktuell (Ziel-Branch)",
|
"ours": "MAIN · Ziel-Branch",
|
||||||
"result": "ERGEBNIS",
|
"result": "ERGEBNIS",
|
||||||
"theirs": "THEIRS · eingehend (Task)",
|
"theirs": "INCOMING · Task-Branch",
|
||||||
"binaryHint": "Binärdateien können hier nicht zusammengeführt werden — brich ab und löse sie in deinem Editor:",
|
"binaryHint": "Binärdateien können hier nicht zusammengeführt werden — brich ab und löse sie in deinem Editor:",
|
||||||
"prevConflict": "Vorheriger Konflikt (Umschalt+F8)",
|
"prevConflict": "Vorheriger Konflikt (Umschalt+F8)",
|
||||||
"nextConflict": "Nächster Konflikt (F8)",
|
"nextConflict": "Nächster Konflikt (F8)",
|
||||||
"acceptOurs": "Ours hinzufügen (Reihenfolge = Stapel)",
|
"acceptOurs": "Main hinzufügen",
|
||||||
"acceptTheirs": "Theirs hinzufügen (Reihenfolge = Stapel)",
|
"acceptTheirs": "Incoming hinzufügen",
|
||||||
"clearConflict": "Konflikt zurücksetzen (neu beginnen)",
|
"removeOurs": "Main entfernen",
|
||||||
|
"removeTheirs": "Incoming entfernen",
|
||||||
"continue": "Lösen & fortfahren",
|
"continue": "Lösen & fortfahren",
|
||||||
"abort": "Merge abbrechen"
|
"abort": "Merge abbrechen"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -400,15 +400,16 @@
|
|||||||
"windowTitle": "Resolve merge conflicts",
|
"windowTitle": "Resolve merge conflicts",
|
||||||
"modalTitle": "RESOLVE CONFLICTS",
|
"modalTitle": "RESOLVE CONFLICTS",
|
||||||
"loading": "Loading conflicts…",
|
"loading": "Loading conflicts…",
|
||||||
"ours": "OURS · current (merge target)",
|
"ours": "MAIN · merge target",
|
||||||
"result": "RESULT",
|
"result": "RESULT",
|
||||||
"theirs": "THEIRS · incoming (task)",
|
"theirs": "INCOMING · task branch",
|
||||||
"binaryHint": "Binary files can't be merged here — abort and resolve them in your editor:",
|
"binaryHint": "Binary files can't be merged here — abort and resolve them in your editor:",
|
||||||
"prevConflict": "Previous conflict (Shift+F8)",
|
"prevConflict": "Previous conflict (Shift+F8)",
|
||||||
"nextConflict": "Next conflict (F8)",
|
"nextConflict": "Next conflict (F8)",
|
||||||
"acceptOurs": "Add ours (stacks in click order)",
|
"acceptOurs": "Add main",
|
||||||
"acceptTheirs": "Add theirs (stacks in click order)",
|
"acceptTheirs": "Add incoming",
|
||||||
"clearConflict": "Clear this conflict (start over)",
|
"removeOurs": "Remove main",
|
||||||
|
"removeTheirs": "Remove incoming",
|
||||||
"continue": "Resolve & continue",
|
"continue": "Resolve & continue",
|
||||||
"abort": "Abort merge"
|
"abort": "Abort merge"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ Design/ — Tokens.axaml (design tokens; merged before styles) + IslandStyle
|
|||||||
- **NotesEditorViewModel** — day navigator + bullet CRUD for daily notes via `INotesApi`.
|
- **NotesEditorViewModel** — day navigator + bullet CRUD for daily notes via `INotesApi`.
|
||||||
- **Modal VMs** — `SettingsModalViewModel` (four tabs: General, Worktrees, Files prompt-paths, Prime Claude incl. `DailyPrepMaxTasks` + prime-schedule rows), `ListSettingsModalViewModel` (name, working dir, commit type, per-list Model/SystemPrompt/AgentPath/MaxTurns with inherited-badge + reset, delete list), `RepoImportModalViewModel` (bulk-create lists from git repos found under chosen parents; already-wired repos disabled), `WeeklyReportModalViewModel` (range pickers default "since last standup weekday → today", cached per range, markdown via MarkdownView), `MergeModalViewModel` (single-task merge form, called from the diff modal), `WorktreesOverviewModalViewModel` (global/per-list worktree rows, batch merge + state ops), `UnfinishedPlanningModalViewModel` (Resume/FinalizeNow/Discard for a draft planning session), `WorkerConnectionModalViewModel` (offline help), `AboutModalViewModel`.
|
- **Modal VMs** — `SettingsModalViewModel` (four tabs: General, Worktrees, Files prompt-paths, Prime Claude incl. `DailyPrepMaxTasks` + prime-schedule rows), `ListSettingsModalViewModel` (name, working dir, commit type, per-list Model/SystemPrompt/AgentPath/MaxTurns with inherited-badge + reset, delete list), `RepoImportModalViewModel` (bulk-create lists from git repos found under chosen parents; already-wired repos disabled), `WeeklyReportModalViewModel` (range pickers default "since last standup weekday → today", cached per range, markdown via MarkdownView), `MergeModalViewModel` (single-task merge form, called from the diff modal), `WorktreesOverviewModalViewModel` (global/per-list worktree rows, batch merge + state ops), `UnfinishedPlanningModalViewModel` (Resume/FinalizeNow/Discard for a draft planning session), `WorkerConnectionModalViewModel` (offline help), `AboutModalViewModel`.
|
||||||
- **Diff stack** — `UnifiedDiffParser` (static; parses `git diff` output into `DiffFileViewModel`s, detecting added/deleted/renamed/binary files and per-line numbers; `Flatten` injects file-header rows for a combined single-pane view). `DiffModalViewModel` has two modes: live worktree (branch diff vs base, with a Merge action) and commit-range `base..head` (`FromCommitRange = true` — shows a merged task's diff after its worktree is gone; no merge action). The view renders a file list with binary/empty placeholders via `DiffLinesView`.
|
- **Diff stack** — `UnifiedDiffParser` (static; parses `git diff` output into `DiffFileViewModel`s, detecting added/deleted/renamed/binary files and per-line numbers; `Flatten` injects file-header rows for a combined single-pane view). `DiffModalViewModel` has two modes: live worktree (branch diff vs base, with a Merge action) and commit-range `base..head` (`FromCommitRange = true` — shows a merged task's diff after its worktree is gone; no merge action). The view renders a file list with binary/empty placeholders via `DiffLinesView`.
|
||||||
- **Planning/Conflicts** — `PlanningDiffViewModel` (per-subtask diffs via `GetPlanningAggregateAsync`, toggle to combined integration-branch diff, conflict warnings), `ConflictResolverViewModel` (in-app **Rider-style 3-pane merge editor** for both single-task and planning unit-merge conflicts: single-task starts the conflict merge, parses each conflicted file into stable/conflict `MergeFileSegment`s via the worker's `GetMergeConflictDocuments`; exposes the active file's three reconstructed documents — `ActiveOursText` / `ActiveResultText` / `ActiveTheirsText` (from `MergeFile.OursText/ResultText/TheirsText`; Result seeds unresolved conflicts with Ours) — plus `ActiveFile`/`SelectFileCommand` (multi-file switcher), `Current`/`Next`/`Previous` (focused-conflict nav), a per-active-file `PositionText` readout, per-block `AcceptOurs/Theirs/Both/Base` + `MergeFile.Compose`, and `CanContinue` gated on every file resolved + no binary; writes each file via `WriteConflictResolution`, continue/abort; **planning mode** via `OpenForPlanningAsync(parentId, subtaskId)` loads the current subtask's mid-merge conflicts without re-starting the merge and routes continue/abort to `ContinuePlanningMerge`/`AbortPlanningMerge`, so a unit-merge conflict re-opens the editor per subtask via the `PlanningMergeConflict` broadcast). The view (`Views/Conflicts/ConflictResolverView`) shows the whole file in three **AvaloniaEdit** panes — Ours (read-only) | editable Result | Theirs (read-only) — with TextMate highlighting by extension (theme `StyleInclude` in `App.axaml`); a code-behind `IBackgroundRenderer` tints each conflict block (unresolved/resolved) across panes, an `IReadOnlySectionProvider` + `TextAnchor` regions keep only conflict spans editable in Result (edits flow back to the block); each unresolved conflict starts EMPTY (a thin marker bar) and the between-pane gutter controls **append** sides in click order — `›` adds ours, `‹` adds theirs (first pick on top, the next below), `✕` clears — so a conflict can take ours, theirs, both, or neither; the three panes share a proportional synced vertical scroll. Conflict block tints live in `Tokens.axaml` (`Merge*TintBrush`).
|
- **Planning/Conflicts** — `PlanningDiffViewModel` (per-subtask diffs via `GetPlanningAggregateAsync`, toggle to combined integration-branch diff, conflict warnings), `ConflictResolverViewModel` (in-app **Rider-style 3-pane merge editor** for both single-task and planning unit-merge conflicts: single-task starts the conflict merge, parses each conflicted file into stable/conflict `MergeFileSegment`s via the worker's `GetMergeConflictDocuments`; exposes the active file's three reconstructed documents — `ActiveOursText` / `ActiveResultText` / `ActiveTheirsText` (from `MergeFile.OursText/ResultText/TheirsText`; Result seeds unresolved conflicts with Ours) — plus `ActiveFile`/`SelectFileCommand` (multi-file switcher), `Current`/`Next`/`Previous` (focused-conflict nav), a per-active-file `PositionText` readout, per-block `AcceptOurs/Theirs/Both/Base` + `MergeFile.Compose`, and `CanContinue` gated on every file resolved + no binary; writes each file via `WriteConflictResolution`, continue/abort; **planning mode** via `OpenForPlanningAsync(parentId, subtaskId)` loads the current subtask's mid-merge conflicts without re-starting the merge and routes continue/abort to `ContinuePlanningMerge`/`AbortPlanningMerge`, so a unit-merge conflict re-opens the editor per subtask via the `PlanningMergeConflict` broadcast). The view (`Views/Conflicts/ConflictResolverView`) shows the whole file in three **AvaloniaEdit** panes — MAIN/ours (read-only) | editable Result | INCOMING/theirs (read-only) — with TextMate highlighting by extension (theme `StyleInclude` in `App.axaml`); a code-behind `IBackgroundRenderer` tints each conflict block (unresolved/resolved) across panes, an `IReadOnlySectionProvider` + `TextAnchor` regions keep only conflict spans editable in Result (edits flow back to the block); each unresolved conflict starts EMPTY (a thin marker bar); the between-pane gutter controls **toggle** each side in/out of the result — `›`/`‹` add MAIN/INCOMING in click order (first pick on top), clicking again removes that side — so a conflict can take main, incoming, both, or neither; a `FilesSummary` readout shows how many files still have conflicts, and the three panes share a proportional synced vertical scroll. Conflict block tints live in `Tokens.axaml` (`Merge*TintBrush`).
|
||||||
|
|
||||||
## Services
|
## Services
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,21 @@ public sealed partial class ConflictResolverViewModel : ObservableObject
|
|||||||
public IReadOnlyList<string> BinaryFilePaths => Files.Where(f => f.IsBinary).Select(f => f.Path).ToList();
|
public IReadOnlyList<string> BinaryFilePaths => Files.Where(f => f.IsBinary).Select(f => f.Path).ToList();
|
||||||
public bool HasBinaryFiles => Files.Any(f => f.IsBinary);
|
public bool HasBinaryFiles => Files.Any(f => f.IsBinary);
|
||||||
|
|
||||||
|
public bool HasMultipleFiles => Files.Count > 1;
|
||||||
|
|
||||||
|
/// <summary>Cross-file progress shown in the editor: how many files still have unresolved
|
||||||
|
/// (or binary) conflicts, so you can see how many more need attention.</summary>
|
||||||
|
public string FilesSummary
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var total = Files.Count;
|
||||||
|
if (total == 0) return "";
|
||||||
|
var unresolved = Files.Count(f => !f.AllResolved);
|
||||||
|
return unresolved == 0 ? $"All {total} files resolved" : $"{unresolved} of {total} files unresolved";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string ContinueHint => HasBinaryFiles
|
public string ContinueHint => HasBinaryFiles
|
||||||
? "Binary conflicts must be resolved externally — abort and resolve in your editor."
|
? "Binary conflicts must be resolved externally — abort and resolve in your editor."
|
||||||
: "";
|
: "";
|
||||||
@@ -171,6 +186,8 @@ public sealed partial class ConflictResolverViewModel : ObservableObject
|
|||||||
OnPropertyChanged(nameof(TotalConflicts));
|
OnPropertyChanged(nameof(TotalConflicts));
|
||||||
OnPropertyChanged(nameof(BinaryFilePaths));
|
OnPropertyChanged(nameof(BinaryFilePaths));
|
||||||
OnPropertyChanged(nameof(HasBinaryFiles));
|
OnPropertyChanged(nameof(HasBinaryFiles));
|
||||||
|
OnPropertyChanged(nameof(HasMultipleFiles));
|
||||||
|
OnPropertyChanged(nameof(FilesSummary));
|
||||||
RecomputeCanContinue();
|
RecomputeCanContinue();
|
||||||
if (_flat.Count > 0)
|
if (_flat.Count > 0)
|
||||||
MoveTo(0); // also sets ActiveFile via MoveTo
|
MoveTo(0); // also sets ActiveFile via MoveTo
|
||||||
@@ -193,6 +210,7 @@ public sealed partial class ConflictResolverViewModel : ObservableObject
|
|||||||
OnPropertyChanged(nameof(ResolvedCount));
|
OnPropertyChanged(nameof(ResolvedCount));
|
||||||
OnPropertyChanged(nameof(PositionText));
|
OnPropertyChanged(nameof(PositionText));
|
||||||
OnPropertyChanged(nameof(ActiveResultText));
|
OnPropertyChanged(nameof(ActiveResultText));
|
||||||
|
OnPropertyChanged(nameof(FilesSummary));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Toolbar: change nav · file switcher · readout -->
|
<!-- 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}">
|
IsVisible="{Binding HasCurrent}">
|
||||||
<Button Grid.Column="0" Classes="btn" Content="↑" Margin="0,0,4,0" Padding="10,4"
|
<Button Grid.Column="0" Classes="btn" Content="↑" Margin="0,0,4,0" Padding="10,4"
|
||||||
ToolTip.Tip="{loc:Tr conflictResolver.prevConflict}"
|
ToolTip.Tip="{loc:Tr conflictResolver.prevConflict}"
|
||||||
@@ -121,7 +121,11 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ComboBox.ItemTemplate>
|
</ComboBox.ItemTemplate>
|
||||||
</ComboBox>
|
</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}"
|
Foreground="{DynamicResource TextDimBrush}"
|
||||||
Text="{Binding PositionText}"/>
|
Text="{Binding PositionText}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public partial class ConflictResolverView : Window
|
|||||||
private List<(int Offset, int Length, MergeConflictBlock Block)> _theirsSpans = new();
|
private List<(int Offset, int Length, MergeConflictBlock Block)> _theirsSpans = new();
|
||||||
|
|
||||||
// Live, edit-tracked conflict regions in the editable result document.
|
// 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 readonly List<MergeConflictBlock> _hookedBlocks = new();
|
||||||
|
|
||||||
private ScrollViewer?[] _scrollViewers = Array.Empty<ScrollViewer?>();
|
private ScrollViewer?[] _scrollViewers = Array.Empty<ScrollViewer?>();
|
||||||
@@ -143,7 +143,7 @@ public partial class ConflictResolverView : Window
|
|||||||
start.MovementType = AnchorMovementType.BeforeInsertion;
|
start.MovementType = AnchorMovementType.BeforeInsertion;
|
||||||
var end = doc.CreateAnchor(offset + length);
|
var end = doc.CreateAnchor(offset + length);
|
||||||
end.MovementType = AnchorMovementType.AfterInsertion;
|
end.MovementType = AnchorMovementType.AfterInsertion;
|
||||||
_resultRegions.Add((block, start, end));
|
_resultRegions.Add(new ResultRegion(block, start, end));
|
||||||
block.PropertyChanged += OnBlockChanged;
|
block.PropertyChanged += OnBlockChanged;
|
||||||
_hookedBlocks.Add(block);
|
_hookedBlocks.Add(block);
|
||||||
}
|
}
|
||||||
@@ -202,45 +202,32 @@ public partial class ConflictResolverView : Window
|
|||||||
private void OnResultDocumentChanged(object? sender, DocumentChangeEventArgs e)
|
private void OnResultDocumentChanged(object? sender, DocumentChangeEventArgs e)
|
||||||
{
|
{
|
||||||
if (_rebuilding || _applyingAccept) return;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QueueGutters();
|
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);
|
// Each side can be included at most once. Clicking adds it (in click order, first on
|
||||||
private void AppendTheirs(MergeConflictBlock block) => AppendSide(block, block.Theirs);
|
// top); clicking again removes it. The region content is rebuilt from the included set.
|
||||||
|
private void ToggleSide(ResultRegion region, char side)
|
||||||
// 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.Order.Contains(side)) region.Order.Remove(side);
|
||||||
if (region.Block is null) return;
|
else region.Order.Add(side);
|
||||||
_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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset a conflict back to empty/unresolved (start over).
|
var text = string.Concat(region.Order.Select(c => c == 'o' ? region.Block.Ours : region.Block.Theirs));
|
||||||
private void ClearRegion(MergeConflictBlock block)
|
|
||||||
{
|
|
||||||
var region = _resultRegions.FirstOrDefault(r => ReferenceEquals(r.Block, block));
|
|
||||||
if (region.Block is null) return;
|
|
||||||
_applyingAccept = true;
|
_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; }
|
finally { _applyingAccept = false; }
|
||||||
block.Resolution = null;
|
|
||||||
|
region.Block.Resolution = region.Order.Count == 0 ? null : text;
|
||||||
InvalidateRenderers();
|
InvalidateRenderers();
|
||||||
PositionGutters();
|
PositionGutters();
|
||||||
}
|
}
|
||||||
@@ -273,32 +260,30 @@ public partial class ConflictResolverView : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
var doc = ResultEditor.Document;
|
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.
|
// Controls stay visible whether or not a side is included, so either can be toggled.
|
||||||
var len = end.Offset - start.Offset;
|
var len = region.End.Offset - region.Start.Offset;
|
||||||
ISegment probe = len > 0
|
ISegment probe = len > 0
|
||||||
? new Seg(start.Offset, len)
|
? new Seg(region.Start.Offset, len)
|
||||||
: new Seg(start.Offset, start.Offset < doc.TextLength ? 1 : 0);
|
: new Seg(region.Start.Offset, region.Start.Offset < doc.TextLength ? 1 : 0);
|
||||||
var rects = BackgroundGeometryBuilder.GetRectsForSegment(tv, probe).ToList();
|
var rects = BackgroundGeometryBuilder.GetRectsForSegment(tv, probe).ToList();
|
||||||
if (rects.Count == 0) continue;
|
if (rects.Count == 0) continue;
|
||||||
var y = rects[0].Top;
|
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 &&
|
if (tv.TranslatePoint(new Point(0, y), LeftGutter) is { } pl &&
|
||||||
pl.Y > -24 && pl.Y < LeftGutter.Bounds.Height + 24)
|
pl.Y > -24 && pl.Y < LeftGutter.Bounds.Height + 24)
|
||||||
{
|
AddAcceptButton(LeftGutter, pl.Y, oursIn ? "−" : "›", () => ToggleSide(r, 'o'),
|
||||||
AddAcceptButton(LeftGutter, pl.Y, "›", () => AppendOurs(capturedBlock),
|
Tr(oursIn ? "conflictResolver.removeOurs" : "conflictResolver.acceptOurs"));
|
||||||
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"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tv.TranslatePoint(new Point(0, y), RightGutter) is { } pr &&
|
if (tv.TranslatePoint(new Point(0, y), RightGutter) is { } pr &&
|
||||||
pr.Y > -24 && pr.Y < RightGutter.Bounds.Height + 24)
|
pr.Y > -24 && pr.Y < RightGutter.Bounds.Height + 24)
|
||||||
AddAcceptButton(RightGutter, pr.Y, "‹", () => AppendTheirs(capturedBlock),
|
AddAcceptButton(RightGutter, pr.Y, theirsIn ? "−" : "‹", () => ToggleSide(r, 't'),
|
||||||
Tr("conflictResolver.acceptTheirs"));
|
Tr(theirsIn ? "conflictResolver.removeTheirs" : "conflictResolver.acceptTheirs"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +329,7 @@ public partial class ConflictResolverView : Window
|
|||||||
{
|
{
|
||||||
if (_vm?.Current is not { } block) return;
|
if (_vm?.Current is not { } block) return;
|
||||||
var region = _resultRegions.FirstOrDefault(r => ReferenceEquals(r.Block, block));
|
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;
|
var line = ResultEditor.Document.GetLineByOffset(region.Start.Offset).LineNumber;
|
||||||
ResultEditor.ScrollToLine(line);
|
ResultEditor.ScrollToLine(line);
|
||||||
QueueGutters();
|
QueueGutters();
|
||||||
@@ -381,6 +366,20 @@ public partial class ConflictResolverView : Window
|
|||||||
public int EndOffset => Offset + Length;
|
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>
|
/// <summary>Paints each conflict block with the unresolved/resolved tint across a pane.</summary>
|
||||||
private sealed class MergeBlockRenderer : IBackgroundRenderer
|
private sealed class MergeBlockRenderer : IBackgroundRenderer
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user