Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Resources/Locales/en_US.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@
<x:String x:Key="Text.Diff.Next" xml:space="preserve">Next Difference</x:String>
<x:String x:Key="Text.Diff.NoChange" xml:space="preserve">NO CHANGES OR ONLY EOL CHANGES</x:String>
<x:String x:Key="Text.Diff.Old" xml:space="preserve">OLD</x:String>
<x:String x:Key="Text.Diff.OverscrollFileNavigation" xml:space="preserve">Scroll Past Edges to Switch Files</x:String>
<x:String x:Key="Text.Diff.Prev" xml:space="preserve">Previous Difference</x:String>
<x:String x:Key="Text.Diff.SaveAsPatch" xml:space="preserve">Save as Patch</x:String>
<x:String x:Key="Text.Diff.ShowHiddenSymbols" xml:space="preserve">Show hidden symbols</x:String>
Expand Down
7 changes: 7 additions & 0 deletions src/ViewModels/Preferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,12 @@ public bool UseSyntaxHighlighting
set => SetProperty(ref _useSyntaxHighlighting, value);
}

public bool EnableOverscrollFileNavigation
{
get => _enableOverscrollFileNavigation;
set => SetProperty(ref _enableOverscrollFileNavigation, value);
}

public bool IgnoreCRAtEOLInDiff
{
get => _ignoreCRAtEOLInDiff;
Expand Down Expand Up @@ -836,6 +842,7 @@ private bool RemoveInvalidRepositoriesRecursive(List<RepositoryNode> collection)
private bool _ignoreWhitespaceChangesInDiff = false;
private bool _ignoreCRAtEOLInDiff = true;
private bool _useSyntaxHighlighting = false;
private bool _enableOverscrollFileNavigation = true;
private bool _enableDiffViewWordWrap = false;
private bool _showHiddenSymbolsInDiffView = false;
private bool _useFullTextDiff = false;
Expand Down
48 changes: 48 additions & 0 deletions src/Views/ChangeCollectionView.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,54 @@ public Models.Change GetNextChangeWithoutSelection()
return null;
}

public Models.Change SelectAdjacentChange(bool next)
{
if (_selection is not { Count: 1, HasFolder: false })
return null;

var current = _selection.Changes[0];
var step = next ? 1 : -1;
Models.Change target = null;

if (Content is ViewModels.ChangeCollectionAsTree tree)
{
var idx = -1;
for (var i = 0; i < tree.Rows.Count; i++)
{
if (tree.Rows[i].Change == current)
{
idx = i;
break;
}
}

if (idx < 0)
return null;

// Walk from the current row in the chosen direction until the first file row; folder rows are skipped.
for (var i = idx + step; i >= 0 && i < tree.Rows.Count; i += step)
{
var row = tree.Rows[i];
if (!row.IsFolder && row.Change != null)
{
target = row.Change;
break;
}
}
}
else if (_changes != null)
{
var idx = _changes.IndexOf(current);
if (idx >= 0 && idx + step >= 0 && idx + step < _changes.Count)
target = _changes[idx + step];
}

if (target != null)
Selection = new ViewModels.ChangeSelection(new List<Models.Change>() { target });

return target;
}

public void TakeFocus()
{
var container = this.FindDescendantOfType<ChangeCollectionContainer>();
Expand Down
8 changes: 8 additions & 0 deletions src/Views/DiffView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,14 @@
<Path Width="13" Height="13" Data="{StaticResource Icons.SyntaxHighlight}" Margin="0,3,0,0"/>
</ToggleButton>

<ToggleButton Classes="line_path"
Width="28"
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=EnableOverscrollFileNavigation, Mode=TwoWay}"
IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.OverscrollFileNavigation}">
<Path Width="12" Height="12" Stretch="Uniform" Data="{StaticResource Icons.DoubleDown}" Margin="0,2,0,0"/>
</ToggleButton>

<ToggleButton Classes="line_path"
Width="28"
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=EnableDiffViewWordWrap, Mode=TwoWay}"
Expand Down
125 changes: 125 additions & 0 deletions src/Views/TextDiffView.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,33 @@ protected override void OnDataContextChanged(EventArgs e)
if (ctx.IsSideBySide() && !IsOld)
return;

// Diffs loading in other windows must not end the wait of the window that switched files.
var isSwitchedWindow = TopLevel.GetTopLevel(this) == s_overscroll.Window;

if (s_overscroll.WaitingForNewDiff && isSwitchedWindow)
{
Dispatcher.UIThread.Post(() =>
{
s_overscroll.WaitingForNewDiff = false;
s_overscroll.Window = null;
s_overscroll.IgnoreUntil = ExtentHeight > ViewportHeight + 1 ? 0 : Environment.TickCount64 + 150;
}, DispatcherPriority.Background);
}

// Navigated upwards: show the previous file from its end instead of the first change.
if (s_overscroll.PendingScrollToEndPath != null && isSwitchedWindow)
{
var scrollToEnd = Environment.TickCount64 - s_overscroll.SwitchTime < 1000 &&
s_overscroll.PendingScrollToEndPath.Equals(ctx.Option.Path, StringComparison.Ordinal);
s_overscroll.PendingScrollToEndPath = null;

if (scrollToEnd)
{
Dispatcher.UIThread.Post(ScrollToEnd, DispatcherPriority.Background);
return;
}
}

var line = ctx.BlockNavigation.GetCurrentBlock()?.Start ?? 0;
if (line == 0)
return;
Expand Down Expand Up @@ -756,6 +783,8 @@ private void OnTextViewPointerChanged(object sender, PointerEventArgs e)

private void OnTextViewPointerWheelChanged(object sender, PointerWheelEventArgs e)
{
TryNavigateToAdjacentChange(e);

if (DataContext is not ViewModels.TextDiffContext { Option: { IsLocalChange: true } })
return;

Expand All @@ -766,6 +795,87 @@ private void OnTextViewPointerWheelChanged(object sender, PointerWheelEventArgs
Dispatcher.UIThread.Post(() => UpdateSelectedChunk(y));
}

private void TryNavigateToAdjacentChange(PointerWheelEventArgs e)
{
if (!ViewModels.Preferences.Instance.EnableOverscrollFileNavigation)
return;

if (DataContext is not ViewModels.TextDiffContext ctx)
return;

// Horizontal-only scrolling (e.g. touchpad) never switches files.
if (e.Delta.Y == 0)
return;

// Shift + wheel scrolls horizontally; any modifier means the user is not just scrolling down/up.
if (e.KeyModifiers != KeyModifiers.None)
return;

var now = Environment.TickCount64;
var elapsed = now - s_overscroll.LastWheelTime;
s_overscroll.LastWheelTime = now;

// Ignore wheel events until the new file is shown (1s fallback for non-text diffs), otherwise they skip it.
if (s_overscroll.WaitingForNewDiff)
{
if (now - s_overscroll.SwitchTime < 1000)
return;

s_overscroll.WaitingForNewDiff = false;
s_overscroll.PendingScrollToEndPath = null;
s_overscroll.Window = null;
}

// Give a short file that fits the viewport a moment on screen before a fast wheel spin moves past it.
if (now < s_overscroll.IgnoreUntil)
return;

var down = e.Delta.Y < 0;
var atEdge = down ? VerticalOffset + ViewportHeight >= ExtentHeight - 1 : VerticalOffset <= 1;
if (!atEdge || down != s_overscroll.IsDown || elapsed > 800)
s_overscroll.Amount = 0;

s_overscroll.IsDown = down;
if (!atEdge)
return;

// Diffs that need scrolling require a bit more overscroll, so the end of a long file is not skipped by accident.
var threshold = ExtentHeight > ViewportHeight + 1 ? 2 : 1;
s_overscroll.Amount += Math.Abs(e.Delta.Y);
if (s_overscroll.Amount < threshold)
return;

s_overscroll.Amount = 0;

var target = FindChangeCollectionViewOf(ctx.Option.Path)?.SelectAdjacentChange(down);
if (target == null)
return;

s_overscroll.PendingScrollToEndPath = down ? null : target.Path;
s_overscroll.WaitingForNewDiff = true;
s_overscroll.SwitchTime = now;
s_overscroll.Window = TopLevel.GetTopLevel(this);
e.Handled = true;
}

private ChangeCollectionView FindChangeCollectionViewOf(string path)
{
for (var parent = this.GetVisualParent(); parent != null; parent = parent.GetVisualParent())
{
foreach (var child in parent.GetVisualDescendants())
{
if (child is ChangeCollectionView { IsEffectivelyVisible: true, Selection: { Count: 1 } selection } view &&
selection.Changes[0].Path.Equals(path, StringComparison.Ordinal))
return view;
}

if (parent is TopLevel)
break;
}

return null;
}

private void OnTextViewVisualLinesChanged(object sender, EventArgs e)
{
if (DataContext is not ViewModels.TextDiffContext ctx)
Expand Down Expand Up @@ -970,6 +1080,21 @@ private async Task CopyAsPatchAsync()
private TextLocation _lastSelectStart = TextLocation.Empty;
private TextLocation _lastSelectEnd = TextLocation.Empty;
private LineStyleTransformer _lineStyleTransformer;

private class OverscrollState
{
public long LastWheelTime { get; set; }
public double Amount { get; set; }
public bool IsDown { get; set; }
public bool WaitingForNewDiff { get; set; }
public long SwitchTime { get; set; }
public long IgnoreUntil { get; set; }
public TopLevel Window { get; set; }
public string PendingScrollToEndPath { get; set; }
}

// Shared across presenters, so both sides of a side-by-side diff count as one gesture.
private static readonly OverscrollState s_overscroll = new();
}

public class CombinedTextDiffPresenter : ThemedTextDiffPresenter
Expand Down