multi_workspace.rs

   1use anyhow::Result;
   2use collections::{HashMap, HashSet};
   3use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
   4use gpui::PathPromptOptions;
   5use gpui::{
   6    AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
   7    ManagedView, MouseButton, Pixels, Render, Subscription, Task, Tiling, Window, WindowId,
   8    actions, deferred, px,
   9};
  10use project::{DirectoryLister, DisableAiSettings, Project, ProjectGroupKey};
  11use settings::Settings;
  12pub use settings::SidebarSide;
  13use std::future::Future;
  14use std::path::Path;
  15use std::path::PathBuf;
  16use std::sync::Arc;
  17use ui::prelude::*;
  18use util::ResultExt;
  19use util::path_list::PathList;
  20use zed_actions::agents_sidebar::ToggleThreadSwitcher;
  21
  22use agent_settings::AgentSettings;
  23use settings::SidebarDockPosition;
  24use ui::{ContextMenu, right_click_menu};
  25
  26const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
  27
  28use crate::AppState;
  29use crate::{
  30    CloseIntent, CloseWindow, DockPosition, Event as WorkspaceEvent, Item, ModalView, OpenMode,
  31    Panel, Workspace, WorkspaceId, client_side_decorations,
  32    persistence::model::MultiWorkspaceState,
  33};
  34
  35actions!(
  36    multi_workspace,
  37    [
  38        /// Toggles the workspace switcher sidebar.
  39        ToggleWorkspaceSidebar,
  40        /// Closes the workspace sidebar.
  41        CloseWorkspaceSidebar,
  42        /// Moves focus to or from the workspace sidebar without closing it.
  43        FocusWorkspaceSidebar,
  44        /// Activates the next project group in the sidebar.
  45        NextProjectGroup,
  46        /// Activates the previous project group in the sidebar.
  47        PreviousProjectGroup,
  48        /// Activates the next thread in sidebar order.
  49        NextThread,
  50        /// Activates the previous thread in sidebar order.
  51        PreviousThread,
  52        /// Expands the thread list for the current project to show more threads.
  53        ShowMoreThreads,
  54        /// Collapses the thread list for the current project to show fewer threads.
  55        ShowFewerThreads,
  56        /// Creates a new thread in the current workspace.
  57        NewThread,
  58        /// Moves the current workspace's project group to a new window.
  59        MoveWorkspaceToNewWindow,
  60    ]
  61);
  62
  63#[derive(Default)]
  64pub struct SidebarRenderState {
  65    pub open: bool,
  66    pub side: SidebarSide,
  67}
  68
  69pub fn sidebar_side_context_menu(
  70    id: impl Into<ElementId>,
  71    cx: &App,
  72) -> ui::RightClickMenu<ContextMenu> {
  73    let current_position = AgentSettings::get_global(cx).sidebar_side;
  74    right_click_menu(id).menu(move |window, cx| {
  75        let fs = <dyn fs::Fs>::global(cx);
  76        ContextMenu::build(window, cx, move |mut menu, _, _cx| {
  77            let positions: [(SidebarDockPosition, &str); 2] = [
  78                (SidebarDockPosition::Left, "Left"),
  79                (SidebarDockPosition::Right, "Right"),
  80            ];
  81            for (position, label) in positions {
  82                let fs = fs.clone();
  83                menu = menu.toggleable_entry(
  84                    label,
  85                    position == current_position,
  86                    IconPosition::Start,
  87                    None,
  88                    move |_window, cx| {
  89                        settings::update_settings_file(fs.clone(), cx, move |settings, _cx| {
  90                            settings
  91                                .agent
  92                                .get_or_insert_default()
  93                                .set_sidebar_side(position);
  94                        });
  95                    },
  96                );
  97            }
  98            menu
  99        })
 100    })
 101}
 102
 103pub enum MultiWorkspaceEvent {
 104    ActiveWorkspaceChanged,
 105    WorkspaceAdded(Entity<Workspace>),
 106    WorkspaceRemoved(EntityId),
 107}
 108
 109pub enum SidebarEvent {
 110    SerializeNeeded,
 111}
 112
 113pub trait Sidebar: Focusable + Render + EventEmitter<SidebarEvent> + Sized {
 114    fn width(&self, cx: &App) -> Pixels;
 115    fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>);
 116    fn has_notifications(&self, cx: &App) -> bool;
 117    fn side(&self, _cx: &App) -> SidebarSide;
 118
 119    fn is_threads_list_view_active(&self) -> bool {
 120        true
 121    }
 122    /// Makes focus reset back to the search editor upon toggling the sidebar from outside
 123    fn prepare_for_focus(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
 124    /// Opens or cycles the thread switcher popup.
 125    fn toggle_thread_switcher(
 126        &mut self,
 127        _select_last: bool,
 128        _window: &mut Window,
 129        _cx: &mut Context<Self>,
 130    ) {
 131    }
 132
 133    /// Activates the next or previous project group.
 134    fn cycle_project_group(
 135        &mut self,
 136        _forward: bool,
 137        _window: &mut Window,
 138        _cx: &mut Context<Self>,
 139    ) {
 140    }
 141
 142    /// Activates the next or previous thread in sidebar order.
 143    fn cycle_thread(&mut self, _forward: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
 144
 145    /// Moves the active workspace's project group to a new window.
 146    fn move_workspace_to_new_window(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
 147
 148    /// Return an opaque JSON blob of sidebar-specific state to persist.
 149    fn serialized_state(&self, _cx: &App) -> Option<String> {
 150        None
 151    }
 152
 153    /// Restore sidebar state from a previously-serialized blob.
 154    fn restore_serialized_state(
 155        &mut self,
 156        _state: &str,
 157        _window: &mut Window,
 158        _cx: &mut Context<Self>,
 159    ) {
 160    }
 161}
 162
 163pub trait SidebarHandle: 'static + Send + Sync {
 164    fn width(&self, cx: &App) -> Pixels;
 165    fn set_width(&self, width: Option<Pixels>, cx: &mut App);
 166    fn focus_handle(&self, cx: &App) -> FocusHandle;
 167    fn focus(&self, window: &mut Window, cx: &mut App);
 168    fn prepare_for_focus(&self, window: &mut Window, cx: &mut App);
 169    fn has_notifications(&self, cx: &App) -> bool;
 170    fn to_any(&self) -> AnyView;
 171    fn entity_id(&self) -> EntityId;
 172    fn toggle_thread_switcher(&self, select_last: bool, window: &mut Window, cx: &mut App);
 173    fn cycle_project_group(&self, forward: bool, window: &mut Window, cx: &mut App);
 174    fn cycle_thread(&self, forward: bool, window: &mut Window, cx: &mut App);
 175    fn move_workspace_to_new_window(&self, window: &mut Window, cx: &mut App);
 176
 177    fn is_threads_list_view_active(&self, cx: &App) -> bool;
 178
 179    fn side(&self, cx: &App) -> SidebarSide;
 180    fn serialized_state(&self, cx: &App) -> Option<String>;
 181    fn restore_serialized_state(&self, state: &str, window: &mut Window, cx: &mut App);
 182}
 183
 184#[derive(Clone)]
 185pub struct DraggedSidebar;
 186
 187impl Render for DraggedSidebar {
 188    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 189        gpui::Empty
 190    }
 191}
 192
 193impl<T: Sidebar> SidebarHandle for Entity<T> {
 194    fn width(&self, cx: &App) -> Pixels {
 195        self.read(cx).width(cx)
 196    }
 197
 198    fn set_width(&self, width: Option<Pixels>, cx: &mut App) {
 199        self.update(cx, |this, cx| this.set_width(width, cx))
 200    }
 201
 202    fn focus_handle(&self, cx: &App) -> FocusHandle {
 203        self.read(cx).focus_handle(cx)
 204    }
 205
 206    fn focus(&self, window: &mut Window, cx: &mut App) {
 207        let handle = self.read(cx).focus_handle(cx);
 208        window.focus(&handle, cx);
 209    }
 210
 211    fn prepare_for_focus(&self, window: &mut Window, cx: &mut App) {
 212        self.update(cx, |this, cx| this.prepare_for_focus(window, cx));
 213    }
 214
 215    fn has_notifications(&self, cx: &App) -> bool {
 216        self.read(cx).has_notifications(cx)
 217    }
 218
 219    fn to_any(&self) -> AnyView {
 220        self.clone().into()
 221    }
 222
 223    fn entity_id(&self) -> EntityId {
 224        Entity::entity_id(self)
 225    }
 226
 227    fn toggle_thread_switcher(&self, select_last: bool, window: &mut Window, cx: &mut App) {
 228        let entity = self.clone();
 229        window.defer(cx, move |window, cx| {
 230            entity.update(cx, |this, cx| {
 231                this.toggle_thread_switcher(select_last, window, cx);
 232            });
 233        });
 234    }
 235
 236    fn cycle_project_group(&self, forward: bool, window: &mut Window, cx: &mut App) {
 237        let entity = self.clone();
 238        window.defer(cx, move |window, cx| {
 239            entity.update(cx, |this, cx| {
 240                this.cycle_project_group(forward, window, cx);
 241            });
 242        });
 243    }
 244
 245    fn cycle_thread(&self, forward: bool, window: &mut Window, cx: &mut App) {
 246        let entity = self.clone();
 247        window.defer(cx, move |window, cx| {
 248            entity.update(cx, |this, cx| {
 249                this.cycle_thread(forward, window, cx);
 250            });
 251        });
 252    }
 253
 254    fn move_workspace_to_new_window(&self, window: &mut Window, cx: &mut App) {
 255        let entity = self.clone();
 256        window.defer(cx, move |window, cx| {
 257            entity.update(cx, |this, cx| {
 258                this.move_workspace_to_new_window(window, cx);
 259            });
 260        });
 261    }
 262
 263    fn is_threads_list_view_active(&self, cx: &App) -> bool {
 264        self.read(cx).is_threads_list_view_active()
 265    }
 266
 267    fn side(&self, cx: &App) -> SidebarSide {
 268        self.read(cx).side(cx)
 269    }
 270
 271    fn serialized_state(&self, cx: &App) -> Option<String> {
 272        self.read(cx).serialized_state(cx)
 273    }
 274
 275    fn restore_serialized_state(&self, state: &str, window: &mut Window, cx: &mut App) {
 276        self.update(cx, |this, cx| {
 277            this.restore_serialized_state(state, window, cx)
 278        })
 279    }
 280}
 281
 282/// Tracks which workspace the user is currently looking at.
 283///
 284/// `Persistent` workspaces live in the `workspaces` vec and are shown in the
 285/// sidebar. `Transient` workspaces exist outside the vec and are discarded
 286/// when the user switches away.
 287enum ActiveWorkspace {
 288    /// A persistent workspace, identified by index into the `workspaces` vec.
 289    Persistent(usize),
 290    /// A workspace not in the `workspaces` vec that will be discarded on
 291    /// switch or promoted to persistent when the sidebar is opened.
 292    Transient(Entity<Workspace>),
 293}
 294
 295impl ActiveWorkspace {
 296    fn persistent_index(&self) -> Option<usize> {
 297        match self {
 298            Self::Persistent(index) => Some(*index),
 299            Self::Transient(_) => None,
 300        }
 301    }
 302
 303    fn transient_workspace(&self) -> Option<&Entity<Workspace>> {
 304        match self {
 305            Self::Transient(workspace) => Some(workspace),
 306            Self::Persistent(_) => None,
 307        }
 308    }
 309
 310    /// Sets the active workspace to transient, returning the previous
 311    /// transient workspace (if any).
 312    fn set_transient(&mut self, workspace: Entity<Workspace>) -> Option<Entity<Workspace>> {
 313        match std::mem::replace(self, Self::Transient(workspace)) {
 314            Self::Transient(old) => Some(old),
 315            Self::Persistent(_) => None,
 316        }
 317    }
 318
 319    /// Sets the active workspace to persistent at the given index,
 320    /// returning the previous transient workspace (if any).
 321    fn set_persistent(&mut self, index: usize) -> Option<Entity<Workspace>> {
 322        match std::mem::replace(self, Self::Persistent(index)) {
 323            Self::Transient(workspace) => Some(workspace),
 324            Self::Persistent(_) => None,
 325        }
 326    }
 327}
 328
 329pub struct MultiWorkspace {
 330    window_id: WindowId,
 331    workspaces: Vec<Entity<Workspace>>,
 332    active_workspace: ActiveWorkspace,
 333    project_group_keys: Vec<ProjectGroupKey>,
 334    provisional_project_group_keys: HashMap<EntityId, ProjectGroupKey>,
 335    sidebar: Option<Box<dyn SidebarHandle>>,
 336    sidebar_open: bool,
 337    sidebar_overlay: Option<AnyView>,
 338    pending_removal_tasks: Vec<Task<()>>,
 339    _serialize_task: Option<Task<()>>,
 340    _subscriptions: Vec<Subscription>,
 341    previous_focus_handle: Option<FocusHandle>,
 342}
 343
 344impl EventEmitter<MultiWorkspaceEvent> for MultiWorkspace {}
 345
 346impl MultiWorkspace {
 347    pub fn sidebar_side(&self, cx: &App) -> SidebarSide {
 348        self.sidebar
 349            .as_ref()
 350            .map_or(SidebarSide::Left, |s| s.side(cx))
 351    }
 352
 353    pub fn sidebar_render_state(&self, cx: &App) -> SidebarRenderState {
 354        SidebarRenderState {
 355            open: self.sidebar_open() && self.multi_workspace_enabled(cx),
 356            side: self.sidebar_side(cx),
 357        }
 358    }
 359
 360    pub fn new(workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
 361        let release_subscription = cx.on_release(|this: &mut MultiWorkspace, _cx| {
 362            if let Some(task) = this._serialize_task.take() {
 363                task.detach();
 364            }
 365            for task in std::mem::take(&mut this.pending_removal_tasks) {
 366                task.detach();
 367            }
 368        });
 369        let quit_subscription = cx.on_app_quit(Self::app_will_quit);
 370        let settings_subscription = cx.observe_global_in::<settings::SettingsStore>(window, {
 371            let mut previous_disable_ai = DisableAiSettings::get_global(cx).disable_ai;
 372            move |this, window, cx| {
 373                if DisableAiSettings::get_global(cx).disable_ai != previous_disable_ai {
 374                    this.collapse_to_single_workspace(window, cx);
 375                    previous_disable_ai = DisableAiSettings::get_global(cx).disable_ai;
 376                }
 377            }
 378        });
 379        Self::subscribe_to_workspace(&workspace, window, cx);
 380        let weak_self = cx.weak_entity();
 381        workspace.update(cx, |workspace, cx| {
 382            workspace.set_multi_workspace(weak_self, cx);
 383        });
 384        Self {
 385            window_id: window.window_handle().window_id(),
 386            project_group_keys: Vec::new(),
 387            provisional_project_group_keys: HashMap::default(),
 388            workspaces: Vec::new(),
 389            active_workspace: ActiveWorkspace::Transient(workspace),
 390            sidebar: None,
 391            sidebar_open: false,
 392            sidebar_overlay: None,
 393            pending_removal_tasks: Vec::new(),
 394            _serialize_task: None,
 395            _subscriptions: vec![
 396                release_subscription,
 397                quit_subscription,
 398                settings_subscription,
 399            ],
 400            previous_focus_handle: None,
 401        }
 402    }
 403
 404    pub fn register_sidebar<T: Sidebar>(&mut self, sidebar: Entity<T>, cx: &mut Context<Self>) {
 405        self._subscriptions
 406            .push(cx.observe(&sidebar, |_this, _, cx| {
 407                cx.notify();
 408            }));
 409        self._subscriptions
 410            .push(cx.subscribe(&sidebar, |this, _, event, cx| match event {
 411                SidebarEvent::SerializeNeeded => {
 412                    this.serialize(cx);
 413                }
 414            }));
 415        self.sidebar = Some(Box::new(sidebar));
 416    }
 417
 418    pub fn sidebar(&self) -> Option<&dyn SidebarHandle> {
 419        self.sidebar.as_deref()
 420    }
 421
 422    pub fn set_sidebar_overlay(&mut self, overlay: Option<AnyView>, cx: &mut Context<Self>) {
 423        self.sidebar_overlay = overlay;
 424        cx.notify();
 425    }
 426
 427    pub fn sidebar_open(&self) -> bool {
 428        self.sidebar_open
 429    }
 430
 431    pub fn sidebar_has_notifications(&self, cx: &App) -> bool {
 432        self.sidebar
 433            .as_ref()
 434            .map_or(false, |s| s.has_notifications(cx))
 435    }
 436
 437    pub fn is_threads_list_view_active(&self, cx: &App) -> bool {
 438        self.sidebar
 439            .as_ref()
 440            .map_or(false, |s| s.is_threads_list_view_active(cx))
 441    }
 442
 443    pub fn multi_workspace_enabled(&self, cx: &App) -> bool {
 444        cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
 445    }
 446
 447    pub fn toggle_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 448        if !self.multi_workspace_enabled(cx) {
 449            return;
 450        }
 451
 452        if self.sidebar_open() {
 453            self.close_sidebar(window, cx);
 454        } else {
 455            self.previous_focus_handle = window.focused(cx);
 456            self.open_sidebar(cx);
 457            if let Some(sidebar) = &self.sidebar {
 458                sidebar.prepare_for_focus(window, cx);
 459                sidebar.focus(window, cx);
 460            }
 461        }
 462    }
 463
 464    pub fn close_sidebar_action(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 465        if !self.multi_workspace_enabled(cx) {
 466            return;
 467        }
 468
 469        if self.sidebar_open() {
 470            self.close_sidebar(window, cx);
 471        }
 472    }
 473
 474    pub fn focus_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 475        if !self.multi_workspace_enabled(cx) {
 476            return;
 477        }
 478
 479        if self.sidebar_open() {
 480            let sidebar_is_focused = self
 481                .sidebar
 482                .as_ref()
 483                .is_some_and(|s| s.focus_handle(cx).contains_focused(window, cx));
 484
 485            if sidebar_is_focused {
 486                self.restore_previous_focus(false, window, cx);
 487            } else {
 488                self.previous_focus_handle = window.focused(cx);
 489                if let Some(sidebar) = &self.sidebar {
 490                    sidebar.prepare_for_focus(window, cx);
 491                    sidebar.focus(window, cx);
 492                }
 493            }
 494        } else {
 495            self.previous_focus_handle = window.focused(cx);
 496            self.open_sidebar(cx);
 497            if let Some(sidebar) = &self.sidebar {
 498                sidebar.prepare_for_focus(window, cx);
 499                sidebar.focus(window, cx);
 500            }
 501        }
 502    }
 503
 504    pub fn open_sidebar(&mut self, cx: &mut Context<Self>) {
 505        self.sidebar_open = true;
 506        if let ActiveWorkspace::Transient(workspace) = &self.active_workspace {
 507            let workspace = workspace.clone();
 508            let index = self.promote_transient(workspace, cx);
 509            self.active_workspace = ActiveWorkspace::Persistent(index);
 510        }
 511        let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
 512        for workspace in self.workspaces.iter() {
 513            workspace.update(cx, |workspace, _cx| {
 514                workspace.set_sidebar_focus_handle(sidebar_focus_handle.clone());
 515            });
 516        }
 517        self.serialize(cx);
 518        cx.notify();
 519    }
 520
 521    pub fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 522        self.sidebar_open = false;
 523        for workspace in self.workspaces.iter() {
 524            workspace.update(cx, |workspace, _cx| {
 525                workspace.set_sidebar_focus_handle(None);
 526            });
 527        }
 528        self.restore_previous_focus(true, window, cx);
 529        self.serialize(cx);
 530        cx.notify();
 531    }
 532
 533    fn restore_previous_focus(&mut self, clear: bool, window: &mut Window, cx: &mut Context<Self>) {
 534        let focus_handle = if clear {
 535            self.previous_focus_handle.take()
 536        } else {
 537            self.previous_focus_handle.clone()
 538        };
 539
 540        if let Some(previous_focus) = focus_handle {
 541            previous_focus.focus(window, cx);
 542        } else {
 543            let pane = self.workspace().read(cx).active_pane().clone();
 544            window.focus(&pane.read(cx).focus_handle(cx), cx);
 545        }
 546    }
 547
 548    pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
 549        cx.spawn_in(window, async move |this, cx| {
 550            let workspaces = this.update(cx, |multi_workspace, _cx| {
 551                multi_workspace.workspaces().cloned().collect::<Vec<_>>()
 552            })?;
 553
 554            for workspace in workspaces {
 555                let should_continue = workspace
 556                    .update_in(cx, |workspace, window, cx| {
 557                        workspace.prepare_to_close(CloseIntent::CloseWindow, window, cx)
 558                    })?
 559                    .await?;
 560                if !should_continue {
 561                    return anyhow::Ok(());
 562                }
 563            }
 564
 565            cx.update(|window, _cx| {
 566                window.remove_window();
 567            })?;
 568
 569            anyhow::Ok(())
 570        })
 571        .detach_and_log_err(cx);
 572    }
 573
 574    fn subscribe_to_workspace(
 575        workspace: &Entity<Workspace>,
 576        window: &Window,
 577        cx: &mut Context<Self>,
 578    ) {
 579        let project = workspace.read(cx).project().clone();
 580        cx.subscribe_in(&project, window, {
 581            let workspace = workspace.downgrade();
 582            move |this, _project, event, _window, cx| match event {
 583                project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => {
 584                    if let Some(workspace) = workspace.upgrade() {
 585                        this.add_project_group_key(workspace.read(cx).project_group_key(cx));
 586                    }
 587                }
 588                project::Event::WorktreeUpdatedRootRepoCommonDir(_) => {
 589                    if let Some(workspace) = workspace.upgrade() {
 590                        this.maybe_clear_provisional_project_group_key(&workspace, cx);
 591                        this.add_project_group_key(
 592                            this.project_group_key_for_workspace(&workspace, cx),
 593                        );
 594                        this.remove_stale_project_group_keys(cx);
 595                        cx.notify();
 596                    }
 597                }
 598                _ => {}
 599            }
 600        })
 601        .detach();
 602
 603        cx.subscribe_in(workspace, window, |this, workspace, event, window, cx| {
 604            if let WorkspaceEvent::Activate = event {
 605                this.activate(workspace.clone(), window, cx);
 606            }
 607        })
 608        .detach();
 609    }
 610
 611    pub fn add_project_group_key(&mut self, project_group_key: ProjectGroupKey) {
 612        if project_group_key.path_list().paths().is_empty() {
 613            return;
 614        }
 615        if self.project_group_keys.contains(&project_group_key) {
 616            return;
 617        }
 618        self.project_group_keys.push(project_group_key);
 619    }
 620
 621    pub fn set_provisional_project_group_key(
 622        &mut self,
 623        workspace: &Entity<Workspace>,
 624        project_group_key: ProjectGroupKey,
 625    ) {
 626        self.provisional_project_group_keys
 627            .insert(workspace.entity_id(), project_group_key.clone());
 628        self.add_project_group_key(project_group_key);
 629    }
 630
 631    pub fn project_group_key_for_workspace(
 632        &self,
 633        workspace: &Entity<Workspace>,
 634        cx: &App,
 635    ) -> ProjectGroupKey {
 636        self.provisional_project_group_keys
 637            .get(&workspace.entity_id())
 638            .cloned()
 639            .unwrap_or_else(|| workspace.read(cx).project_group_key(cx))
 640    }
 641
 642    fn maybe_clear_provisional_project_group_key(
 643        &mut self,
 644        workspace: &Entity<Workspace>,
 645        cx: &App,
 646    ) {
 647        let live_key = workspace.read(cx).project_group_key(cx);
 648        if self
 649            .provisional_project_group_keys
 650            .get(&workspace.entity_id())
 651            .is_some_and(|key| *key == live_key)
 652        {
 653            self.provisional_project_group_keys
 654                .remove(&workspace.entity_id());
 655        }
 656    }
 657
 658    fn remove_stale_project_group_keys(&mut self, cx: &App) {
 659        let workspace_keys: HashSet<ProjectGroupKey> = self
 660            .workspaces
 661            .iter()
 662            .map(|workspace| self.project_group_key_for_workspace(workspace, cx))
 663            .collect();
 664        self.project_group_keys
 665            .retain(|key| workspace_keys.contains(key));
 666    }
 667
 668    pub fn restore_project_group_keys(&mut self, keys: Vec<ProjectGroupKey>) {
 669        let mut restored = keys;
 670        for existing_key in &self.project_group_keys {
 671            if !restored.contains(existing_key) {
 672                restored.push(existing_key.clone());
 673            }
 674        }
 675        self.project_group_keys = restored;
 676    }
 677
 678    pub fn project_group_keys(&self) -> impl Iterator<Item = &ProjectGroupKey> {
 679        self.project_group_keys.iter()
 680    }
 681
 682    /// Returns the project groups, ordered by most recently added.
 683    pub fn project_groups(
 684        &self,
 685        cx: &App,
 686    ) -> impl Iterator<Item = (ProjectGroupKey, Vec<Entity<Workspace>>)> {
 687        let mut groups = self
 688            .project_group_keys
 689            .iter()
 690            .rev()
 691            .map(|key| (key.clone(), Vec::new()))
 692            .collect::<Vec<_>>();
 693        for workspace in &self.workspaces {
 694            let key = self.project_group_key_for_workspace(workspace, cx);
 695            if let Some((_, workspaces)) = groups.iter_mut().find(|(k, _)| k == &key) {
 696                workspaces.push(workspace.clone());
 697            }
 698        }
 699        groups.into_iter()
 700    }
 701
 702    pub fn workspaces_for_project_group(
 703        &self,
 704        project_group_key: &ProjectGroupKey,
 705        cx: &App,
 706    ) -> impl Iterator<Item = &Entity<Workspace>> {
 707        self.workspaces.iter().filter(move |workspace| {
 708            self.project_group_key_for_workspace(workspace, cx) == *project_group_key
 709        })
 710    }
 711
 712    pub fn remove_folder_from_project_group(
 713        &mut self,
 714        project_group_key: &ProjectGroupKey,
 715        path: &Path,
 716        cx: &mut Context<Self>,
 717    ) {
 718        let new_path_list = project_group_key.path_list().without_path(path);
 719        if new_path_list.is_empty() {
 720            return;
 721        }
 722
 723        let new_key = ProjectGroupKey::new(project_group_key.host(), new_path_list);
 724
 725        let workspaces: Vec<_> = self
 726            .workspaces_for_project_group(project_group_key, cx)
 727            .cloned()
 728            .collect();
 729
 730        self.add_project_group_key(new_key);
 731
 732        for workspace in workspaces {
 733            let project = workspace.read(cx).project().clone();
 734            project.update(cx, |project, cx| {
 735                project.remove_worktree_for_main_worktree_path(path, cx);
 736            });
 737        }
 738
 739        self.serialize(cx);
 740        cx.notify();
 741    }
 742
 743    pub fn prompt_to_add_folders_to_project_group(
 744        &mut self,
 745        key: &ProjectGroupKey,
 746        window: &mut Window,
 747        cx: &mut Context<Self>,
 748    ) {
 749        let paths = self.workspace().update(cx, |workspace, cx| {
 750            workspace.prompt_for_open_path(
 751                PathPromptOptions {
 752                    files: false,
 753                    directories: true,
 754                    multiple: true,
 755                    prompt: None,
 756                },
 757                DirectoryLister::Project(workspace.project().clone()),
 758                window,
 759                cx,
 760            )
 761        });
 762
 763        let key = key.clone();
 764        cx.spawn_in(window, async move |this, cx| {
 765            if let Some(new_paths) = paths.await.ok().flatten() {
 766                if !new_paths.is_empty() {
 767                    this.update(cx, |multi_workspace, cx| {
 768                        multi_workspace.add_folders_to_project_group(&key, new_paths, cx);
 769                    })?;
 770                }
 771            }
 772            anyhow::Ok(())
 773        })
 774        .detach_and_log_err(cx);
 775    }
 776
 777    pub fn add_folders_to_project_group(
 778        &mut self,
 779        project_group_key: &ProjectGroupKey,
 780        new_paths: Vec<PathBuf>,
 781        cx: &mut Context<Self>,
 782    ) {
 783        let mut all_paths: Vec<PathBuf> = project_group_key.path_list().paths().to_vec();
 784        all_paths.extend(new_paths.iter().cloned());
 785        let new_path_list = PathList::new(&all_paths);
 786        let new_key = ProjectGroupKey::new(project_group_key.host(), new_path_list);
 787
 788        let workspaces: Vec<_> = self
 789            .workspaces_for_project_group(project_group_key, cx)
 790            .cloned()
 791            .collect();
 792
 793        self.add_project_group_key(new_key);
 794
 795        for workspace in workspaces {
 796            let project = workspace.read(cx).project().clone();
 797            for path in &new_paths {
 798                project
 799                    .update(cx, |project, cx| {
 800                        project.find_or_create_worktree(path, true, cx)
 801                    })
 802                    .detach_and_log_err(cx);
 803            }
 804        }
 805
 806        self.serialize(cx);
 807        cx.notify();
 808    }
 809
 810    pub fn remove_project_group(
 811        &mut self,
 812        key: &ProjectGroupKey,
 813        window: &mut Window,
 814        cx: &mut Context<Self>,
 815    ) {
 816        self.project_group_keys.retain(|k| k != key);
 817
 818        let workspaces: Vec<_> = self
 819            .workspaces_for_project_group(key, cx)
 820            .cloned()
 821            .collect();
 822        for workspace in workspaces {
 823            self.remove(&workspace, window, cx);
 824        }
 825
 826        self.serialize(cx);
 827        cx.notify();
 828    }
 829
 830    /// Finds an existing workspace in this multi-workspace whose paths match,
 831    /// or creates a new one (deserializing its saved state from the database).
 832    /// Never searches other windows or matches workspaces with a superset of
 833    /// the requested paths.
 834    pub fn find_or_create_local_workspace(
 835        &mut self,
 836        path_list: PathList,
 837        window: &mut Window,
 838        cx: &mut Context<Self>,
 839    ) -> Task<Result<Entity<Workspace>>> {
 840        if let Some(workspace) = self
 841            .workspaces
 842            .iter()
 843            .find(|ws| PathList::new(&ws.read(cx).root_paths(cx)) == path_list)
 844            .cloned()
 845        {
 846            self.activate(workspace.clone(), window, cx);
 847            return Task::ready(Ok(workspace));
 848        }
 849
 850        if let Some(transient) = self.active_workspace.transient_workspace() {
 851            if transient.read(cx).project_group_key(cx).path_list() == &path_list {
 852                return Task::ready(Ok(transient.clone()));
 853            }
 854        }
 855
 856        let paths = path_list.paths().to_vec();
 857        let app_state = self.workspace().read(cx).app_state().clone();
 858        let requesting_window = window.window_handle().downcast::<MultiWorkspace>();
 859
 860        cx.spawn(async move |_this, cx| {
 861            let result = cx
 862                .update(|cx| {
 863                    Workspace::new_local(
 864                        paths,
 865                        app_state,
 866                        requesting_window,
 867                        None,
 868                        None,
 869                        OpenMode::Activate,
 870                        cx,
 871                    )
 872                })
 873                .await?;
 874            Ok(result.workspace)
 875        })
 876    }
 877
 878    pub fn workspace(&self) -> &Entity<Workspace> {
 879        match &self.active_workspace {
 880            ActiveWorkspace::Persistent(index) => &self.workspaces[*index],
 881            ActiveWorkspace::Transient(workspace) => workspace,
 882        }
 883    }
 884
 885    pub fn workspaces(&self) -> impl Iterator<Item = &Entity<Workspace>> {
 886        self.workspaces
 887            .iter()
 888            .chain(self.active_workspace.transient_workspace())
 889    }
 890
 891    /// Adds a workspace to this window as persistent without changing which
 892    /// workspace is active. Unlike `activate()`, this always inserts into the
 893    /// persistent list regardless of sidebar state — it's used for system-
 894    /// initiated additions like deserialization and worktree discovery.
 895    pub fn add(&mut self, workspace: Entity<Workspace>, window: &Window, cx: &mut Context<Self>) {
 896        self.insert_workspace(workspace, window, cx);
 897    }
 898
 899    /// Ensures the workspace is in the multiworkspace and makes it the active one.
 900    pub fn activate(
 901        &mut self,
 902        workspace: Entity<Workspace>,
 903        window: &mut Window,
 904        cx: &mut Context<Self>,
 905    ) {
 906        // Re-activating the current workspace is a no-op.
 907        if self.workspace() == &workspace {
 908            self.focus_active_workspace(window, cx);
 909            return;
 910        }
 911
 912        // Resolve where we're going.
 913        let new_index = if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) {
 914            Some(index)
 915        } else if self.sidebar_open {
 916            Some(self.insert_workspace(workspace.clone(), &*window, cx))
 917        } else {
 918            None
 919        };
 920
 921        // Transition the active workspace.
 922        if let Some(index) = new_index {
 923            if let Some(old) = self.active_workspace.set_persistent(index) {
 924                if self.sidebar_open {
 925                    self.promote_transient(old, cx);
 926                } else {
 927                    self.detach_workspace(&old, cx);
 928                    cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(old.entity_id()));
 929                }
 930            }
 931        } else {
 932            Self::subscribe_to_workspace(&workspace, window, cx);
 933            let weak_self = cx.weak_entity();
 934            workspace.update(cx, |workspace, cx| {
 935                workspace.set_multi_workspace(weak_self, cx);
 936            });
 937            if let Some(old) = self.active_workspace.set_transient(workspace) {
 938                self.detach_workspace(&old, cx);
 939                cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(old.entity_id()));
 940            }
 941        }
 942
 943        cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
 944        self.serialize(cx);
 945        self.focus_active_workspace(window, cx);
 946        cx.notify();
 947    }
 948
 949    /// Promotes the currently active workspace to persistent if it is
 950    /// transient, so it is retained across workspace switches even when
 951    /// the sidebar is closed. No-op if the workspace is already persistent.
 952    pub fn retain_active_workspace(&mut self, cx: &mut Context<Self>) {
 953        if let ActiveWorkspace::Transient(workspace) = &self.active_workspace {
 954            let workspace = workspace.clone();
 955            let index = self.promote_transient(workspace, cx);
 956            self.active_workspace = ActiveWorkspace::Persistent(index);
 957            self.serialize(cx);
 958            cx.notify();
 959        }
 960    }
 961
 962    /// Promotes a former transient workspace into the persistent list.
 963    /// Returns the index of the newly inserted workspace.
 964    fn promote_transient(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) -> usize {
 965        let project_group_key = self.project_group_key_for_workspace(&workspace, cx);
 966        self.add_project_group_key(project_group_key);
 967        self.workspaces.push(workspace.clone());
 968        cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
 969        self.workspaces.len() - 1
 970    }
 971
 972    /// Collapses to a single transient workspace, discarding all persistent
 973    /// workspaces. Used when multi-workspace is disabled (e.g. disable_ai).
 974    fn collapse_to_single_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 975        if self.sidebar_open {
 976            self.close_sidebar(window, cx);
 977        }
 978        let active = self.workspace().clone();
 979        for workspace in std::mem::take(&mut self.workspaces) {
 980            if workspace != active {
 981                self.detach_workspace(&workspace, cx);
 982                cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(workspace.entity_id()));
 983            }
 984        }
 985        self.project_group_keys.clear();
 986        self.active_workspace = ActiveWorkspace::Transient(active);
 987        cx.notify();
 988    }
 989
 990    /// Inserts a workspace into the list if not already present. Returns the
 991    /// index of the workspace (existing or newly inserted). Does not change
 992    /// the active workspace index.
 993    fn insert_workspace(
 994        &mut self,
 995        workspace: Entity<Workspace>,
 996        window: &Window,
 997        cx: &mut Context<Self>,
 998    ) -> usize {
 999        if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) {
1000            index
1001        } else {
1002            let project_group_key = self.project_group_key_for_workspace(&workspace, cx);
1003
1004            Self::subscribe_to_workspace(&workspace, window, cx);
1005            self.sync_sidebar_to_workspace(&workspace, cx);
1006            let weak_self = cx.weak_entity();
1007            workspace.update(cx, |workspace, cx| {
1008                workspace.set_multi_workspace(weak_self, cx);
1009            });
1010
1011            self.add_project_group_key(project_group_key);
1012            self.workspaces.push(workspace.clone());
1013            cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
1014            cx.notify();
1015            self.workspaces.len() - 1
1016        }
1017    }
1018
1019    /// Clears session state and DB binding for a workspace that is being
1020    /// removed or replaced. The DB row is preserved so the workspace still
1021    /// appears in the recent-projects list.
1022    fn detach_workspace(&mut self, workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
1023        workspace.update(cx, |workspace, _cx| {
1024            workspace.session_id.take();
1025            workspace._schedule_serialize_workspace.take();
1026            workspace._serialize_workspace_task.take();
1027        });
1028
1029        if let Some(workspace_id) = workspace.read(cx).database_id() {
1030            let db = crate::persistence::WorkspaceDb::global(cx);
1031            self.pending_removal_tasks.retain(|task| !task.is_ready());
1032            self.pending_removal_tasks
1033                .push(cx.background_spawn(async move {
1034                    db.set_session_binding(workspace_id, None, None)
1035                        .await
1036                        .log_err();
1037                }));
1038        }
1039    }
1040
1041    fn sync_sidebar_to_workspace(&self, workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
1042        if self.sidebar_open() {
1043            let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
1044            workspace.update(cx, |workspace, _| {
1045                workspace.set_sidebar_focus_handle(sidebar_focus_handle);
1046            });
1047        }
1048    }
1049
1050    pub(crate) fn serialize(&mut self, cx: &mut Context<Self>) {
1051        self._serialize_task = Some(cx.spawn(async move |this, cx| {
1052            let Some((window_id, state)) = this
1053                .read_with(cx, |this, cx| {
1054                    let state = MultiWorkspaceState {
1055                        active_workspace_id: this.workspace().read(cx).database_id(),
1056                        project_group_keys: this
1057                            .project_group_keys()
1058                            .cloned()
1059                            .map(Into::into)
1060                            .collect::<Vec<_>>(),
1061                        sidebar_open: this.sidebar_open,
1062                        sidebar_state: this.sidebar.as_ref().and_then(|s| s.serialized_state(cx)),
1063                    };
1064                    (this.window_id, state)
1065                })
1066                .ok()
1067            else {
1068                return;
1069            };
1070            let kvp = cx.update(|cx| db::kvp::KeyValueStore::global(cx));
1071            crate::persistence::write_multi_workspace_state(&kvp, window_id, state).await;
1072        }));
1073    }
1074
1075    /// Returns the in-flight serialization task (if any) so the caller can
1076    /// await it. Used by the quit handler to ensure pending DB writes
1077    /// complete before the process exits.
1078    pub fn flush_serialization(&mut self) -> Task<()> {
1079        self._serialize_task.take().unwrap_or(Task::ready(()))
1080    }
1081
1082    fn app_will_quit(&mut self, _cx: &mut Context<Self>) -> impl Future<Output = ()> + use<> {
1083        let mut tasks: Vec<Task<()>> = Vec::new();
1084        if let Some(task) = self._serialize_task.take() {
1085            tasks.push(task);
1086        }
1087        tasks.extend(std::mem::take(&mut self.pending_removal_tasks));
1088
1089        async move {
1090            futures::future::join_all(tasks).await;
1091        }
1092    }
1093
1094    pub fn focus_active_workspace(&self, window: &mut Window, cx: &mut App) {
1095        // If a dock panel is zoomed, focus it instead of the center pane.
1096        // Otherwise, focusing the center pane triggers dismiss_zoomed_items_to_reveal
1097        // which closes the zoomed dock.
1098        let focus_handle = {
1099            let workspace = self.workspace().read(cx);
1100            let mut target = None;
1101            for dock in workspace.all_docks() {
1102                let dock = dock.read(cx);
1103                if dock.is_open() {
1104                    if let Some(panel) = dock.active_panel() {
1105                        if panel.is_zoomed(window, cx) {
1106                            target = Some(panel.panel_focus_handle(cx));
1107                            break;
1108                        }
1109                    }
1110                }
1111            }
1112            target.unwrap_or_else(|| {
1113                let pane = workspace.active_pane().clone();
1114                pane.read(cx).focus_handle(cx)
1115            })
1116        };
1117        window.focus(&focus_handle, cx);
1118    }
1119
1120    pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
1121        self.workspace().read(cx).panel::<T>(cx)
1122    }
1123
1124    pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
1125        self.workspace().read(cx).active_modal::<V>(cx)
1126    }
1127
1128    pub fn add_panel<T: Panel>(
1129        &mut self,
1130        panel: Entity<T>,
1131        window: &mut Window,
1132        cx: &mut Context<Self>,
1133    ) {
1134        self.workspace().update(cx, |workspace, cx| {
1135            workspace.add_panel(panel, window, cx);
1136        });
1137    }
1138
1139    pub fn focus_panel<T: Panel>(
1140        &mut self,
1141        window: &mut Window,
1142        cx: &mut Context<Self>,
1143    ) -> Option<Entity<T>> {
1144        self.workspace()
1145            .update(cx, |workspace, cx| workspace.focus_panel::<T>(window, cx))
1146    }
1147
1148    // used in a test
1149    pub fn toggle_modal<V: ModalView, B>(
1150        &mut self,
1151        window: &mut Window,
1152        cx: &mut Context<Self>,
1153        build: B,
1154    ) where
1155        B: FnOnce(&mut Window, &mut gpui::Context<V>) -> V,
1156    {
1157        self.workspace().update(cx, |workspace, cx| {
1158            workspace.toggle_modal(window, cx, build);
1159        });
1160    }
1161
1162    pub fn toggle_dock(
1163        &mut self,
1164        dock_side: DockPosition,
1165        window: &mut Window,
1166        cx: &mut Context<Self>,
1167    ) {
1168        self.workspace().update(cx, |workspace, cx| {
1169            workspace.toggle_dock(dock_side, window, cx);
1170        });
1171    }
1172
1173    pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
1174        self.workspace().read(cx).active_item_as::<I>(cx)
1175    }
1176
1177    pub fn items_of_type<'a, T: Item>(
1178        &'a self,
1179        cx: &'a App,
1180    ) -> impl 'a + Iterator<Item = Entity<T>> {
1181        self.workspace().read(cx).items_of_type::<T>(cx)
1182    }
1183
1184    pub fn database_id(&self, cx: &App) -> Option<WorkspaceId> {
1185        self.workspace().read(cx).database_id()
1186    }
1187
1188    pub fn take_pending_removal_tasks(&mut self) -> Vec<Task<()>> {
1189        let tasks: Vec<Task<()>> = std::mem::take(&mut self.pending_removal_tasks)
1190            .into_iter()
1191            .filter(|task| !task.is_ready())
1192            .collect();
1193        tasks
1194    }
1195
1196    #[cfg(any(test, feature = "test-support"))]
1197    pub fn set_random_database_id(&mut self, cx: &mut Context<Self>) {
1198        self.workspace().update(cx, |workspace, _cx| {
1199            workspace.set_random_database_id();
1200        });
1201    }
1202
1203    #[cfg(any(test, feature = "test-support"))]
1204    pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
1205        let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
1206        Self::new(workspace, window, cx)
1207    }
1208
1209    #[cfg(any(test, feature = "test-support"))]
1210    pub fn test_add_workspace(
1211        &mut self,
1212        project: Entity<Project>,
1213        window: &mut Window,
1214        cx: &mut Context<Self>,
1215    ) -> Entity<Workspace> {
1216        let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
1217        self.activate(workspace.clone(), window, cx);
1218        workspace
1219    }
1220
1221    #[cfg(any(test, feature = "test-support"))]
1222    pub fn create_test_workspace(
1223        &mut self,
1224        window: &mut Window,
1225        cx: &mut Context<Self>,
1226    ) -> Task<()> {
1227        let app_state = self.workspace().read(cx).app_state().clone();
1228        let project = Project::local(
1229            app_state.client.clone(),
1230            app_state.node_runtime.clone(),
1231            app_state.user_store.clone(),
1232            app_state.languages.clone(),
1233            app_state.fs.clone(),
1234            None,
1235            project::LocalProjectFlags::default(),
1236            cx,
1237        );
1238        let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
1239        self.activate(new_workspace.clone(), window, cx);
1240
1241        let weak_workspace = new_workspace.downgrade();
1242        let db = crate::persistence::WorkspaceDb::global(cx);
1243        cx.spawn_in(window, async move |this, cx| {
1244            let workspace_id = db.next_id().await.unwrap();
1245            let workspace = weak_workspace.upgrade().unwrap();
1246            let task: Task<()> = this
1247                .update_in(cx, |this, window, cx| {
1248                    let session_id = workspace.read(cx).session_id();
1249                    let window_id = window.window_handle().window_id().as_u64();
1250                    workspace.update(cx, |workspace, _cx| {
1251                        workspace.set_database_id(workspace_id);
1252                    });
1253                    this.serialize(cx);
1254                    let db = db.clone();
1255                    cx.background_spawn(async move {
1256                        db.set_session_binding(workspace_id, session_id, Some(window_id))
1257                            .await
1258                            .log_err();
1259                    })
1260                })
1261                .unwrap();
1262            task.await
1263        })
1264    }
1265
1266    pub fn remove(
1267        &mut self,
1268        workspace: &Entity<Workspace>,
1269        window: &mut Window,
1270        cx: &mut Context<Self>,
1271    ) -> bool {
1272        let Some(index) = self.workspaces.iter().position(|w| w == workspace) else {
1273            return false;
1274        };
1275
1276        let old_key = self.project_group_key_for_workspace(workspace, cx);
1277
1278        if self.workspaces.len() <= 1 {
1279            let has_worktrees = workspace.read(cx).visible_worktrees(cx).next().is_some();
1280
1281            if !has_worktrees {
1282                return false;
1283            }
1284
1285            let old_workspace = workspace.clone();
1286            let old_entity_id = old_workspace.entity_id();
1287
1288            let app_state = old_workspace.read(cx).app_state().clone();
1289
1290            let project = Project::local(
1291                app_state.client.clone(),
1292                app_state.node_runtime.clone(),
1293                app_state.user_store.clone(),
1294                app_state.languages.clone(),
1295                app_state.fs.clone(),
1296                None,
1297                project::LocalProjectFlags::default(),
1298                cx,
1299            );
1300
1301            let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
1302
1303            self.workspaces[0] = new_workspace.clone();
1304            self.active_workspace = ActiveWorkspace::Persistent(0);
1305
1306            Self::subscribe_to_workspace(&new_workspace, window, cx);
1307
1308            self.sync_sidebar_to_workspace(&new_workspace, cx);
1309
1310            let weak_self = cx.weak_entity();
1311
1312            new_workspace.update(cx, |workspace, cx| {
1313                workspace.set_multi_workspace(weak_self, cx);
1314            });
1315
1316            self.detach_workspace(&old_workspace, cx);
1317
1318            cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(old_entity_id));
1319            cx.emit(MultiWorkspaceEvent::WorkspaceAdded(new_workspace));
1320            cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
1321        } else {
1322            let removed_workspace = self.workspaces.remove(index);
1323            self.provisional_project_group_keys
1324                .remove(&removed_workspace.entity_id());
1325
1326            if let Some(active_index) = self.active_workspace.persistent_index() {
1327                if active_index >= self.workspaces.len() {
1328                    self.active_workspace = ActiveWorkspace::Persistent(self.workspaces.len() - 1);
1329                } else if active_index > index {
1330                    self.active_workspace = ActiveWorkspace::Persistent(active_index - 1);
1331                }
1332            }
1333
1334            self.detach_workspace(&removed_workspace, cx);
1335
1336            cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(
1337                removed_workspace.entity_id(),
1338            ));
1339            cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
1340        }
1341
1342        let key_still_in_use = self
1343            .workspaces
1344            .iter()
1345            .any(|workspace| self.project_group_key_for_workspace(workspace, cx) == old_key);
1346
1347        if !key_still_in_use {
1348            self.project_group_keys.retain(|k| k != &old_key);
1349        }
1350
1351        self.serialize(cx);
1352        self.focus_active_workspace(window, cx);
1353        cx.notify();
1354
1355        true
1356    }
1357
1358    pub fn move_workspace_to_new_window(
1359        &mut self,
1360        workspace: &Entity<Workspace>,
1361        window: &mut Window,
1362        cx: &mut Context<Self>,
1363    ) {
1364        let workspace = workspace.clone();
1365        if !self.remove(&workspace, window, cx) {
1366            return;
1367        }
1368
1369        let app_state: Arc<AppState> = workspace.read(cx).app_state().clone();
1370
1371        cx.defer(move |cx| {
1372            let options = (app_state.build_window_options)(None, cx);
1373
1374            let Ok(window) = cx.open_window(options, |window, cx| {
1375                cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
1376            }) else {
1377                return;
1378            };
1379
1380            let _ = window.update(cx, |_, window, _| {
1381                window.activate_window();
1382            });
1383        });
1384    }
1385
1386    pub fn move_project_group_to_new_window(
1387        &mut self,
1388        key: &ProjectGroupKey,
1389        window: &mut Window,
1390        cx: &mut Context<Self>,
1391    ) {
1392        let workspaces: Vec<_> = self
1393            .workspaces_for_project_group(key, cx)
1394            .cloned()
1395            .collect();
1396        if workspaces.is_empty() {
1397            return;
1398        }
1399
1400        self.project_group_keys.retain(|k| k != key);
1401
1402        let mut removed = Vec::new();
1403        for workspace in &workspaces {
1404            if self.remove(workspace, window, cx) {
1405                removed.push(workspace.clone());
1406            }
1407        }
1408
1409        if removed.is_empty() {
1410            return;
1411        }
1412
1413        let app_state = removed[0].read(cx).app_state().clone();
1414
1415        cx.defer(move |cx| {
1416            let options = (app_state.build_window_options)(None, cx);
1417
1418            let first = removed[0].clone();
1419            let rest = removed[1..].to_vec();
1420
1421            let Ok(new_window) = cx.open_window(options, |window, cx| {
1422                cx.new(|cx| MultiWorkspace::new(first, window, cx))
1423            }) else {
1424                return;
1425            };
1426
1427            new_window
1428                .update(cx, |mw, window, cx| {
1429                    for workspace in rest {
1430                        mw.activate(workspace, window, cx);
1431                    }
1432                    window.activate_window();
1433                })
1434                .log_err();
1435        });
1436    }
1437
1438    pub fn open_project(
1439        &mut self,
1440        paths: Vec<PathBuf>,
1441        open_mode: OpenMode,
1442        window: &mut Window,
1443        cx: &mut Context<Self>,
1444    ) -> Task<Result<Entity<Workspace>>> {
1445        if self.multi_workspace_enabled(cx) {
1446            self.find_or_create_local_workspace(PathList::new(&paths), window, cx)
1447        } else {
1448            let workspace = self.workspace().clone();
1449            cx.spawn_in(window, async move |_this, cx| {
1450                let should_continue = workspace
1451                    .update_in(cx, |workspace, window, cx| {
1452                        workspace.prepare_to_close(crate::CloseIntent::ReplaceWindow, window, cx)
1453                    })?
1454                    .await?;
1455                if should_continue {
1456                    workspace
1457                        .update_in(cx, |workspace, window, cx| {
1458                            workspace.open_workspace_for_paths(open_mode, paths, window, cx)
1459                        })?
1460                        .await
1461                } else {
1462                    Ok(workspace)
1463                }
1464            })
1465        }
1466    }
1467}
1468
1469impl Render for MultiWorkspace {
1470    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1471        let multi_workspace_enabled = self.multi_workspace_enabled(cx);
1472        let sidebar_side = self.sidebar_side(cx);
1473        let sidebar_on_right = sidebar_side == SidebarSide::Right;
1474
1475        let sidebar: Option<AnyElement> = if multi_workspace_enabled && self.sidebar_open() {
1476            self.sidebar.as_ref().map(|sidebar_handle| {
1477                let weak = cx.weak_entity();
1478
1479                let sidebar_width = sidebar_handle.width(cx);
1480                let resize_handle = deferred(
1481                    div()
1482                        .id("sidebar-resize-handle")
1483                        .absolute()
1484                        .when(!sidebar_on_right, |el| {
1485                            el.right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
1486                        })
1487                        .when(sidebar_on_right, |el| {
1488                            el.left(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
1489                        })
1490                        .top(px(0.))
1491                        .h_full()
1492                        .w(SIDEBAR_RESIZE_HANDLE_SIZE)
1493                        .cursor_col_resize()
1494                        .on_drag(DraggedSidebar, |dragged, _, _, cx| {
1495                            cx.stop_propagation();
1496                            cx.new(|_| dragged.clone())
1497                        })
1498                        .on_mouse_down(MouseButton::Left, |_, _, cx| {
1499                            cx.stop_propagation();
1500                        })
1501                        .on_mouse_up(MouseButton::Left, move |event, _, cx| {
1502                            if event.click_count == 2 {
1503                                weak.update(cx, |this, cx| {
1504                                    if let Some(sidebar) = this.sidebar.as_mut() {
1505                                        sidebar.set_width(None, cx);
1506                                    }
1507                                    this.serialize(cx);
1508                                })
1509                                .ok();
1510                                cx.stop_propagation();
1511                            } else {
1512                                weak.update(cx, |this, cx| {
1513                                    this.serialize(cx);
1514                                })
1515                                .ok();
1516                            }
1517                        })
1518                        .occlude(),
1519                );
1520
1521                div()
1522                    .id("sidebar-container")
1523                    .relative()
1524                    .h_full()
1525                    .w(sidebar_width)
1526                    .flex_shrink_0()
1527                    .child(sidebar_handle.to_any())
1528                    .child(resize_handle)
1529                    .into_any_element()
1530            })
1531        } else {
1532            None
1533        };
1534
1535        let (left_sidebar, right_sidebar) = if sidebar_on_right {
1536            (None, sidebar)
1537        } else {
1538            (sidebar, None)
1539        };
1540
1541        let ui_font = theme_settings::setup_ui_font(window, cx);
1542        let text_color = cx.theme().colors().text;
1543
1544        let workspace = self.workspace().clone();
1545        let workspace_key_context = workspace.update(cx, |workspace, cx| workspace.key_context(cx));
1546        let root = workspace.update(cx, |workspace, cx| workspace.actions(h_flex(), window, cx));
1547
1548        client_side_decorations(
1549            root.key_context(workspace_key_context)
1550                .relative()
1551                .size_full()
1552                .font(ui_font)
1553                .text_color(text_color)
1554                .on_action(cx.listener(Self::close_window))
1555                .when(self.multi_workspace_enabled(cx), |this| {
1556                    this.on_action(cx.listener(
1557                        |this: &mut Self, _: &ToggleWorkspaceSidebar, window, cx| {
1558                            this.toggle_sidebar(window, cx);
1559                        },
1560                    ))
1561                    .on_action(cx.listener(
1562                        |this: &mut Self, _: &CloseWorkspaceSidebar, window, cx| {
1563                            this.close_sidebar_action(window, cx);
1564                        },
1565                    ))
1566                    .on_action(cx.listener(
1567                        |this: &mut Self, _: &FocusWorkspaceSidebar, window, cx| {
1568                            this.focus_sidebar(window, cx);
1569                        },
1570                    ))
1571                    .on_action(cx.listener(
1572                        |this: &mut Self, action: &ToggleThreadSwitcher, window, cx| {
1573                            if let Some(sidebar) = &this.sidebar {
1574                                sidebar.toggle_thread_switcher(action.select_last, window, cx);
1575                            }
1576                        },
1577                    ))
1578                    .on_action(
1579                        cx.listener(|this: &mut Self, _: &NextProjectGroup, window, cx| {
1580                            if let Some(sidebar) = &this.sidebar {
1581                                sidebar.cycle_project_group(true, window, cx);
1582                            }
1583                        }),
1584                    )
1585                    .on_action(cx.listener(
1586                        |this: &mut Self, _: &PreviousProjectGroup, window, cx| {
1587                            if let Some(sidebar) = &this.sidebar {
1588                                sidebar.cycle_project_group(false, window, cx);
1589                            }
1590                        },
1591                    ))
1592                    .on_action(cx.listener(|this: &mut Self, _: &NextThread, window, cx| {
1593                        if let Some(sidebar) = &this.sidebar {
1594                            sidebar.cycle_thread(true, window, cx);
1595                        }
1596                    }))
1597                    .on_action(
1598                        cx.listener(|this: &mut Self, _: &PreviousThread, window, cx| {
1599                            if let Some(sidebar) = &this.sidebar {
1600                                sidebar.cycle_thread(false, window, cx);
1601                            }
1602                        }),
1603                    )
1604                    .on_action(cx.listener(
1605                        |this: &mut Self, _: &MoveWorkspaceToNewWindow, window, cx| {
1606                            if let Some(sidebar) = &this.sidebar {
1607                                sidebar.move_workspace_to_new_window(window, cx);
1608                            }
1609                        },
1610                    ))
1611                })
1612                .when(
1613                    self.sidebar_open() && self.multi_workspace_enabled(cx),
1614                    |this| {
1615                        this.on_drag_move(cx.listener(
1616                            move |this: &mut Self,
1617                                  e: &DragMoveEvent<DraggedSidebar>,
1618                                  window,
1619                                  cx| {
1620                                if let Some(sidebar) = &this.sidebar {
1621                                    let new_width = if sidebar_on_right {
1622                                        window.bounds().size.width - e.event.position.x
1623                                    } else {
1624                                        e.event.position.x
1625                                    };
1626                                    sidebar.set_width(Some(new_width), cx);
1627                                }
1628                            },
1629                        ))
1630                    },
1631                )
1632                .children(left_sidebar)
1633                .child(
1634                    div()
1635                        .flex()
1636                        .flex_1()
1637                        .size_full()
1638                        .overflow_hidden()
1639                        .child(self.workspace().clone()),
1640                )
1641                .children(right_sidebar)
1642                .child(self.workspace().read(cx).modal_layer.clone())
1643                .children(self.sidebar_overlay.as_ref().map(|view| {
1644                    deferred(div().absolute().size_full().inset_0().occlude().child(
1645                        v_flex().h(px(0.0)).top_20().items_center().child(
1646                            h_flex().occlude().child(view.clone()).on_mouse_down(
1647                                MouseButton::Left,
1648                                |_, _, cx| {
1649                                    cx.stop_propagation();
1650                                },
1651                            ),
1652                        ),
1653                    ))
1654                    .with_priority(2)
1655                })),
1656            window,
1657            cx,
1658            Tiling {
1659                left: !sidebar_on_right && multi_workspace_enabled && self.sidebar_open(),
1660                right: sidebar_on_right && multi_workspace_enabled && self.sidebar_open(),
1661                ..Tiling::default()
1662            },
1663        )
1664    }
1665}