diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index 916a79b1b..27f1ac43b 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -174,7 +174,6 @@ public static CommitGraph Generate(List commits, bool firstParentOnlyEna break; } } - commit.IsHighlightedInGraph = isHighlighted; // If no path found, create new curve for branch head // Otherwise, create new curve for new merged commit diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index a22f810cd..b74e98784 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; @@ -110,12 +111,6 @@ public bool IsCommitTimeColumnVisible public List Commits { get => _commits; - set - { - GenerateGraph(value); - if (SetProperty(ref _commits, value)) - PostCommitsChanged(); - } } public Models.CommitGraph Graph @@ -132,7 +127,7 @@ public Models.CommitGraphHighlighting GraphHighlighting if (_repo.UIStates.GraphHighlighting != value) { _repo.UIStates.GraphHighlighting = value; - GenerateGraph(_commits); + RefreshGraph(); } } } @@ -254,6 +249,46 @@ public void ToggleShowFlag(Models.HistoryShowFlags flag) _repo.RefreshCommits(); } + public void SetCommits(List commits, Models.CommitGraph graph, HashSet graphExtraHeads) + { + if (_cancellationRefreshGraph is { IsCancellationRequested: false }) + _cancellationRefreshGraph.Cancel(); + + ApplyGraph(commits, graph); + + if (SetProperty(ref _commits, commits, nameof(Commits))) + PostCommitsChanged(); + + // The selection may have changed while the graph was generated in background. + if (!graphExtraHeads.SetEquals(CollectGraphExtraHeads())) + RefreshGraph(); + } + + /// + /// Must be called on the UI thread: `_selectedCommits` is only modified there. + /// + public HashSet CollectGraphExtraHeads() + { + var extraHeads = new HashSet(); + if (_repo.UIStates.GraphHighlighting >= Models.CommitGraphHighlighting.SelectedCommitsOnly) + { + foreach (var c in _selectedCommits) + extraHeads.Add(c.SHA); + } + + return extraHeads; + } + + /// + /// Thread-safe as long as `extraHeads` is not shared with the UI thread. + /// + public Models.CommitGraph GenerateGraph(List commits, HashSet extraHeads) + { + var firstParentOnly = _repo.UIStates.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly); + var highlighting = _repo.UIStates.GraphHighlighting; + return Models.CommitGraph.Generate(commits, firstParentOnly, highlighting, extraHeads); + } + public Models.BisectState UpdateBisectInfo() { var test = Path.Combine(_repo.GitDir, "BISECT_START"); @@ -564,22 +599,38 @@ private void PostSelectedCommitsChanged() } if (_repo.UIStates.GraphHighlighting >= Models.CommitGraphHighlighting.SelectedCommitsOnly) - GenerateGraph(_commits); + RefreshGraph(); } - private void GenerateGraph(List commits) + private void RefreshGraph() { - var firstParentOnly = _repo.UIStates.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly); - var highlighting = _repo.UIStates.GraphHighlighting; - var extraHeads = new HashSet(); + if (_cancellationRefreshGraph is { IsCancellationRequested: false }) + _cancellationRefreshGraph.Cancel(); + + _cancellationRefreshGraph = new CancellationTokenSource(); + var token = _cancellationRefreshGraph.Token; + var commits = _commits; + var extraHeads = CollectGraphExtraHeads(); - if (highlighting >= Models.CommitGraphHighlighting.SelectedCommitsOnly) + Task.Run(() => { - foreach (var c in _selectedCommits) - extraHeads.Add(c.SHA); - } + var graph = GenerateGraph(commits, extraHeads); + Dispatcher.UIThread.Post(() => + { + if (!token.IsCancellationRequested) + ApplyGraph(commits, graph); + }); + }, token); + } + + private void ApplyGraph(List commits, Models.CommitGraph graph) + { + // There is exactly one dot for each commit (in the same order). Update `IsHighlightedInGraph` here + // so that the property-changed notifications of displayed commits are raised on the UI thread. + for (var i = 0; i < commits.Count; i++) + commits[i].IsHighlightedInGraph = graph.Dots[i].IsHighlighted; - Graph = Models.CommitGraph.Generate(commits, firstParentOnly, highlighting, extraHeads); + Graph = graph; } private Repository _repo = null; @@ -589,6 +640,7 @@ private void GenerateGraph(List commits) private bool _isLoading = true; private List _commits = []; private Models.CommitGraph _graph = null; + private CancellationTokenSource _cancellationRefreshGraph = null; private List _selectedCommits = []; private Models.Bisect _bisect = null; private object _detailContext = Models.Null.Instance; diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 4d3b5541c..2b8c79525 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -1255,7 +1255,15 @@ public void RefreshCommits() Task.Run(async () => { - await Dispatcher.UIThread.InvokeAsync(() => _histories.IsLoading = true); + var graphExtraHeads = new HashSet(); + await Dispatcher.UIThread.InvokeAsync(() => + { + if (_histories == null) + return; + + _histories.IsLoading = true; + graphExtraHeads = _histories.CollectGraphExtraHeads(); + }); var builder = new StringBuilder(); builder @@ -1292,6 +1300,12 @@ public void RefreshCommits() } } + var histories = _histories; + if (token.IsCancellationRequested || histories == null) + return; + + var graph = histories.GenerateGraph(commits, graphExtraHeads); + Dispatcher.UIThread.Invoke(() => { if (token.IsCancellationRequested) @@ -1300,7 +1314,7 @@ public void RefreshCommits() if (_histories != null) { _histories.IsLoading = false; - _histories.Commits = commits; + _histories.SetCommits(commits, graph, graphExtraHeads); BisectState = _histories.UpdateBisectInfo(); if (!string.IsNullOrEmpty(_navigateToCommitDelayed))