From 0251cfeae3acbdf51b57fdee164a316d8b316a6e Mon Sep 17 00:00:00 2001 From: Sina Hinderks Date: Sun, 4 Jan 2026 15:39:02 +0100 Subject: [PATCH] fix: last line change may disable hunk operation in two-side diff If the last line is deleted or added, it was impossible to stage, unstage or discard the last hunk in a two-side diff view, if the hunk was selected on the side where the last line is missing (that's the left side for added lines and the right side for deleted lines). Since the button(s) for hunk operations are on the right side of the diff view, it was easier to trigger this bug for a deleted last line. This bug was introduced in commit 6511d15c01 while rewriting the text diff views. This fixes the bug mentioned in https://github.com/sourcegit-scm/sourcegit/issues/1950#issuecomment-3630580428. --- src/ViewModels/TextDiffContext.cs | 61 ++++++++++++------------------- src/Views/TextDiffView.axaml.cs | 6 ++- 2 files changed, 28 insertions(+), 39 deletions(-) diff --git a/src/ViewModels/TextDiffContext.cs b/src/ViewModels/TextDiffContext.cs index 90c7df6e2..2025b2513 100644 --- a/src/ViewModels/TextDiffContext.cs +++ b/src/ViewModels/TextDiffContext.cs @@ -191,18 +191,24 @@ public TwoSideTextDiff(Models.DiffOption option, Models.TextDiff diff, TextDiffC _option = option; _data = diff; - foreach (var line in diff.Lines) + var sourceIndices = new int[diff.Lines.Count]; + + for (var sourceIndex = 0; sourceIndex < diff.Lines.Count; sourceIndex++) { + var line = diff.Lines[sourceIndex]; switch (line.Type) { case Models.TextDiffLineType.Added: + sourceIndices[New.Count] = sourceIndex; New.Add(line); break; case Models.TextDiffLineType.Deleted: + sourceIndices[Old.Count] = sourceIndex; Old.Add(line); break; default: FillEmptyLines(); + sourceIndices[Old.Count] = sourceIndex; Old.Add(line); New.Add(line); break; @@ -210,6 +216,7 @@ public TwoSideTextDiff(Models.DiffOption option, Models.TextDiff diff, TextDiffC } FillEmptyLines(); + _sourceIndices = ResizeSourceIndices(sourceIndices); TryKeepPrevState(previous, Old); } @@ -223,43 +230,9 @@ public override TextDiffContext SwitchMode() return new CombinedTextDiff(_option, _data, this); } - public void ConvertsToCombinedRange(ref int startLine, ref int endLine, bool isOldSide) + public int ConvertToCombined(int line) { - endLine = Math.Min(endLine, _data.Lines.Count - 1); - - var oneSide = isOldSide ? Old : New; - var firstContentLine = -1; - for (int i = startLine; i <= endLine; i++) - { - var line = oneSide[i]; - if (line.Type != Models.TextDiffLineType.None) - { - firstContentLine = i; - break; - } - } - - if (firstContentLine < 0) - return; - - var endContentLine = -1; - for (int i = Math.Min(endLine, oneSide.Count - 1); i >= startLine; i--) - { - var line = oneSide[i]; - if (line.Type != Models.TextDiffLineType.None) - { - endContentLine = i; - break; - } - } - - if (endContentLine < 0) - return; - - var firstContent = oneSide[firstContentLine]; - var endContent = oneSide[endContentLine]; - startLine = _data.Lines.IndexOf(firstContent); - endLine = _data.Lines.IndexOf(endContent); + return 0 <= line && line < _sourceIndices.Length ? _sourceIndices[line] : line; } private void FillEmptyLines() @@ -277,5 +250,19 @@ private void FillEmptyLines() New.Add(new Models.TextDiffLine()); } } + + private int[] ResizeSourceIndices(int[] sourceIndices) + { + var length = Old.Count; // same as `New.Count` + + if (length == sourceIndices.Length) + return sourceIndices; + + var resized = new int[length]; + Array.Copy(sourceIndices, resized, length); + return resized; + } + + private readonly int[] _sourceIndices; } } diff --git a/src/Views/TextDiffView.axaml.cs b/src/Views/TextDiffView.axaml.cs index 164ad3cac..314e5ba6b 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -1183,7 +1183,8 @@ protected override void UpdateSelectedChunk(double y) endLine.GetTextLineVisualYPosition(endLine.TextLines[^1], VisualYPosition.TextBottom) - view.VerticalOffset : view.Bounds.Height; - diff.ConvertsToCombinedRange(ref startIdx, ref endIdx, IsOld); + startIdx = diff.ConvertToCombined(startIdx); + endIdx = diff.ConvertToCombined(endIdx); TrySetChunk(new(rectStartY, rectEndY - rectStartY, startIdx, endIdx, false, IsOld)); } else @@ -1229,7 +1230,8 @@ protected override void UpdateSelectedChunk(double y) endLine.GetTextLineVisualYPosition(endLine.TextLines[^1], VisualYPosition.TextBottom) - view.VerticalOffset : view.Bounds.Height; - diff.ConvertsToCombinedRange(ref startIdx, ref endIdx, IsOld); + startIdx = diff.ConvertToCombined(startIdx); + endIdx = diff.ConvertToCombined(endIdx); TrySetChunk(new(rectStartY, rectEndY - rectStartY, startIdx, endIdx, true, false)); } }