Show renames as old → new in the file header and suppress the misleading "+0 −0" stats for a pure rename (the R badge already conveys it).
140 lines
5.0 KiB
C#
140 lines
5.0 KiB
C#
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;
|
||
|
||
/// 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 <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, 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<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;
|
||
}
|
||
}
|