git_graph: Add loading icon when loading initial commit chunk (#47514)

Anthony Eid , Remco Smits , and Marco Mihai Condrache created

Release Notes:

- N/A

Co-authored-by: Remco Smits <djsmits12@gmail.com>
Co-authored-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>

Change summary

crates/git_graph/src/git_graph.rs | 57 +++++++++++++++++++++++---------
crates/project/src/git_store.rs   | 12 ++++--
2 files changed, 47 insertions(+), 22 deletions(-)

Detailed changes

crates/git_graph/src/git_graph.rs 🔗

@@ -20,7 +20,10 @@ use smallvec::{SmallVec, smallvec};
 use std::{ops::Range, rc::Rc, sync::Arc, sync::OnceLock};
 use theme::{AccentColors, ThemeSettings};
 use time::{OffsetDateTime, UtcOffset, format_description::BorrowedFormatItem};
-use ui::{ContextMenu, ScrollableHandle, Table, TableInteractionState, Tooltip, prelude::*};
+use ui::{
+    CommonAnimationExt as _, ContextMenu, ScrollableHandle, Table, TableInteractionState, Tooltip,
+    prelude::*,
+};
 use workspace::{
     Workspace,
     item::{Item, ItemEvent, SerializableItem},
@@ -623,7 +626,7 @@ impl GitGraph {
                 // This won't overlap with loading commits from the repository because
                 // we either have all commits or commits loaded in chunks and loading commits
                 // from the repository event is always adding the last chunk of commits.
-                let commits =
+                let (commits, _) =
                     repository.graph_data(log_source.clone(), log_order, 0..usize::MAX, cx);
                 graph.add_commits(commits);
             });
@@ -674,7 +677,7 @@ impl GitGraph {
                 let old_count = self.graph_data.commits.len();
 
                 repository.update(cx, |repository, cx| {
-                    let commits = repository.graph_data(
+                    let (commits, _) = repository.graph_data(
                         self.log_source.clone(),
                         self.log_order,
                         old_count..*commit_count,
@@ -882,6 +885,15 @@ impl GitGraph {
         })
     }
 
+    fn render_loading_spinner(&self, cx: &App) -> AnyElement {
+        let rems = TextSize::Large.rems(cx);
+        Icon::new(IconName::LoadCircle)
+            .size(IconSize::Custom(rems))
+            .color(Color::Accent)
+            .with_rotate_animation(3)
+            .into_any_element()
+    }
+
     fn render_commit_detail_panel(
         &self,
         window: &mut Window,
@@ -1443,35 +1455,45 @@ impl Render for GitGraph {
         let author_width_fraction = 0.10;
         let commit_width_fraction = 0.06;
 
-        let commit_count = match self.graph_data.max_commit_count {
-            AllCommitCount::Loaded(count) => count,
+        let (commit_count, is_loading) = match self.graph_data.max_commit_count {
+            AllCommitCount::Loaded(count) => (count, true),
             AllCommitCount::NotLoaded => {
-                self.project.update(cx, |project, cx| {
+                let is_loading = self.project.update(cx, |project, cx| {
                     if let Some(repository) = project.active_repository(cx) {
                         repository.update(cx, |repository, cx| {
                             // Start loading the graph data if we haven't started already
-                            repository.graph_data(
-                                self.log_source.clone(),
-                                self.log_order,
-                                0..0,
-                                cx,
-                            );
+                            repository
+                                .graph_data(self.log_source.clone(), self.log_order, 0..0, cx)
+                                .1
                         })
+                    } else {
+                        false
                     }
-                });
+                }) && self.graph_data.commits.is_empty();
 
-                self.graph_data.commits.len()
+                (self.graph_data.commits.len(), is_loading)
             }
         };
 
         let content = if self.graph_data.commits.is_empty() {
-            let message = "No commits found";
+            let message = if is_loading {
+                "Loading"
+            } else {
+                "No commits found"
+            };
+            let label = Label::new(message)
+                .color(Color::Muted)
+                .size(LabelSize::Large);
             div()
                 .size_full()
-                .flex()
+                .h_flex()
+                .gap_1()
                 .items_center()
                 .justify_center()
-                .child(Label::new(message).color(Color::Muted))
+                .child(label)
+                .when(is_loading, |this| {
+                    this.child(self.render_loading_spinner(cx))
+                })
         } else {
             div()
                 .size_full()
@@ -2349,6 +2371,7 @@ mod tests {
                 0..usize::MAX,
                 cx,
             )
+            .0
             .to_vec()
         });
 

crates/project/src/git_store.rs 🔗

@@ -4240,8 +4240,8 @@ impl Repository {
         log_order: LogOrder,
         range: Range<usize>,
         cx: &mut Context<Self>,
-    ) -> &[Arc<InitialGraphCommitData>] {
-        let initial_commit_data = &self
+    ) -> (&[Arc<InitialGraphCommitData>], bool) {
+        let (loading_task, initial_commit_data) = self
             .initial_graph_data
             .entry((log_order, log_source.clone()))
             .or_insert_with(|| {
@@ -4267,12 +4267,14 @@ impl Repository {
                     }),
                     vec![],
                 )
-            })
-            .1;
+            });
 
         let max_start = initial_commit_data.len().saturating_sub(1);
         let max_end = initial_commit_data.len();
-        &initial_commit_data[range.start.min(max_start)..range.end.min(max_end)]
+        (
+            &initial_commit_data[range.start.min(max_start)..range.end.min(max_end)],
+            !loading_task.is_ready(),
+        )
     }
 
     async fn local_git_graph_data(