From 76a2fede86b13f1e8ebad5a0c8776aec07277fbc Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Thu, 1 Jan 2026 14:09:58 +0100 Subject: [PATCH 1/3] refactor: remove old branch selection logic --- src/Views/BranchTree.axaml | 1 - src/Views/BranchTree.axaml.cs | 22 ---------------------- 2 files changed, 23 deletions(-) diff --git a/src/Views/BranchTree.axaml b/src/Views/BranchTree.axaml index a627a0d90..1c1e2ce4a 100644 --- a/src/Views/BranchTree.axaml +++ b/src/Views/BranchTree.axaml @@ -36,7 +36,6 @@ diff --git a/src/Views/BranchTree.axaml.cs b/src/Views/BranchTree.axaml.cs index fd14fc3a2..7d0b709e4 100644 --- a/src/Views/BranchTree.axaml.cs +++ b/src/Views/BranchTree.axaml.cs @@ -418,28 +418,6 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang } } - private void OnNodePointerPressed(object sender, PointerPressedEventArgs e) - { - var ctrl = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; - if (e.KeyModifiers.HasFlag(ctrl) || e.KeyModifiers.HasFlag(KeyModifiers.Shift)) - return; - - var p = e.GetCurrentPoint(this); - if (!p.Properties.IsLeftButtonPressed) - return; - - if (DataContext is not ViewModels.Repository repo) - return; - - if (sender is not Border { DataContext: ViewModels.BranchTreeNode node }) - return; - - if (node.Backend is not Models.Branch branch) - return; - - repo.NavigateToCommit(branch.Head); - } - private void OnNodesSelectionChanged(object _, SelectionChangedEventArgs e) { if (_disableSelectionChangingEvent) From 9f24828425965e2897ac51bbfe364d0bb59a4fa9 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Thu, 1 Jan 2026 14:13:28 +0100 Subject: [PATCH 2/3] refactor: allow multiple auto selected commits --- src/App.Extensions.cs | 41 ++++++++++++++++++++++++++++++++++++ src/ViewModels/Fetch.cs | 3 ++- src/ViewModels/Histories.cs | 32 +++++++++++++++++----------- src/ViewModels/Repository.cs | 2 +- src/Views/Histories.axaml | 3 ++- src/Views/Histories.axaml.cs | 9 +++++--- 6 files changed, 72 insertions(+), 18 deletions(-) diff --git a/src/App.Extensions.cs b/src/App.Extensions.cs index d18882b86..30402c0ed 100644 --- a/src/App.Extensions.cs +++ b/src/App.Extensions.cs @@ -1,4 +1,7 @@ using System; +using System.Collections; +using Avalonia; +using Avalonia.Controls; namespace SourceGit { @@ -23,4 +26,42 @@ public static T Use(this T cmd, Models.ICommandLog log) where T : Commands.Co return cmd; } } + + public class DataGridExtension + { + public static readonly AttachedProperty SelectedItemsProperty = + AvaloniaProperty.RegisterAttached("SelectedItems"); + + public static void SetSelectedItems(DataGrid obj, IList value) => obj.SetValue(SelectedItemsProperty, value); + public static IList GetSelectedItems(DataGrid obj) => obj.GetValue(SelectedItemsProperty); + + + public static readonly AttachedProperty IsUpdatingSelectedItemsProperty = + AvaloniaProperty.RegisterAttached("IsUpdatingSelectedItems"); + + public static void SetIsUpdatingSelectedItems(DataGrid obj, bool value) => + obj.SetValue(IsUpdatingSelectedItemsProperty, value); + + public static bool GetIsUpdatingSelectedItems(DataGrid obj) => obj.GetValue(IsUpdatingSelectedItemsProperty); + + static DataGridExtension() + { + SelectedItemsProperty.Changed.AddClassHandler((DataGrid target, + AvaloniaPropertyChangedEventArgs args) => + { + SetIsUpdatingSelectedItems(target, true); + target.SelectedItems.Clear(); + var newItems = args.GetNewValue(); + if (newItems != null) + { + foreach (var item in newItems) + { + target.SelectedItems.Add(item); + } + } + + SetIsUpdatingSelectedItems(target, false); + }); + } + } } diff --git a/src/ViewModels/Fetch.cs b/src/ViewModels/Fetch.cs index 43bc5a18a..05cd9b17e 100644 --- a/src/ViewModels/Fetch.cs +++ b/src/ViewModels/Fetch.cs @@ -68,7 +68,8 @@ public override async Task Sure() { using var lockWatcher = _repo.LockWatcher(); - var navigateToUpstreamHEAD = _repo.SelectedView is Histories { AutoSelectedCommit: { IsCurrentHead: true } }; + var navigateToUpstreamHEAD = _repo.SelectedView is Histories { AutoSelectedCommits.Count: 1 } h && + h.AutoSelectedCommits[0].IsCurrentHead; var notags = _repo.Settings.FetchWithoutTags; var force = _repo.Settings.EnableForceOnFetch; var log = _repo.CreateLog("Fetch"); diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 455ce43ba..165c65673 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -23,11 +23,13 @@ public List Commits get => _commits; set { - var lastSelected = AutoSelectedCommit; + var lastSelected = AutoSelectedCommits; if (SetProperty(ref _commits, value)) { if (value.Count > 0 && lastSelected != null) - AutoSelectedCommit = value.Find(x => x.SHA == lastSelected.SHA); + { + AutoSelectedCommits = value.Where(x => lastSelected.Any(s => x.SHA == s.SHA)).ToArray(); + } } } } @@ -38,10 +40,10 @@ public Models.CommitGraph Graph set => SetProperty(ref _graph, value); } - public Models.Commit AutoSelectedCommit + public IReadOnlyList AutoSelectedCommits { - get => _autoSelectedCommit; - set => SetProperty(ref _autoSelectedCommit, value); + get => _autoSelectedCommits; + set => SetProperty(ref _autoSelectedCommits, value); } public long NavigationId @@ -97,7 +99,7 @@ public void Dispose() Commits = []; _repo = null; _graph = null; - _autoSelectedCommit = null; + _autoSelectedCommits = null; _detailContext?.Dispose(); _detailContext = null; } @@ -153,6 +155,7 @@ public void NavigateTo(string commitSHA) } public void Select(IList commits) + public void Select(IList commits, bool autoSelect) { if (commits.Count == 0) { @@ -163,10 +166,8 @@ public void Select(IList commits) { var commit = (commits[0] as Models.Commit)!; if (_repo.SearchCommitContext.Selected == null || _repo.SearchCommitContext.Selected.SHA != commit.SHA) - _repo.SearchCommitContext.Selected = _repo.SearchCommitContext.Results?.Find(x => x.SHA == commit.SHA); - - AutoSelectedCommit = commit; - NavigationId = _navigationId + 1; + _repo.SearchCommitContext.Selected = + _repo.SearchCommitContext.Results?.Find(x => x.SHA == commit.SHA); if (_detailContext is CommitDetail detail) { @@ -192,6 +193,13 @@ public void Select(IList commits) _repo.SearchCommitContext.Selected = null; DetailContext = new Models.Count(commits.Count); } + + if (autoSelect && commits.Count > 0) + { + AutoSelectedCommits = + commits as IReadOnlyList ?? commits.OfType().ToArray(); + NavigationId = _navigationId + 1; + } } public async Task CheckoutBranchByDecoratorAsync(Models.Decorator decorator) @@ -397,7 +405,7 @@ public void CompareWithWorktree(Models.Commit commit) private void NavigateTo(Models.Commit commit) { - AutoSelectedCommit = commit; + AutoSelectedCommits = [commit]; if (commit == null) { @@ -425,7 +433,7 @@ private void NavigateTo(Models.Commit commit) private bool _isLoading = true; private List _commits = new List(); private Models.CommitGraph _graph = null; - private Models.Commit _autoSelectedCommit = null; + private IReadOnlyList _autoSelectedCommits = null; private Models.Bisect _bisect = null; private long _navigationId = 0; private IDisposable _detailContext = null; diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 088be4f1d..0bb738bcd 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -1438,7 +1438,7 @@ public async Task CompareBranchWithWorktreeAsync(Models.Branch branch) _searchCommitContext.Selected = null; var target = await new Commands.QuerySingleCommit(FullPath, branch.Head).GetResultAsync(); - _histories.AutoSelectedCommit = null; + _histories.AutoSelectedCommits = null; _histories.DetailContext = new RevisionCompare(FullPath, target, null); } } diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index 01bcb3087..5d26ac3bd 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -6,6 +6,7 @@ xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" xmlns:c="using:SourceGit.Converters" + xmlns:sourceGit="clr-namespace:SourceGit" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.Histories" x:DataType="vm:Histories" @@ -30,7 +31,7 @@ Background="{DynamicResource Brush.Window}" SelectionMode="Extended" ItemsSource="{Binding Commits, Mode=OneWay}" - SelectedItem="{Binding AutoSelectedCommit, Mode=OneWay}" + sourceGit:DataGridExtension.SelectedItems="{Binding AutoSelectedCommits, Mode=OneWay}" CanUserReorderColumns="False" CanUserResizeColumns="True" CanUserSortColumns="False" diff --git a/src/Views/Histories.axaml.cs b/src/Views/Histories.axaml.cs index 3a381bae1..7eeecd80a 100644 --- a/src/Views/Histories.axaml.cs +++ b/src/Views/Histories.axaml.cs @@ -142,8 +142,8 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (change.Property == NavigationIdProperty) { - if (CommitListContainer is { SelectedItems.Count: 1, IsLoaded: true } dataGrid) - dataGrid.ScrollIntoView(dataGrid.SelectedItem, null); + if (CommitListContainer is { SelectedItems.Count: > 0, IsLoaded: true } dataGrid) + dataGrid.ScrollIntoView(dataGrid.SelectedItems[^1], null); } } @@ -208,8 +208,11 @@ private void OnScrollToTopPointerPressed(object sender, PointerPressedEventArgs private void OnCommitListSelectionChanged(object _, SelectionChangedEventArgs e) { + if (DataGridExtension.GetIsUpdatingSelectedItems(CommitListContainer)) + return; + if (DataContext is ViewModels.Histories histories) - histories.Select(CommitListContainer.SelectedItems); + histories.Select(CommitListContainer.SelectedItems, false); e.Handled = true; } From e77a93630579491b2c7642b483db9518f9c7da2b Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Thu, 1 Jan 2026 14:14:15 +0100 Subject: [PATCH 3/3] feat: collect and select sidebar items --- src/ViewModels/BranchTreeNode.cs | 5 ++- src/ViewModels/Histories.cs | 73 ++++++++++++++++++++++++++++++- src/ViewModels/ICommitTreeNode.cs | 11 +++++ src/ViewModels/TagCollection.cs | 6 ++- src/Views/Repository.axaml.cs | 11 +++++ 5 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 src/ViewModels/ICommitTreeNode.cs diff --git a/src/ViewModels/BranchTreeNode.cs b/src/ViewModels/BranchTreeNode.cs index 53ac1df9b..4db92b364 100644 --- a/src/ViewModels/BranchTreeNode.cs +++ b/src/ViewModels/BranchTreeNode.cs @@ -5,7 +5,7 @@ namespace SourceGit.ViewModels { - public class BranchTreeNode : ObservableObject + public class BranchTreeNode : ObservableObject, ICommitTreeNode { public string Name { get; private set; } = string.Empty; public string Path { get; private set; } = string.Empty; @@ -16,6 +16,9 @@ public class BranchTreeNode : ObservableObject public List Children { get; private set; } = new List(); public int Counter { get; set; } = 0; + IEnumerable ICommitTreeNode.Children => Children; + string ICommitTreeNode.CommitSHA => Backend is Models.Branch b ? b.Head : string.Empty; + public Models.FilterMode FilterMode { get => _filterMode; diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 165c65673..4151a6c6c 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -2,11 +2,12 @@ using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; - using Avalonia.Controls; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; +using SourceGit.Models; namespace SourceGit.ViewModels { @@ -154,7 +155,75 @@ public void NavigateTo(string commitSHA) }); } - public void Select(IList commits) + public void UpdateFromSelection() + { + var localBranchCommitSHAs = + CollectSelectedCommitSHAsRecursive(_repo.LocalBranchTrees); + + var remoteBranchCommitSHAs = + CollectSelectedCommitSHAsRecursive(_repo.RemoteBranchTrees); + + var tagsCommitSHAs = + _repo.VisibleTags switch + { + TagCollectionAsTree tree => + CollectSelectedCommitSHAsRecursive(tree.Tree), + TagCollectionAsList list => list.TagItems.Where(t => t.IsSelected) + .Select(t => t.Tag.SHA), + _ => Enumerable.Empty() + }; + + var neededCommitSHAs = + localBranchCommitSHAs.Union(remoteBranchCommitSHAs).Union(tagsCommitSHAs).ToHashSet(); + var foundCommits = new List(); + + // perf: we expect only a view commits to be selected (1-2), hence the cost is similar to the + // NaviateToCommit() even though we have a O(N*M) loop here, we loop the large commit list once, + // and the needed commits N times + foreach (var commit in _commits) + { + var match = neededCommitSHAs.FirstOrDefault(a => commit.SHA.StartsWith(a, StringComparison.Ordinal)); + if (match != null) + { + foundCommits.Add(commit); + neededCommitSHAs.Remove(match); + } + } + + if (neededCommitSHAs.Count == 0) + { + Dispatcher.UIThread.Post(() => Select(foundCommits, true)); + } + else + { + Task.Run(async () => + { + var remaining = await Task.WhenAll(neededCommitSHAs.Select(sha => + new Commands.QuerySingleCommit(_repo.FullPath, sha) + .GetResultAsync() + )).ConfigureAwait(false); + foundCommits.AddRange(remaining); + Dispatcher.UIThread.Post(() => Select(foundCommits, true)); + }); + } + } + + private static IEnumerable CollectSelectedCommitSHAsRecursive(IEnumerable treeNodes) + { + foreach (var node in treeNodes) + { + if (node.IsSelected && !string.IsNullOrEmpty(node.CommitSHA)) + { + yield return node.CommitSHA; + } + + foreach (var child in CollectSelectedCommitSHAsRecursive(node.Children)) + { + yield return child; + } + } + } + public void Select(IList commits, bool autoSelect) { if (commits.Count == 0) diff --git a/src/ViewModels/ICommitTreeNode.cs b/src/ViewModels/ICommitTreeNode.cs new file mode 100644 index 000000000..c232a8a91 --- /dev/null +++ b/src/ViewModels/ICommitTreeNode.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace SourceGit.ViewModels +{ + public interface ICommitTreeNode + { + IEnumerable Children { get; } + string CommitSHA { get; } + bool IsSelected { get; } + } +} diff --git a/src/ViewModels/TagCollection.cs b/src/ViewModels/TagCollection.cs index 50503dc22..741806234 100644 --- a/src/ViewModels/TagCollection.cs +++ b/src/ViewModels/TagCollection.cs @@ -26,7 +26,7 @@ public TagToolTip(Models.Tag t) } } - public class TagTreeNode : ObservableObject + public class TagTreeNode : ObservableObject, ICommitTreeNode { public string FullPath { get; private set; } public int Depth { get; private set; } = 0; @@ -34,6 +34,10 @@ public class TagTreeNode : ObservableObject public TagToolTip ToolTip { get; private set; } = null; public List Children { get; private set; } = []; public int Counter { get; set; } = 0; + + IEnumerable ICommitTreeNode.Children => Children; + string ICommitTreeNode.CommitSHA => Tag.SHA; + public bool IsFolder { diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index 8f040310d..c217f2fcd 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -72,12 +72,22 @@ private void OnLocalBranchTreeSelectionChanged(object _1, RoutedEventArgs _2) { RemoteBranchTree.UnselectAll(); TagsList.UnselectAll(); + UpdateHistoriesSelection(); } private void OnRemoteBranchTreeSelectionChanged(object _1, RoutedEventArgs _2) { LocalBranchTree.UnselectAll(); TagsList.UnselectAll(); + UpdateHistoriesSelection(); + } + + private void UpdateHistoriesSelection() + { + if (DataContext is ViewModels.Repository { SelectedView: ViewModels.Histories histories }) + { + histories.UpdateFromSelection(); + } } private void OnTagsRowsChanged(object _, RoutedEventArgs e) @@ -90,6 +100,7 @@ private void OnTagsSelectionChanged(object _1, RoutedEventArgs _2) { LocalBranchTree.UnselectAll(); RemoteBranchTree.UnselectAll(); + UpdateHistoriesSelection(); } private void OnSubmodulesRowsChanged(object _, RoutedEventArgs e)