Move agent drawer together with threads panel

Max Brunsfeld created

Change summary

crates/agent_ui/src/threads_panel.rs       |  2 
crates/git_ui/src/git_panel.rs             |  2 
crates/project_panel/src/project_panel.rs  |  2 
crates/terminal_view/src/terminal_panel.rs |  2 
crates/workspace/src/dock.rs               | 95 +++++++++--------------
crates/workspace/src/persistence.rs        |  1 
crates/workspace/src/workspace.rs          | 49 +++++++++++
crates/zed/src/zed.rs                      | 79 ++++++++++++-------
8 files changed, 140 insertions(+), 92 deletions(-)

Detailed changes

crates/workspace/src/dock.rs 🔗

@@ -90,6 +90,14 @@ pub trait PanelHandle: Send + Sync {
     fn to_any(&self) -> AnyView;
     fn activation_priority(&self, cx: &App) -> u32;
     fn enabled(&self, cx: &App) -> bool;
+    fn add_to_dock(
+        &self,
+        dock: &Entity<Dock>,
+        workspace: WeakEntity<Workspace>,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> usize;
+    fn remove_from_dock(&self, dock: &Entity<Dock>, window: &mut Window, cx: &mut App);
     fn move_to_next_position(&self, window: &mut Window, cx: &mut App) {
         let current_position = self.position(window, cx);
         let next_position = [
@@ -190,6 +198,22 @@ where
     fn enabled(&self, cx: &App) -> bool {
         self.read(cx).enabled(cx)
     }
+
+    fn add_to_dock(
+        &self,
+        dock: &Entity<Dock>,
+        workspace: WeakEntity<Workspace>,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> usize {
+        dock.update(cx, |dock, cx| {
+            dock.add_panel(self.clone(), workspace, window, cx)
+        })
+    }
+
+    fn remove_from_dock(&self, dock: &Entity<Dock>, window: &mut Window, cx: &mut App) {
+        dock.update(cx, |dock, cx| dock.remove_panel(self, window, cx));
+    }
 }
 
 impl From<&dyn PanelHandle> for AnyView {
@@ -265,7 +289,7 @@ impl DockPosition {
 
 struct PanelEntry {
     panel: Arc<dyn PanelHandle>,
-    _subscriptions: [Subscription; 3],
+    _subscriptions: [Subscription; 2],
 }
 
 impl Dock {
@@ -465,57 +489,6 @@ impl Dock {
     ) -> usize {
         let subscriptions = [
             cx.observe(&panel, |_, _, cx| cx.notify()),
-            cx.observe_global_in::<SettingsStore>(window, {
-                let workspace = workspace.clone();
-                let panel = panel.clone();
-
-                move |this, window, cx| {
-                    let new_position = panel.read(cx).position(window, cx);
-                    if new_position == this.position {
-                        return;
-                    }
-
-                    let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
-                        if panel.is_zoomed(window, cx) {
-                            workspace.zoomed_position = Some(new_position);
-                        }
-                        match new_position {
-                            DockPosition::Left => &workspace.left_dock,
-                            DockPosition::Bottom => &workspace.bottom_dock,
-                            DockPosition::Right => &workspace.right_dock,
-                        }
-                        .clone()
-                    }) else {
-                        return;
-                    };
-
-                    let was_visible = this.is_open()
-                        && this.visible_panel().is_some_and(|active_panel| {
-                            active_panel.panel_id() == Entity::entity_id(&panel)
-                        });
-
-                    this.remove_panel(&panel, window, cx);
-
-                    new_dock.update(cx, |new_dock, cx| {
-                        new_dock.remove_panel(&panel, window, cx);
-                    });
-
-                    new_dock.update(cx, |new_dock, cx| {
-                        let index =
-                            new_dock.add_panel(panel.clone(), workspace.clone(), window, cx);
-                        if was_visible {
-                            new_dock.set_open(true, window, cx);
-                            new_dock.activate_panel(index, window, cx);
-                        }
-                    });
-
-                    workspace
-                        .update(cx, |workspace, cx| {
-                            workspace.serialize_workspace(window, cx);
-                        })
-                        .ok();
-                }
-            }),
             cx.subscribe_in(
                 &panel,
                 window,
@@ -571,11 +544,15 @@ impl Dock {
             ),
         ];
 
-        let index = match self
-            .panel_entries
-            .binary_search_by_key(&panel.read(cx).activation_priority(), |entry| {
-                entry.panel.activation_priority(cx)
-            }) {
+        let position = self.position;
+        let priority = panel.activation_priority(cx);
+        let index = match self.panel_entries.binary_search_by(|probe| {
+            let other_priority = probe.panel.activation_priority(cx);
+            match position {
+                DockPosition::Left | DockPosition::Bottom => other_priority.cmp(&priority),
+                DockPosition::Right => priority.cmp(&other_priority),
+            }
+        }) {
             Ok(ix) => {
                 if cfg!(debug_assertions) {
                     panic!(
@@ -668,6 +645,10 @@ impl Dock {
         self.panel_entries.len()
     }
 
+    pub fn panels(&self) -> impl Iterator<Item = &Arc<dyn PanelHandle>> {
+        self.panel_entries.iter().map(|entry| &entry.panel)
+    }
+
     pub fn activate_panel(&mut self, panel_ix: usize, window: &mut Window, cx: &mut Context<Self>) {
         if Some(panel_ix) != self.active_panel_index {
             if let Some(active_panel) = self.active_panel_entry() {

crates/workspace/src/persistence.rs 🔗

@@ -4002,7 +4002,6 @@ mod tests {
         use crate::multi_workspace::MultiWorkspace;
         use crate::persistence::read_multi_workspace_state;
         use feature_flags::FeatureFlagAppExt;
-
         use project::Project;
 
         crate::tests::init_test(cx);

crates/workspace/src/workspace.rs 🔗

@@ -1394,7 +1394,7 @@ impl Workspace {
             })
             .detach();
 
-            cx.observe_global::<SettingsStore>(|_, cx| {
+            cx.observe_global_in::<SettingsStore>(window, |this, window, cx| {
                 if ProjectSettings::get_global(cx).session.trust_all_worktrees {
                     if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
                         trusted_worktrees.update(cx, |trusted_worktrees, cx| {
@@ -1402,6 +1402,7 @@ impl Workspace {
                         })
                     }
                 }
+                this.reposition_panels(window, cx);
             })
             .detach();
         }
@@ -2153,6 +2154,40 @@ impl Workspace {
         }
     }
 
+    fn reposition_panels(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        let mut panels_to_move = Vec::new();
+        for dock_entity in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
+            let dock = dock_entity.read(cx);
+            let old_position = dock.position();
+            let visible_panel_id = dock.visible_panel().map(|panel| panel.panel_id());
+            for panel in dock.panels() {
+                let new_position = panel.position(window, cx);
+                if new_position != old_position {
+                    let was_visible = Some(panel.panel_id()) == visible_panel_id;
+                    panels_to_move.push((dock_entity, panel.clone(), new_position, was_visible));
+                }
+            }
+        }
+
+        for (old_dock, panel, new_position, was_visible) in panels_to_move {
+            if panel.is_zoomed(window, cx) {
+                self.zoomed_position = Some(new_position);
+            }
+
+            panel.remove_from_dock(old_dock, window, cx);
+            let new_dock = self.dock_at_position(new_position).clone();
+            let index = panel.add_to_dock(&new_dock, self.weak_self.clone(), window, cx);
+            if was_visible {
+                new_dock.update(cx, |new_dock, cx| {
+                    new_dock.set_open(true, window, cx);
+                    new_dock.activate_panel(index, window, cx);
+                });
+            }
+        }
+
+        self.serialize_workspace(window, cx);
+    }
+
     pub fn status_bar(&self) -> &Entity<StatusBar> {
         &self.status_bar
     }
@@ -7012,6 +7047,11 @@ impl Workspace {
         view: Entity<V>,
         cx: &mut Context<Self>,
     ) {
+        if let Some(drawer) = self.right_drawer.as_mut() {
+            if drawer.view.entity_id() == view.entity_id() {
+                self.right_drawer.take();
+            }
+        }
         self.left_drawer = Some(Drawer::new(view));
         cx.notify();
     }
@@ -7021,6 +7061,11 @@ impl Workspace {
         view: Entity<V>,
         cx: &mut Context<Self>,
     ) {
+        if let Some(drawer) = self.left_drawer.as_mut() {
+            if drawer.view.entity_id() == view.entity_id() {
+                self.left_drawer.take();
+            }
+        }
         self.right_drawer = Some(Drawer::new(view));
         cx.notify();
     }
@@ -7938,7 +7983,7 @@ impl Drawer {
         Self {
             view: view.into(),
             focus_handle_fn: Box::new(move |cx| entity.focus_handle(cx)),
-            open: true,
+            open: false,
             custom_width: None,
         }
     }

crates/zed/src/zed.rs 🔗

@@ -83,6 +83,7 @@ use util::rel_path::RelPath;
 use util::{ResultExt, asset_str, maybe};
 use uuid::Uuid;
 use vim_mode_setting::VimModeSetting;
+use workspace::dock::PanelHandle;
 use workspace::notifications::{
     NotificationId, SuppressEvent, dismiss_app_notification, show_app_notification,
 };
@@ -669,41 +670,63 @@ fn setup_or_teardown_ai_panels(
         || cfg!(test);
     let existing_panel = workspace.panel::<agent_ui::ThreadsPanel>(cx);
     match (disable_ai, existing_panel) {
-        (false, None) => cx.spawn_in(window, async move |workspace, cx| {
-            let agent_drawer =
-                agent_ui::AgentPanel::load(workspace.clone(), prompt_builder.clone(), cx.clone())
-                    .await?;
-            let sidebar_panel = agent_ui::ThreadsPanel::load(workspace.clone(), cx.clone()).await?;
-            workspace.update_in(cx, |workspace, window, cx| {
-                let disable_ai = SettingsStore::global(cx)
-                    .get::<DisableAiSettings>(None)
-                    .disable_ai;
-                let have_panel = workspace.panel::<agent_ui::ThreadsPanel>(cx).is_some();
-                if !disable_ai && !have_panel {
-                    let position =
-                        workspace::dock::PanelHandle::position(&sidebar_panel, window, cx);
-                    workspace.add_panel(sidebar_panel, window, cx);
-                    match position {
-                        workspace::dock::DockPosition::Left => {
-                            workspace.set_left_drawer(agent_drawer, cx)
-                        }
-                        workspace::dock::DockPosition::Right => {
-                            workspace.set_right_drawer(agent_drawer, cx)
-                        }
-                        workspace::dock::DockPosition::Bottom => {
-                            unreachable!("drawers cannot go on the bottom")
+        (false, None) => {
+            return cx.spawn_in(window, async move |workspace, cx| {
+                let agent_drawer = agent_ui::AgentPanel::load(
+                    workspace.clone(),
+                    prompt_builder.clone(),
+                    cx.clone(),
+                )
+                .await?;
+                let threads_panel =
+                    agent_ui::ThreadsPanel::load(workspace.clone(), cx.clone()).await?;
+                workspace.update_in(cx, |workspace, window, cx| {
+                    let disable_ai = SettingsStore::global(cx)
+                        .get::<DisableAiSettings>(None)
+                        .disable_ai;
+                    let have_panel = workspace.panel::<agent_ui::ThreadsPanel>(cx).is_some();
+                    let position = PanelHandle::position(&threads_panel, window, cx);
+                    if !disable_ai && !have_panel {
+                        workspace.add_panel(threads_panel, window, cx);
+                        match position {
+                            workspace::dock::DockPosition::Left => {
+                                workspace.set_left_drawer(agent_drawer, cx)
+                            }
+                            workspace::dock::DockPosition::Right => {
+                                workspace.set_right_drawer(agent_drawer, cx)
+                            }
+                            workspace::dock::DockPosition::Bottom => {
+                                unreachable!("drawers cannot go on the bottom")
+                            }
                         }
                     }
-                }
-            })
-        }),
+                })
+            });
+        }
         (true, Some(existing_panel)) => {
             workspace.remove_panel(&existing_panel, window, cx);
             workspace.remove_drawer::<agent_ui::AgentPanel>(cx);
-            Task::ready(Ok(()))
         }
-        _ => Task::ready(Ok(())),
+        (false, Some(existing_panel)) => {
+            let position = PanelHandle::position(&existing_panel, window, cx);
+            if let Some(agent_drawer) = workspace.drawer::<agent_ui::AgentPanel>() {
+                match position {
+                    workspace::dock::DockPosition::Left => {
+                        workspace.set_left_drawer(agent_drawer, cx)
+                    }
+                    workspace::dock::DockPosition::Right => {
+                        workspace.set_right_drawer(agent_drawer, cx)
+                    }
+                    workspace::dock::DockPosition::Bottom => {
+                        unreachable!("drawers cannot go on the bottom")
+                    }
+                }
+            }
+        }
+        _ => {}
     }
+
+    Task::ready(Ok(()))
 }
 
 async fn initialize_agent_panel(