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.
94 lines
3.9 KiB
C#
94 lines
3.9 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
|
|
namespace ClaudeDo.Ui.ViewModels.Conflicts;
|
|
|
|
/// <summary>
|
|
/// One conflict region in a file: the two competing versions (and the merge base when the
|
|
/// merge used diff3 style), plus the chosen <see cref="Resolution"/> (null until resolved).
|
|
/// </summary>
|
|
public sealed partial class MergeConflictBlock : ObservableObject
|
|
{
|
|
public string Ours { get; }
|
|
public string? Base { get; }
|
|
public string Theirs { get; }
|
|
|
|
[ObservableProperty] private string? _resolution;
|
|
|
|
public bool IsResolved => Resolution is not null;
|
|
public bool HasBase => Base is not null;
|
|
|
|
public MergeConflictBlock(string ours, string? @base, string theirs)
|
|
{
|
|
Ours = ours;
|
|
Base = @base;
|
|
Theirs = theirs;
|
|
}
|
|
|
|
partial void OnResolutionChanged(string? value) => OnPropertyChanged(nameof(IsResolved));
|
|
|
|
[RelayCommand] private void AcceptOurs() => Resolution = Ours;
|
|
[RelayCommand] private void AcceptTheirs() => Resolution = Theirs;
|
|
[RelayCommand] private void AcceptBoth() => Resolution = Ours + Theirs;
|
|
[RelayCommand] private void AcceptBase() => Resolution = Base ?? "";
|
|
}
|
|
|
|
/// <summary>An ordered piece of a conflicted file: either stable common text or a conflict block.</summary>
|
|
public sealed class MergeFileSegment
|
|
{
|
|
public bool IsConflict { get; }
|
|
public string StableText { get; }
|
|
public MergeConflictBlock? Conflict { get; }
|
|
|
|
private MergeFileSegment(bool isConflict, string stableText, MergeConflictBlock? conflict)
|
|
{
|
|
IsConflict = isConflict;
|
|
StableText = stableText;
|
|
Conflict = conflict;
|
|
}
|
|
|
|
public static MergeFileSegment Stable(string text) => new(false, text, null);
|
|
public static MergeFileSegment FromConflict(MergeConflictBlock block) => new(true, "", block);
|
|
}
|
|
|
|
/// <summary>A conflicted file: its ordered segments (for reassembly) and just its conflict blocks.</summary>
|
|
public sealed class MergeFile
|
|
{
|
|
public string Path { get; }
|
|
public bool IsBinary { get; }
|
|
public IReadOnlyList<MergeFileSegment> Segments { get; }
|
|
public IReadOnlyList<MergeConflictBlock> Conflicts { get; }
|
|
|
|
public MergeFile(string path, bool isBinary, IReadOnlyList<MergeFileSegment> segments)
|
|
{
|
|
Path = path;
|
|
IsBinary = isBinary;
|
|
Segments = segments;
|
|
Conflicts = segments.Where(s => s.IsConflict).Select(s => s.Conflict!).ToList();
|
|
}
|
|
|
|
/// <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);
|
|
|
|
/// <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(
|
|
Segments.Select(s => s.IsConflict ? (s.Conflict!.Resolution ?? "") : s.StableText));
|
|
|
|
/// <summary>Left pane document: stable regions verbatim, conflict regions show Ours text.</summary>
|
|
public string OursText => string.Concat(
|
|
Segments.Select(s => s.IsConflict ? s.Conflict!.Ours : s.StableText));
|
|
|
|
/// <summary>Right pane document: stable regions verbatim, conflict regions show Theirs text.</summary>
|
|
public string TheirsText => string.Concat(
|
|
Segments.Select(s => s.IsConflict ? s.Conflict!.Theirs : s.StableText));
|
|
|
|
/// <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(
|
|
Segments.Select(s => s.IsConflict ? (s.Conflict!.Resolution ?? "") : s.StableText));
|
|
}
|