using System.Collections.Generic; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace ClaudeDo.Ui.ViewModels.Conflicts; /// /// One conflict region in a file: the two competing versions (and the merge base when the /// merge used diff3 style), plus the chosen (null until resolved). /// 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 ?? ""; } /// An ordered piece of a conflicted file: either stable common text or a conflict block. 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); } /// A conflicted file: its ordered segments (for reassembly) and just its conflict blocks. public sealed class MergeFile { public string Path { get; } public bool IsBinary { get; } public IReadOnlyList Segments { get; } public IReadOnlyList Conflicts { get; } public MergeFile(string path, bool isBinary, IReadOnlyList segments) { Path = path; IsBinary = isBinary; Segments = segments; Conflicts = segments.Where(s => s.IsConflict).Select(s => s.Conflict!).ToList(); } /// A binary file can't be resolved in-app; a text file is done once every block is resolved. public bool AllResolved => !IsBinary && Conflicts.All(c => c.IsResolved); /// 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 /// so an unresolved conflict never actually reaches here). public string Compose() => string.Concat( Segments.Select(s => s.IsConflict ? (s.Conflict!.Resolution ?? "") : s.StableText)); /// Left pane document: stable regions verbatim, conflict regions show Ours text. public string OursText => string.Concat( Segments.Select(s => s.IsConflict ? s.Conflict!.Ours : s.StableText)); /// Right pane document: stable regions verbatim, conflict regions show Theirs text. public string TheirsText => string.Concat( Segments.Select(s => s.IsConflict ? s.Conflict!.Theirs : s.StableText)); /// 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). public string ResultText => string.Concat( Segments.Select(s => s.IsConflict ? (s.Conflict!.Resolution ?? "") : s.StableText)); }