Compare commits

...

19 commits

Author SHA1 Message Date
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
leo
882878dbe5
refactor: text diff view go to next/prev change
Some checks are pending
Continuous Integration / Package (push) Blocked by required conditions
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Signed-off-by: leo <longshuang@msn.cn>
2024-11-16 18:24:37 +08: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
leo
52c7388a38
project: upgrade to .NET 9 (#694)
Signed-off-by: leo <longshuang@msn.cn>
2024-11-16 16:06:30 +08:00
leo
134c71064e
feature: add buttons to go to prev/next change in text diff view (#616)
Signed-off-by: leo <longshuang@msn.cn>
2024-11-16 15:17:29 +08:00
leo
cd137e222c
feature: enable --no-ext-diff for git diff command
Signed-off-by: leo <longshuang@msn.cn>
2024-11-16 09:26:13 +08:00
leo
8d84d0f6a1
enhance: improve update filter mode performance
Signed-off-by: leo <longshuang@msn.cn>
2024-11-16 09:14:57 +08:00
13 changed files with 494 additions and 83 deletions

View file

@ -32,7 +32,7 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: 8.0.x dotnet-version: 9.0.x
- name: Configure arm64 packages - name: Configure arm64 packages
if: ${{ matrix.runtime == 'linux-arm64' }} if: ${{ matrix.runtime == 'linux-arm64' }}
run: | run: |

View file

@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "8.0.0", "version": "9.0.0",
"rollForward": "latestMajor", "rollForward": "latestMajor",
"allowPrerelease": false "allowPrerelease": false
} }

View file

@ -28,9 +28,9 @@ namespace SourceGit.Commands
Context = repo; Context = repo;
if (ignoreWhitespace) if (ignoreWhitespace)
Args = $"diff --patch --ignore-cr-at-eol --ignore-all-space --unified={unified} {opt}"; Args = $"diff --no-ext-diff --patch --ignore-cr-at-eol --ignore-all-space --unified={unified} {opt}";
else else
Args = $"diff --patch --ignore-cr-at-eol --unified={unified} {opt}"; Args = $"diff --no-ext-diff --patch --ignore-cr-at-eol --unified={unified} {opt}";
} }
public Models.DiffResult Result() public Models.DiffResult Result()
@ -51,6 +51,9 @@ namespace SourceGit.Commands
_result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine); _result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine);
} }
if (_result.TextDiff != null)
_result.TextDiff.ProcessChangeBlocks();
return _result; return _result;
} }

View file

@ -2,6 +2,8 @@
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using CommunityToolkit.Mvvm.ComponentModel;
using Avalonia; using Avalonia;
using Avalonia.Media.Imaging; 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 string File { get; set; } = string.Empty;
public List<TextDiffLine> Lines { get; set; } = new List<TextDiffLine>(); public List<TextDiffLine> Lines { get; set; } = new List<TextDiffLine>();
public Vector ScrollOffset { get; set; } = Vector.Zero; public Vector ScrollOffset { get; set; } = Vector.Zero;
public int MaxLineNumber = 0; public int MaxLineNumber = 0;
public int CurrentChangeBlockIdx
{
get => _currentChangeBlockIdx;
set => SetProperty(ref _currentChangeBlockIdx, value);
}
public string Repo { get; set; } = null; public string Repo { get; set; } = null;
public DiffOption Option { 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) public TextDiffSelection MakeSelection(int startLine, int endLine, bool isCombined, bool isOldSide)
{ {
var rs = new TextDiffSelection(); var rs = new TextDiffSelection();
@ -626,6 +682,8 @@ namespace SourceGit.Models
return true; return true;
} }
private int _currentChangeBlockIdx = -1; // NOTE: Use -1 as "not set".
[GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")] [GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")]
private static partial Regex REG_INDICATOR(); private static partial Regex REG_INDICATOR();
} }

View file

@ -152,18 +152,12 @@ namespace SourceGit.Models
set; set;
} = "---"; } = "---";
public FilterMode GetHistoriesFilterMode(string pattern, FilterType type) public Dictionary<string, FilterMode> CollectHistoriesFilters()
{ {
var map = new Dictionary<string, FilterMode>();
foreach (var filter in HistoriesFilters) foreach (var filter in HistoriesFilters)
{ map.Add(filter.Pattern, filter.Mode);
if (filter.Type != type) return map;
continue;
if (filter.Pattern.Equals(pattern, StringComparison.Ordinal))
return filter.Mode;
}
return FilterMode.None;
} }
public bool UpdateHistoriesFilter(string pattern, FilterType type, FilterMode mode) public bool UpdateHistoriesFilter(string pattern, FilterType type, FilterMode mode)

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>App.manifest</ApplicationManifest> <ApplicationManifest>App.manifest</ApplicationManifest>
<ApplicationIcon>App.ico</ApplicationIcon> <ApplicationIcon>App.ico</ApplicationIcon>

View file

@ -51,6 +51,12 @@ namespace SourceGit.ViewModels
private set => SetProperty(ref _unifiedLines, value); 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) public DiffContext(string repo, Models.DiffOption option, DiffContext previous = null)
{ {
_repo = repo; _repo = repo;
@ -73,6 +79,54 @@ namespace SourceGit.ViewModels
LoadDiffContent(); 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() public void ToggleFullTextDiff()
{ {
Preference.Instance.UseFullTextDiff = !Preference.Instance.UseFullTextDiff; Preference.Instance.UseFullTextDiff = !Preference.Instance.UseFullTextDiff;
@ -217,7 +271,9 @@ namespace SourceGit.ViewModels
FileModeChange = latest.FileModeChange; FileModeChange = latest.FileModeChange;
Content = rs; Content = rs;
IsTextDiff = rs is Models.TextDiff; IsTextDiff = rs is Models.TextDiff;
});
RefreshChangeBlockIndicator();
});
}); });
} }
@ -281,6 +337,7 @@ namespace SourceGit.ViewModels
private string _title; private string _title;
private string _fileModeChange = string.Empty; private string _fileModeChange = string.Empty;
private int _unifiedLines = 4; private int _unifiedLines = 4;
private string _changeBlockIndicator = "-/-";
private bool _isTextDiff = false; private bool _isTextDiff = false;
private bool _ignoreWhitespace = false; private bool _ignoreWhitespace = false;
private object _content = null; private object _content = null;

View file

@ -827,9 +827,6 @@ namespace SourceGit.ViewModels
public void RefreshTags() public void RefreshTags()
{ {
var tags = new Commands.QueryTags(_fullpath).Result(); var tags = new Commands.QueryTags(_fullpath).Result();
foreach (var tag in tags)
tag.FilterMode = _settings.GetHistoriesFilterMode(tag.Name, Models.FilterType.Tag);
Dispatcher.UIThread.Invoke(() => Dispatcher.UIThread.Invoke(() =>
{ {
Tags = tags; Tags = tags;
@ -2035,8 +2032,9 @@ namespace SourceGit.ViewModels
builder.Run(visibles, remotes, true); builder.Run(visibles, remotes, true);
} }
UpdateBranchTreeFilterMode(builder.Locals, true); var historiesFilters = _settings.CollectHistoriesFilters();
UpdateBranchTreeFilterMode(builder.Remotes, false); UpdateBranchTreeFilterMode(builder.Locals, historiesFilters);
UpdateBranchTreeFilterMode(builder.Remotes, historiesFilters);
return builder; return builder;
} }
@ -2056,7 +2054,8 @@ namespace SourceGit.ViewModels
} }
} }
UpdateTagFilterMode(); var historiesFilters = _settings.CollectHistoriesFilters();
UpdateTagFilterMode(historiesFilters);
return visible; return visible;
} }
@ -2080,32 +2079,36 @@ namespace SourceGit.ViewModels
private void RefreshHistoriesFilters() private void RefreshHistoriesFilters()
{ {
UpdateBranchTreeFilterMode(LocalBranchTrees, true); var filters = _settings.CollectHistoriesFilters();
UpdateBranchTreeFilterMode(RemoteBranchTrees, false); UpdateBranchTreeFilterMode(LocalBranchTrees, filters);
UpdateTagFilterMode(); UpdateBranchTreeFilterMode(RemoteBranchTrees, filters);
UpdateTagFilterMode(filters);
Task.Run(RefreshCommits); Task.Run(RefreshCommits);
} }
private void UpdateBranchTreeFilterMode(List<BranchTreeNode> nodes, bool isLocal) private void UpdateBranchTreeFilterMode(List<BranchTreeNode> nodes, Dictionary<string, Models.FilterMode> filters)
{ {
foreach (var node in nodes) foreach (var node in nodes)
{ {
if (node.IsBranch) if (filters.TryGetValue(node.Path, out var value))
{ node.FilterMode = value;
node.FilterMode = _settings.GetHistoriesFilterMode(node.Path, isLocal ? Models.FilterType.LocalBranch : Models.FilterType.RemoteBranch);
}
else else
{ node.FilterMode = Models.FilterMode.None;
node.FilterMode = _settings.GetHistoriesFilterMode(node.Path, isLocal ? Models.FilterType.LocalBranchFolder : Models.FilterType.RemoteBranchFolder);
UpdateBranchTreeFilterMode(node.Children, isLocal); if (!node.IsBranch)
} UpdateBranchTreeFilterMode(node.Children, filters);
} }
} }
private void UpdateTagFilterMode() private void UpdateTagFilterMode(Dictionary<string, Models.FilterMode> filters)
{ {
foreach (var tag in _tags) foreach (var tag in _tags)
tag.FilterMode = _settings.GetHistoriesFilterMode(tag.Name, Models.FilterType.Tag); {
if (filters.TryGetValue(tag.Name, out var value))
tag.FilterMode = value;
else
tag.FilterMode = Models.FilterMode.None;
}
} }
private void ResetBranchTreeFilterMode(List<BranchTreeNode> nodes) private void ResetBranchTreeFilterMode(List<BranchTreeNode> nodes)

View file

@ -45,10 +45,43 @@ namespace SourceGit.ViewModels
FillEmptyLines(); FillEmptyLines();
ProcessChangeBlocks();
if (previous != null && previous.File == File) if (previous != null && previous.File == File)
_syncScrollOffset = previous._syncScrollOffset; _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) public void ConvertsToCombinedRange(Models.TextDiff combined, ref int startLine, ref int endLine, bool isOldSide)
{ {
endLine = Math.Min(endLine, combined.Lines.Count - 1); endLine = Math.Min(endLine, combined.Lines.Count - 1);

View file

@ -34,8 +34,31 @@
<!-- Toolbar Buttons --> <!-- Toolbar Buttons -->
<StackPanel Grid.Column="3" Margin="8,0,0,0" Orientation="Horizontal" VerticalAlignment="Center"> <StackPanel Grid.Column="3" Margin="8,0,0,0" Orientation="Horizontal" VerticalAlignment="Center">
<Button Classes="icon_button"
Width="28"
Click="OnGotoPrevChange"
IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.Prev}">
<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"
IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.Next}">
<Path Width="12" Height="12" Stretch="Uniform" Margin="0,6,0,0" Data="{StaticResource Icons.Down}"/>
</Button>
<Button Classes="icon_button" <Button Classes="icon_button"
Width="32" Width="28"
Command="{Binding IncrUnified}" Command="{Binding IncrUnified}"
IsVisible="{Binding IsTextDiff}" IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.VisualLines.Incr}"> ToolTip.Tip="{DynamicResource Text.Diff.VisualLines.Incr}">
@ -46,7 +69,7 @@
</Button> </Button>
<Button Classes="icon_button" <Button Classes="icon_button"
Width="32" Width="28"
Command="{Binding DecrUnified}" Command="{Binding DecrUnified}"
IsVisible="{Binding IsTextDiff}" IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.VisualLines.Decr}"> ToolTip.Tip="{DynamicResource Text.Diff.VisualLines.Decr}">
@ -60,9 +83,7 @@
</Button> </Button>
<ToggleButton Classes="line_path" <ToggleButton Classes="line_path"
Width="32" Height="18" Width="28"
Background="Transparent"
Padding="9,6"
Command="{Binding ToggleFullTextDiff}" Command="{Binding ToggleFullTextDiff}"
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=UseFullTextDiff, Mode=OneWay}" IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=UseFullTextDiff, Mode=OneWay}"
IsVisible="{Binding IsTextDiff}" IsVisible="{Binding IsTextDiff}"
@ -71,9 +92,8 @@
</ToggleButton> </ToggleButton>
<ToggleButton Classes="line_path" <ToggleButton Classes="line_path"
Width="32" Height="18" Width="28"
Background="Transparent" Background="Transparent"
Padding="9,6"
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting, Mode=TwoWay}" IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting, Mode=TwoWay}"
IsVisible="{Binding IsTextDiff}" IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.SyntaxHighlight}"> ToolTip.Tip="{DynamicResource Text.Diff.SyntaxHighlight}">
@ -81,9 +101,7 @@
</ToggleButton> </ToggleButton>
<ToggleButton Classes="line_path" <ToggleButton Classes="line_path"
Width="32" Height="18" Width="28"
Background="Transparent"
Padding="9,6"
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap, Mode=TwoWay}" IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap, Mode=TwoWay}"
ToolTip.Tip="{DynamicResource Text.Diff.ToggleWordWrap}"> ToolTip.Tip="{DynamicResource Text.Diff.ToggleWordWrap}">
<ToggleButton.IsVisible> <ToggleButton.IsVisible>
@ -97,14 +115,14 @@
</ToggleButton> </ToggleButton>
<ToggleButton Classes="line_path" <ToggleButton Classes="line_path"
Width="32" Width="28"
IsChecked="{Binding IgnoreWhitespace, Mode=TwoWay}" IsChecked="{Binding IgnoreWhitespace, Mode=TwoWay}"
ToolTip.Tip="{DynamicResource Text.Diff.IgnoreWhitespace}"> ToolTip.Tip="{DynamicResource Text.Diff.IgnoreWhitespace}">
<Path Width="14" Height="14" Stretch="Uniform" Data="{StaticResource Icons.Whitespace}"/> <Path Width="14" Height="14" Stretch="Uniform" Data="{StaticResource Icons.Whitespace}"/>
</ToggleButton> </ToggleButton>
<ToggleButton Classes="line_path" <ToggleButton Classes="line_path"
Width="32" Width="28"
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView, Mode=TwoWay}" IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView, Mode=TwoWay}"
IsVisible="{Binding IsTextDiff}" IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.ShowHiddenSymbols}"> ToolTip.Tip="{DynamicResource Text.Diff.ShowHiddenSymbols}">
@ -112,16 +130,14 @@
</ToggleButton> </ToggleButton>
<ToggleButton Classes="line_path" <ToggleButton Classes="line_path"
Width="32" Height="18" Width="28" Height="18"
Background="Transparent"
Padding="9,6"
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSideBySideDiff, Mode=TwoWay}" IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSideBySideDiff, Mode=TwoWay}"
IsVisible="{Binding IsTextDiff}" IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.SideBySide}"> ToolTip.Tip="{DynamicResource Text.Diff.SideBySide}">
<Path Width="12" Height="12" Data="{StaticResource Icons.LayoutHorizontal}" Margin="0,2,0,0"/> <Path Width="12" Height="12" Data="{StaticResource Icons.LayoutHorizontal}" Margin="0,2,0,0"/>
</ToggleButton> </ToggleButton>
<Button Classes="icon_button" Width="32" Command="{Binding OpenExternalMergeTool}" ToolTip.Tip="{DynamicResource Text.Diff.UseMerger}"> <Button Classes="icon_button" Width="28" Command="{Binding OpenExternalMergeTool}" ToolTip.Tip="{DynamicResource Text.Diff.UseMerger}">
<Path Width="12" Height="12" Stretch="Uniform" Data="{StaticResource Icons.OpenWith}"/> <Path Width="12" Height="12" Stretch="Uniform" Data="{StaticResource Icons.OpenWith}"/>
</Button> </Button>
</StackPanel> </StackPanel>
@ -232,7 +248,8 @@
<DataTemplate DataType="m:TextDiff"> <DataTemplate DataType="m:TextDiff">
<v:TextDiffView <v:TextDiffView
UseSideBySideDiff="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSideBySideDiff, Mode=OneWay}" 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> </DataTemplate>
<!-- Empty or only EOL changes --> <!-- Empty or only EOL changes -->

View file

@ -1,4 +1,6 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace SourceGit.Views namespace SourceGit.Views
{ {
@ -8,5 +10,49 @@ namespace SourceGit.Views
{ {
InitializeComponent(); 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)
return;
textDiff.GotoPrevChange();
if (textDiff is SingleSideTextDiffPresenter presenter)
presenter.ForceSyncScrollOffset();
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)
return;
textDiff.GotoNextChange();
if (textDiff is SingleSideTextDiffPresenter presenter)
presenter.ForceSyncScrollOffset();
e.Handled = true;
}
}
} }
} }

View file

@ -27,6 +27,7 @@
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}" UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap}" WordWrap="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap}"
ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}" ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}"
CurrentChangeBlockIdx="{Binding #ThisControl.CurrentChangeBlockIdx}"
EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}" EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}"
SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"/> SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"/>
</DataTemplate> </DataTemplate>
@ -49,6 +50,7 @@
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}" UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="False" WordWrap="False"
ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}" ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}"
CurrentChangeBlockIdx="{Binding #ThisControl.CurrentChangeBlockIdx}"
EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}" EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}"
SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"/> SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"/>
@ -70,6 +72,7 @@
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}" UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="False" WordWrap="False"
ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}" ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}"
CurrentChangeBlockIdx="{Binding #ThisControl.CurrentChangeBlockIdx}"
EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}" EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}"
SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"/> SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"/>
</Grid> </Grid>

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
using Avalonia; using Avalonia;
@ -243,6 +244,10 @@ namespace SourceGit.Views
if (_presenter.Document == null || !textView.VisualLinesValid) if (_presenter.Document == null || !textView.VisualLinesValid)
return; 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 lines = _presenter.GetLines();
var width = textView.Bounds.Width; var width = textView.Bounds.Width;
foreach (var line in textView.VisualLines) foreach (var line in textView.VisualLines)
@ -255,51 +260,63 @@ namespace SourceGit.Views
break; break;
var info = lines[index - 1]; 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 startY = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.LineTop) - textView.VerticalOffset;
var endY = line.GetTextLineVisualYPosition(line.TextLines[^1], VisualYPosition.LineBottom) - textView.VerticalOffset; var endY = line.GetTextLineVisualYPosition(line.TextLines[^1], VisualYPosition.LineBottom) - textView.VerticalOffset;
drawingContext.DrawRectangle(bg, null, new Rect(0, startY, width, endY - startY));
if (info.Highlights.Count > 0) var bg = GetBrushByLineType(info.Type);
if (bg != null)
{ {
var highlightBG = info.Type == Models.TextDiffLineType.Added ? _presenter.AddedHighlightBrush : _presenter.DeletedHighlightBrush; if (bg != null)
var processingIdxStart = 0; drawingContext.DrawRectangle(bg, null, new Rect(0, startY, width, endY - startY));
var processingIdxEnd = 0;
var nextHightlight = 0;
foreach (var tl in line.TextLines) if (info.Highlights.Count > 0)
{ {
processingIdxEnd += tl.Length; var highlightBG = info.Type == Models.TextDiffLineType.Added ? _presenter.AddedHighlightBrush : _presenter.DeletedHighlightBrush;
var processingIdxStart = 0;
var processingIdxEnd = 0;
var nextHighlight = 0;
var y = line.GetTextLineVisualYPosition(tl, VisualYPosition.LineTop) - textView.VerticalOffset; foreach (var tl in line.TextLines)
var h = line.GetTextLineVisualYPosition(tl, VisualYPosition.LineBottom) - textView.VerticalOffset - y;
while (nextHightlight < info.Highlights.Count)
{ {
var highlight = info.Highlights[nextHightlight]; processingIdxEnd += tl.Length;
if (highlight.Start >= processingIdxEnd)
break;
var start = line.GetVisualColumn(highlight.Start < processingIdxStart ? processingIdxStart : highlight.Start); var y = line.GetTextLineVisualYPosition(tl, VisualYPosition.LineTop) - textView.VerticalOffset;
var end = line.GetVisualColumn(highlight.End >= processingIdxEnd ? processingIdxEnd : highlight.End + 1); var h = line.GetTextLineVisualYPosition(tl, VisualYPosition.LineBottom) - textView.VerticalOffset - y;
var x = line.GetTextLineVisualXPosition(tl, start) - textView.HorizontalOffset; while (nextHighlight < info.Highlights.Count)
var w = line.GetTextLineVisualXPosition(tl, end) - textView.HorizontalOffset - x; {
var rect = new Rect(x, y, w, h); var highlight = info.Highlights[nextHighlight];
drawingContext.DrawRectangle(highlightBG, null, rect); if (highlight.Start >= processingIdxEnd)
break;
if (highlight.End >= processingIdxEnd) var start = line.GetVisualColumn(highlight.Start < processingIdxStart ? processingIdxStart : highlight.Start);
break; var end = line.GetVisualColumn(highlight.End >= processingIdxEnd ? processingIdxEnd : highlight.End + 1);
nextHightlight++; var x = line.GetTextLineVisualXPosition(tl, start) - textView.HorizontalOffset;
var w = line.GetTextLineVisualXPosition(tl, end) - textView.HorizontalOffset - x;
var rect = new Rect(x, y, w, h);
drawingContext.DrawRectangle(highlightBG, null, rect);
if (highlight.End >= processingIdxEnd)
break;
nextHighlight++;
}
processingIdxStart = processingIdxEnd;
} }
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));
}
} }
} }
@ -466,6 +483,15 @@ namespace SourceGit.Views
set => SetValue(SelectedChunkProperty, value); set => SetValue(SelectedChunkProperty, 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); protected override Type StyleKeyOverride => typeof(TextEditor);
public ThemedTextDiffPresenter(TextArea area, TextDocument doc) : base(area, doc) public ThemedTextDiffPresenter(TextArea area, TextDocument doc) : base(area, doc)
@ -498,6 +524,127 @@ namespace SourceGit.Views
{ {
} }
public void GotoPrevChange()
{
var view = TextArea.TextView;
var lines = GetLines();
var firstLineIdx = lines.Count;
foreach (var line in view.VisualLines)
{
if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted)
continue;
var index = line.FirstDocumentLine.LineNumber - 1;
if (index >= lines.Count)
continue;
if (firstLineIdx > index)
firstLineIdx = index;
}
if (firstLineIdx <= 1)
return;
var firstLineType = lines[firstLineIdx].Type;
var prevLineType = lines[firstLineIdx - 1].Type;
var isChangeFirstLine = firstLineType != Models.TextDiffLineType.Normal && firstLineType != Models.TextDiffLineType.Indicator;
var isChangePrevLine = prevLineType != Models.TextDiffLineType.Normal && prevLineType != Models.TextDiffLineType.Indicator;
if (isChangeFirstLine && isChangePrevLine)
{
for (var i = firstLineIdx - 2; i >= 0; i--)
{
var prevType = lines[i].Type;
if (prevType == Models.TextDiffLineType.Normal || prevType == Models.TextDiffLineType.Indicator)
{
ScrollToLine(i + 2);
return;
}
}
}
var findChange = false;
for (var i = firstLineIdx - 1; i >= 0; i--)
{
var prevType = lines[i].Type;
if (prevType == Models.TextDiffLineType.Normal || prevType == Models.TextDiffLineType.Indicator)
{
if (findChange)
{
ScrollToLine(i + 2);
return;
}
}
else if (!findChange)
{
findChange = true;
}
}
}
public void GotoNextChange()
{
var view = TextArea.TextView;
var lines = GetLines();
var lastLineIdx = -1;
foreach (var line in view.VisualLines)
{
if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted)
continue;
var index = line.FirstDocumentLine.LineNumber - 1;
if (index >= lines.Count)
continue;
if (lastLineIdx < index)
lastLineIdx = index;
}
if (lastLineIdx >= lines.Count - 1)
return;
var lastLineType = lines[lastLineIdx].Type;
var findNormalLine = lastLineType == Models.TextDiffLineType.Normal || lastLineType == Models.TextDiffLineType.Indicator;
for (var idx = lastLineIdx + 1; idx < lines.Count; idx++)
{
var nextType = lines[idx].Type;
if (nextType == Models.TextDiffLineType.None ||
nextType == Models.TextDiffLineType.Added ||
nextType == Models.TextDiffLineType.Deleted)
{
if (findNormalLine)
{
ScrollToLine(idx + 1);
return;
}
}
else if (!findNormalLine)
{
findNormalLine = true;
}
}
}
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) public override void Render(DrawingContext context)
{ {
base.Render(context); base.Render(context);
@ -895,6 +1042,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) protected override void OnLoaded(RoutedEventArgs e)
{ {
base.OnLoaded(e); base.OnLoaded(e);
@ -968,6 +1125,12 @@ namespace SourceGit.Views
TextArea.LeftMargins.Add(new LineModifyTypeMargin()); TextArea.LeftMargins.Add(new LineModifyTypeMargin());
} }
public void ForceSyncScrollOffset()
{
if (DataContext is ViewModels.TwoSideTextDiff diff)
diff.SyncScrollOffset = _scrollViewer.Offset;
}
public override List<Models.TextDiffLine> GetLines() public override List<Models.TextDiffLine> GetLines()
{ {
if (DataContext is ViewModels.TwoSideTextDiff diff) if (DataContext is ViewModels.TwoSideTextDiff diff)
@ -1109,6 +1272,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) protected override void OnLoaded(RoutedEventArgs e)
{ {
base.OnLoaded(e); base.OnLoaded(e);
@ -1241,6 +1414,15 @@ namespace SourceGit.Views
set => SetValue(EnableChunkSelectionProperty, value); 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() static TextDiffView()
{ {
UseSideBySideDiffProperty.Changed.AddClassHandler<TextDiffView>((v, _) => UseSideBySideDiffProperty.Changed.AddClassHandler<TextDiffView>((v, _) =>
@ -1267,6 +1449,19 @@ namespace SourceGit.Views
v.Popup.Margin = new Thickness(0, top, right, 0); v.Popup.Margin = new Thickness(0, top, right, 0);
v.Popup.IsVisible = true; v.Popup.IsVisible = true;
}); });
CurrentChangeBlockIdxProperty.Changed.AddClassHandler<TextDiffView>((v, e) =>
{
if (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() public TextDiffView()
@ -1314,6 +1509,8 @@ namespace SourceGit.Views
IsUnstagedChange = diff.Option.IsUnstaged; IsUnstagedChange = diff.Option.IsUnstaged;
EnableChunkSelection = diff.Option.WorkingCopyChange != null; EnableChunkSelection = diff.Option.WorkingCopyChange != null;
diff.CurrentChangeBlockIdx = -1; // Unset current change block.
} }
private void OnStageChunk(object _1, RoutedEventArgs _2) private void OnStageChunk(object _1, RoutedEventArgs _2)