Compare commits

...

18 commits

Author SHA1 Message Date
Göran W
4ccb39bc44
Merge 408ece148e into e224f59ea7 2024-11-27 14:20:42 -06:00
goran-w
408ece148e Added ToggleTwoSideDiff(), so we can refresh change-block indicator 2024-11-17 23:24:04 +01:00
goran-w
11a02343a0 Added safeguards for edge cases 2024-11-17 22:41:25 +01:00
goran-w
aeea77078b Merge remote-tracking branch 'origin/develop' into diff-prev-next-change-616 2024-11-17 22:21:12 +01:00
goran-w
760e240db7 The 2 implementations can now be switched
Added a bool property DiffView.UseChangeBlocks.
It's not bound from UI yet, but could be used for runtime switching between the two different implementations of prev/next change.
The buttons are now using the OnGoto[Prev|Next]Change Click-handler, regardless of implementation.
2024-11-16 18:39:04 +01:00
goran-w
636be4a7a8 Merge branch 'develop' into diff-prev-next-change-616 2024-11-16 18:09:56 +01:00
goran-w
4882ad0ad6 Added indicator of current/total change-blocks in Diff toolbar 2024-11-16 13:33:10 +01:00
goran-w
dc5bd42477 Make sure SyncScrollOffset is updated after JumpToChangeBlock() 2024-11-16 13:31:39 +01:00
goran-w
5597d25313 Re-enabled my implementation, after merge from pushed alternative 2024-11-16 11:36:10 +01:00
goran-w
07cf4e6fe0 Corrected method duplication mistake, from rebase conflict resolve 2024-11-16 10:47:10 +01:00
goran-w
57e147e84c Revert "Added icons for "Previous/Next Difference""
This reverts commit 1f8dc29de20708a78cba26a341d3451d11304ef9.
2024-11-16 10:43:13 +01:00
goran-w
1a99ce54d3 Cherrypick - feature: add buttons to go to prev/next change in text diff view (#616)
Signed-off-by: leo <longshuang@msn.cn>
(cherry picked from commit 134c71064e)

# Conflicts:
#	src/Views/DiffView.axaml
#	src/Views/TextDiffView.axaml.cs
2024-11-16 10:43:13 +01:00
goran-w
96a9019487 Prev/next will (re-)scroll to first/last change-block in edge-cases
I.e when unset or already at first/last change-block (or the only one).
2024-11-16 10:43:13 +01:00
goran-w
e0c219b46d Unset current change-block in RefreshContent() 2024-11-16 10:43:13 +01:00
goran-w
0007072789 Implemented change-block navigation 2024-11-16 10:43:13 +01:00
goran-w
d0dc9ac1fe Corrected misspelled local variable nextHigh(t)light 2024-11-16 10:41:33 +01:00
goran-w
fbb07cf75f Added 2 new buttons for prev/next change in Diff
These new buttons in DiffView toolbar are visible when IsTextDiff.
They invoke new (and currently empty) methods PrevChange() / NextChange() in DiffContext.
2024-11-16 10:41:33 +01:00
goran-w
875d4b5382 Added icons for "Previous/Next Difference"
New StreamGeometry "Icons.Diff.Prev" / "Icons.Diff.Next" using SVG paths from "arrow_up_regular" / "arrow_down_regular" at https://avaloniaui.github.io/icons.html.
2024-11-16 10:39:46 +01:00
8 changed files with 327 additions and 47 deletions

View file

@ -51,6 +51,9 @@ namespace SourceGit.Commands
_result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine);
}
if (_result.TextDiff != null)
_result.TextDiff.ProcessChangeBlocks();
return _result;
}

View file

@ -2,6 +2,8 @@
using System.Text;
using System.Text.RegularExpressions;
using CommunityToolkit.Mvvm.ComponentModel;
using Avalonia;
using Avalonia.Media.Imaging;
@ -59,16 +61,70 @@ namespace SourceGit.Models
}
}
public partial class TextDiff
public class TextDiffChangeBlock
{
public TextDiffChangeBlock(int startLine, int endLine)
{
StartLine = startLine;
EndLine = endLine;
}
public int StartLine { get; set; } = 0;
public int EndLine { get; set; } = 0;
public bool IsInRange(int line)
{
return line >= StartLine && line <= EndLine;
}
}
public partial class TextDiff : ObservableObject
{
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 int CurrentChangeBlockIdx
{
get => _currentChangeBlockIdx;
set => SetProperty(ref _currentChangeBlockIdx, value);
}
public string Repo { get; set; } = null;
public DiffOption Option { get; set; } = null;
public List<TextDiffChangeBlock> ChangeBlocks { get; set; } = [];
public void ProcessChangeBlocks()
{
ChangeBlocks.Clear();
int lineIdx = 0, blockStartIdx = 0;
bool isNewBlock = true;
foreach (var line in Lines)
{
lineIdx++;
if (line.Type == Models.TextDiffLineType.Added ||
line.Type == Models.TextDiffLineType.Deleted ||
line.Type == Models.TextDiffLineType.None) // Empty
{
if (isNewBlock)
{
isNewBlock = false;
blockStartIdx = lineIdx;
}
}
else
{
if (!isNewBlock)
{
ChangeBlocks.Add(new TextDiffChangeBlock(blockStartIdx, lineIdx - 1));
isNewBlock = true;
}
}
}
}
public TextDiffSelection MakeSelection(int startLine, int endLine, bool isCombined, bool isOldSide)
{
var rs = new TextDiffSelection();
@ -626,6 +682,8 @@ namespace SourceGit.Models
return true;
}
private int _currentChangeBlockIdx = -1; // NOTE: Use -1 as "not set".
[GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")]
private static partial Regex REG_INDICATOR();
}

View file

@ -51,6 +51,12 @@ namespace SourceGit.ViewModels
private set => SetProperty(ref _unifiedLines, value);
}
public string ChangeBlockIndicator
{
get => _changeBlockIndicator;
private set => SetProperty(ref _changeBlockIndicator, value);
}
public DiffContext(string repo, Models.DiffOption option, DiffContext previous = null)
{
_repo = repo;
@ -73,6 +79,54 @@ namespace SourceGit.ViewModels
LoadDiffContent();
}
public void PrevChange()
{
if (_content is Models.TextDiff textDiff)
{
if (textDiff.CurrentChangeBlockIdx > 0)
{
textDiff.CurrentChangeBlockIdx--;
}
else if (textDiff.ChangeBlocks.Count > 0)
{
// Force property value change and (re-)jump to first change block
textDiff.CurrentChangeBlockIdx = -1;
textDiff.CurrentChangeBlockIdx = 0;
}
}
RefreshChangeBlockIndicator();
}
public void NextChange()
{
if (_content is Models.TextDiff textDiff)
{
if (textDiff.CurrentChangeBlockIdx < textDiff.ChangeBlocks.Count - 1)
{
textDiff.CurrentChangeBlockIdx++;
}
else if (textDiff.ChangeBlocks.Count > 0)
{
// Force property value change and (re-)jump to last change block
textDiff.CurrentChangeBlockIdx = -1;
textDiff.CurrentChangeBlockIdx = textDiff.ChangeBlocks.Count - 1;
}
RefreshChangeBlockIndicator();
}
}
public void RefreshChangeBlockIndicator()
{
string curr = "-", tot = "-";
if (_content is Models.TextDiff textDiff)
{
if (textDiff.CurrentChangeBlockIdx >= 0)
curr = (textDiff.CurrentChangeBlockIdx + 1).ToString();
tot = (textDiff.ChangeBlocks.Count).ToString();
}
ChangeBlockIndicator = curr + "/" + tot;
}
public void ToggleFullTextDiff()
{
Preference.Instance.UseFullTextDiff = !Preference.Instance.UseFullTextDiff;
@ -91,6 +145,12 @@ namespace SourceGit.ViewModels
LoadDiffContent();
}
public void ToggleTwoSideDiff()
{
Preference.Instance.UseSideBySideDiff = !Preference.Instance.UseSideBySideDiff;
RefreshChangeBlockIndicator();
}
public void OpenExternalMergeTool()
{
var toolType = Preference.Instance.ExternalMergeToolType;
@ -217,6 +277,8 @@ namespace SourceGit.ViewModels
FileModeChange = latest.FileModeChange;
Content = rs;
IsTextDiff = rs is Models.TextDiff;
RefreshChangeBlockIndicator();
});
});
}
@ -281,6 +343,7 @@ namespace SourceGit.ViewModels
private string _title;
private string _fileModeChange = string.Empty;
private int _unifiedLines = 4;
private string _changeBlockIndicator = "-/-";
private bool _isTextDiff = false;
private bool _ignoreWhitespace = false;
private object _content = null;

View file

@ -45,10 +45,43 @@ namespace SourceGit.ViewModels
FillEmptyLines();
ProcessChangeBlocks();
if (previous != null && previous.File == File)
_syncScrollOffset = previous._syncScrollOffset;
}
public List<Models.TextDiffChangeBlock> ChangeBlocks { get; set; } = [];
public void ProcessChangeBlocks()
{
ChangeBlocks.Clear();
int lineIdx = 0, blockStartIdx = 0;
bool isNewBlock = true;
foreach (var line in Old) // NOTE: Same block size in both Old and New lines.
{
lineIdx++;
if (line.Type == Models.TextDiffLineType.Added ||
line.Type == Models.TextDiffLineType.Deleted ||
line.Type == Models.TextDiffLineType.None) // Empty
{
if (isNewBlock)
{
isNewBlock = false;
blockStartIdx = lineIdx;
}
}
else
{
if (!isNewBlock)
{
ChangeBlocks.Add(new Models.TextDiffChangeBlock(blockStartIdx, lineIdx - 1));
isNewBlock = true;
}
}
}
}
public void ConvertsToCombinedRange(Models.TextDiff combined, ref int startLine, ref int endLine, bool isOldSide)
{
endLine = Math.Min(endLine, combined.Lines.Count - 1);

View file

@ -42,6 +42,13 @@
<Path Width="12" Height="12" Stretch="Uniform" Margin="0,6,0,0" Data="{StaticResource Icons.Up}"/>
</Button>
<TextBlock Classes="primary"
Margin="0,0,0,0"
Text="{Binding ChangeBlockIndicator}"
FontSize="11"
TextTrimming="CharacterEllipsis"
IsVisible="{Binding IsTextDiff}"/>
<Button Classes="icon_button"
Width="28"
Click="OnGotoNextChange"
@ -124,7 +131,8 @@
<ToggleButton Classes="line_path"
Width="28" Height="18"
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSideBySideDiff, Mode=TwoWay}"
Command="{Binding ToggleTwoSideDiff}"
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSideBySideDiff, Mode=OneWay}"
IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.SideBySide}">
<Path Width="12" Height="12" Data="{StaticResource Icons.LayoutHorizontal}" Margin="0,2,0,0"/>
@ -241,7 +249,8 @@
<DataTemplate DataType="m:TextDiff">
<v:TextDiffView
UseSideBySideDiff="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSideBySideDiff, Mode=OneWay}"
UseFullTextDiff="{Binding Source={x:Static vm:Preference.Instance}, Path=UseFullTextDiff, Mode=OneWay}"/>
UseFullTextDiff="{Binding Source={x:Static vm:Preference.Instance}, Path=UseFullTextDiff, Mode=OneWay}"
CurrentChangeBlockIdx="{Binding CurrentChangeBlockIdx, Mode=OneWay}"/>
</DataTemplate>
<!-- Empty or only EOL changes -->

View file

@ -11,7 +11,16 @@ namespace SourceGit.Views
InitializeComponent();
}
public bool UseChangeBlocks { get; set; } = true;
private void OnGotoPrevChange(object _, RoutedEventArgs e)
{
if (UseChangeBlocks)
{
if (DataContext is ViewModels.DiffContext diffCtx)
diffCtx.PrevChange();
}
else
{
var textDiff = this.FindDescendantOfType<ThemedTextDiffPresenter>();
if (textDiff == null)
@ -23,8 +32,16 @@ namespace SourceGit.Views
e.Handled = true;
}
}
private void OnGotoNextChange(object _, RoutedEventArgs e)
{
if (UseChangeBlocks)
{
if (DataContext is ViewModels.DiffContext diffCtx)
diffCtx.NextChange();
}
else
{
var textDiff = this.FindDescendantOfType<ThemedTextDiffPresenter>();
if (textDiff == null)
@ -37,4 +54,5 @@ namespace SourceGit.Views
e.Handled = true;
}
}
}
}

View file

@ -30,6 +30,7 @@
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap}"
ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}"
CurrentChangeBlockIdx="{Binding #ThisControl.CurrentChangeBlockIdx}"
EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}"
SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"/>
@ -61,6 +62,7 @@
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="False"
ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}"
CurrentChangeBlockIdx="{Binding #ThisControl.CurrentChangeBlockIdx}"
EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}"
SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"/>
@ -82,6 +84,7 @@
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="False"
ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}"
CurrentChangeBlockIdx="{Binding #ThisControl.CurrentChangeBlockIdx}"
EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}"
SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"/>

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using Avalonia;
@ -254,6 +255,10 @@ namespace SourceGit.Views
if (_presenter.Document == null || !textView.VisualLinesValid)
return;
var changeBlock = _presenter.GetCurrentChangeBlock();
Brush changeBlockBG = new SolidColorBrush(Colors.Gray, 0.25);
Pen changeBlockFG = new Pen(Brushes.Gray, 1);
var lines = _presenter.GetLines();
var width = textView.Bounds.Width;
foreach (var line in textView.VisualLines)
@ -266,12 +271,14 @@ namespace SourceGit.Views
break;
var info = lines[index - 1];
var bg = GetBrushByLineType(info.Type);
if (bg == null)
continue;
var startY = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.LineTop) - textView.VerticalOffset;
var endY = line.GetTextLineVisualYPosition(line.TextLines[^1], VisualYPosition.LineBottom) - textView.VerticalOffset;
var bg = GetBrushByLineType(info.Type);
if (bg != null)
{
if (bg != null)
drawingContext.DrawRectangle(bg, null, new Rect(0, startY, width, endY - startY));
if (info.Highlights.Count > 0)
@ -279,7 +286,7 @@ namespace SourceGit.Views
var highlightBG = info.Type == Models.TextDiffLineType.Added ? _presenter.AddedHighlightBrush : _presenter.DeletedHighlightBrush;
var processingIdxStart = 0;
var processingIdxEnd = 0;
var nextHightlight = 0;
var nextHighlight = 0;
foreach (var tl in line.TextLines)
{
@ -288,9 +295,9 @@ namespace SourceGit.Views
var y = line.GetTextLineVisualYPosition(tl, VisualYPosition.LineTop) - textView.VerticalOffset;
var h = line.GetTextLineVisualYPosition(tl, VisualYPosition.LineBottom) - textView.VerticalOffset - y;
while (nextHightlight < info.Highlights.Count)
while (nextHighlight < info.Highlights.Count)
{
var highlight = info.Highlights[nextHightlight];
var highlight = info.Highlights[nextHighlight];
if (highlight.Start >= processingIdxEnd)
break;
@ -305,13 +312,23 @@ namespace SourceGit.Views
if (highlight.End >= processingIdxEnd)
break;
nextHightlight++;
nextHighlight++;
}
processingIdxStart = processingIdxEnd;
}
}
}
if (changeBlock != null && changeBlock.IsInRange(index))
{
drawingContext.DrawRectangle(changeBlockBG, null, new Rect(0, startY, width, endY - startY));
if (index == changeBlock.StartLine)
drawingContext.DrawLine(changeBlockFG, new Point(0, startY), new Point(width, startY));
if (index == changeBlock.EndLine)
drawingContext.DrawLine(changeBlockFG, new Point(0, endY), new Point(width, endY));
}
}
}
private IBrush GetBrushByLineType(Models.TextDiffLineType type)
@ -486,6 +503,15 @@ namespace SourceGit.Views
set => SetValue(DisplayRangeProperty, value);
}
public static readonly StyledProperty<int> CurrentChangeBlockIdxProperty =
AvaloniaProperty.Register<ThemedTextDiffPresenter, int>(nameof(CurrentChangeBlockIdx));
public int CurrentChangeBlockIdx
{
get => GetValue(CurrentChangeBlockIdxProperty);
set => SetValue(CurrentChangeBlockIdxProperty, value);
}
protected override Type StyleKeyOverride => typeof(TextEditor);
public ThemedTextDiffPresenter(TextArea area, TextDocument doc) : base(area, doc)
@ -590,6 +616,27 @@ namespace SourceGit.Views
}
}
public Models.TextDiffChangeBlock GetCurrentChangeBlock()
{
return GetChangeBlock(CurrentChangeBlockIdx);
}
public virtual Models.TextDiffChangeBlock GetChangeBlock(int changeBlockIdx)
{
return null;
}
public void JumpToChangeBlock(int changeBlockIdx)
{
var changeBlock = GetChangeBlock(changeBlockIdx);
if (changeBlock != null)
{
TextArea.Caret.Line = changeBlock.StartLine;
//TextArea.Caret.BringCaretToView(); // NOTE: Brings caret line (barely) into view.
ScrollToLine(changeBlock.StartLine); // NOTE: Brings specified line into center of view.
}
}
public override void Render(DrawingContext context)
{
base.Render(context);
@ -1018,6 +1065,16 @@ namespace SourceGit.Views
}
}
public override Models.TextDiffChangeBlock GetChangeBlock(int changeBlockIdx)
{
if (DataContext is Models.TextDiff diff)
{
if (changeBlockIdx >= 0 && changeBlockIdx < diff.ChangeBlocks.Count)
return diff.ChangeBlocks[changeBlockIdx];
}
return null;
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
@ -1089,6 +1146,8 @@ namespace SourceGit.Views
public void ForceSyncScrollOffset()
{
if (_scrollViewer == null)
return;
if (DataContext is ViewModels.TwoSideTextDiff diff)
diff.SyncScrollOffset = _scrollViewer?.Offset ?? Vector.Zero;
}
@ -1234,6 +1293,16 @@ namespace SourceGit.Views
}
}
public override Models.TextDiffChangeBlock GetChangeBlock(int changeBlockIdx)
{
if (DataContext is ViewModels.TwoSideTextDiff diff)
{
if (changeBlockIdx >= 0 && changeBlockIdx < diff.ChangeBlocks.Count)
return diff.ChangeBlocks[changeBlockIdx];
}
return null;
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
@ -1480,6 +1549,15 @@ namespace SourceGit.Views
set => SetValue(EnableChunkSelectionProperty, value);
}
public static readonly StyledProperty<int> CurrentChangeBlockIdxProperty =
AvaloniaProperty.Register<TextDiffView, int>(nameof(CurrentChangeBlockIdx));
public int CurrentChangeBlockIdx
{
get => GetValue(CurrentChangeBlockIdxProperty);
set => SetValue(CurrentChangeBlockIdxProperty, value);
}
static TextDiffView()
{
UseSideBySideDiffProperty.Changed.AddClassHandler<TextDiffView>((v, _) =>
@ -1506,6 +1584,19 @@ namespace SourceGit.Views
v.Popup.Margin = new Thickness(0, top, right, 0);
v.Popup.IsVisible = true;
});
CurrentChangeBlockIdxProperty.Changed.AddClassHandler<TextDiffView>((v, e) =>
{
if ((int)e.NewValue >= 0 && v.Editor.Presenter != null)
{
foreach (var p in v.Editor.Presenter.GetVisualDescendants().OfType<ThemedTextDiffPresenter>())
{
p.JumpToChangeBlock((int)e.NewValue);
if (p is SingleSideTextDiffPresenter ssp)
ssp.ForceSyncScrollOffset();
}
}
});
}
public TextDiffView()
@ -1553,6 +1644,8 @@ namespace SourceGit.Views
IsUnstagedChange = diff.Option.IsUnstaged;
EnableChunkSelection = diff.Option.WorkingCopyChange != null;
diff.CurrentChangeBlockIdx = -1; // Unset current change block.
}
private void OnStageChunk(object _1, RoutedEventArgs _2)