From e25b35d792af779185560f370ffb867db2d4e957 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 Mar 2026 17:50:36 -0700 Subject: [PATCH] Rearrange panels in agent mode --- crates/agent_ui/src/agent_panel.rs | 12 +- crates/agent_ui/src/connection_view.rs | 4 +- .../tests/integration/following_tests.rs | 7 +- crates/collab/tests/integration/git_tests.rs | 10 +- .../remote_editing_collaboration_tests.rs | 7 +- crates/debugger_ui/src/tests.rs | 7 +- crates/outline_panel/src/outline_panel.rs | 3 +- .../project_panel/src/project_panel_tests.rs | 27 ++-- crates/sidebar/src/sidebar.rs | 4 +- crates/workspace/src/dock.rs | 110 ++++++++-------- crates/workspace/src/multi_workspace.rs | 5 +- crates/workspace/src/workspace.rs | 107 ++++++++++++++-- crates/zed/src/visual_test_runner.rs | 3 +- crates/zed/src/zed.rs | 119 +++++++++++++++--- 14 files changed, 314 insertions(+), 111 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index c49b7f668ab12ad4d2b04e8ec48488f7afab3c1c..a622ead71a02cfd70d675c3143cba5e3352831d0 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -4908,7 +4908,8 @@ mod tests { let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); let panel = cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx)); - workspace.add_panel(panel, window, cx); + let position = panel.read(cx).position(window, cx); + workspace.add_panel(panel, position, window, cx); }); cx.run_until_parked(); @@ -5132,7 +5133,8 @@ mod tests { let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); let panel = cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx)); - workspace.add_panel(panel.clone(), window, cx); + let position = panel.read(cx).position(window, cx); + workspace.add_panel(panel.clone(), position, window, cx); panel }); @@ -5242,7 +5244,8 @@ mod tests { let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); let panel = cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx)); - workspace.add_panel(panel.clone(), window, cx); + let position = panel.read(cx).position(window, cx); + workspace.add_panel(panel.clone(), position, window, cx); panel }); @@ -5327,7 +5330,8 @@ mod tests { let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); let panel = cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx)); - workspace.add_panel(panel.clone(), window, cx); + let position = panel.read(cx).position(window, cx); + workspace.add_panel(panel.clone(), position, window, cx); panel }); diff --git a/crates/agent_ui/src/connection_view.rs b/crates/agent_ui/src/connection_view.rs index 07841c42215795ffcccf9f7e5ca684f42a59b498..49e0c4ea9e01ad7bbe34b68263616ffc02f2f811 100644 --- a/crates/agent_ui/src/connection_view.rs +++ b/crates/agent_ui/src/connection_view.rs @@ -2780,6 +2780,7 @@ pub(crate) mod tests { use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::Arc; + use workspace::dock::Panel; use workspace::{Item, MultiWorkspace}; use crate::agent_panel; @@ -3459,7 +3460,8 @@ pub(crate) mod tests { cx.new(|cx| TextThreadStore::fake(workspace.project().clone(), cx)); let panel = cx.new(|cx| crate::AgentPanel::new(workspace, text_thread_store, None, window, cx)); - workspace.add_panel(panel, window, cx); + let position = panel.read(cx).position(window, cx); + workspace.add_panel(panel, position, window, cx); // Open the dock and activate the agent panel so it's visible workspace.focus_panel::(window, cx); diff --git a/crates/collab/tests/integration/following_tests.rs b/crates/collab/tests/integration/following_tests.rs index c4031788c87f747c3125f4dbc509d68ea3720b43..4c8837778e7b6caadb141df23ab0776705f0a364 100644 --- a/crates/collab/tests/integration/following_tests.rs +++ b/crates/collab/tests/integration/following_tests.rs @@ -18,8 +18,8 @@ use settings::SettingsStore; use text::{Point, ToPoint}; use util::{path, rel_path::rel_path, test::sample_text}; use workspace::{ - CloseWindow, CollaboratorId, MultiWorkspace, ParticipantLocation, SplitDirection, Workspace, - item::ItemHandle as _, + CloseWindow, CollaboratorId, MultiWorkspace, Panel as _, ParticipantLocation, SplitDirection, + Workspace, item::ItemHandle as _, }; use super::TestClient; @@ -534,7 +534,8 @@ async fn test_basic_following( // Client B activates a panel, and the previously-opened screen-sharing item gets activated. let panel = cx_b.new(|cx| TestPanel::new(DockPosition::Left, 100, cx)); workspace_b.update_in(cx_b, |workspace, window, cx| { - workspace.add_panel(panel, window, cx); + let position = panel.read(cx).position(window, cx); + workspace.add_panel(panel, position, window, cx); workspace.toggle_panel_focus::(window, cx); }); executor.run_until_parked(); diff --git a/crates/collab/tests/integration/git_tests.rs b/crates/collab/tests/integration/git_tests.rs index dccc99a07769e66a3eb318a8201d8e14a29ef4f2..ece14fbd64cfd8af042168c04ce72d1ad63ca497 100644 --- a/crates/collab/tests/integration/git_tests.rs +++ b/crates/collab/tests/integration/git_tests.rs @@ -12,6 +12,7 @@ use project::ProjectPath; use serde_json::json; use util::{path, rel_path::rel_path}; +use workspace::dock::Panel; use workspace::{MultiWorkspace, Workspace}; use crate::TestServer; @@ -371,12 +372,14 @@ async fn test_diff_stat_sync_between_host_and_downstream_client( let panel_a = workspace_a.update_in(cx_a, GitPanel::new_test); workspace_a.update_in(cx_a, |workspace, window, cx| { - workspace.add_panel(panel_a.clone(), window, cx); + let position = panel_a.read(cx).position(window, cx); + workspace.add_panel(panel_a.clone(), position, window, cx); }); let panel_b = workspace_b.update_in(cx_b, GitPanel::new_test); workspace_b.update_in(cx_b, |workspace, window, cx| { - workspace.add_panel(panel_b.clone(), window, cx); + let position = panel_b.read(cx).position(window, cx); + workspace.add_panel(panel_b.clone(), position, window, cx); }); cx_a.run_until_parked(); @@ -488,7 +491,8 @@ async fn test_diff_stat_sync_between_host_and_downstream_client( let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); let panel_b = workspace_b.update_in(cx_b, GitPanel::new_test); workspace_b.update_in(cx_b, |workspace, window, cx| { - workspace.add_panel(panel_b.clone(), window, cx); + let position = panel_b.read(cx).position(window, cx); + workspace.add_panel(panel_b.clone(), position, window, cx); }); cx_b.run_until_parked(); diff --git a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs index 6825c468e783ee8d3a2a6107a031accfc108abd0..cb94853dcbc7b0f57cfc83b3a9caa55be75d1a25 100644 --- a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs +++ b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs @@ -42,6 +42,7 @@ use std::{ }; use task::TcpArgumentsTemplate; use util::{path, rel_path::rel_path}; +use workspace::dock::Panel; #[gpui::test(iterations = 10)] async fn test_sharing_an_ssh_remote_project( @@ -783,7 +784,8 @@ async fn test_remote_server_debugger( .unwrap(); workspace.update_in(cx_a, |workspace, window, cx| { - workspace.add_panel(debugger_panel, window, cx); + let position = debugger_panel.read(cx).position(window, cx); + workspace.add_panel(debugger_panel, position, window, cx); }); cx_a.run_until_parked(); @@ -896,7 +898,8 @@ async fn test_slow_adapter_startup_retries( .unwrap(); workspace.update_in(cx_a, |workspace, window, cx| { - workspace.add_panel(debugger_panel, window, cx); + let position = debugger_panel.read(cx).position(window, cx); + workspace.add_panel(debugger_panel, position, window, cx); }); cx_a.run_until_parked(); diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index c183f8941c3f30cb43ffaa638eae4e6b387e226d..944b53a4aa1541a38b088b0d5f984481b31d0429 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -9,6 +9,7 @@ use settings::SettingsStore; use task::SharedTaskContext; use terminal_view::terminal_panel::TerminalPanel; use workspace::MultiWorkspace; +use workspace::dock::Panel; use crate::{debugger_panel::DebugPanel, session::DebugSession}; @@ -82,8 +83,10 @@ pub async fn init_test_workspace( workspace_handle .update(cx, |multi, window, cx| { multi.workspace().update(cx, |workspace, cx| { - workspace.add_panel(debugger_panel, window, cx); - workspace.add_panel(terminal_panel, window, cx); + let position = debugger_panel.read(cx).position(window, cx); + workspace.add_panel(debugger_panel, position, window, cx); + let position = terminal_panel.read(cx).position(window, cx); + workspace.add_panel(terminal_panel, position, window, cx); }); }) .unwrap(); diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index ec85fc14a2eefe280afd0d44ed92b4b8502f460c..4831cb63e23f55b6537206a31f21b40244524aa0 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -6812,7 +6812,8 @@ outline: struct OutlineEntryExcerpt window .update(cx, |multi_workspace, window, cx| { multi_workspace.workspace().update(cx, |workspace, cx| { - workspace.add_panel(outline_panel, window, cx); + let position = outline_panel.read(cx).position(window, cx); + workspace.add_panel(outline_panel, position, window, cx); }); }) .unwrap(); diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index af84a7f522a60abf2608bf1f3435b367d24f6bdc..5efb8dd8efb8991563809868e81208a377c156f8 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -11,6 +11,7 @@ use std::path::{Path, PathBuf}; use util::{path, paths::PathStyle, rel_path::rel_path}; use workspace::{ AppState, ItemHandle, MultiWorkspace, Pane, Workspace, + dock::DockPosition, item::{Item, ProjectItem}, register_project_item, }; @@ -527,7 +528,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { let cx = &mut VisualTestContext::from_window(window.into(), cx); let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); + workspace.add_panel(panel.clone(), DockPosition::Left, window, cx); panel }); cx.run_until_parked(); @@ -960,7 +961,7 @@ async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { let cx = &mut VisualTestContext::from_window(window.into(), cx); let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); + workspace.add_panel(panel.clone(), DockPosition::Left, window, cx); panel }); cx.run_until_parked(); @@ -1073,7 +1074,7 @@ async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) { let cx = &mut VisualTestContext::from_window(window.into(), cx); let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); + workspace.add_panel(panel.clone(), DockPosition::Left, window, cx); panel }); cx.run_until_parked(); @@ -2335,7 +2336,7 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { let cx = &mut VisualTestContext::from_window(window.into(), cx); let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); + workspace.add_panel(panel.clone(), DockPosition::Left, window, cx); panel }); cx.run_until_parked(); @@ -2541,7 +2542,7 @@ async fn test_create_duplicate_items_and_check_history(cx: &mut gpui::TestAppCon let cx = &mut VisualTestContext::from_window(window.into(), cx); let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); + workspace.add_panel(panel.clone(), DockPosition::Left, window, cx); panel }); cx.run_until_parked(); @@ -2807,7 +2808,7 @@ async fn test_rename_item_and_check_history(cx: &mut gpui::TestAppContext) { let cx = &mut VisualTestContext::from_window(window.into(), cx); let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); + workspace.add_panel(panel.clone(), DockPosition::Left, window, cx); panel }); cx.run_until_parked(); @@ -5269,7 +5270,7 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) { let cx = &mut VisualTestContext::from_window(window.into(), cx); let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); + workspace.add_panel(panel.clone(), DockPosition::Left, window, cx); panel }); cx.run_until_parked(); @@ -5454,7 +5455,7 @@ async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppC let cx = &mut VisualTestContext::from_window(window.into(), cx); let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); + workspace.add_panel(panel.clone(), DockPosition::Left, window, cx); panel }); cx.run_until_parked(); @@ -7471,7 +7472,7 @@ async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) { let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); + workspace.add_panel(panel.clone(), DockPosition::Left, window, cx); panel }); cx.run_until_parked(); @@ -7551,7 +7552,7 @@ async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppC let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); + workspace.add_panel(panel.clone(), DockPosition::Left, window, cx); panel }); cx.run_until_parked(); @@ -7696,7 +7697,7 @@ async fn test_create_entry_with_trailing_dot_windows(cx: &mut gpui::TestAppConte let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); + workspace.add_panel(panel.clone(), DockPosition::Left, window, cx); panel }); cx.run_until_parked(); @@ -9305,7 +9306,7 @@ async fn test_preserve_temporary_unfolded_active_index_on_blur_from_context_menu let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); + workspace.add_panel(panel.clone(), DockPosition::Left, window, cx); panel }); @@ -9489,7 +9490,7 @@ async fn run_create_file_in_folded_path_case( let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); + workspace.add_panel(panel.clone(), DockPosition::Left, window, cx); panel }); diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index a163d48a595383980f0169b6cb5096d144bc15ba..081578ed9a8105a054095f369071f95c874c68ac 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -1494,6 +1494,7 @@ mod tests { use settings::SettingsStore; use std::sync::Arc; use util::path_list::PathList; + use workspace::dock::Panel; fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { @@ -2500,7 +2501,8 @@ mod tests { workspace.update_in(cx, |workspace, window, cx| { let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); let panel = cx.new(|cx| AgentPanel::test_new(workspace, text_thread_store, window, cx)); - workspace.add_panel(panel.clone(), window, cx); + let position = panel.read(cx).position(window, cx); + workspace.add_panel(panel.clone(), position, window, cx); panel }) } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 439c6df5ee45938368895a67834d57df695fde89..7fdc2ab5a646b299b8e85fc46e2851b72c1c772a 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -83,6 +83,13 @@ 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: &mut Dock, + workspace: WeakEntity, + window: &mut Window, + cx: &mut Context, + ) -> usize; fn move_to_next_position(&self, window: &mut Window, cx: &mut App) { let current_position = self.position(window, cx); let next_position = [ @@ -187,6 +194,16 @@ where fn enabled(&self, cx: &App) -> bool { self.read(cx).enabled(cx) } + + fn add_to_dock( + &self, + dock: &mut Dock, + workspace: WeakEntity, + window: &mut Window, + cx: &mut Context, + ) -> usize { + dock.add_panel(self.clone(), workspace, window, cx) + } } impl From<&dyn PanelHandle> for AnyView { @@ -262,7 +279,7 @@ impl DockPosition { struct PanelEntry { panel: Arc, - _subscriptions: [Subscription; 3], + _subscriptions: [Subscription; 2], } pub struct PanelButtons { @@ -467,57 +484,6 @@ impl Dock { ) -> usize { let subscriptions = [ cx.observe(&panel, |_, _, cx| cx.notify()), - cx.observe_global_in::(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, @@ -666,6 +632,46 @@ impl Dock { } } + pub fn remove_panel_by_id( + &mut self, + panel_id: EntityId, + window: &mut Window, + cx: &mut Context, + ) -> bool { + if let Some(panel_ix) = self + .panel_entries + .iter() + .position(|entry| entry.panel.panel_id() == panel_id) + { + if let Some(active_panel_index) = self.active_panel_index.as_mut() { + match panel_ix.cmp(active_panel_index) { + std::cmp::Ordering::Less => { + *active_panel_index -= 1; + } + std::cmp::Ordering::Equal => { + self.active_panel_index = None; + self.set_open(false, window, cx); + } + std::cmp::Ordering::Greater => {} + } + } + + self.panel_entries.remove(panel_ix); + cx.notify(); + + true + } else { + false + } + } + + pub fn panel_ids(&self) -> Vec { + self.panel_entries + .iter() + .map(|entry| entry.panel.panel_id()) + .collect() + } + pub fn panels_len(&self) -> usize { self.panel_entries.len() } diff --git a/crates/workspace/src/multi_workspace.rs b/crates/workspace/src/multi_workspace.rs index 1c36fb295e09e5e309483df79e99101a350e8f77..f9e0bb13047eb6a1f1393b21289515e677679b0a 100644 --- a/crates/workspace/src/multi_workspace.rs +++ b/crates/workspace/src/multi_workspace.rs @@ -146,7 +146,7 @@ impl MultiWorkspace { active_workspace_index: 0, sidebar: None, sidebar_open: false, - is_singleton: false, + is_singleton: true, _sidebar_subscription: None, pending_removal_tasks: Vec::new(), _serialize_task: None, @@ -488,11 +488,12 @@ impl MultiWorkspace { pub fn add_panel( &mut self, panel: Entity, + position: DockPosition, window: &mut Window, cx: &mut Context, ) { self.workspace().update(cx, |workspace, cx| { - workspace.add_panel(panel, window, cx); + workspace.add_panel(panel, position, window, cx); }); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 90f05d07a3a87a53ca25a1dc15da7663a95984a8..f4726323049ebd11727a4dfa50279551c0a41065 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2121,6 +2121,7 @@ impl Workspace { pub fn add_panel( &mut self, panel: Entity, + position: DockPosition, window: &mut Window, cx: &mut Context, ) { @@ -2128,8 +2129,7 @@ impl Workspace { cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused) .detach(); - let dock_position = panel.position(window, cx); - let dock = self.dock_at_position(dock_position); + let dock = self.dock_at_position(position); let any_panel = panel.to_any(); dock.update(cx, |dock, cx| { @@ -2139,6 +2139,79 @@ impl Workspace { cx.emit(Event::PanelAdded(any_panel)); } + pub fn move_panel_to_dock( + &mut self, + panel_id: EntityId, + new_position: DockPosition, + window: &mut Window, + cx: &mut Context, + ) { + let current_dock_position = self + .all_docks() + .iter() + .find(|dock| dock.read(cx).panel_for_id(panel_id).is_some()) + .map(|dock| dock.read(cx).position()); + + let Some(current_dock_position) = current_dock_position else { + return; + }; + + if current_dock_position == new_position { + return; + } + + let current_dock = self.dock_at_position(current_dock_position).clone(); + + let was_visible = current_dock.read(cx).is_open() + && current_dock + .read(cx) + .visible_panel() + .is_some_and(|active_panel| active_panel.panel_id() == panel_id); + + let panel_handle = current_dock.read(cx).panel_for_id(panel_id).cloned(); + + let Some(panel_handle) = panel_handle else { + return; + }; + + if panel_handle.is_zoomed(window, cx) { + self.zoomed_position = Some(new_position); + } + + current_dock.update(cx, |dock, cx| { + dock.remove_panel_by_id(panel_id, window, cx); + }); + + let new_dock = self.dock_at_position(new_position).clone(); + + new_dock.update(cx, |dock, cx| { + dock.remove_panel_by_id(panel_id, window, cx); + }); + + let weak_self = self.weak_self.clone(); + new_dock.update(cx, |dock, cx| { + let index = panel_handle.add_to_dock(dock, weak_self, window, cx); + if was_visible { + dock.set_open(true, window, cx); + dock.activate_panel(index, window, cx); + } + }); + + self.serialize_workspace(window, cx); + } + + pub fn all_panel_ids_and_positions(&self, cx: &App) -> Vec<(EntityId, DockPosition)> { + let mut result = Vec::new(); + for dock in self.all_docks() { + let dock = dock.read(cx); + let position = dock.position(); + for panel_id in dock.panel_ids() { + result.push((panel_id, position)); + } + } + result + } + pub fn remove_panel( &mut self, panel: &Entity, @@ -10907,7 +10980,8 @@ mod tests { let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx)); - workspace.add_panel(panel.clone(), window, cx); + let position = panel.read(cx).position(window, cx); + workspace.add_panel(panel.clone(), position, window, cx); workspace .right_dock() @@ -11057,7 +11131,8 @@ mod tests { let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx)); - workspace.add_panel(panel.clone(), window, cx); + let position = panel.read(cx).position(window, cx); + workspace.add_panel(panel.clone(), position, window, cx); panel }); @@ -11352,10 +11427,12 @@ mod tests { // Open two docks (left and right) with one panel each let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| { let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx)); - workspace.add_panel(left_panel.clone(), window, cx); + let position = left_panel.read(cx).position(window, cx); + workspace.add_panel(left_panel.clone(), position, window, cx); let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx)); - workspace.add_panel(right_panel.clone(), window, cx); + let position = right_panel.read(cx).position(window, cx); + workspace.add_panel(right_panel.clone(), position, window, cx); workspace.toggle_dock(DockPosition::Left, window, cx); workspace.toggle_dock(DockPosition::Right, window, cx); @@ -11784,10 +11861,12 @@ mod tests { let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| { let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx)); - workspace.add_panel(panel_1.clone(), window, cx); + let position = panel_1.read(cx).position(window, cx); + workspace.add_panel(panel_1.clone(), position, window, cx); workspace.toggle_dock(DockPosition::Left, window, cx); let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx)); - workspace.add_panel(panel_2.clone(), window, cx); + let position = panel_2.read(cx).position(window, cx); + workspace.add_panel(panel_2.clone(), position, window, cx); workspace.toggle_dock(DockPosition::Right, window, cx); let left_dock = workspace.left_dock(); @@ -12695,7 +12774,8 @@ mod tests { // focus to the new panel. let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx)); - workspace.add_panel(panel.clone(), window, cx); + let position = panel.read(cx).position(window, cx); + workspace.add_panel(panel.clone(), position, window, cx); workspace .right_dock() @@ -13383,7 +13463,8 @@ mod tests { let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx)); - workspace.add_panel(panel.clone(), window, cx); + let position = panel.read(cx).position(window, cx); + workspace.add_panel(panel.clone(), position, window, cx); workspace .right_dock() @@ -13468,7 +13549,8 @@ mod tests { // Add a panel to workspace A's right dock and open the dock let panel = workspace_a.update_in(cx, |workspace, window, cx| { let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx)); - workspace.add_panel(panel.clone(), window, cx); + let position = panel.read(cx).position(window, cx); + workspace.add_panel(panel.clone(), position, window, cx); workspace .right_dock() .update(cx, |dock, cx| dock.set_open(true, window, cx)); @@ -13570,7 +13652,8 @@ mod tests { let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx)); - workspace.add_panel(panel.clone(), window, cx); + let position = panel.read(cx).position(window, cx); + workspace.add_panel(panel.clone(), position, window, cx); workspace .right_dock() .update(cx, |dock, cx| dock.set_open(true, window, cx)); diff --git a/crates/zed/src/visual_test_runner.rs b/crates/zed/src/visual_test_runner.rs index ead16b911e3ccf9ebd1b9f54113cb01dca849e9d..acf419f0be930fb876d53d7db80d92b2942bdbaf 100644 --- a/crates/zed/src/visual_test_runner.rs +++ b/crates/zed/src/visual_test_runner.rs @@ -321,7 +321,8 @@ fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()> workspace_window .update(&mut cx, |workspace, window, cx| { - workspace.add_panel(panel, window, cx); + let position = panel.read(cx).position(window, cx); + workspace.add_panel(panel, position, window, cx); }) .log_err(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 703e65231c0e3fea604651705f1c940ebb659af3..abe89990a2bb28255d8989b6a659c2ec65f20bb9 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -84,10 +84,12 @@ 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, }; +use workspace::dock::DockPosition; use workspace::{ AppState, MultiWorkspace, NewFile, NewWindow, OpenLog, Panel, Toast, Workspace, WorkspaceSettings, create_and_open_local_file, @@ -293,11 +295,13 @@ pub fn init(cx: &mut App) { let Some(handle) = window.downcast::() else { return; }; - handle - .update(cx, |multi_workspace, window, cx| { - toggle_agent_mode(multi_workspace, window, cx); - }) - .log_err(); + cx.defer(move |cx| { + handle + .update(cx, |multi_workspace, window, cx| { + toggle_agent_mode(multi_workspace, window, cx); + }) + .log_err(); + }); }); } @@ -306,13 +310,19 @@ fn toggle_agent_mode( window: &mut Window, cx: &mut Context<'_, MultiWorkspace>, ) { - let is_singleton = multi_workspace.is_singleton(); + let mut is_singleton = multi_workspace.is_singleton(); if is_singleton { multi_workspace.set_singleton(false, window, cx); multi_workspace.open_sidebar(cx); } else { multi_workspace.set_singleton(true, window, cx); } + is_singleton = !is_singleton; + let agent_mode = !is_singleton; + let workspace = multi_workspace.workspace(); + workspace.update(cx, |workspace, cx| { + update_panel_positions(workspace, window, agent_mode, cx); + }); } fn bind_on_window_closed(cx: &mut App) -> Option { @@ -648,6 +658,33 @@ fn show_software_emulation_warning_if_needed( } } +fn is_agent_mode(cx: &mut AsyncWindowContext) -> bool { + cx.window_handle() + .downcast::() + .and_then(|handle| { + handle + .read_with(cx, |multi_workspace, _| !multi_workspace.is_singleton()) + .ok() + }) + .unwrap_or(false) +} + +fn interpret_panel_dock_position( + panel: &dyn PanelHandle, + preferred: DockPosition, + agent_mode: bool, +) -> DockPosition { + let is_agent_panel = panel.panel_key() == agent_ui::AgentPanel::panel_key(); + if agent_mode { + if is_agent_panel { + return DockPosition::Left; + } else if preferred == DockPosition::Left { + return DockPosition::Right; + } + } + preferred +} + fn initialize_panels( prompt_builder: Arc, window: &mut Window, @@ -666,16 +703,18 @@ fn initialize_panels( ); let debug_panel = DebugPanel::load(workspace_handle.clone(), cx); - async fn add_panel_when_ready( - panel_task: impl Future>> + 'static, + async fn add_panel_when_ready( + panel_task: impl Future>> + 'static, workspace_handle: WeakEntity, mut cx: gpui::AsyncWindowContext, ) { - if let Some(panel) = panel_task.await.context("failed to load panel").log_err() - { + if let Some(panel) = panel_task.await.context("failed to load panel").log_err() { + let agent_mode = is_agent_mode(&mut cx); workspace_handle .update_in(&mut cx, |workspace, window, cx| { - workspace.add_panel(panel, window, cx); + let preferred = panel.position(window, cx); + let position = interpret_panel_dock_position(&panel, preferred, agent_mode); + workspace.add_panel(panel, position, window, cx); }) .log_err(); } @@ -689,13 +728,62 @@ fn initialize_panels( add_panel_when_ready(channels_panel, workspace_handle.clone(), cx.clone()), add_panel_when_ready(notification_panel, workspace_handle.clone(), cx.clone()), add_panel_when_ready(debug_panel, workspace_handle.clone(), cx.clone()), - initialize_agent_panel(workspace_handle, prompt_builder, cx.clone()).map(|r| r.log_err()), + initialize_agent_panel(workspace_handle.clone(), prompt_builder, cx.clone()) + .map(|r| r.log_err()), ); + let mut cx = cx.clone(); + workspace_handle.update_in(&mut cx, |workspace, window, cx| { + observe_settings_for_panel_positions(workspace, window, cx); + })?; + anyhow::Ok(()) }) } +fn observe_settings_for_panel_positions( + _workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, +) { + cx.observe_global_in::(window, move |workspace, window, cx| { + let agent_mode = window + .root::() + .flatten() + .is_some_and(|handle| !handle.read(cx).is_singleton()); + update_panel_positions(workspace, window, agent_mode, cx); + }) + .detach(); +} + +fn update_panel_positions( + workspace: &mut Workspace, + window: &mut Window, + agent_mode: bool, + cx: &mut Context, +) { + let panels_and_positions = workspace.all_panel_ids_and_positions(cx); + for (panel_id, current_position) in panels_and_positions { + let panel_handle = workspace + .dock_at_position(current_position) + .read(cx) + .panel_for_id(panel_id) + .cloned(); + + let Some(panel_handle) = panel_handle else { + continue; + }; + + let preferred_position = panel_handle.position(window, cx); + let target_position = + interpret_panel_dock_position(panel_handle.as_ref(), preferred_position, agent_mode); + + if target_position != current_position { + workspace.move_panel_to_dock(panel_id, target_position, window, cx); + } + } +} + fn setup_or_teardown_ai_panel( workspace: &mut Workspace, window: &mut Window, @@ -712,15 +800,18 @@ fn setup_or_teardown_ai_panel( || cfg!(test); let existing_panel = workspace.panel::

(cx); match (disable_ai, existing_panel) { - (false, None) => cx.spawn_in(window, async move |workspace, cx| { + (false, None) => cx.spawn_in(window, async move |workspace, mut cx| { let panel = load_panel(workspace.clone(), cx.clone()).await?; + let agent_mode = is_agent_mode(&mut cx); workspace.update_in(cx, |workspace, window, cx| { let disable_ai = SettingsStore::global(cx) .get::(None) .disable_ai; let have_panel = workspace.panel::

(cx).is_some(); if !disable_ai && !have_panel { - workspace.add_panel(panel, window, cx); + let preferred = panel.position(window, cx); + let position = interpret_panel_dock_position(&panel, preferred, agent_mode); + workspace.add_panel(panel, position, window, cx); } }) }),