feat(diff): align parsed diff lines into side-by-side rows

This commit is contained in:
mika kuns
2026-08-07 09:13:59 +02:00
parent 6e9a7cea87
commit 3cd0f0879e
2 changed files with 299 additions and 0 deletions
@@ -0,0 +1,167 @@
using ClaudeDo.Ui.ViewModels.Modals;
using Xunit;
namespace ClaudeDo.Ui.Tests.ViewModels;
public class DiffAlignmentTests
{
private static DiffLineViewModel Ctx(int oldNo, int newNo, string text) =>
new() { Kind = DiffLineKind.Ctx, OldNo = oldNo, NewNo = newNo, Text = text };
private static DiffLineViewModel Del(int oldNo, string text) =>
new() { Kind = DiffLineKind.Del, OldNo = oldNo, Text = text };
private static DiffLineViewModel Add(int newNo, string text) =>
new() { Kind = DiffLineKind.Add, NewNo = newNo, Text = text };
[Fact]
public void NullOrEmpty_YieldsEmptyDiff()
{
Assert.Empty(DiffAlignment.Build(null).SplitRows);
Assert.Empty(DiffAlignment.Build(Array.Empty<DiffLineViewModel>()).SplitRows);
}
[Fact]
public void ContextOnly_MirrorsBothSides_WithNoFillers()
{
var result = DiffAlignment.Build(new[] { Ctx(1, 1, "a"), Ctx(2, 2, "b") });
Assert.Equal(2, result.SplitRows.Count);
Assert.All(result.SplitRows, r =>
{
Assert.Equal(AlignedSide.Ctx, r.LeftKind);
Assert.Equal(AlignedSide.Ctx, r.RightKind);
Assert.Equal(r.LeftText, r.RightText);
});
Assert.Equal("a\nb", result.LeftText);
Assert.Equal("a\nb", result.RightText);
}
[Fact]
public void EqualSizedChangeBlock_PairsOneToOne()
{
var result = DiffAlignment.Build(new[]
{
Del(1, "old one"), Del(2, "old two"),
Add(1, "new one"), Add(2, "new two"),
});
Assert.Equal(2, result.SplitRows.Count);
Assert.Equal(AlignedSide.Del, result.SplitRows[0].LeftKind);
Assert.Equal(AlignedSide.Add, result.SplitRows[0].RightKind);
Assert.Equal("old one", result.SplitRows[0].LeftText);
Assert.Equal("new one", result.SplitRows[0].RightText);
Assert.Equal(1, result.SplitRows[0].OldNo!.Value);
Assert.Equal(1, result.SplitRows[0].NewNo!.Value);
}
[Fact]
public void MoreAddsThanDels_PadsTheLeftSideWithFillers()
{
var result = DiffAlignment.Build(new[]
{
Del(1, "d1"),
Add(1, "a1"), Add(2, "a2"), Add(3, "a3"),
});
Assert.Equal(3, result.SplitRows.Count);
Assert.Equal(AlignedSide.Del, result.SplitRows[0].LeftKind);
Assert.Equal(AlignedSide.Filler, result.SplitRows[1].LeftKind);
Assert.Equal(AlignedSide.Filler, result.SplitRows[2].LeftKind);
Assert.Equal("", result.SplitRows[1].LeftText);
Assert.Null(result.SplitRows[1].OldNo);
Assert.All(result.SplitRows, r => Assert.Equal(AlignedSide.Add, r.RightKind));
}
[Fact]
public void MoreDelsThanAdds_PadsTheRightSideWithFillers()
{
var result = DiffAlignment.Build(new[]
{
Del(1, "d1"), Del(2, "d2"), Del(3, "d3"),
Add(1, "a1"),
});
Assert.Equal(3, result.SplitRows.Count);
Assert.Equal(AlignedSide.Add, result.SplitRows[0].RightKind);
Assert.Equal(AlignedSide.Filler, result.SplitRows[1].RightKind);
Assert.Equal(AlignedSide.Filler, result.SplitRows[2].RightKind);
Assert.All(result.SplitRows, r => Assert.Equal(AlignedSide.Del, r.LeftKind));
}
[Fact]
public void NonContiguousLineNumbers_InsertOneGapRow()
{
var result = DiffAlignment.Build(new[] { Ctx(1, 1, "a"), Ctx(40, 40, "b") });
Assert.Equal(3, result.SplitRows.Count);
Assert.Equal(AlignedSide.Gap, result.SplitRows[1].LeftKind);
Assert.Equal(AlignedSide.Gap, result.SplitRows[1].RightKind);
Assert.Null(result.SplitRows[1].OldNo);
Assert.Null(result.SplitRows[1].NewNo);
}
[Fact]
public void FirstLineNeverProducesALeadingGap()
{
var result = DiffAlignment.Build(new[] { Ctx(120, 118, "a") });
Assert.Single(result.SplitRows);
Assert.Equal(AlignedSide.Ctx, result.SplitRows[0].LeftKind);
}
[Fact]
public void UnifiedRows_KeepGitOrder_DeletionsThenAdditions()
{
var result = DiffAlignment.Build(new[]
{
Ctx(1, 1, "keep"),
Del(2, "d1"), Del(3, "d2"),
Add(2, "a1"),
});
Assert.Equal(4, result.UnifiedRows.Count);
Assert.Equal(AlignedSide.Ctx, result.UnifiedRows[0].Kind);
Assert.Equal(AlignedSide.Del, result.UnifiedRows[1].Kind);
Assert.Equal(AlignedSide.Del, result.UnifiedRows[2].Kind);
Assert.Equal(AlignedSide.Add, result.UnifiedRows[3].Kind);
Assert.Equal("keep\nd1\nd2\na1", result.UnifiedText);
}
[Fact]
public void RowIndex_MapsToDocumentLine_OnBothSides()
{
var result = DiffAlignment.Build(new[]
{
Ctx(1, 1, "a"),
Del(2, "d"), Add(2, "x"), Add(3, "y"),
Ctx(3, 4, "b"),
});
var leftLines = result.LeftText.Split('\n');
var rightLines = result.RightText.Split('\n');
Assert.Equal(result.SplitRows.Count, leftLines.Length);
Assert.Equal(result.SplitRows.Count, rightLines.Length);
for (var i = 0; i < result.SplitRows.Count; i++)
{
Assert.Equal(result.SplitRows[i].LeftText, leftLines[i]);
Assert.Equal(result.SplitRows[i].RightText, rightLines[i]);
}
var unifiedLines = result.UnifiedText.Split('\n');
Assert.Equal(result.UnifiedRows.Count, unifiedLines.Length);
}
[Fact]
public void FileHeaderRows_AreIgnored()
{
var result = DiffAlignment.Build(new[]
{
new DiffLineViewModel { Kind = DiffLineKind.File, Text = "src/Foo.cs" },
Ctx(1, 1, "a"),
});
Assert.Single(result.SplitRows);
Assert.Equal("a", result.SplitRows[0].LeftText);
}
}