using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; namespace ClaudeDo.Ui.ViewModels.Modals; // Shared diff models used by UnifiedDiffParser, DiffLinesView and DiffViewerViewModel. public enum DiffLineKind { Add, Del, Ctx, File } public enum DiffFileStatus { Modified, Added, Deleted, Renamed } public sealed class DiffLineViewModel { public required DiffLineKind Kind { get; init; } public int? OldNo { get; init; } public int? NewNo { get; init; } public required string Text { get; init; } public string ClassName => Kind switch { DiffLineKind.Add => "add", DiffLineKind.Del => "del", DiffLineKind.File => "file", _ => "ctx", }; public string Sign => Kind switch { DiffLineKind.Add => "+", DiffLineKind.Del => "-", _ => " ", }; } public sealed class DiffFileViewModel { public required string Path { get; set; } public string? OldPath { get; set; } public DiffFileStatus Status { get; set; } = DiffFileStatus.Modified; public bool IsBinary { get; set; } public int Additions { get; set; } public int Deletions { get; set; } public ObservableCollection Lines { get; } = new(); /// Single-letter badge for the file's change kind (A/M/D/R). public string StatusCode => Status switch { DiffFileStatus.Added => "A", DiffFileStatus.Deleted => "D", DiffFileStatus.Renamed => "R", _ => "M", }; public bool HasLines => Lines.Count > 0; /// A text file that produced no diff hunks (e.g. a newly added empty file). public bool IsEmptyContent => !IsBinary && Lines.Count == 0; /// A rename with no content change — showing "+0 −0" for it is misleading. public bool IsPureRename => Status == DiffFileStatus.Renamed && Additions == 0 && Deletions == 0; /// Header path label: for a rename, spell out old → new; otherwise just the path. public string HeaderPath => Status == DiffFileStatus.Renamed && !string.IsNullOrEmpty(OldPath) ? $"{OldPath} → {Path}" : Path; } /// One row in the planning subtask list (left pane in Planning mode). public sealed record SubtaskDiffRow(string Id, string Title, string? DiffStat, string UnifiedDiff); /// Folder/file node for the file-tree nav (left pane in Files mode). File leaves carry /// their parsed so selection swaps the right pane with no /// further git calls. public sealed partial class DiffTreeNodeViewModel : ViewModelBase { public required string Name { get; init; } public bool IsDirectory { get; init; } public string RelativePath { get; init; } = ""; public DiffFileViewModel? File { get; init; } public ObservableCollection Children { get; } = new(); [ObservableProperty] private bool _isExpanded = true; public string? StatusCode => File?.StatusCode; public bool ShowStats => File is { IsBinary: false, IsPureRename: false }; public int Additions => File?.Additions ?? 0; public int Deletions => File?.Deletions ?? 0; } /// Builds a folder-grouped tree from a flat list of parsed diff files. public static class DiffTree { public static List Build(IEnumerable files) { var roots = new List(); var dirs = new Dictionary(StringComparer.Ordinal); foreach (var file in files) { var path = file.Path.Replace('\\', '/'); var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries); if (segments.Length == 0) continue; DiffTreeNodeViewModel? parent = null; var accumulated = ""; for (var i = 0; i < segments.Length - 1; i++) { accumulated = accumulated.Length == 0 ? segments[i] : accumulated + "/" + segments[i]; if (!dirs.TryGetValue(accumulated, out var dir)) { dir = new DiffTreeNodeViewModel { Name = segments[i], IsDirectory = true, RelativePath = accumulated }; dirs[accumulated] = dir; if (parent is null) roots.Add(dir); else parent.Children.Add(dir); } parent = dir; } var leaf = new DiffTreeNodeViewModel { Name = segments[^1], IsDirectory = false, RelativePath = path, File = file, }; if (parent is null) roots.Add(leaf); else parent.Children.Add(leaf); } return roots; } public static DiffTreeNodeViewModel? FirstLeaf(IEnumerable nodes) { foreach (var n in nodes) { if (!n.IsDirectory) return n; var nested = FirstLeaf(n.Children); if (nested is not null) return nested; } return null; } }