refactor: rewrite text diff view

- Move some data-only code from `Views` to `ViewModels`
- Remove unnecessary attributes
- This commit also contains a feature request #1722

Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
leo
2025-08-18 18:12:13 +08:00
parent 5c33198fc7
commit 6511d15c01
10 changed files with 588 additions and 629 deletions

View File

@@ -48,17 +48,11 @@ namespace SourceGit.ViewModels
}
}
public BlockNavigation(object context)
public BlockNavigation(List<Models.TextDiffLine> lines)
{
Blocks.Clear();
Current = -1;
var lines = new List<Models.TextDiffLine>();
if (context is Models.TextDiff combined)
lines = combined.Lines;
else if (context is TwoSideTextDiff twoSide)
lines = twoSide.Old;
if (lines.Count == 0)
return;
@@ -96,7 +90,10 @@ namespace SourceGit.ViewModels
public Block GetCurrentBlock()
{
return (_current >= 0 && _current < Blocks.Count) ? Blocks[_current] : null;
if (_current >= 0 && _current < Blocks.Count)
return Blocks[_current];
return Blocks.Count > 0 ? Blocks[0] : null;
}
public Block GotoFirst()
@@ -105,6 +102,7 @@ namespace SourceGit.ViewModels
return null;
Current = 0;
OnPropertyChanged(nameof(Indicator));
return Blocks[_current];
}
@@ -117,6 +115,8 @@ namespace SourceGit.ViewModels
Current = 0;
else if (_current > 0)
Current = _current - 1;
OnPropertyChanged(nameof(Indicator));
return Blocks[_current];
}
@@ -127,6 +127,8 @@ namespace SourceGit.ViewModels
if (_current < Blocks.Count - 1)
Current = _current + 1;
OnPropertyChanged(nameof(Indicator));
return Blocks[_current];
}
@@ -136,9 +138,35 @@ namespace SourceGit.ViewModels
return null;
Current = Blocks.Count - 1;
OnPropertyChanged(nameof(Indicator));
return Blocks[_current];
}
public void AutoUpdate(int start, int end)
{
if (_current >= 0 && _current < Blocks.Count)
{
var block = Blocks[_current];
if ((block.Start >= start && block.Start <= end) ||
(block.End >= start && block.End <= end) ||
(block.Start <= start && block.End >= end))
return;
}
for (var i = 0; i < Blocks.Count; i++)
{
var block = Blocks[i];
if ((block.Start >= start && block.Start <= end) ||
(block.End >= start && block.End <= end) ||
(block.Start <= start && block.End >= end))
{
Current = i;
OnPropertyChanged(nameof(Indicator));
return;
}
}
}
private int _current = -1;
}
}

View File

@@ -1,9 +1,7 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
@@ -24,7 +22,58 @@ namespace SourceGit.ViewModels
{
Preferences.Instance.IgnoreWhitespaceChangesInDiff = value;
OnPropertyChanged();
LoadDiffContent();
if (_isTextDiff)
LoadContent();
}
}
}
public bool ShowEntireFile
{
get => Preferences.Instance.UseFullTextDiff;
set
{
if (value != Preferences.Instance.UseFullTextDiff)
{
Preferences.Instance.UseFullTextDiff = value;
OnPropertyChanged();
if (Content is TextDiffContext ctx)
{
ctx.Data.File = string.Empty; // Just to ignore both previous `ScrollOffset` and `BlockNavigation`
LoadContent();
}
}
}
}
public bool UseBlockNavigation
{
get => Preferences.Instance.UseBlockNavigationInDiffView;
set
{
if (value != Preferences.Instance.UseBlockNavigationInDiffView)
{
Preferences.Instance.UseBlockNavigationInDiffView = value;
OnPropertyChanged();
(Content as TextDiffContext)?.ResetBlockNavigation(value);
}
}
}
public bool UseSideBySide
{
get => Preferences.Instance.UseSideBySideDiff;
set
{
if (value != Preferences.Instance.UseSideBySideDiff)
{
Preferences.Instance.UseSideBySideDiff = value;
OnPropertyChanged();
if (Content is TextDiffContext ctx && ctx.IsSideBySide() != value)
Content = ctx.SwitchMode();
}
}
}
@@ -72,25 +121,19 @@ namespace SourceGit.ViewModels
else
Title = $"{_option.OrgPath} → {_option.Path}";
LoadDiffContent();
}
public void ToggleFullTextDiff()
{
Preferences.Instance.UseFullTextDiff = !Preferences.Instance.UseFullTextDiff;
LoadDiffContent();
LoadContent();
}
public void IncrUnified()
{
UnifiedLines = _unifiedLines + 1;
LoadDiffContent();
LoadContent();
}
public void DecrUnified()
{
UnifiedLines = Math.Max(4, _unifiedLines - 1);
LoadDiffContent();
LoadContent();
}
public void OpenExternalMergeTool()
@@ -98,7 +141,29 @@ namespace SourceGit.ViewModels
new Commands.DiffTool(_repo, _option).Open();
}
private void LoadDiffContent()
public void CheckSettings()
{
if (Content is TextDiffContext ctx)
{
if ((ShowEntireFile && _info.UnifiedLines != _entireFileLine) ||
(!ShowEntireFile && _info.UnifiedLines == _entireFileLine) ||
(IgnoreWhitespace != _info.IgnoreWhitespace))
{
LoadContent();
return;
}
if (ctx.IsSideBySide() != UseSideBySide)
{
ctx = ctx.SwitchMode();
Content = ctx;
}
ctx.ResetBlockNavigation(UseBlockNavigation);
}
}
private void LoadContent()
{
if (_option.Path.EndsWith('/'))
{
@@ -109,7 +174,7 @@ namespace SourceGit.ViewModels
Task.Run(async () =>
{
var numLines = Preferences.Instance.UseFullTextDiff ? 999999999 : _unifiedLines;
var numLines = Preferences.Instance.UseFullTextDiff ? _entireFileLine : _unifiedLines;
var ignoreWhitespace = Preferences.Instance.IgnoreWhitespaceChangesInDiff;
var latest = await new Commands.Diff(_repo, _option, numLines, ignoreWhitespace)
@@ -228,12 +293,23 @@ namespace SourceGit.ViewModels
Dispatcher.UIThread.Post(() =>
{
if (_content is Models.TextDiff old && rs is Models.TextDiff cur && old.File == cur.File)
cur.ScrollOffset = old.ScrollOffset;
FileModeChange = latest.FileModeChange;
Content = rs;
IsTextDiff = rs is Models.TextDiff;
if (rs is Models.TextDiff cur)
{
IsTextDiff = true;
var hasBlockNavigation = Preferences.Instance.UseBlockNavigationInDiffView;
if (Preferences.Instance.UseSideBySideDiff)
Content = new TwoSideTextDiff(cur, hasBlockNavigation, _content as TwoSideTextDiff);
else
Content = new CombinedTextDiff(cur, hasBlockNavigation, _content as CombinedTextDiff);
}
else
{
IsTextDiff = false;
Content = rs;
}
});
});
}
@@ -279,6 +355,7 @@ namespace SourceGit.ViewModels
}
}
private readonly int _entireFileLine = 999999999;
private readonly string _repo;
private readonly Models.DiffOption _option = null;
private string _fileModeChange = string.Empty;

View File

@@ -0,0 +1,291 @@
using System;
using System.Collections.Generic;
using Avalonia;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public record TextDiffSelectedChunk(double y, double h, int start, int end, bool combined, bool isOldSide)
{
public double Y { get; set; } = y;
public double Height { get; set; } = h;
public int StartIdx { get; set; } = start;
public int EndIdx { get; set; } = end;
public bool Combined { get; set; } = combined;
public bool IsOldSide { get; set; } = isOldSide;
public static bool IsChanged(TextDiffSelectedChunk oldValue, TextDiffSelectedChunk newValue)
{
if (newValue == null)
return oldValue != null;
if (oldValue == null)
return true;
return Math.Abs(newValue.Y - oldValue.Y) > 0.001 ||
Math.Abs(newValue.Height - oldValue.Height) > 0.001 ||
newValue.StartIdx != oldValue.StartIdx ||
newValue.EndIdx != oldValue.EndIdx ||
newValue.Combined != oldValue.Combined ||
newValue.IsOldSide != oldValue.IsOldSide;
}
}
public record TextDiffDisplayRange(int start, int end)
{
public int Start { get; set; } = start;
public int End { get; set; } = end;
}
public class TextDiffContext : ObservableObject
{
public Models.TextDiff Data => _data;
public string File => _data.File;
public bool IsUnstaged => _data.Option.IsUnstaged;
public bool EnableChunkOption => _data.Option.WorkingCopyChange != null;
public Vector ScrollOffset
{
get => _scrollOffset;
set => SetProperty(ref _scrollOffset, value);
}
public BlockNavigation BlockNavigation
{
get => _blockNavigation;
set => SetProperty(ref _blockNavigation, value);
}
public TextDiffDisplayRange DisplayRange
{
get => _displayRange;
set => SetProperty(ref _displayRange, value);
}
public TextDiffSelectedChunk SelectedChunk
{
get => _selectedChunk;
set => SetProperty(ref _selectedChunk, value);
}
public void ResetBlockNavigation(bool enabled)
{
if (!enabled)
BlockNavigation = null;
else if (_blockNavigation == null)
BlockNavigation = CreateBlockNavigation();
}
public (int, int) FindRangeByIndex(List<Models.TextDiffLine> lines, int lineIdx)
{
var startIdx = -1;
var endIdx = -1;
var normalLineCount = 0;
var modifiedLineCount = 0;
for (int i = lineIdx; i >= 0; i--)
{
var line = lines[i];
if (line.Type == Models.TextDiffLineType.Indicator)
{
startIdx = i;
break;
}
if (line.Type == Models.TextDiffLineType.Normal)
{
normalLineCount++;
if (normalLineCount >= 2)
{
startIdx = i;
break;
}
}
else
{
normalLineCount = 0;
modifiedLineCount++;
}
}
normalLineCount = lines[lineIdx].Type == Models.TextDiffLineType.Normal ? 1 : 0;
for (int i = lineIdx + 1; i < lines.Count; i++)
{
var line = lines[i];
if (line.Type == Models.TextDiffLineType.Indicator)
{
endIdx = i;
break;
}
if (line.Type == Models.TextDiffLineType.Normal)
{
normalLineCount++;
if (normalLineCount >= 2)
{
endIdx = i;
break;
}
}
else
{
normalLineCount = 0;
modifiedLineCount++;
}
}
if (endIdx == -1)
endIdx = lines.Count - 1;
return modifiedLineCount > 0 ? (startIdx, endIdx) : (-1, -1);
}
public virtual bool IsSideBySide()
{
return false;
}
public virtual TextDiffContext SwitchMode()
{
return null;
}
public virtual BlockNavigation CreateBlockNavigation()
{
return new BlockNavigation(_data.Lines);
}
protected Models.TextDiff _data = null;
protected Vector _scrollOffset = Vector.Zero;
protected BlockNavigation _blockNavigation = null;
protected TextDiffDisplayRange _displayRange = null;
protected TextDiffSelectedChunk _selectedChunk = null;
}
public class CombinedTextDiff : TextDiffContext
{
public CombinedTextDiff(Models.TextDiff diff, bool hasBlockNavigation, CombinedTextDiff previous = null)
{
_data = diff;
if (previous != null && previous.File.Equals(File, StringComparison.Ordinal))
_scrollOffset = previous.ScrollOffset;
if (hasBlockNavigation)
_blockNavigation = CreateBlockNavigation();
}
public override TextDiffContext SwitchMode()
{
return new TwoSideTextDiff(_data, _blockNavigation != null);
}
}
public class TwoSideTextDiff : TextDiffContext
{
public List<Models.TextDiffLine> Old { get; } = new List<Models.TextDiffLine>();
public List<Models.TextDiffLine> New { get; } = new List<Models.TextDiffLine>();
public TwoSideTextDiff(Models.TextDiff diff, bool hasBlockNavigation, TwoSideTextDiff previous = null)
{
_data = diff;
foreach (var line in diff.Lines)
{
switch (line.Type)
{
case Models.TextDiffLineType.Added:
New.Add(line);
break;
case Models.TextDiffLineType.Deleted:
Old.Add(line);
break;
default:
FillEmptyLines();
Old.Add(line);
New.Add(line);
break;
}
}
FillEmptyLines();
if (previous != null && previous.File.Equals(File, StringComparison.Ordinal))
_scrollOffset = previous._scrollOffset;
if (hasBlockNavigation)
_blockNavigation = CreateBlockNavigation();
}
public override bool IsSideBySide()
{
return true;
}
public override TextDiffContext SwitchMode()
{
return new CombinedTextDiff(_data, _blockNavigation != null);
}
public override BlockNavigation CreateBlockNavigation()
{
return new BlockNavigation(Old);
}
public void ConvertsToCombinedRange(ref int startLine, ref int endLine, bool isOldSide)
{
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);
}
private void FillEmptyLines()
{
if (Old.Count < New.Count)
{
int diff = New.Count - Old.Count;
for (int i = 0; i < diff; i++)
Old.Add(new Models.TextDiffLine());
}
else if (Old.Count > New.Count)
{
int diff = Old.Count - New.Count;
for (int i = 0; i < diff; i++)
New.Add(new Models.TextDiffLine());
}
}
}
}

View File

@@ -1,109 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class TwoSideTextDiff : ObservableObject
{
public string File { get; set; }
public List<Models.TextDiffLine> Old { get; set; } = new List<Models.TextDiffLine>();
public List<Models.TextDiffLine> New { get; set; } = new List<Models.TextDiffLine>();
public int MaxLineNumber = 0;
public Vector SyncScrollOffset
{
get => _syncScrollOffset;
set => SetProperty(ref _syncScrollOffset, value);
}
public TwoSideTextDiff(Models.TextDiff diff, TwoSideTextDiff previous = null)
{
File = diff.File;
MaxLineNumber = diff.MaxLineNumber;
foreach (var line in diff.Lines)
{
switch (line.Type)
{
case Models.TextDiffLineType.Added:
New.Add(line);
break;
case Models.TextDiffLineType.Deleted:
Old.Add(line);
break;
default:
FillEmptyLines();
Old.Add(line);
New.Add(line);
break;
}
}
FillEmptyLines();
if (previous != null && previous.File == File)
_syncScrollOffset = previous._syncScrollOffset;
}
public void ConvertsToCombinedRange(Models.TextDiff combined, ref int startLine, ref int endLine, bool isOldSide)
{
endLine = Math.Min(endLine, combined.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 = combined.Lines.IndexOf(firstContent);
endLine = combined.Lines.IndexOf(endContent);
}
private void FillEmptyLines()
{
if (Old.Count < New.Count)
{
int diff = New.Count - Old.Count;
for (int i = 0; i < diff; i++)
Old.Add(new Models.TextDiffLine());
}
else if (Old.Count > New.Count)
{
int diff = Old.Count - New.Count;
for (int i = 0; i < diff; i++)
New.Add(new Models.TextDiffLine());
}
}
private Vector _syncScrollOffset = Vector.Zero;
}
}