Files
ClaudeDo/src/ClaudeDo.Ui/ViewModels/Modals/DiffModels.cs
T
mika kuns b9b3053051 fix(ui): clearer rename display in diff viewer
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).
2026-07-24 11:21:20 +02:00

140 lines
5.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}