fix(merge): unresolved conflicts compose to empty, not Ours (+ review nits)
Code-review follow-ups before push:
- MergeFile.ResultText/Compose() fell back to Ours for unresolved conflicts while
the editor seeds them empty — align both on empty so the public model matches the
pane and Continue can't silently auto-accept Ours.
- Bound the gutter re-layout retry (was an unbounded Background re-post when the
editor isn't laid out, e.g. minimized).
- Pluralize the readout ('1 conflict' not '1 conflicts'). Tests updated. Ui 128 green.
This commit is contained in:
@@ -72,9 +72,11 @@ public sealed class MergeFile
|
|||||||
/// <summary>A binary file can't be resolved in-app; a text file is done once every block is resolved.</summary>
|
/// <summary>A binary file can't be resolved in-app; a text file is done once every block is resolved.</summary>
|
||||||
public bool AllResolved => !IsBinary && Conflicts.All(c => c.IsResolved);
|
public bool AllResolved => !IsBinary && Conflicts.All(c => c.IsResolved);
|
||||||
|
|
||||||
/// <summary>Reassemble the file: stable text verbatim, each conflict replaced by its resolution.</summary>
|
/// <summary>Reassemble the file: stable text verbatim, each conflict replaced by its resolution
|
||||||
|
/// (empty when unresolved — the same "empty start" the editor shows; Continue is gated on
|
||||||
|
/// <see cref="AllResolved"/> so an unresolved conflict never actually reaches here).</summary>
|
||||||
public string Compose() => string.Concat(
|
public string Compose() => string.Concat(
|
||||||
Segments.Select(s => s.IsConflict ? (s.Conflict!.Resolution ?? s.Conflict.Ours) : s.StableText));
|
Segments.Select(s => s.IsConflict ? (s.Conflict!.Resolution ?? "") : s.StableText));
|
||||||
|
|
||||||
/// <summary>Left pane document: stable regions verbatim, conflict regions show Ours text.</summary>
|
/// <summary>Left pane document: stable regions verbatim, conflict regions show Ours text.</summary>
|
||||||
public string OursText => string.Concat(
|
public string OursText => string.Concat(
|
||||||
@@ -84,7 +86,8 @@ public sealed class MergeFile
|
|||||||
public string TheirsText => string.Concat(
|
public string TheirsText => string.Concat(
|
||||||
Segments.Select(s => s.IsConflict ? s.Conflict!.Theirs : s.StableText));
|
Segments.Select(s => s.IsConflict ? s.Conflict!.Theirs : s.StableText));
|
||||||
|
|
||||||
/// <summary>Middle (result) pane document: stable regions verbatim, conflict regions show Resolution if set, else Ours.</summary>
|
/// <summary>Middle (result) pane document: stable regions verbatim, conflict regions show the
|
||||||
|
/// chosen Resolution, or empty when unresolved (the editor builds each conflict up from empty).</summary>
|
||||||
public string ResultText => string.Concat(
|
public string ResultText => string.Concat(
|
||||||
Segments.Select(s => s.IsConflict ? (s.Conflict!.Resolution ?? s.Conflict.Ours) : s.StableText));
|
Segments.Select(s => s.IsConflict ? (s.Conflict!.Resolution ?? "") : s.StableText));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ public sealed partial class ConflictResolverViewModel : ObservableObject
|
|||||||
if (ActiveFile is null || ActiveFile.Conflicts.Count == 0) return "No text conflicts";
|
if (ActiveFile is null || ActiveFile.Conflicts.Count == 0) return "No text conflicts";
|
||||||
var count = ActiveFile.Conflicts.Count;
|
var count = ActiveFile.Conflicts.Count;
|
||||||
var resolved = ActiveFile.Conflicts.Count(c => c.IsResolved);
|
var resolved = ActiveFile.Conflicts.Count(c => c.IsResolved);
|
||||||
return $"{count} conflicts · {resolved} resolved";
|
return $"{count} {(count == 1 ? "conflict" : "conflicts")} · {resolved} resolved";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ public partial class ConflictResolverView : Window
|
|||||||
private bool _applyingAccept;
|
private bool _applyingAccept;
|
||||||
private bool _syncing;
|
private bool _syncing;
|
||||||
private bool _gutterPending;
|
private bool _gutterPending;
|
||||||
|
private int _gutterRetries;
|
||||||
|
|
||||||
public ConflictResolverView()
|
public ConflictResolverView()
|
||||||
{
|
{
|
||||||
@@ -109,6 +110,7 @@ public partial class ConflictResolverView : Window
|
|||||||
{
|
{
|
||||||
if (_vm is null) return;
|
if (_vm is null) return;
|
||||||
_rebuilding = true;
|
_rebuilding = true;
|
||||||
|
_gutterRetries = 0; // fresh retry budget for this file's gutter layout
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ClearGutters();
|
ClearGutters();
|
||||||
@@ -257,9 +259,12 @@ public partial class ConflictResolverView : Window
|
|||||||
var tv = ResultEditor.TextArea.TextView;
|
var tv = ResultEditor.TextArea.TextView;
|
||||||
if (!tv.VisualLinesValid)
|
if (!tv.VisualLinesValid)
|
||||||
{
|
{
|
||||||
QueueGutters();
|
// Retry until the editor is laid out, but bounded so a never-laid-out editor
|
||||||
|
// (e.g. minimized window) can't busy-loop the dispatcher.
|
||||||
|
if (_gutterRetries++ < 40) QueueGutters();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_gutterRetries = 0;
|
||||||
|
|
||||||
var doc = ResultEditor.Document;
|
var doc = ResultEditor.Document;
|
||||||
foreach (var region in _resultRegions)
|
foreach (var region in _resultRegions)
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ public class ConflictResolverViewModelTests
|
|||||||
|
|
||||||
Assert.Equal("a\no\nz\n", file.OursText);
|
Assert.Equal("a\no\nz\n", file.OursText);
|
||||||
Assert.Equal("a\nt\nz\n", file.TheirsText);
|
Assert.Equal("a\nt\nz\n", file.TheirsText);
|
||||||
Assert.Equal("a\no\nz\n", file.ResultText); // unresolved seeds Ours
|
Assert.Equal("a\nz\n", file.ResultText); // unresolved conflicts start empty
|
||||||
|
|
||||||
// After resolving: ResultText reflects the resolution
|
// After resolving: ResultText reflects the resolution
|
||||||
file.Conflicts[0].Resolution = "r\n";
|
file.Conflicts[0].Resolution = "r\n";
|
||||||
@@ -286,7 +286,7 @@ public class ConflictResolverViewModelTests
|
|||||||
// Switch to file B
|
// Switch to file B
|
||||||
vm.SelectFileCommand.Execute(vm.Files[1]);
|
vm.SelectFileCommand.Execute(vm.Files[1]);
|
||||||
Assert.Equal("b.cs", vm.ActiveFile!.Path);
|
Assert.Equal("b.cs", vm.ActiveFile!.Path);
|
||||||
Assert.Equal("ours-b\n", vm.ActiveResultText); // unresolved seeds Ours
|
Assert.Equal("", vm.ActiveResultText); // unresolved conflicts start empty
|
||||||
|
|
||||||
// Switch back to file A
|
// Switch back to file A
|
||||||
vm.SelectFileCommand.Execute(vm.Files[0]);
|
vm.SelectFileCommand.Execute(vm.Files[0]);
|
||||||
@@ -303,11 +303,11 @@ public class ConflictResolverViewModelTests
|
|||||||
await vm.OpenAsync("main");
|
await vm.OpenAsync("main");
|
||||||
|
|
||||||
// 1 conflict, 0 resolved
|
// 1 conflict, 0 resolved
|
||||||
Assert.Equal("1 conflicts · 0 resolved", vm.PositionText);
|
Assert.Equal("1 conflict · 0 resolved", vm.PositionText);
|
||||||
|
|
||||||
vm.Current!.AcceptOursCommand.Execute(null);
|
vm.Current!.AcceptOursCommand.Execute(null);
|
||||||
|
|
||||||
// 1 conflict, 1 resolved
|
// 1 conflict, 1 resolved
|
||||||
Assert.Equal("1 conflicts · 1 resolved", vm.PositionText);
|
Assert.Equal("1 conflict · 1 resolved", vm.PositionText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user