git: Move all state into the panel (#23185)

Cole Miller created

This should fix the problem with the panel not updating when switching
projects.

Release Notes:

- N/A

Change summary

crates/git_ui/src/git_panel.rs | 184 +++++++++++++++--------------------
crates/git_ui/src/git_ui.rs    |  18 --
2 files changed, 81 insertions(+), 121 deletions(-)

Detailed changes

crates/git_ui/src/git_panel.rs 🔗

@@ -88,7 +88,7 @@ pub struct GitPanel {
     selected_entry: Option<usize>,
     show_scrollbar: bool,
     rebuild_requested: Arc<AtomicBool>,
-    git_state: Model<GitState>,
+    git_state: GitState,
     commit_editor: View<Editor>,
     /// The visible entries in the list, accounting for folding & expanded state.
     ///
@@ -112,11 +112,8 @@ impl GitPanel {
         let fs = workspace.app_state().fs.clone();
         let project = workspace.project().clone();
         let language_registry = workspace.app_state().languages.clone();
-        let git_state = GitState::get_global(cx);
-        let current_commit_message = {
-            let state = git_state.read(cx);
-            state.commit_message.clone()
-        };
+        let mut git_state = GitState::new(cx);
+        let current_commit_message = git_state.commit_message.clone();
 
         let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
             let focus_handle = cx.focus_handle();
@@ -128,89 +125,74 @@ impl GitPanel {
             cx.subscribe(&project, move |this, project, event, cx| {
                 use project::Event;
 
+                let git_state = &mut this.git_state;
                 let first_worktree_id = project.read(cx).worktrees(cx).next().map(|worktree| {
                     let snapshot = worktree.read(cx).snapshot();
                     snapshot.id()
                 });
                 let first_repo_in_project = first_repository_in_project(&project, cx);
 
-                // TODO: Don't get another git_state here
-                // was running into a borrow issue
-                let git_state = GitState::get_global(cx);
-
                 match event {
                     project::Event::WorktreeRemoved(id) => {
-                        git_state.update(cx, |state, _| {
-                            state.all_repositories.remove(id);
-                            let Some((worktree_id, _, _)) = state.active_repository.as_ref() else {
-                                return;
-                            };
-                            if worktree_id == id {
-                                state.active_repository = first_repo_in_project;
-                                this.schedule_update();
-                            }
-                        });
+                        git_state.all_repositories.remove(id);
+                        let Some((worktree_id, _, _)) = git_state.active_repository.as_ref() else {
+                            return;
+                        };
+                        if worktree_id == id {
+                            git_state.active_repository = first_repo_in_project;
+                            this.schedule_update();
+                        }
                     }
                     project::Event::WorktreeOrderChanged => {
                         // activate the new first worktree if the first was moved
                         let Some(first_id) = first_worktree_id else {
                             return;
                         };
-                        git_state.update(cx, |state, _| {
-                            if !state
-                                .active_repository
-                                .as_ref()
-                                .is_some_and(|(id, _, _)| id == &first_id)
-                            {
-                                state.active_repository = first_repo_in_project;
-                                this.schedule_update();
-                            }
-                        });
+                        if !git_state
+                            .active_repository
+                            .as_ref()
+                            .is_some_and(|(id, _, _)| id == &first_id)
+                        {
+                            git_state.active_repository = first_repo_in_project;
+                            this.schedule_update();
+                        }
                     }
                     Event::WorktreeAdded(id) => {
-                        git_state.update(cx, |state, cx| {
-                            let Some(worktree) = project.read(cx).worktree_for_id(*id, cx) else {
-                                return;
-                            };
-                            let snapshot = worktree.read(cx).snapshot();
-                            state
-                                .all_repositories
-                                .insert(*id, snapshot.repositories().clone());
-                        });
+                        let Some(worktree) = project.read(cx).worktree_for_id(*id, cx) else {
+                            return;
+                        };
+                        let snapshot = worktree.read(cx).snapshot();
+                        git_state
+                            .all_repositories
+                            .insert(*id, snapshot.repositories().clone());
                         let Some(first_id) = first_worktree_id else {
                             return;
                         };
-                        git_state.update(cx, |state, _| {
-                            if !state
-                                .active_repository
-                                .as_ref()
-                                .is_some_and(|(id, _, _)| id == &first_id)
-                            {
-                                state.active_repository = first_repo_in_project;
-                                this.schedule_update();
-                            }
-                        });
+                        if !git_state
+                            .active_repository
+                            .as_ref()
+                            .is_some_and(|(id, _, _)| id == &first_id)
+                        {
+                            git_state.active_repository = first_repo_in_project;
+                            this.schedule_update();
+                        }
                     }
                     project::Event::WorktreeUpdatedEntries(id, _) => {
-                        git_state.update(cx, |state, _| {
-                            if state
-                                .active_repository
-                                .as_ref()
-                                .is_some_and(|(active_id, _, _)| active_id == id)
-                            {
-                                state.active_repository = first_repo_in_project;
-                                this.schedule_update();
-                            }
-                        });
+                        if git_state
+                            .active_repository
+                            .as_ref()
+                            .is_some_and(|(active_id, _, _)| active_id == id)
+                        {
+                            git_state.active_repository = first_repo_in_project;
+                            this.schedule_update();
+                        }
                     }
                     project::Event::WorktreeUpdatedGitRepositories(_) => {
                         let Some(first) = first_repo_in_project else {
                             return;
                         };
-                        git_state.update(cx, |state, _| {
-                            state.active_repository = Some(first);
-                            this.schedule_update();
-                        });
+                        git_state.active_repository = Some(first);
+                        this.schedule_update();
                     }
                     project::Event::Closed => {
                         this.reveal_in_editor = Task::ready(());
@@ -272,20 +254,18 @@ impl GitPanel {
 
             let scroll_handle = UniformListScrollHandle::new();
 
-            git_state.update(cx, |state, cx| {
-                let mut visible_worktrees = project.read(cx).visible_worktrees(cx);
-                let Some(first_worktree) = visible_worktrees.next() else {
-                    return;
-                };
-                drop(visible_worktrees);
+            let mut visible_worktrees = project.read(cx).visible_worktrees(cx);
+            let first_worktree = visible_worktrees.next();
+            drop(visible_worktrees);
+            if let Some(first_worktree) = first_worktree {
                 let snapshot = first_worktree.read(cx).snapshot();
 
                 if let Some((repo, git_repo)) =
                     first_worktree_repository(&project, snapshot.id(), cx)
                 {
-                    state.activate_repository(snapshot.id(), repo, git_repo);
+                    git_state.activate_repository(snapshot.id(), repo, git_repo);
                 }
-            });
+            };
 
             let rebuild_requested = Arc::new(AtomicBool::new(false));
             let flag = rebuild_requested.clone();
@@ -569,18 +549,16 @@ impl GitPanel {
             .and_then(|i| self.visible_entries.get(i))
     }
 
-    fn toggle_staged_for_entry(&self, entry: &GitListEntry, cx: &mut ViewContext<Self>) {
-        self.git_state
-            .clone()
-            .update(cx, |state, _| match entry.status.is_staged() {
-                Some(true) | None => state.unstage_entry(entry.repo_path.clone()),
-                Some(false) => state.stage_entry(entry.repo_path.clone()),
-            });
+    fn toggle_staged_for_entry(&mut self, entry: &GitListEntry, cx: &mut ViewContext<Self>) {
+        match entry.status.is_staged() {
+            Some(true) | None => self.git_state.unstage_entry(entry.repo_path.clone()),
+            Some(false) => self.git_state.stage_entry(entry.repo_path.clone()),
+        }
         cx.notify();
     }
 
     fn toggle_staged_for_selected(&mut self, _: &ToggleStaged, cx: &mut ViewContext<Self>) {
-        if let Some(selected_entry) = self.get_selected_entry() {
+        if let Some(selected_entry) = self.get_selected_entry().cloned() {
             self.toggle_staged_for_entry(&selected_entry, cx);
         }
     }
@@ -595,11 +573,14 @@ impl GitPanel {
     }
 
     fn open_entry(&self, entry: &GitListEntry, cx: &mut ViewContext<Self>) {
-        let Some((worktree_id, path)) = GitState::get_global(cx).update(cx, |state, _| {
-            state.active_repository.as_ref().and_then(|(id, repo, _)| {
-                Some((*id, repo.work_directory.unrelativize(&entry.repo_path)?))
-            })
-        }) else {
+        let Some((worktree_id, path)) =
+            self.git_state
+                .active_repository
+                .as_ref()
+                .and_then(|(id, repo, _)| {
+                    Some((*id, repo.work_directory.unrelativize(&entry.repo_path)?))
+                })
+        else {
             return;
         };
         let path = (worktree_id, path).into();
@@ -612,7 +593,7 @@ impl GitPanel {
         cx.emit(Event::OpenedEntry { path });
     }
 
-    fn stage_all(&mut self, _: &StageAll, cx: &mut ViewContext<Self>) {
+    fn stage_all(&mut self, _: &StageAll, _cx: &mut ViewContext<Self>) {
         let to_stage = self
             .visible_entries
             .iter_mut()
@@ -623,19 +604,16 @@ impl GitPanel {
             })
             .collect();
         self.all_staged = Some(true);
-        self.git_state
-            .update(cx, |state, _| state.stage_entries(to_stage));
+        self.git_state.stage_entries(to_stage);
     }
 
-    fn unstage_all(&mut self, _: &UnstageAll, cx: &mut ViewContext<Self>) {
+    fn unstage_all(&mut self, _: &UnstageAll, _cx: &mut ViewContext<Self>) {
         // This should only be called when all entries are staged.
         for entry in &mut self.visible_entries {
             entry.is_staged = Some(false);
         }
         self.all_staged = Some(false);
-        self.git_state.update(cx, |state, _| {
-            state.unstage_all();
-        });
+        self.git_state.unstage_all();
     }
 
     fn discard_all(&mut self, _: &RevertAll, _cx: &mut ViewContext<Self>) {
@@ -644,8 +622,7 @@ impl GitPanel {
     }
 
     fn clear_message(&mut self, cx: &mut ViewContext<Self>) {
-        self.git_state
-            .update(cx, |state, _cx| state.clear_commit_message());
+        self.git_state.clear_commit_message();
         self.commit_editor
             .update(cx, |editor, cx| editor.set_text("", cx));
     }
@@ -713,11 +690,9 @@ impl GitPanel {
 
     #[track_caller]
     fn update_visible_entries(&mut self, cx: &mut ViewContext<Self>) {
-        let git_state = self.git_state.read(cx);
-
         self.visible_entries.clear();
 
-        let Some((_, repo, _)) = git_state.active_repository().as_ref() else {
+        let Some((_, repo, _)) = self.git_state.active_repository().as_ref() else {
             // Just clear entries if no repository is active.
             cx.notify();
             return;
@@ -790,9 +765,7 @@ impl GitPanel {
         if let language::BufferEvent::Reparsed | language::BufferEvent::Edited = event {
             let commit_message = self.commit_editor.update(cx, |editor, cx| editor.text(cx));
 
-            self.git_state.update(cx, |state, _cx| {
-                state.commit_message = Some(commit_message.into());
-            });
+            self.git_state.commit_message = Some(commit_message.into());
 
             cx.notify();
         }
@@ -1096,7 +1069,6 @@ impl GitPanel {
         entry_details: GitListEntry,
         cx: &ViewContext<Self>,
     ) -> impl IntoElement {
-        let state = self.git_state.clone();
         let repo_path = entry_details.repo_path.clone();
         let selected = self.selected_entry == Some(ix);
         let status_style = GitPanelSettings::get_global(cx).status_style;
@@ -1122,7 +1094,7 @@ impl GitPanel {
         let entry_id = ElementId::Name(format!("entry_{}", entry_details.display_name).into());
         let checkbox_id =
             ElementId::Name(format!("checkbox_{}", entry_details.display_name).into());
-        let view_mode = state.read(cx).list_view_mode.clone();
+        let view_mode = self.git_state.list_view_mode;
         let handle = cx.view().downgrade();
 
         let end_slot = h_flex()
@@ -1185,15 +1157,13 @@ impl GitPanel {
                                 ToggleState::Selected => Some(true),
                                 ToggleState::Unselected => Some(false),
                                 ToggleState::Indeterminate => None,
-                            }
-                        });
-                        state.update(cx, {
+                            };
                             let repo_path = repo_path.clone();
-                            move |state, _| match toggle {
+                            match toggle {
                                 ToggleState::Selected | ToggleState::Indeterminate => {
-                                    state.stage_entry(repo_path);
+                                    this.git_state.stage_entry(repo_path);
                                 }
-                                ToggleState::Unselected => state.unstage_entry(repo_path),
+                                ToggleState::Unselected => this.git_state.unstage_entry(repo_path),
                             }
                         });
                     }

crates/git_ui/src/git_ui.rs 🔗

@@ -4,7 +4,7 @@ use futures::channel::mpsc;
 use futures::StreamExt as _;
 use git::repository::{GitFileStatus, GitRepository, RepoPath};
 use git_panel_settings::GitPanelSettings;
-use gpui::{actions, AppContext, Context, Global, Hsla, Model, ModelContext};
+use gpui::{actions, AppContext, Hsla, Model};
 use project::{Project, WorktreeId};
 use std::sync::Arc;
 use sum_tree::SumTree;
@@ -35,21 +35,15 @@ actions!(
 
 pub fn init(cx: &mut AppContext) {
     GitPanelSettings::register(cx);
-    let git_state = cx.new_model(GitState::new);
-    cx.set_global(GlobalGitState(git_state));
 }
 
-#[derive(Default, Debug, PartialEq, Eq, Clone)]
+#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
 pub enum GitViewMode {
     #[default]
     List,
     Tree,
 }
 
-struct GlobalGitState(Model<GitState>);
-
-impl Global for GlobalGitState {}
-
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 enum StatusAction {
     Stage,
@@ -72,10 +66,10 @@ pub struct GitState {
 }
 
 impl GitState {
-    pub fn new(cx: &mut ModelContext<'_, Self>) -> Self {
+    pub fn new(cx: &AppContext) -> Self {
         let (updater_tx, mut updater_rx) =
             mpsc::unbounded::<(Arc<dyn GitRepository>, Vec<RepoPath>, StatusAction)>();
-        cx.spawn(|_, cx| async move {
+        cx.spawn(|cx| async move {
             while let Some((git_repo, paths, action)) = updater_rx.next().await {
                 cx.background_executor()
                     .spawn(async move {
@@ -98,10 +92,6 @@ impl GitState {
         }
     }
 
-    pub fn get_global(cx: &mut AppContext) -> Model<GitState> {
-        cx.global::<GlobalGitState>().0.clone()
-    }
-
     pub fn activate_repository(
         &mut self,
         worktree_id: WorktreeId,