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

@@ -20,11 +20,7 @@ namespace SourceGit.Commands
public Diff(string repo, Models.DiffOption opt, int unified, bool ignoreWhitespace)
{
_result.TextDiff = new Models.TextDiff()
{
Repo = repo,
Option = opt,
};
_result.TextDiff = new Models.TextDiff() { Option = opt };
WorkingDirectory = repo;
Context = repo;

View File

@@ -1,8 +1,6 @@
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using Avalonia;
using Avalonia.Media.Imaging;
namespace SourceGit.Models
@@ -62,12 +60,9 @@ namespace SourceGit.Models
public partial class TextDiff
{
public string File { get; set; } = string.Empty;
public List<TextDiffLine> Lines { get; set; } = new List<TextDiffLine>();
public Vector ScrollOffset { get; set; } = Vector.Zero;
public int MaxLineNumber = 0;
public string Repo { get; set; } = null;
public DiffOption Option { get; set; } = null;
public List<TextDiffLine> Lines { get; set; } = new List<TextDiffLine>();
public int MaxLineNumber = 0;
public TextDiffSelection MakeSelection(int startLine, int endLine, bool isCombined, bool isOldSide)
{

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;
}
}

View File

@@ -48,7 +48,7 @@
<Button.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="IsTextDiff"/>
<Binding Source="{x:Static vm:Preferences.Instance}" Path="UseBlockNavigationInDiffView" Mode="OneWay"/>
<Binding Path="UseBlockNavigation"/>
</MultiBinding>
</Button.IsVisible>
<Path Width="12" Height="12" Stretch="Uniform" Margin="0,6,0,0" Data="{StaticResource Icons.Top}"/>
@@ -73,11 +73,20 @@
<Border.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="IsTextDiff"/>
<Binding Source="{x:Static vm:Preferences.Instance}" Path="UseBlockNavigationInDiffView" Mode="OneWay"/>
<Binding Path="UseBlockNavigation"/>
</MultiBinding>
</Border.IsVisible>
<TextBlock x:Name="BlockNavigationIndicator" Classes="primary" Margin="0,0,0,0" FontSize="11" Text="-/-"/>
<ContentControl Content="{Binding Content, Mode=OneWay}">
<ContentControl.DataTemplates>
<DataTemplate DataType="vm:TextDiffContext">
<ContentControl Content="{Binding BlockNavigation}"/>
</DataTemplate>
<DataTemplate DataType="vm:BlockNavigation">
<TextBlock Classes="primary" Margin="0,0,0,0" FontSize="11" Text="{Binding Indicator}"/>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</Border>
<Button Classes="icon_button"
@@ -109,7 +118,7 @@
<Button.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="IsTextDiff"/>
<Binding Source="{x:Static vm:Preferences.Instance}" Path="UseBlockNavigationInDiffView" Mode="OneWay"/>
<Binding Path="UseBlockNavigation"/>
</MultiBinding>
</Button.IsVisible>
<Path Width="12" Height="12" Stretch="Uniform" Margin="0,6,0,0" Data="{StaticResource Icons.Bottom}"/>
@@ -117,7 +126,7 @@
<ToggleButton Classes="line_path"
Width="28"
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseBlockNavigationInDiffView, Mode=TwoWay}"
IsChecked="{Binding UseBlockNavigation, Mode=TwoWay}"
IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.UseBlockNavigation}">
<Path Width="13" Height="13" Data="{StaticResource Icons.CodeBlock}" Margin="0,3,0,0"/>
@@ -127,10 +136,8 @@
Width="28"
Command="{Binding IncrUnified}"
IsVisible="{Binding IsTextDiff}"
IsEnabled="{Binding ShowEntireFile, Mode=OneWay, Converter={x:Static BoolConverters.Not}}"
ToolTip.Tip="{DynamicResource Text.Diff.VisualLines.Incr}">
<Button.IsEnabled>
<Binding Source="{x:Static vm:Preferences.Instance}" Path="UseFullTextDiff" Mode="OneWay" Converter="{x:Static BoolConverters.Not}"/>
</Button.IsEnabled>
<Path Width="12" Height="12" Stretch="Uniform" Margin="0,6,0,0" Data="{StaticResource Icons.Lines.Incr}"/>
</Button>
@@ -142,7 +149,7 @@
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="UnifiedLines" Mode="OneWay" Converter="{x:Static c:IntConverters.IsGreaterThanFour}"/>
<Binding Source="{x:Static vm:Preferences.Instance}" Path="UseFullTextDiff" Mode="OneWay" Converter="{x:Static BoolConverters.Not}"/>
<Binding Path="ShowEntireFile" Mode="OneWay" Converter="{x:Static BoolConverters.Not}"/>
</MultiBinding>
</Button.IsEnabled>
<Path Width="12" Height="12" Stretch="Uniform" Margin="0,6,0,0" Data="{StaticResource Icons.Lines.Decr}"/>
@@ -150,8 +157,7 @@
<ToggleButton Classes="line_path"
Width="28"
Click="OnUseFullTextDiffClicked"
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseFullTextDiff, Mode=OneWay}"
IsChecked="{Binding ShowEntireFile, Mode=TwoWay}"
IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.VisualLines.All}">
<Path Width="13" Height="13" Data="{StaticResource Icons.Lines.All}" Margin="0,3,0,0"/>
@@ -172,8 +178,8 @@
ToolTip.Tip="{DynamicResource Text.Diff.ToggleWordWrap}">
<ToggleButton.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="IsTextDiff"/>
<Binding Source="{x:Static vm:Preferences.Instance}" Path="UseSideBySideDiff" Mode="OneWay" Converter="{x:Static BoolConverters.Not}"/>
<Binding Path="IsTextDiff" Mode="OneWay"/>
<Binding Path="UseSideBySide" Mode="OneWay" Converter="{x:Static BoolConverters.Not}"/>
</MultiBinding>
</ToggleButton.IsVisible>
@@ -197,7 +203,7 @@
<ToggleButton Classes="line_path"
Width="28" Height="18"
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseSideBySideDiff, Mode=TwoWay}"
IsChecked="{Binding UseSideBySide, Mode=TwoWay}"
IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.SideBySide}">
<Path Width="12" Height="12" Data="{StaticResource Icons.Layout}" Margin="0,2,0,0"/>
@@ -361,10 +367,8 @@
</DataTemplate>
<!-- Text Diff -->
<DataTemplate DataType="m:TextDiff">
<v:TextDiffView UseSideBySideDiff="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseSideBySideDiff, Mode=OneWay}"
UseBlockNavigation="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseBlockNavigationInDiffView, Mode=OneWay}"
BlockNavigationChanged="OnBlockNavigationChanged"/>
<DataTemplate DataType="vm:TextDiffContext">
<v:TextDiffView SelectedChunk="{Binding SelectedChunk, Mode=OneWay}"/>
</DataTemplate>
<!-- Empty or only EOL changes -->

View File

@@ -1,4 +1,3 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
@@ -12,50 +11,35 @@ namespace SourceGit.Views
InitializeComponent();
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
if (DataContext is ViewModels.DiffContext vm)
vm.CheckSettings();
}
private void OnGotoFirstChange(object _, RoutedEventArgs e)
{
this.FindDescendantOfType<TextDiffView>()?.GotoFirstChange();
this.FindDescendantOfType<ThemedTextDiffPresenter>()?.GotoFirstChange();
e.Handled = true;
}
private void OnGotoPrevChange(object _, RoutedEventArgs e)
{
this.FindDescendantOfType<TextDiffView>()?.GotoPrevChange();
this.FindDescendantOfType<ThemedTextDiffPresenter>()?.GotoPrevChange();
e.Handled = true;
}
private void OnGotoNextChange(object _, RoutedEventArgs e)
{
this.FindDescendantOfType<TextDiffView>()?.GotoNextChange();
this.FindDescendantOfType<ThemedTextDiffPresenter>()?.GotoNextChange();
e.Handled = true;
}
private void OnGotoLastChange(object _, RoutedEventArgs e)
{
this.FindDescendantOfType<TextDiffView>()?.GotoLastChange();
e.Handled = true;
}
private void OnBlockNavigationChanged(object sender, RoutedEventArgs e)
{
if (sender is TextDiffView textDiff)
BlockNavigationIndicator.Text = textDiff.BlockNavigation?.Indicator ?? string.Empty;
}
private void OnUseFullTextDiffClicked(object sender, RoutedEventArgs e)
{
var textDiffView = this.FindDescendantOfType<TextDiffView>();
var presenter = textDiffView?.FindDescendantOfType<ThemedTextDiffPresenter>();
if (presenter == null)
return;
if (presenter.DataContext is Models.TextDiff combined)
combined.ScrollOffset = Vector.Zero;
else if (presenter.DataContext is ViewModels.TwoSideTextDiff twoSides)
twoSides.File = string.Empty; // Just to reset `SyncScrollOffset` without affect UI refresh.
(DataContext as ViewModels.DiffContext)?.ToggleFullTextDiff();
this.FindDescendantOfType<ThemedTextDiffPresenter>()?.GotoLastChange();
e.Handled = true;
}
}

View File

@@ -7,15 +7,14 @@
xmlns:v="using:SourceGit.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.TextDiffView"
x:Name="ThisControl"
x:DataType="vm:TextDiffContext"
Background="{DynamicResource Brush.Contents}">
<Grid>
<ContentControl x:Name="Editor">
<ContentControl Content="{Binding}">
<ContentControl.DataTemplates>
<DataTemplate DataType="m:TextDiff">
<DataTemplate DataType="vm:CombinedTextDiff">
<Grid ColumnDefinitions="*,1,8">
<v:CombinedTextDiffPresenter Grid.Column="0"
x:Name="CombinedPresenter"
FileName="{Binding File}"
Foreground="{DynamicResource Brush.FG1}"
LineBrush="{DynamicResource Brush.Border2}"
@@ -31,14 +30,14 @@
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="{Binding Source={x:Static vm:Preferences.Instance}, Path=EnableDiffViewWordWrap}"
ShowHiddenSymbols="{Binding Source={x:Static vm:Preferences.Instance}, Path=ShowHiddenSymbolsInDiffView}"
EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}"
SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"
BlockNavigation="{Binding #ThisControl.BlockNavigation, Mode=TwoWay}"/>
EnableChunkSelection="{Binding EnableChunkOption}"
SelectedChunk="{Binding SelectedChunk, Mode=TwoWay}"
BlockNavigation="{Binding BlockNavigation, Mode=OneWay}"/>
<Rectangle Grid.Column="1" Fill="{DynamicResource Brush.Border2}" Width="1" HorizontalAlignment="Center" VerticalAlignment="Stretch"/>
<v:TextDiffViewMinimap Grid.Column="2"
DisplayRange="{Binding #CombinedPresenter.DisplayRange}"
DisplayRange="{Binding DisplayRange, Mode=OneWay}"
AddedLineBrush="{DynamicResource Brush.Diff.AddedBG}"
DeletedLineBrush="{DynamicResource Brush.Diff.DeletedBG}"/>
</Grid>
@@ -47,7 +46,6 @@
<DataTemplate DataType="vm:TwoSideTextDiff">
<Grid ColumnDefinitions="*,1,*,1,12">
<v:SingleSideTextDiffPresenter Grid.Column="0"
x:Name="LeftSidePresenter"
IsOld="True"
FileName="{Binding File}"
Foreground="{DynamicResource Brush.FG1}"
@@ -64,9 +62,9 @@
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="False"
ShowHiddenSymbols="{Binding Source={x:Static vm:Preferences.Instance}, Path=ShowHiddenSymbolsInDiffView}"
EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}"
SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"
BlockNavigation="{Binding #ThisControl.BlockNavigation, Mode=TwoWay}"/>
EnableChunkSelection="{Binding EnableChunkOption, Mode=OneWay}"
SelectedChunk="{Binding SelectedChunk, Mode=TwoWay}"
BlockNavigation="{Binding BlockNavigation, Mode=OneWay}"/>
<Rectangle Grid.Column="1" Fill="{DynamicResource Brush.Border2}" Width="1" HorizontalAlignment="Center" VerticalAlignment="Stretch"/>
@@ -87,14 +85,14 @@
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="False"
ShowHiddenSymbols="{Binding Source={x:Static vm:Preferences.Instance}, Path=ShowHiddenSymbolsInDiffView}"
EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}"
SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"
BlockNavigation="{Binding #ThisControl.BlockNavigation, Mode=TwoWay}"/>
EnableChunkSelection="{Binding EnableChunkOption, Mode=OneWay}"
SelectedChunk="{Binding SelectedChunk, Mode=TwoWay}"
BlockNavigation="{Binding BlockNavigation, Mode=OneWay}"/>
<Rectangle Grid.Column="3" Fill="{DynamicResource Brush.Border2}" Width="1" HorizontalAlignment="Center" VerticalAlignment="Stretch"/>
<v:TextDiffViewMinimap Grid.Column="4"
DisplayRange="{Binding #LeftSidePresenter.DisplayRange}"
DisplayRange="{Binding DisplayRange, Mode=OneWay}"
AddedLineBrush="{DynamicResource Brush.Diff.AddedBG}"
DeletedLineBrush="{DynamicResource Brush.Diff.DeletedBG}"/>
</Grid>
@@ -103,7 +101,7 @@
</ContentControl>
<StackPanel x:Name="Popup" IsVisible="False" Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Right" Effect="drop-shadow(0 0 8 #80000000)">
<Button Classes="flat" Click="OnStageChunk" HotKey="{OnPlatform Ctrl+S, macOS=⌘+S}" IsVisible="{Binding #ThisControl.IsUnstagedChange}">
<Button Classes="flat" Click="OnStageChunk" HotKey="{OnPlatform Ctrl+S, macOS=⌘+S}" IsVisible="{Binding IsUnstaged}">
<TextBlock>
<Run Text="{DynamicResource Text.Hunk.Stage}"/>
<Run Text=" "/>
@@ -114,7 +112,7 @@
</TextBlock>
</Button>
<Button Classes="flat" Click="OnUnstageChunk" HotKey="{OnPlatform Ctrl+U, macOS=⌘+U}" IsVisible="{Binding #ThisControl.IsUnstagedChange, Converter={x:Static BoolConverters.Not}}">
<Button Classes="flat" Click="OnUnstageChunk" HotKey="{OnPlatform Ctrl+U, macOS=⌘+U}" IsVisible="{Binding IsUnstaged, Converter={x:Static BoolConverters.Not}}">
<TextBlock>
<Run Text="{DynamicResource Text.Hunk.Unstage}"/>
<Run Text=" "/>
@@ -125,7 +123,7 @@
</TextBlock>
</Button>
<Button Classes="flat" Margin="8,0,0,0" HotKey="{OnPlatform Ctrl+D, macOS=⌘+D}" Click="OnDiscardChunk" IsVisible="{Binding #ThisControl.IsUnstagedChange}">
<Button Classes="flat" Margin="8,0,0,0" HotKey="{OnPlatform Ctrl+D, macOS=⌘+D}" Click="OnDiscardChunk" IsVisible="{Binding IsUnstaged}">
<TextBlock>
<Run Text="{DynamicResource Text.Hunk.Discard}"/>
<Run Text=" "/>

View File

@@ -5,6 +5,7 @@ using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
@@ -23,41 +24,6 @@ using AvaloniaEdit.Utils;
namespace SourceGit.Views
{
public class TextDiffViewChunk
{
public double Y { get; set; } = 0.0;
public double Height { get; set; } = 0.0;
public int StartIdx { get; set; } = 0;
public int EndIdx { get; set; } = 0;
public bool Combined { get; set; } = true;
public bool IsOldSide { get; set; } = false;
public bool ShouldReplace(TextDiffViewChunk old)
{
if (old == null)
return true;
return Math.Abs(Y - old.Y) > 0.001 ||
Math.Abs(Height - old.Height) > 0.001 ||
StartIdx != old.StartIdx ||
EndIdx != old.EndIdx ||
Combined != old.Combined ||
IsOldSide != old.IsOldSide;
}
}
public record TextDiffViewRange
{
public int StartIdx { get; set; } = 0;
public int EndIdx { get; set; } = 0;
public TextDiffViewRange(int startIdx, int endIdx)
{
StartIdx = startIdx;
EndIdx = endIdx;
}
}
public class ThemedTextDiffPresenter : TextEditor
{
public class VerticalSeparatorMargin : AbstractMargin
@@ -134,13 +100,12 @@ namespace SourceGit.Views
protected override Size MeasureOverride(Size availableSize)
{
var presenter = this.FindAncestorOfType<ThemedTextDiffPresenter>();
if (presenter == null)
if (presenter is not { DataContext: ViewModels.TextDiffContext ctx })
return new Size(32, 0);
var maxLineNumber = presenter.GetMaxLineNumber();
var typeface = TextView.CreateTypeface();
var test = new FormattedText(
$"{maxLineNumber}",
$"{ctx.Data.MaxLineNumber}",
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
@@ -482,24 +447,15 @@ namespace SourceGit.Views
set => SetValue(EnableChunkSelectionProperty, value);
}
public static readonly StyledProperty<TextDiffViewChunk> SelectedChunkProperty =
AvaloniaProperty.Register<ThemedTextDiffPresenter, TextDiffViewChunk>(nameof(SelectedChunk));
public static readonly StyledProperty<ViewModels.TextDiffSelectedChunk> SelectedChunkProperty =
AvaloniaProperty.Register<ThemedTextDiffPresenter, ViewModels.TextDiffSelectedChunk>(nameof(SelectedChunk));
public TextDiffViewChunk SelectedChunk
public ViewModels.TextDiffSelectedChunk SelectedChunk
{
get => GetValue(SelectedChunkProperty);
set => SetValue(SelectedChunkProperty, value);
}
public static readonly StyledProperty<TextDiffViewRange> DisplayRangeProperty =
AvaloniaProperty.Register<ThemedTextDiffPresenter, TextDiffViewRange>(nameof(DisplayRange), new TextDiffViewRange(0, 0));
public TextDiffViewRange DisplayRange
{
get => GetValue(DisplayRangeProperty);
set => SetValue(DisplayRangeProperty, value);
}
public static readonly StyledProperty<ViewModels.BlockNavigation> BlockNavigationProperty =
AvaloniaProperty.Register<ThemedTextDiffPresenter, ViewModels.BlockNavigation>(nameof(BlockNavigation));
@@ -534,11 +490,6 @@ namespace SourceGit.Views
return [];
}
public virtual int GetMaxLineNumber()
{
return 0;
}
public virtual void UpdateSelectedChunk(double y)
{
}
@@ -569,7 +520,10 @@ namespace SourceGit.Views
return;
}
var firstLineIdx = DisplayRange.StartIdx;
if (DataContext is not ViewModels.TextDiffContext { DisplayRange: { } range })
return;
var firstLineIdx = range.Start;
if (firstLineIdx <= 1)
return;
@@ -625,8 +579,11 @@ namespace SourceGit.Views
return;
}
if (DataContext is not ViewModels.TextDiffContext { DisplayRange: { } range })
return;
var lines = GetLines();
var lastLineIdx = DisplayRange.EndIdx;
var lastLineIdx = range.End;
if (lastLineIdx >= lines.Count - 1)
return;
@@ -746,12 +703,6 @@ namespace SourceGit.Views
}
else if (change.Property == BlockNavigationProperty)
{
if (change.OldValue is ViewModels.BlockNavigation oldValue)
oldValue.PropertyChanged -= OnBlockNavigationPropertyChanged;
if (change.NewValue is ViewModels.BlockNavigation newValue)
newValue.PropertyChanged += OnBlockNavigationPropertyChanged;
TextArea?.TextView?.Redraw();
}
}
@@ -771,12 +722,6 @@ namespace SourceGit.Views
base.OnKeyDown(e);
}
private void OnBlockNavigationPropertyChanged(object _1, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Current")
TextArea?.TextView?.Redraw();
}
private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e)
{
var selection = TextArea.Selection;
@@ -850,9 +795,15 @@ namespace SourceGit.Views
private void OnTextViewVisualLinesChanged(object sender, EventArgs e)
{
if (DataContext is not ViewModels.TextDiffContext ctx)
return;
if (ctx.IsSideBySide() && !IsOld)
return;
if (!TextArea.TextView.VisualLinesValid)
{
SetCurrentValue(DisplayRangeProperty, new TextDiffViewRange(0, 0));
ctx.DisplayRange = null;
return;
}
@@ -873,89 +824,16 @@ namespace SourceGit.Views
start = index;
}
SetCurrentValue(DisplayRangeProperty, new TextDiffViewRange(start, start + count));
ctx.DisplayRange = new ViewModels.TextDiffDisplayRange(start, start + count);
BlockNavigation?.AutoUpdate(start + 1, start + count);
}
protected void TrySetChunk(TextDiffViewChunk chunk)
protected void TrySetChunk(ViewModels.TextDiffSelectedChunk chunk)
{
var old = SelectedChunk;
if (chunk == null)
{
if (old != null)
SetCurrentValue(SelectedChunkProperty, null);
return;
}
if (chunk.ShouldReplace(old))
if (ViewModels.TextDiffSelectedChunk.IsChanged(SelectedChunk, chunk))
SetCurrentValue(SelectedChunkProperty, chunk);
}
protected (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);
}
private void UpdateTextMate()
{
if (UseSyntaxHighlighting)
@@ -1072,21 +950,14 @@ namespace SourceGit.Views
public override List<Models.TextDiffLine> GetLines()
{
if (DataContext is Models.TextDiff diff)
if (DataContext is ViewModels.CombinedTextDiff { Data: { } diff })
return diff.Lines;
return [];
}
public override int GetMaxLineNumber()
{
if (DataContext is Models.TextDiff diff)
return diff.MaxLineNumber;
return 0;
}
public override void UpdateSelectedChunk(double y)
{
if (DataContext is not Models.TextDiff diff)
if (DataContext is not ViewModels.CombinedTextDiff { Data: { } diff } combined)
return;
var view = TextArea.TextView;
@@ -1134,15 +1005,7 @@ namespace SourceGit.Views
endLine.GetTextLineVisualYPosition(endLine.TextLines[^1], VisualYPosition.TextBottom) - view.VerticalOffset :
view.Bounds.Height;
TrySetChunk(new TextDiffViewChunk()
{
Y = rectStartY,
Height = rectEndY - rectStartY,
StartIdx = startIdx,
EndIdx = endIdx,
Combined = true,
IsOldSide = false,
});
TrySetChunk(new(rectStartY, rectEndY - rectStartY, startIdx, endIdx, true, false));
}
else
{
@@ -1170,7 +1033,7 @@ namespace SourceGit.Views
return;
}
var (startIdx, endIdx) = FindRangeByIndex(diff.Lines, lineIdx);
var (startIdx, endIdx) = combined.FindRangeByIndex(diff.Lines, lineIdx);
if (startIdx == -1)
{
TrySetChunk(null);
@@ -1187,15 +1050,7 @@ namespace SourceGit.Views
endLine.GetTextLineVisualYPosition(endLine.TextLines[^1], VisualYPosition.TextBottom) - view.VerticalOffset :
view.Bounds.Height;
TrySetChunk(new TextDiffViewChunk()
{
Y = rectStartY,
Height = rectEndY - rectStartY,
StartIdx = startIdx,
EndIdx = endIdx,
Combined = true,
IsOldSide = false,
});
TrySetChunk(new(rectStartY, rectEndY - rectStartY, startIdx, endIdx, true, false));
}
}
@@ -1223,10 +1078,10 @@ namespace SourceGit.Views
{
base.OnDataContextChanged(e);
if (DataContext is Models.TextDiff textDiff)
if (DataContext is ViewModels.CombinedTextDiff { Data: { } diff })
{
var builder = new StringBuilder();
foreach (var line in textDiff.Lines)
foreach (var line in diff.Lines)
{
if (line.Content.Length > 10000)
{
@@ -1279,35 +1134,28 @@ namespace SourceGit.Views
return [];
}
public override int GetMaxLineNumber()
{
if (DataContext is ViewModels.TwoSideTextDiff diff)
return diff.MaxLineNumber;
return 0;
}
public override void GotoFirstChange()
{
base.GotoFirstChange();
DirectSyncScrollOffset();
SyncScrollOffset();
}
public override void GotoPrevChange()
{
base.GotoPrevChange();
DirectSyncScrollOffset();
SyncScrollOffset();
}
public override void GotoNextChange()
{
base.GotoNextChange();
DirectSyncScrollOffset();
SyncScrollOffset();
}
public override void GotoLastChange()
{
base.GotoLastChange();
DirectSyncScrollOffset();
SyncScrollOffset();
}
public override void UpdateSelectedChunk(double y)
@@ -1315,10 +1163,6 @@ namespace SourceGit.Views
if (DataContext is not ViewModels.TwoSideTextDiff diff)
return;
var parent = this.FindAncestorOfType<TextDiffView>();
if (parent == null)
return;
var view = TextArea.TextView;
var lines = IsOld ? diff.Old : diff.New;
var selection = TextArea.Selection;
@@ -1365,22 +1209,10 @@ namespace SourceGit.Views
endLine.GetTextLineVisualYPosition(endLine.TextLines[^1], VisualYPosition.TextBottom) - view.VerticalOffset :
view.Bounds.Height;
diff.ConvertsToCombinedRange(parent.DataContext as Models.TextDiff, ref startIdx, ref endIdx, IsOld);
TrySetChunk(new TextDiffViewChunk()
{
Y = rectStartY,
Height = rectEndY - rectStartY,
StartIdx = startIdx,
EndIdx = endIdx,
Combined = false,
IsOldSide = IsOld,
});
return;
diff.ConvertsToCombinedRange(ref startIdx, ref endIdx, IsOld);
TrySetChunk(new (rectStartY, rectEndY - rectStartY, startIdx, endIdx, false, IsOld));
}
if (this.FindAncestorOfType<TextDiffView>()?.DataContext is Models.TextDiff textDiff)
else
{
var lineIdx = -1;
foreach (var line in view.VisualLines)
@@ -1406,7 +1238,7 @@ namespace SourceGit.Views
return;
}
var (startIdx, endIdx) = FindRangeByIndex(lines, lineIdx);
var (startIdx, endIdx) = diff.FindRangeByIndex(lines, lineIdx);
if (startIdx == -1)
{
TrySetChunk(null);
@@ -1423,15 +1255,21 @@ namespace SourceGit.Views
endLine.GetTextLineVisualYPosition(endLine.TextLines[^1], VisualYPosition.TextBottom) - view.VerticalOffset :
view.Bounds.Height;
TrySetChunk(new TextDiffViewChunk()
{
Y = rectStartY,
Height = rectEndY - rectStartY,
StartIdx = textDiff.Lines.IndexOf(lines[startIdx]),
EndIdx = endIdx == lines.Count - 1 ? textDiff.Lines.Count - 1 : textDiff.Lines.IndexOf(lines[endIdx]),
Combined = true,
IsOldSide = false,
});
diff.ConvertsToCombinedRange(ref startIdx, ref endIdx, IsOld);
TrySetChunk(new (rectStartY, rectEndY - rectStartY, startIdx, endIdx, true, false));
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == BlockNavigationProperty)
{
if (change.OldValue is ViewModels.BlockNavigation oldValue)
oldValue.PropertyChanged -= OnBlockNavigationPropertyChanged;
if (change.NewValue is ViewModels.BlockNavigation newValue)
newValue.PropertyChanged += OnBlockNavigationPropertyChanged;
}
}
@@ -1443,7 +1281,7 @@ namespace SourceGit.Views
if (_scrollViewer != null)
{
_scrollViewer.ScrollChanged += OnTextViewScrollChanged;
_scrollViewer.Bind(ScrollViewer.OffsetProperty, new Binding("SyncScrollOffset", BindingMode.OneWay));
_scrollViewer.Bind(ScrollViewer.OffsetProperty, new Binding("ScrollOffset", BindingMode.OneWay));
}
}
@@ -1498,22 +1336,28 @@ namespace SourceGit.Views
if (_scrollViewer == null || DataContext is not ViewModels.TwoSideTextDiff diff)
return;
if (diff.SyncScrollOffset.NearlyEquals(_scrollViewer.Offset))
if (diff.ScrollOffset.NearlyEquals(_scrollViewer.Offset))
return;
if (IsPointerOver || !e.OffsetDelta.NearlyEquals(Vector.Zero))
{
diff.SyncScrollOffset = _scrollViewer.Offset;
diff.ScrollOffset = _scrollViewer.Offset;
if (!TextArea.TextView.IsPointerOver)
TrySetChunk(null);
}
}
private void DirectSyncScrollOffset()
private void OnBlockNavigationPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName.Equals("Current", StringComparison.Ordinal))
TextArea?.TextView?.Redraw();
}
private void SyncScrollOffset()
{
if (_scrollViewer is not null && DataContext is ViewModels.TwoSideTextDiff diff)
diff.SyncScrollOffset = _scrollViewer.Offset;
diff.ScrollOffset = _scrollViewer.Offset;
}
private ScrollViewer _scrollViewer = null;
@@ -1539,10 +1383,10 @@ namespace SourceGit.Views
set => SetValue(DeletedLineBrushProperty, value);
}
public static readonly StyledProperty<TextDiffViewRange> DisplayRangeProperty =
AvaloniaProperty.Register<TextDiffViewMinimap, TextDiffViewRange>(nameof(DisplayRange), new TextDiffViewRange(0, 0));
public static readonly StyledProperty<ViewModels.TextDiffDisplayRange> DisplayRangeProperty =
AvaloniaProperty.Register<TextDiffViewMinimap, ViewModels.TextDiffDisplayRange>(nameof(DisplayRange));
public TextDiffViewRange DisplayRange
public ViewModels.TextDiffDisplayRange DisplayRange
{
get => GetValue(DisplayRangeProperty);
set => SetValue(DisplayRangeProperty, value);
@@ -1576,18 +1420,19 @@ namespace SourceGit.Views
RenderSingleSide(context, twoSideDiff.Old, 0, halfWidth);
RenderSingleSide(context, twoSideDiff.New, halfWidth, halfWidth);
}
else if (DataContext is Models.TextDiff diff)
else if (DataContext is ViewModels.CombinedTextDiff combined)
{
total = diff.Lines.Count;
RenderSingleSide(context, diff.Lines, 0, Bounds.Width);
var data = combined.Data;
total = data.Lines.Count;
RenderSingleSide(context, data.Lines, 0, Bounds.Width);
}
var range = DisplayRange;
if (range.EndIdx == 0)
if (range == null || range.End == 0)
return;
var startY = range.StartIdx / (total * 1.0) * Bounds.Height;
var endY = range.EndIdx / (total * 1.0) * Bounds.Height;
var startY = range.Start / (total * 1.0) * Bounds.Height;
var endY = range.End / (total * 1.0) * Bounds.Height;
var color = DisplayRangeColor;
var brush = new SolidColorBrush(color, 0.2);
var pen = new Pen(color.ToUInt32());
@@ -1639,189 +1484,53 @@ namespace SourceGit.Views
public partial class TextDiffView : UserControl
{
public static readonly StyledProperty<bool> UseSideBySideDiffProperty =
AvaloniaProperty.Register<TextDiffView, bool>(nameof(UseSideBySideDiff));
public static readonly StyledProperty<ViewModels.TextDiffSelectedChunk> SelectedChunkProperty =
AvaloniaProperty.Register<TextDiffView, ViewModels.TextDiffSelectedChunk>(nameof(SelectedChunk));
public bool UseSideBySideDiff
{
get => GetValue(UseSideBySideDiffProperty);
set => SetValue(UseSideBySideDiffProperty, value);
}
public static readonly StyledProperty<TextDiffViewChunk> SelectedChunkProperty =
AvaloniaProperty.Register<TextDiffView, TextDiffViewChunk>(nameof(SelectedChunk));
public TextDiffViewChunk SelectedChunk
public ViewModels.TextDiffSelectedChunk SelectedChunk
{
get => GetValue(SelectedChunkProperty);
set => SetValue(SelectedChunkProperty, value);
}
public static readonly StyledProperty<bool> IsUnstagedChangeProperty =
AvaloniaProperty.Register<TextDiffView, bool>(nameof(IsUnstagedChange));
public bool IsUnstagedChange
{
get => GetValue(IsUnstagedChangeProperty);
set => SetValue(IsUnstagedChangeProperty, value);
}
public static readonly StyledProperty<bool> EnableChunkSelectionProperty =
AvaloniaProperty.Register<TextDiffView, bool>(nameof(EnableChunkSelection));
public bool EnableChunkSelection
{
get => GetValue(EnableChunkSelectionProperty);
set => SetValue(EnableChunkSelectionProperty, value);
}
public static readonly StyledProperty<bool> UseBlockNavigationProperty =
AvaloniaProperty.Register<TextDiffView, bool>(nameof(UseBlockNavigation));
public bool UseBlockNavigation
{
get => GetValue(UseBlockNavigationProperty);
set => SetValue(UseBlockNavigationProperty, value);
}
public static readonly StyledProperty<ViewModels.BlockNavigation> BlockNavigationProperty =
AvaloniaProperty.Register<TextDiffView, ViewModels.BlockNavigation>(nameof(BlockNavigation));
public ViewModels.BlockNavigation BlockNavigation
{
get => GetValue(BlockNavigationProperty);
set => SetValue(BlockNavigationProperty, value);
}
public static readonly RoutedEvent<RoutedEventArgs> BlockNavigationChangedEvent =
RoutedEvent.Register<TextDiffView, RoutedEventArgs>(nameof(BlockNavigationChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
public event EventHandler<RoutedEventArgs> BlockNavigationChanged
{
add { AddHandler(BlockNavigationChangedEvent, value); }
remove { RemoveHandler(BlockNavigationChangedEvent, value); }
}
static TextDiffView()
{
UseSideBySideDiffProperty.Changed.AddClassHandler<TextDiffView>((v, _) =>
{
v.RefreshContent(v.DataContext as Models.TextDiff, false);
});
UseBlockNavigationProperty.Changed.AddClassHandler<TextDiffView>((v, _) =>
{
v.RefreshBlockNavigation();
});
SelectedChunkProperty.Changed.AddClassHandler<TextDiffView>((v, _) =>
{
var chunk = v.SelectedChunk;
if (chunk == null)
{
v.Popup.IsVisible = false;
return;
}
var top = chunk.Y + (chunk.Height >= 36 ? 8 : 2);
var right = (chunk.Combined || !chunk.IsOldSide) ? 26 : (v.Bounds.Width * 0.5f) + 26;
v.Popup.Margin = new Thickness(0, top, right, 0);
v.Popup.IsVisible = true;
});
}
public TextDiffView()
{
InitializeComponent();
}
public void GotoFirstChange()
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
this.FindDescendantOfType<ThemedTextDiffPresenter>()?.GotoFirstChange();
TryRaiseBlockNavigationChanged();
}
base.OnPropertyChanged(change);
public void GotoPrevChange()
{
this.FindDescendantOfType<ThemedTextDiffPresenter>()?.GotoPrevChange();
TryRaiseBlockNavigationChanged();
}
public void GotoNextChange()
{
this.FindDescendantOfType<ThemedTextDiffPresenter>()?.GotoNextChange();
TryRaiseBlockNavigationChanged();
}
public void GotoLastChange()
{
this.FindDescendantOfType<ThemedTextDiffPresenter>()?.GotoLastChange();
TryRaiseBlockNavigationChanged();
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
RefreshContent(DataContext as Models.TextDiff);
if (change.Property == SelectedChunkProperty)
{
if (SelectedChunk is { } chunk)
{
var top = chunk.Y + (chunk.Height >= 36 ? 8 : 2);
var right = (chunk.Combined || !chunk.IsOldSide) ? 26 : (Bounds.Width * 0.5f) + 26;
Popup.Margin = new Thickness(0, top, right, 0);
Popup.IsVisible = true;
}
else
{
Popup.IsVisible = false;
}
}
}
protected override void OnPointerExited(PointerEventArgs e)
{
base.OnPointerExited(e);
if (SelectedChunk != null)
SetCurrentValue(SelectedChunkProperty, null);
}
private void RefreshContent(Models.TextDiff diff, bool keepScrollOffset = true)
{
if (SelectedChunk != null)
SetCurrentValue(SelectedChunkProperty, null);
if (diff == null)
{
Editor.Content = null;
GC.Collect();
return;
}
if (UseSideBySideDiff)
{
var previousContent = Editor.Content as ViewModels.TwoSideTextDiff;
Editor.Content = new ViewModels.TwoSideTextDiff(diff, keepScrollOffset ? previousContent : null);
}
else
{
if (!keepScrollOffset)
diff.ScrollOffset = Vector.Zero;
Editor.Content = diff;
}
RefreshBlockNavigation();
IsUnstagedChange = diff.Option.IsUnstaged;
EnableChunkSelection = diff.Option.WorkingCopyChange != null;
}
private void RefreshBlockNavigation()
{
if (UseBlockNavigation)
BlockNavigation = new ViewModels.BlockNavigation(Editor.Content);
else
BlockNavigation = null;
TryRaiseBlockNavigationChanged();
if (DataContext is ViewModels.TextDiffContext ctx)
ctx.SelectedChunk = null;
}
private async void OnStageChunk(object _1, RoutedEventArgs _2)
{
var chunk = SelectedChunk;
if (chunk == null)
if (DataContext is not ViewModels.TextDiffContext { SelectedChunk: { } chunk, Data: { } diff })
return;
var diff = DataContext as Models.TextDiff;
var change = diff?.Option.WorkingCopyChange;
var change = diff.Option.WorkingCopyChange;
if (change == null)
return;
@@ -1830,7 +1539,6 @@ namespace SourceGit.Views
return;
var repoView = this.FindAncestorOfType<Repository>();
if (repoView?.DataContext is not ViewModels.Repository repo)
return;
@@ -1849,16 +1557,16 @@ namespace SourceGit.Views
}
else if (chunk.Combined)
{
var treeGuid = await new Commands.QueryStagedFileBlobGuid(diff.Repo, change.Path).GetResultAsync();
var treeGuid = await new Commands.QueryStagedFileBlobGuid(repo.FullPath, change.Path).GetResultAsync();
diff.GeneratePatchFromSelection(change, treeGuid, selection, false, tmpFile);
}
else
{
var treeGuid = await new Commands.QueryStagedFileBlobGuid(diff.Repo, change.Path).GetResultAsync();
var treeGuid = await new Commands.QueryStagedFileBlobGuid(repo.FullPath, change.Path).GetResultAsync();
diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, false, chunk.IsOldSide, tmpFile);
}
await new Commands.Apply(diff.Repo, tmpFile, true, "nowarn", "--cache --index").ExecAsync();
await new Commands.Apply(repo.FullPath, tmpFile, true, "nowarn", "--cache --index").ExecAsync();
File.Delete(tmpFile);
}
@@ -1868,13 +1576,10 @@ namespace SourceGit.Views
private async void OnUnstageChunk(object _1, RoutedEventArgs _2)
{
var chunk = SelectedChunk;
if (chunk == null)
if (DataContext is not ViewModels.TextDiffContext { SelectedChunk: { } chunk, Data: { } diff })
return;
var diff = DataContext as Models.TextDiff;
var change = diff?.Option.WorkingCopyChange;
var change = diff.Option.WorkingCopyChange;
if (change == null)
return;
@@ -1883,7 +1588,6 @@ namespace SourceGit.Views
return;
var repoView = this.FindAncestorOfType<Repository>();
if (repoView?.DataContext is not ViewModels.Repository repo)
return;
@@ -1898,7 +1602,7 @@ namespace SourceGit.Views
}
else
{
var treeGuid = await new Commands.QueryStagedFileBlobGuid(diff.Repo, change.Path).GetResultAsync();
var treeGuid = await new Commands.QueryStagedFileBlobGuid(repo.FullPath, change.Path).GetResultAsync();
var tmpFile = Path.GetTempFileName();
if (change.Index == Models.ChangeState.Added)
diff.GenerateNewPatchFromSelection(change, treeGuid, selection, true, tmpFile);
@@ -1907,7 +1611,7 @@ namespace SourceGit.Views
else
diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, true, chunk.IsOldSide, tmpFile);
await new Commands.Apply(diff.Repo, tmpFile, true, "nowarn", "--cache --index --reverse").ExecAsync();
await new Commands.Apply(repo.FullPath, tmpFile, true, "nowarn", "--cache --index --reverse").ExecAsync();
File.Delete(tmpFile);
}
@@ -1917,13 +1621,10 @@ namespace SourceGit.Views
private async void OnDiscardChunk(object _1, RoutedEventArgs _2)
{
var chunk = SelectedChunk;
if (chunk == null)
if (DataContext is not ViewModels.TextDiffContext { SelectedChunk: { } chunk, Data: { } diff })
return;
var diff = DataContext as Models.TextDiff;
var change = diff?.Option.WorkingCopyChange;
var change = diff.Option.WorkingCopyChange;
if (change == null)
return;
@@ -1951,27 +1652,21 @@ namespace SourceGit.Views
}
else if (chunk.Combined)
{
var treeGuid = await new Commands.QueryStagedFileBlobGuid(diff.Repo, change.Path).GetResultAsync();
var treeGuid = await new Commands.QueryStagedFileBlobGuid(repo.FullPath, change.Path).GetResultAsync();
diff.GeneratePatchFromSelection(change, treeGuid, selection, true, tmpFile);
}
else
{
var treeGuid = await new Commands.QueryStagedFileBlobGuid(diff.Repo, change.Path).GetResultAsync();
var treeGuid = await new Commands.QueryStagedFileBlobGuid(repo.FullPath, change.Path).GetResultAsync();
diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, true, chunk.IsOldSide, tmpFile);
}
await new Commands.Apply(diff.Repo, tmpFile, true, "nowarn", "--reverse").ExecAsync();
await new Commands.Apply(repo.FullPath, tmpFile, true, "nowarn", "--reverse").ExecAsync();
File.Delete(tmpFile);
}
repo.MarkWorkingCopyDirtyManually();
repo.SetWatcherEnabled(true);
}
private void TryRaiseBlockNavigationChanged()
{
if (UseBlockNavigation)
RaiseEvent(new RoutedEventArgs(BlockNavigationChangedEvent));
}
}
}