refactor(diff): single DiffViewer replaces DiffModal + WorktreeModal + PlanningDiff
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
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<DiffLineViewModel> 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;
|
||||
}
|
||||
|
||||
/// 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 <see cref="DiffFileViewModel"/> 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<DiffTreeNodeViewModel> Children { get; } = new();
|
||||
[ObservableProperty] private bool _isExpanded = true;
|
||||
|
||||
public string? StatusCode => File?.StatusCode;
|
||||
public bool ShowStats => File is { IsBinary: 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<DiffTreeNodeViewModel> Build(IEnumerable<DiffFileViewModel> files)
|
||||
{
|
||||
var roots = new List<DiffTreeNodeViewModel>();
|
||||
var dirs = new Dictionary<string, DiffTreeNodeViewModel>(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<DiffTreeNodeViewModel> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user