multi_workspace.rs

   1use anyhow::Result;
   2use fs::Fs;
   3
   4use gpui::{
   5    AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
   6    ManagedView, MouseButton, Pixels, Render, Subscription, Task, Tiling, WeakEntity, Window,
   7    WindowId, actions, deferred, px,
   8};
   9pub use project::ProjectGroupKey;
  10use project::{DisableAiSettings, Project};
  11use release_channel::ReleaseChannel;
  12use remote::RemoteConnectionOptions;
  13use settings::Settings;
  14pub use settings::SidebarSide;
  15use std::future::Future;
  16
  17use std::path::PathBuf;
  18use ui::prelude::*;
  19use util::ResultExt;
  20use util::path_list::PathList;
  21use zed_actions::agents_sidebar::ToggleThreadSwitcher;
  22
  23use agent_settings::AgentSettings;
  24use settings::SidebarDockPosition;
  25use ui::{ContextMenu, right_click_menu};
  26
  27const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
  28
  29use crate::open_remote_project_with_existing_connection;
  30use crate::{
  31    CloseIntent, CloseWindow, DockPosition, Event as WorkspaceEvent, Item, ModalView, OpenMode,
  32    Panel, Workspace, WorkspaceId, client_side_decorations,
  33    persistence::model::MultiWorkspaceState,
  34};
  35
  36actions!(
  37    multi_workspace,
  38    [
  39        /// Toggles the workspace switcher sidebar.
  40        ToggleWorkspaceSidebar,
  41        /// Closes the workspace sidebar.
  42        CloseWorkspaceSidebar,
  43        /// Moves focus to or from the workspace sidebar without closing it.
  44        FocusWorkspaceSidebar,
  45        /// Activates the next project in the sidebar.
  46        NextProject,
  47        /// Activates the previous project in the sidebar.
  48        PreviousProject,
  49        /// Activates the next thread in sidebar order.
  50        NextThread,
  51        /// Activates the previous thread in sidebar order.
  52        PreviousThread,
  53        /// Creates a new thread in the current workspace.
  54        NewThread,
  55        /// Moves the active project to a new window.
  56        MoveProjectToNewWindow,
  57    ]
  58);
  59
  60#[derive(Default)]
  61pub struct SidebarRenderState {
  62    pub open: bool,
  63    pub side: SidebarSide,
  64}
  65
  66pub fn sidebar_side_context_menu(
  67    id: impl Into<ElementId>,
  68    cx: &App,
  69) -> ui::RightClickMenu<ContextMenu> {
  70    let current_position = AgentSettings::get_global(cx).sidebar_side;
  71    right_click_menu(id).menu(move |window, cx| {
  72        let fs = <dyn fs::Fs>::global(cx);
  73        ContextMenu::build(window, cx, move |mut menu, _, _cx| {
  74            let positions: [(SidebarDockPosition, &str); 2] = [
  75                (SidebarDockPosition::Left, "Left"),
  76                (SidebarDockPosition::Right, "Right"),
  77            ];
  78            for (position, label) in positions {
  79                let fs = fs.clone();
  80                menu = menu.toggleable_entry(
  81                    label,
  82                    position == current_position,
  83                    IconPosition::Start,
  84                    None,
  85                    move |_window, cx| {
  86                        let side = match position {
  87                            SidebarDockPosition::Left => "left",
  88                            SidebarDockPosition::Right => "right",
  89                        };
  90                        telemetry::event!("Sidebar Side Changed", side = side);
  91                        settings::update_settings_file(fs.clone(), cx, move |settings, _cx| {
  92                            settings
  93                                .agent
  94                                .get_or_insert_default()
  95                                .set_sidebar_side(position);
  96                        });
  97                    },
  98                );
  99            }
 100            menu
 101        })
 102    })
 103}
 104
 105pub enum MultiWorkspaceEvent {
 106    ActiveWorkspaceChanged {
 107        source_workspace: Option<WeakEntity<Workspace>>,
 108    },
 109    WorkspaceAdded(Entity<Workspace>),
 110    WorkspaceRemoved(EntityId),
 111    ProjectGroupsChanged,
 112}
 113
 114pub enum SidebarEvent {
 115    SerializeNeeded,
 116}
 117
 118pub trait Sidebar: Focusable + Render + EventEmitter<SidebarEvent> + Sized {
 119    fn width(&self, cx: &App) -> Pixels;
 120    fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>);
 121    fn has_notifications(&self, cx: &App) -> bool;
 122    fn side(&self, _cx: &App) -> SidebarSide;
 123
 124    fn is_threads_list_view_active(&self) -> bool {
 125        true
 126    }
 127    /// Makes focus reset back to the search editor upon toggling the sidebar from outside
 128    fn prepare_for_focus(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
 129    /// Opens or cycles the thread switcher popup.
 130    fn toggle_thread_switcher(
 131        &mut self,
 132        _select_last: bool,
 133        _window: &mut Window,
 134        _cx: &mut Context<Self>,
 135    ) {
 136    }
 137
 138    /// Activates the next or previous project.
 139    fn cycle_project(&mut self, _forward: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
 140
 141    /// Activates the next or previous thread in sidebar order.
 142    fn cycle_thread(&mut self, _forward: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
 143
 144    /// Return an opaque JSON blob of sidebar-specific state to persist.
 145    fn serialized_state(&self, _cx: &App) -> Option<String> {
 146        None
 147    }
 148
 149    /// Restore sidebar state from a previously-serialized blob.
 150    fn restore_serialized_state(
 151        &mut self,
 152        _state: &str,
 153        _window: &mut Window,
 154        _cx: &mut Context<Self>,
 155    ) {
 156    }
 157}
 158
 159pub trait SidebarHandle: 'static + Send + Sync {
 160    fn width(&self, cx: &App) -> Pixels;
 161    fn set_width(&self, width: Option<Pixels>, cx: &mut App);
 162    fn focus_handle(&self, cx: &App) -> FocusHandle;
 163    fn focus(&self, window: &mut Window, cx: &mut App);
 164    fn prepare_for_focus(&self, window: &mut Window, cx: &mut App);
 165    fn has_notifications(&self, cx: &App) -> bool;
 166    fn to_any(&self) -> AnyView;
 167    fn entity_id(&self) -> EntityId;
 168    fn toggle_thread_switcher(&self, select_last: bool, window: &mut Window, cx: &mut App);
 169    fn cycle_project(&self, forward: bool, window: &mut Window, cx: &mut App);
 170    fn cycle_thread(&self, forward: bool, window: &mut Window, cx: &mut App);
 171
 172    fn is_threads_list_view_active(&self, cx: &App) -> bool;
 173
 174    fn side(&self, cx: &App) -> SidebarSide;
 175    fn serialized_state(&self, cx: &App) -> Option<String>;
 176    fn restore_serialized_state(&self, state: &str, window: &mut Window, cx: &mut App);
 177}
 178
 179#[derive(Clone)]
 180pub struct DraggedSidebar;
 181
 182impl Render for DraggedSidebar {
 183    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 184        gpui::Empty
 185    }
 186}
 187
 188impl<T: Sidebar> SidebarHandle for Entity<T> {
 189    fn width(&self, cx: &App) -> Pixels {
 190        self.read(cx).width(cx)
 191    }
 192
 193    fn set_width(&self, width: Option<Pixels>, cx: &mut App) {
 194        self.update(cx, |this, cx| this.set_width(width, cx))
 195    }
 196
 197    fn focus_handle(&self, cx: &App) -> FocusHandle {
 198        self.read(cx).focus_handle(cx)
 199    }
 200
 201    fn focus(&self, window: &mut Window, cx: &mut App) {
 202        let handle = self.read(cx).focus_handle(cx);
 203        window.focus(&handle, cx);
 204    }
 205
 206    fn prepare_for_focus(&self, window: &mut Window, cx: &mut App) {
 207        self.update(cx, |this, cx| this.prepare_for_focus(window, cx));
 208    }
 209
 210    fn has_notifications(&self, cx: &App) -> bool {
 211        self.read(cx).has_notifications(cx)
 212    }
 213
 214    fn to_any(&self) -> AnyView {
 215        self.clone().into()
 216    }
 217
 218    fn entity_id(&self) -> EntityId {
 219        Entity::entity_id(self)
 220    }
 221
 222    fn toggle_thread_switcher(&self, select_last: bool, window: &mut Window, cx: &mut App) {
 223        let entity = self.clone();
 224        window.defer(cx, move |window, cx| {
 225            entity.update(cx, |this, cx| {
 226                this.toggle_thread_switcher(select_last, window, cx);
 227            });
 228        });
 229    }
 230
 231    fn cycle_project(&self, forward: bool, window: &mut Window, cx: &mut App) {
 232        let entity = self.clone();
 233        window.defer(cx, move |window, cx| {
 234            entity.update(cx, |this, cx| {
 235                this.cycle_project(forward, window, cx);
 236            });
 237        });
 238    }
 239
 240    fn cycle_thread(&self, forward: bool, window: &mut Window, cx: &mut App) {
 241        let entity = self.clone();
 242        window.defer(cx, move |window, cx| {
 243            entity.update(cx, |this, cx| {
 244                this.cycle_thread(forward, window, cx);
 245            });
 246        });
 247    }
 248
 249    fn is_threads_list_view_active(&self, cx: &App) -> bool {
 250        self.read(cx).is_threads_list_view_active()
 251    }
 252
 253    fn side(&self, cx: &App) -> SidebarSide {
 254        self.read(cx).side(cx)
 255    }
 256
 257    fn serialized_state(&self, cx: &App) -> Option<String> {
 258        self.read(cx).serialized_state(cx)
 259    }
 260
 261    fn restore_serialized_state(&self, state: &str, window: &mut Window, cx: &mut App) {
 262        self.update(cx, |this, cx| {
 263            this.restore_serialized_state(state, window, cx)
 264        })
 265    }
 266}
 267
 268#[derive(Clone)]
 269pub struct ProjectGroup {
 270    pub key: ProjectGroupKey,
 271    pub workspaces: Vec<Entity<Workspace>>,
 272    pub expanded: bool,
 273}
 274
 275pub struct SerializedProjectGroupState {
 276    pub key: ProjectGroupKey,
 277    pub expanded: bool,
 278}
 279
 280#[derive(Clone)]
 281pub struct ProjectGroupState {
 282    pub key: ProjectGroupKey,
 283    pub expanded: bool,
 284    pub last_active_workspace: Option<WeakEntity<Workspace>>,
 285}
 286
 287pub struct MultiWorkspace {
 288    window_id: WindowId,
 289    retained_workspaces: Vec<Entity<Workspace>>,
 290    project_groups: Vec<ProjectGroupState>,
 291    active_workspace: Entity<Workspace>,
 292    sidebar: Option<Box<dyn SidebarHandle>>,
 293    sidebar_open: bool,
 294    sidebar_overlay: Option<AnyView>,
 295    pending_removal_tasks: Vec<Task<()>>,
 296    _serialize_task: Option<Task<()>>,
 297    _subscriptions: Vec<Subscription>,
 298    previous_focus_handle: Option<FocusHandle>,
 299}
 300
 301impl EventEmitter<MultiWorkspaceEvent> for MultiWorkspace {}
 302
 303impl MultiWorkspace {
 304    pub fn sidebar_side(&self, cx: &App) -> SidebarSide {
 305        self.sidebar
 306            .as_ref()
 307            .map_or(SidebarSide::Left, |s| s.side(cx))
 308    }
 309
 310    pub fn sidebar_render_state(&self, cx: &App) -> SidebarRenderState {
 311        SidebarRenderState {
 312            open: self.sidebar_open() && self.multi_workspace_enabled(cx),
 313            side: self.sidebar_side(cx),
 314        }
 315    }
 316
 317    pub fn new(workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
 318        let release_subscription = cx.on_release(|this: &mut MultiWorkspace, _cx| {
 319            if let Some(task) = this._serialize_task.take() {
 320                task.detach();
 321            }
 322            for task in std::mem::take(&mut this.pending_removal_tasks) {
 323                task.detach();
 324            }
 325        });
 326        let quit_subscription = cx.on_app_quit(Self::app_will_quit);
 327        let settings_subscription = cx.observe_global_in::<settings::SettingsStore>(window, {
 328            let mut previous_disable_ai = DisableAiSettings::get_global(cx).disable_ai;
 329            move |this, window, cx| {
 330                if DisableAiSettings::get_global(cx).disable_ai != previous_disable_ai {
 331                    this.collapse_to_single_workspace(window, cx);
 332                    previous_disable_ai = DisableAiSettings::get_global(cx).disable_ai;
 333                }
 334            }
 335        });
 336        Self::subscribe_to_workspace(&workspace, window, cx);
 337        let weak_self = cx.weak_entity();
 338        workspace.update(cx, |workspace, cx| {
 339            workspace.set_multi_workspace(weak_self, cx);
 340        });
 341        Self {
 342            window_id: window.window_handle().window_id(),
 343            retained_workspaces: Vec::new(),
 344            project_groups: Vec::new(),
 345            active_workspace: workspace,
 346            sidebar: None,
 347            sidebar_open: false,
 348            sidebar_overlay: None,
 349            pending_removal_tasks: Vec::new(),
 350            _serialize_task: None,
 351            _subscriptions: vec![
 352                release_subscription,
 353                quit_subscription,
 354                settings_subscription,
 355            ],
 356            previous_focus_handle: None,
 357        }
 358    }
 359
 360    pub fn register_sidebar<T: Sidebar>(&mut self, sidebar: Entity<T>, cx: &mut Context<Self>) {
 361        self._subscriptions
 362            .push(cx.observe(&sidebar, |_this, _, cx| {
 363                cx.notify();
 364            }));
 365        self._subscriptions
 366            .push(cx.subscribe(&sidebar, |this, _, event, cx| match event {
 367                SidebarEvent::SerializeNeeded => {
 368                    this.serialize(cx);
 369                }
 370            }));
 371        self.sidebar = Some(Box::new(sidebar));
 372    }
 373
 374    pub fn sidebar(&self) -> Option<&dyn SidebarHandle> {
 375        self.sidebar.as_deref()
 376    }
 377
 378    pub fn set_sidebar_overlay(&mut self, overlay: Option<AnyView>, cx: &mut Context<Self>) {
 379        self.sidebar_overlay = overlay;
 380        cx.notify();
 381    }
 382
 383    pub fn sidebar_open(&self) -> bool {
 384        self.sidebar_open
 385    }
 386
 387    pub fn sidebar_has_notifications(&self, cx: &App) -> bool {
 388        self.sidebar
 389            .as_ref()
 390            .map_or(false, |s| s.has_notifications(cx))
 391    }
 392
 393    pub fn is_threads_list_view_active(&self, cx: &App) -> bool {
 394        self.sidebar
 395            .as_ref()
 396            .map_or(false, |s| s.is_threads_list_view_active(cx))
 397    }
 398
 399    pub fn multi_workspace_enabled(&self, cx: &App) -> bool {
 400        !matches!(ReleaseChannel::try_global(cx), Some(ReleaseChannel::Stable))
 401            && !DisableAiSettings::get_global(cx).disable_ai
 402    }
 403
 404    pub fn toggle_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 405        if !self.multi_workspace_enabled(cx) {
 406            return;
 407        }
 408
 409        if self.sidebar_open() {
 410            self.close_sidebar(window, cx);
 411        } else {
 412            self.previous_focus_handle = window.focused(cx);
 413            self.open_sidebar(cx);
 414            if let Some(sidebar) = &self.sidebar {
 415                sidebar.prepare_for_focus(window, cx);
 416                sidebar.focus(window, cx);
 417            }
 418        }
 419    }
 420
 421    pub fn close_sidebar_action(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 422        if !self.multi_workspace_enabled(cx) {
 423            return;
 424        }
 425
 426        if self.sidebar_open() {
 427            self.close_sidebar(window, cx);
 428        }
 429    }
 430
 431    pub fn focus_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 432        if !self.multi_workspace_enabled(cx) {
 433            return;
 434        }
 435
 436        if self.sidebar_open() {
 437            let sidebar_is_focused = self
 438                .sidebar
 439                .as_ref()
 440                .is_some_and(|s| s.focus_handle(cx).contains_focused(window, cx));
 441
 442            if sidebar_is_focused {
 443                self.restore_previous_focus(false, window, cx);
 444            } else {
 445                self.previous_focus_handle = window.focused(cx);
 446                if let Some(sidebar) = &self.sidebar {
 447                    sidebar.prepare_for_focus(window, cx);
 448                    sidebar.focus(window, cx);
 449                }
 450            }
 451        } else {
 452            self.previous_focus_handle = window.focused(cx);
 453            self.open_sidebar(cx);
 454            if let Some(sidebar) = &self.sidebar {
 455                sidebar.prepare_for_focus(window, cx);
 456                sidebar.focus(window, cx);
 457            }
 458        }
 459    }
 460
 461    pub fn open_sidebar(&mut self, cx: &mut Context<Self>) {
 462        let side = match self.sidebar_side(cx) {
 463            SidebarSide::Left => "left",
 464            SidebarSide::Right => "right",
 465        };
 466        telemetry::event!("Sidebar Toggled", action = "open", side = side);
 467        self.apply_open_sidebar(cx);
 468    }
 469
 470    /// Restores the sidebar to open state from persisted session data without
 471    /// firing a telemetry event, since this is not a user-initiated action.
 472    pub(crate) fn restore_open_sidebar(&mut self, cx: &mut Context<Self>) {
 473        self.apply_open_sidebar(cx);
 474    }
 475
 476    fn apply_open_sidebar(&mut self, cx: &mut Context<Self>) {
 477        self.sidebar_open = true;
 478        self.retain_active_workspace(cx);
 479        let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
 480        for workspace in self.retained_workspaces.clone() {
 481            workspace.update(cx, |workspace, _cx| {
 482                workspace.set_sidebar_focus_handle(sidebar_focus_handle.clone());
 483            });
 484        }
 485        self.serialize(cx);
 486        cx.notify();
 487    }
 488
 489    pub fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 490        let side = match self.sidebar_side(cx) {
 491            SidebarSide::Left => "left",
 492            SidebarSide::Right => "right",
 493        };
 494        telemetry::event!("Sidebar Toggled", action = "close", side = side);
 495        self.sidebar_open = false;
 496        for workspace in self.retained_workspaces.clone() {
 497            workspace.update(cx, |workspace, _cx| {
 498                workspace.set_sidebar_focus_handle(None);
 499            });
 500        }
 501        let sidebar_has_focus = self
 502            .sidebar
 503            .as_ref()
 504            .is_some_and(|s| s.focus_handle(cx).contains_focused(window, cx));
 505        if sidebar_has_focus {
 506            self.restore_previous_focus(true, window, cx);
 507        } else {
 508            self.previous_focus_handle.take();
 509        }
 510        self.serialize(cx);
 511        cx.notify();
 512    }
 513
 514    fn restore_previous_focus(&mut self, clear: bool, window: &mut Window, cx: &mut Context<Self>) {
 515        let focus_handle = if clear {
 516            self.previous_focus_handle.take()
 517        } else {
 518            self.previous_focus_handle.clone()
 519        };
 520
 521        if let Some(previous_focus) = focus_handle {
 522            previous_focus.focus(window, cx);
 523        } else {
 524            let pane = self.workspace().read(cx).active_pane().clone();
 525            window.focus(&pane.read(cx).focus_handle(cx), cx);
 526        }
 527    }
 528
 529    pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
 530        cx.spawn_in(window, async move |this, cx| {
 531            let workspaces = this.update(cx, |multi_workspace, _cx| {
 532                multi_workspace.workspaces().cloned().collect::<Vec<_>>()
 533            })?;
 534
 535            for workspace in workspaces {
 536                let should_continue = workspace
 537                    .update_in(cx, |workspace, window, cx| {
 538                        workspace.prepare_to_close(CloseIntent::CloseWindow, window, cx)
 539                    })?
 540                    .await?;
 541                if !should_continue {
 542                    return anyhow::Ok(());
 543                }
 544            }
 545
 546            cx.update(|window, _cx| {
 547                window.remove_window();
 548            })?;
 549
 550            anyhow::Ok(())
 551        })
 552        .detach_and_log_err(cx);
 553    }
 554
 555    fn subscribe_to_workspace(
 556        workspace: &Entity<Workspace>,
 557        window: &Window,
 558        cx: &mut Context<Self>,
 559    ) {
 560        let project = workspace.read(cx).project().clone();
 561        cx.subscribe_in(&project, window, {
 562            let workspace = workspace.downgrade();
 563            move |this, _project, event, _window, cx| match event {
 564                project::Event::WorktreePathsChanged { old_worktree_paths } => {
 565                    if let Some(workspace) = workspace.upgrade() {
 566                        let host = workspace
 567                            .read(cx)
 568                            .project()
 569                            .read(cx)
 570                            .remote_connection_options(cx);
 571                        let old_key =
 572                            ProjectGroupKey::from_worktree_paths(old_worktree_paths, host);
 573                        this.handle_project_group_key_change(&workspace, &old_key, cx);
 574                    }
 575                }
 576                _ => {}
 577            }
 578        })
 579        .detach();
 580
 581        cx.subscribe_in(workspace, window, |this, workspace, event, window, cx| {
 582            if let WorkspaceEvent::Activate = event {
 583                this.activate(workspace.clone(), None, window, cx);
 584            }
 585        })
 586        .detach();
 587    }
 588
 589    fn handle_project_group_key_change(
 590        &mut self,
 591        workspace: &Entity<Workspace>,
 592        old_key: &ProjectGroupKey,
 593        cx: &mut Context<Self>,
 594    ) {
 595        if !self.is_workspace_retained(workspace) {
 596            return;
 597        }
 598
 599        let new_key = workspace.read(cx).project_group_key(cx);
 600        if new_key.path_list().paths().is_empty() {
 601            return;
 602        }
 603
 604        // The Project already emitted WorktreePathsChanged which the
 605        // sidebar handles for thread migration.
 606        self.rekey_project_group(old_key, &new_key, cx);
 607        self.serialize(cx);
 608        cx.notify();
 609    }
 610
 611    pub fn is_workspace_retained(&self, workspace: &Entity<Workspace>) -> bool {
 612        self.retained_workspaces
 613            .iter()
 614            .any(|retained| retained == workspace)
 615    }
 616
 617    pub fn active_workspace_is_retained(&self) -> bool {
 618        self.is_workspace_retained(&self.active_workspace)
 619    }
 620
 621    pub fn retained_workspaces(&self) -> &[Entity<Workspace>] {
 622        &self.retained_workspaces
 623    }
 624
 625    /// Ensures a project group exists for `key`, creating one if needed.
 626    fn ensure_project_group_state(&mut self, key: ProjectGroupKey) {
 627        if key.path_list().paths().is_empty() {
 628            return;
 629        }
 630
 631        if self.project_groups.iter().any(|group| group.key == key) {
 632            return;
 633        }
 634
 635        self.project_groups.insert(
 636            0,
 637            ProjectGroupState {
 638                key,
 639                expanded: true,
 640                last_active_workspace: None,
 641            },
 642        );
 643    }
 644
 645    /// Transitions a project group from `old_key` to `new_key`.
 646    ///
 647    /// On collision (both keys have groups), the active workspace's
 648    /// Re-keys a project group from `old_key` to `new_key`, handling
 649    /// collisions. When two groups collide, the active workspace's
 650    /// group always wins. Otherwise the old key's state is preserved
 651    /// — it represents the group the user or system just acted on.
 652    /// The losing group is removed, and the winner is re-keyed in
 653    /// place to preserve sidebar order.
 654    fn rekey_project_group(
 655        &mut self,
 656        old_key: &ProjectGroupKey,
 657        new_key: &ProjectGroupKey,
 658        cx: &App,
 659    ) {
 660        if old_key == new_key {
 661            return;
 662        }
 663
 664        if new_key.path_list().paths().is_empty() {
 665            return;
 666        }
 667
 668        let old_key_exists = self.project_groups.iter().any(|g| g.key == *old_key);
 669        let new_key_exists = self.project_groups.iter().any(|g| g.key == *new_key);
 670
 671        if !old_key_exists {
 672            self.ensure_project_group_state(new_key.clone());
 673            return;
 674        }
 675
 676        if new_key_exists {
 677            let active_key = self.active_workspace.read(cx).project_group_key(cx);
 678            if active_key == *new_key {
 679                self.project_groups.retain(|g| g.key != *old_key);
 680            } else {
 681                self.project_groups.retain(|g| g.key != *new_key);
 682                if let Some(group) = self.project_groups.iter_mut().find(|g| g.key == *old_key) {
 683                    group.key = new_key.clone();
 684                }
 685            }
 686        } else {
 687            if let Some(group) = self.project_groups.iter_mut().find(|g| g.key == *old_key) {
 688                group.key = new_key.clone();
 689            }
 690        }
 691
 692        // If another retained workspace still has the old key (e.g. a
 693        // linked worktree workspace), re-create the old group so it
 694        // remains reachable in the sidebar.
 695        let other_workspace_needs_old_key = self
 696            .retained_workspaces
 697            .iter()
 698            .any(|ws| ws.read(cx).project_group_key(cx) == *old_key);
 699        if other_workspace_needs_old_key {
 700            self.ensure_project_group_state(old_key.clone());
 701        }
 702    }
 703
 704    pub(crate) fn retain_workspace(
 705        &mut self,
 706        workspace: Entity<Workspace>,
 707        key: ProjectGroupKey,
 708        cx: &mut Context<Self>,
 709    ) {
 710        self.ensure_project_group_state(key);
 711        if self.is_workspace_retained(&workspace) {
 712            return;
 713        }
 714
 715        self.retained_workspaces.push(workspace.clone());
 716        cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
 717    }
 718
 719    pub(crate) fn activate_provisional_workspace(
 720        &mut self,
 721        workspace: Entity<Workspace>,
 722        provisional_key: ProjectGroupKey,
 723        window: &mut Window,
 724        cx: &mut Context<Self>,
 725    ) {
 726        if workspace != self.active_workspace {
 727            self.register_workspace(&workspace, window, cx);
 728        }
 729
 730        self.ensure_project_group_state(provisional_key);
 731        if !self.is_workspace_retained(&workspace) {
 732            self.retained_workspaces.push(workspace.clone());
 733        }
 734
 735        self.activate(workspace.clone(), None, window, cx);
 736        cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
 737    }
 738
 739    fn register_workspace(
 740        &mut self,
 741        workspace: &Entity<Workspace>,
 742        window: &Window,
 743        cx: &mut Context<Self>,
 744    ) {
 745        Self::subscribe_to_workspace(workspace, window, cx);
 746        let weak_self = cx.weak_entity();
 747        workspace.update(cx, |workspace, cx| {
 748            workspace.set_multi_workspace(weak_self, cx);
 749        });
 750
 751        let entity = cx.entity();
 752        cx.defer({
 753            let workspace = workspace.clone();
 754            move |cx| {
 755                entity.update(cx, |this, cx| {
 756                    this.sync_sidebar_to_workspace(&workspace, cx);
 757                })
 758            }
 759        });
 760    }
 761
 762    pub fn project_group_key_for_workspace(
 763        &self,
 764        workspace: &Entity<Workspace>,
 765        cx: &App,
 766    ) -> ProjectGroupKey {
 767        workspace.read(cx).project_group_key(cx)
 768    }
 769
 770    pub fn restore_project_groups(
 771        &mut self,
 772        groups: Vec<SerializedProjectGroupState>,
 773        _cx: &mut Context<Self>,
 774    ) {
 775        let mut restored: Vec<ProjectGroupState> = Vec::new();
 776        for SerializedProjectGroupState { key, expanded } in groups {
 777            if key.path_list().paths().is_empty() {
 778                continue;
 779            }
 780            if restored.iter().any(|group| group.key == key) {
 781                continue;
 782            }
 783            restored.push(ProjectGroupState {
 784                key,
 785                expanded,
 786                last_active_workspace: None,
 787            });
 788        }
 789        for existing in std::mem::take(&mut self.project_groups) {
 790            if !restored.iter().any(|group| group.key == existing.key) {
 791                restored.push(existing);
 792            }
 793        }
 794        self.project_groups = restored;
 795    }
 796
 797    pub fn project_group_keys(&self) -> Vec<ProjectGroupKey> {
 798        self.project_groups
 799            .iter()
 800            .map(|group| group.key.clone())
 801            .collect()
 802    }
 803
 804    fn derived_project_groups(&self, cx: &App) -> Vec<ProjectGroup> {
 805        self.project_groups
 806            .iter()
 807            .map(|group| ProjectGroup {
 808                key: group.key.clone(),
 809                workspaces: self
 810                    .retained_workspaces
 811                    .iter()
 812                    .filter(|workspace| workspace.read(cx).project_group_key(cx) == group.key)
 813                    .cloned()
 814                    .collect(),
 815                expanded: group.expanded,
 816            })
 817            .collect()
 818    }
 819
 820    pub fn project_groups(&self, cx: &App) -> Vec<ProjectGroup> {
 821        self.derived_project_groups(cx)
 822    }
 823
 824    pub fn last_active_workspace_for_group(
 825        &self,
 826        key: &ProjectGroupKey,
 827        cx: &App,
 828    ) -> Option<Entity<Workspace>> {
 829        let group = self.project_groups.iter().find(|g| g.key == *key)?;
 830        let weak = group.last_active_workspace.as_ref()?;
 831        let workspace = weak.upgrade()?;
 832        (workspace.read(cx).project_group_key(cx) == *key).then_some(workspace)
 833    }
 834
 835    pub fn group_state_by_key(&self, key: &ProjectGroupKey) -> Option<&ProjectGroupState> {
 836        self.project_groups.iter().find(|group| group.key == *key)
 837    }
 838
 839    pub fn group_state_by_key_mut(
 840        &mut self,
 841        key: &ProjectGroupKey,
 842    ) -> Option<&mut ProjectGroupState> {
 843        self.project_groups
 844            .iter_mut()
 845            .find(|group| group.key == *key)
 846    }
 847
 848    pub fn set_all_groups_expanded(&mut self, expanded: bool) {
 849        for group in &mut self.project_groups {
 850            group.expanded = expanded;
 851        }
 852    }
 853
 854    pub fn workspaces_for_project_group(
 855        &self,
 856        key: &ProjectGroupKey,
 857        cx: &App,
 858    ) -> Option<Vec<Entity<Workspace>>> {
 859        let has_group = self.project_groups.iter().any(|group| group.key == *key)
 860            || self
 861                .retained_workspaces
 862                .iter()
 863                .any(|workspace| workspace.read(cx).project_group_key(cx) == *key);
 864
 865        has_group.then(|| {
 866            self.retained_workspaces
 867                .iter()
 868                .filter(|workspace| workspace.read(cx).project_group_key(cx) == *key)
 869                .cloned()
 870                .collect()
 871        })
 872    }
 873
 874    pub fn close_workspace(
 875        &mut self,
 876        workspace: &Entity<Workspace>,
 877        window: &mut Window,
 878        cx: &mut Context<Self>,
 879    ) -> Task<Result<bool>> {
 880        let group_key = workspace.read(cx).project_group_key(cx);
 881        let excluded_workspace = workspace.clone();
 882
 883        self.remove(
 884            [workspace.clone()],
 885            move |this, window, cx| {
 886                if let Some(workspace) = this
 887                    .workspaces_for_project_group(&group_key, cx)
 888                    .unwrap_or_default()
 889                    .into_iter()
 890                    .find(|candidate| candidate != &excluded_workspace)
 891                {
 892                    return Task::ready(Ok(workspace));
 893                }
 894
 895                let current_group_index = this
 896                    .project_groups
 897                    .iter()
 898                    .position(|group| group.key == group_key);
 899
 900                if let Some(current_group_index) = current_group_index {
 901                    for distance in 1..this.project_groups.len() {
 902                        for neighboring_index in [
 903                            current_group_index.checked_add(distance),
 904                            current_group_index.checked_sub(distance),
 905                        ]
 906                        .into_iter()
 907                        .flatten()
 908                        {
 909                            let Some(neighboring_group) =
 910                                this.project_groups.get(neighboring_index)
 911                            else {
 912                                continue;
 913                            };
 914
 915                            if let Some(workspace) = this
 916                                .last_active_workspace_for_group(&neighboring_group.key, cx)
 917                                .or_else(|| {
 918                                    this.workspaces_for_project_group(&neighboring_group.key, cx)
 919                                        .unwrap_or_default()
 920                                        .into_iter()
 921                                        .find(|candidate| candidate != &excluded_workspace)
 922                                })
 923                            {
 924                                return Task::ready(Ok(workspace));
 925                            }
 926                        }
 927                    }
 928                }
 929
 930                let neighboring_group_key = current_group_index.and_then(|index| {
 931                    this.project_groups
 932                        .get(index + 1)
 933                        .or_else(|| {
 934                            index
 935                                .checked_sub(1)
 936                                .and_then(|previous| this.project_groups.get(previous))
 937                        })
 938                        .map(|group| group.key.clone())
 939                });
 940
 941                if let Some(neighboring_group_key) = neighboring_group_key {
 942                    return this.find_or_create_local_workspace(
 943                        neighboring_group_key.path_list().clone(),
 944                        Some(neighboring_group_key),
 945                        std::slice::from_ref(&excluded_workspace),
 946                        None,
 947                        OpenMode::Activate,
 948                        window,
 949                        cx,
 950                    );
 951                }
 952
 953                let app_state = this.workspace().read(cx).app_state().clone();
 954                let project = Project::local(
 955                    app_state.client.clone(),
 956                    app_state.node_runtime.clone(),
 957                    app_state.user_store.clone(),
 958                    app_state.languages.clone(),
 959                    app_state.fs.clone(),
 960                    None,
 961                    project::LocalProjectFlags::default(),
 962                    cx,
 963                );
 964                let new_workspace =
 965                    cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
 966                Task::ready(Ok(new_workspace))
 967            },
 968            window,
 969            cx,
 970        )
 971    }
 972
 973    pub fn remove_project_group(
 974        &mut self,
 975        group_key: &ProjectGroupKey,
 976        window: &mut Window,
 977        cx: &mut Context<Self>,
 978    ) -> Task<Result<bool>> {
 979        let pos = self
 980            .project_groups
 981            .iter()
 982            .position(|group| group.key == *group_key);
 983        let workspaces = self
 984            .workspaces_for_project_group(group_key, cx)
 985            .unwrap_or_default();
 986
 987        // Compute the neighbor while the group is still in the list.
 988        let neighbor_key = pos.and_then(|pos| {
 989            self.project_groups
 990                .get(pos + 1)
 991                .or_else(|| pos.checked_sub(1).and_then(|i| self.project_groups.get(i)))
 992                .map(|group| group.key.clone())
 993        });
 994
 995        // Now remove the group.
 996        self.project_groups.retain(|group| group.key != *group_key);
 997        cx.emit(MultiWorkspaceEvent::ProjectGroupsChanged);
 998
 999        let excluded_workspaces = workspaces.clone();
1000        self.remove(
1001            workspaces,
1002            move |this, window, cx| {
1003                if let Some(neighbor_key) = neighbor_key {
1004                    return this.find_or_create_local_workspace(
1005                        neighbor_key.path_list().clone(),
1006                        Some(neighbor_key.clone()),
1007                        &excluded_workspaces,
1008                        None,
1009                        OpenMode::Activate,
1010                        window,
1011                        cx,
1012                    );
1013                }
1014
1015                // No other project groups remain — create an empty workspace.
1016                let app_state = this.workspace().read(cx).app_state().clone();
1017                let project = Project::local(
1018                    app_state.client.clone(),
1019                    app_state.node_runtime.clone(),
1020                    app_state.user_store.clone(),
1021                    app_state.languages.clone(),
1022                    app_state.fs.clone(),
1023                    None,
1024                    project::LocalProjectFlags::default(),
1025                    cx,
1026                );
1027                let new_workspace =
1028                    cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
1029                Task::ready(Ok(new_workspace))
1030            },
1031            window,
1032            cx,
1033        )
1034    }
1035
1036    /// Goes through sqlite: serialize -> close -> open new window
1037    /// This avoids issues with pending tasks having the wrong window
1038    pub fn open_project_group_in_new_window(
1039        &mut self,
1040        key: &ProjectGroupKey,
1041        window: &mut Window,
1042        cx: &mut Context<Self>,
1043    ) -> Task<Result<()>> {
1044        let paths: Vec<PathBuf> = key.path_list().ordered_paths().cloned().collect();
1045        if paths.is_empty() {
1046            return Task::ready(Ok(()));
1047        }
1048
1049        let app_state = self.workspace().read(cx).app_state().clone();
1050
1051        let workspaces: Vec<_> = self
1052            .workspaces_for_project_group(key, cx)
1053            .unwrap_or_default();
1054        let mut serialization_tasks = Vec::new();
1055        for workspace in &workspaces {
1056            serialization_tasks.push(workspace.update(cx, |workspace, inner_cx| {
1057                workspace.flush_serialization(window, inner_cx)
1058            }));
1059        }
1060
1061        let remove_task = self.remove_project_group(key, window, cx);
1062
1063        cx.spawn(async move |_this, cx| {
1064            futures::future::join_all(serialization_tasks).await;
1065
1066            let removed = remove_task.await?;
1067            if !removed {
1068                return Ok(());
1069            }
1070
1071            cx.update(|cx| {
1072                Workspace::new_local(paths, app_state, None, None, None, OpenMode::NewWindow, cx)
1073            })
1074            .await?;
1075
1076            Ok(())
1077        })
1078    }
1079
1080    /// Finds an existing workspace whose root paths and host exactly match.
1081    pub fn workspace_for_paths(
1082        &self,
1083        path_list: &PathList,
1084        host: Option<&RemoteConnectionOptions>,
1085        cx: &App,
1086    ) -> Option<Entity<Workspace>> {
1087        self.workspace_for_paths_excluding(path_list, host, &[], cx)
1088    }
1089
1090    fn workspace_for_paths_excluding(
1091        &self,
1092        path_list: &PathList,
1093        host: Option<&RemoteConnectionOptions>,
1094        excluding: &[Entity<Workspace>],
1095        cx: &App,
1096    ) -> Option<Entity<Workspace>> {
1097        for workspace in self.workspaces() {
1098            if excluding.contains(workspace) {
1099                continue;
1100            }
1101            let root_paths = PathList::new(&workspace.read(cx).root_paths(cx));
1102            let key = workspace.read(cx).project_group_key(cx);
1103            let host_matches = key.host().as_ref() == host;
1104            let paths_match = root_paths == *path_list;
1105            if host_matches && paths_match {
1106                return Some(workspace.clone());
1107            }
1108        }
1109
1110        None
1111    }
1112
1113    /// Finds an existing workspace whose paths match, or creates a new one.
1114    ///
1115    /// For local projects (`host` is `None`), this delegates to
1116    /// [`Self::find_or_create_local_workspace`]. For remote projects, it
1117    /// tries an exact path match and, if no existing workspace is found,
1118    /// calls `connect_remote` to establish a connection and creates a new
1119    /// remote workspace.
1120    ///
1121    /// The `connect_remote` closure is responsible for any user-facing
1122    /// connection UI (e.g. password prompts). It receives the connection
1123    /// options and should return a [`Task`] that resolves to the
1124    /// [`RemoteClient`] session, or `None` if the connection was
1125    /// cancelled.
1126    pub fn find_or_create_workspace(
1127        &mut self,
1128        paths: PathList,
1129        host: Option<RemoteConnectionOptions>,
1130        provisional_project_group_key: Option<ProjectGroupKey>,
1131        connect_remote: impl FnOnce(
1132            RemoteConnectionOptions,
1133            &mut Window,
1134            &mut Context<Self>,
1135        ) -> Task<Result<Option<Entity<remote::RemoteClient>>>>
1136        + 'static,
1137        excluding: &[Entity<Workspace>],
1138        init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
1139        open_mode: OpenMode,
1140        window: &mut Window,
1141        cx: &mut Context<Self>,
1142    ) -> Task<Result<Entity<Workspace>>> {
1143        self.find_or_create_workspace_with_source_workspace(
1144            paths,
1145            host,
1146            provisional_project_group_key,
1147            connect_remote,
1148            excluding,
1149            init,
1150            open_mode,
1151            None,
1152            window,
1153            cx,
1154        )
1155    }
1156
1157    pub fn find_or_create_workspace_with_source_workspace(
1158        &mut self,
1159        paths: PathList,
1160        host: Option<RemoteConnectionOptions>,
1161        provisional_project_group_key: Option<ProjectGroupKey>,
1162        connect_remote: impl FnOnce(
1163            RemoteConnectionOptions,
1164            &mut Window,
1165            &mut Context<Self>,
1166        ) -> Task<Result<Option<Entity<remote::RemoteClient>>>>
1167        + 'static,
1168        excluding: &[Entity<Workspace>],
1169        init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
1170        open_mode: OpenMode,
1171        source_workspace: Option<WeakEntity<Workspace>>,
1172        window: &mut Window,
1173        cx: &mut Context<Self>,
1174    ) -> Task<Result<Entity<Workspace>>> {
1175        if let Some(workspace) = self.workspace_for_paths(&paths, host.as_ref(), cx) {
1176            self.activate(workspace.clone(), source_workspace, window, cx);
1177            return Task::ready(Ok(workspace));
1178        }
1179
1180        let Some(connection_options) = host else {
1181            return self.find_or_create_local_workspace_with_source_workspace(
1182                paths,
1183                provisional_project_group_key,
1184                excluding,
1185                init,
1186                open_mode,
1187                source_workspace,
1188                window,
1189                cx,
1190            );
1191        };
1192
1193        let app_state = self.workspace().read(cx).app_state().clone();
1194        let window_handle = window.window_handle().downcast::<MultiWorkspace>();
1195        let connect_task = connect_remote(connection_options.clone(), window, cx);
1196        let paths_vec = paths.paths().to_vec();
1197
1198        cx.spawn(async move |_this, cx| {
1199            let session = connect_task
1200                .await?
1201                .ok_or_else(|| anyhow::anyhow!("Remote connection was cancelled"))?;
1202
1203            let new_project = cx.update(|cx| {
1204                Project::remote(
1205                    session,
1206                    app_state.client.clone(),
1207                    app_state.node_runtime.clone(),
1208                    app_state.user_store.clone(),
1209                    app_state.languages.clone(),
1210                    app_state.fs.clone(),
1211                    true,
1212                    cx,
1213                )
1214            });
1215
1216            let effective_paths_vec =
1217                if let Some(project_group) = provisional_project_group_key.as_ref() {
1218                    let resolve_tasks = cx.update(|cx| {
1219                        let project = new_project.read(cx);
1220                        paths_vec
1221                            .iter()
1222                            .map(|path| project.resolve_abs_path(&path.to_string_lossy(), cx))
1223                            .collect::<Vec<_>>()
1224                    });
1225                    let resolved = futures::future::join_all(resolve_tasks).await;
1226                    // `resolve_abs_path` returns `None` for both "definitely
1227                    // absent" and transport errors (it swallows the error via
1228                    // `log_err`). This is a weaker guarantee than the local
1229                    // `Ok(None)` check, but it matches how the rest of the
1230                    // codebase consumes this API.
1231                    let all_paths_missing =
1232                        !paths_vec.is_empty() && resolved.iter().all(|resolved| resolved.is_none());
1233
1234                    if all_paths_missing {
1235                        project_group.path_list().paths().to_vec()
1236                    } else {
1237                        paths_vec
1238                    }
1239                } else {
1240                    paths_vec
1241                };
1242
1243            let window_handle =
1244                window_handle.ok_or_else(|| anyhow::anyhow!("Window is not a MultiWorkspace"))?;
1245
1246            open_remote_project_with_existing_connection(
1247                connection_options,
1248                new_project,
1249                effective_paths_vec,
1250                app_state,
1251                window_handle,
1252                provisional_project_group_key,
1253                source_workspace,
1254                cx,
1255            )
1256            .await?;
1257
1258            window_handle.update(cx, |multi_workspace, window, cx| {
1259                let workspace = multi_workspace.workspace().clone();
1260                multi_workspace.add(workspace.clone(), window, cx);
1261                workspace
1262            })
1263        })
1264    }
1265
1266    /// Finds an existing workspace in this multi-workspace whose paths match,
1267    /// or creates a new one (deserializing its saved state from the database).
1268    /// Never searches other windows or matches workspaces with a superset of
1269    /// the requested paths.
1270    ///
1271    /// `excluding` lists workspaces that should be skipped during the search
1272    /// (e.g. workspaces that are about to be removed).
1273    pub fn find_or_create_local_workspace(
1274        &mut self,
1275        path_list: PathList,
1276        project_group: Option<ProjectGroupKey>,
1277        excluding: &[Entity<Workspace>],
1278        init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
1279        open_mode: OpenMode,
1280        window: &mut Window,
1281        cx: &mut Context<Self>,
1282    ) -> Task<Result<Entity<Workspace>>> {
1283        self.find_or_create_local_workspace_with_source_workspace(
1284            path_list,
1285            project_group,
1286            excluding,
1287            init,
1288            open_mode,
1289            None,
1290            window,
1291            cx,
1292        )
1293    }
1294
1295    pub fn find_or_create_local_workspace_with_source_workspace(
1296        &mut self,
1297        path_list: PathList,
1298        project_group: Option<ProjectGroupKey>,
1299        excluding: &[Entity<Workspace>],
1300        init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
1301        open_mode: OpenMode,
1302        source_workspace: Option<WeakEntity<Workspace>>,
1303        window: &mut Window,
1304        cx: &mut Context<Self>,
1305    ) -> Task<Result<Entity<Workspace>>> {
1306        if let Some(workspace) = self.workspace_for_paths_excluding(&path_list, None, excluding, cx)
1307        {
1308            self.activate(workspace.clone(), source_workspace, window, cx);
1309            return Task::ready(Ok(workspace));
1310        }
1311
1312        let paths = path_list.paths().to_vec();
1313        let app_state = self.workspace().read(cx).app_state().clone();
1314        let requesting_window = window.window_handle().downcast::<MultiWorkspace>();
1315        let fs = <dyn Fs>::global(cx);
1316        let excluding = excluding.to_vec();
1317
1318        cx.spawn(async move |_this, cx| {
1319            let effective_path_list = if let Some(project_group) = project_group {
1320                let metadata_tasks: Vec<_> = paths
1321                    .iter()
1322                    .map(|path| fs.metadata(path.as_path()))
1323                    .collect();
1324                let metadata_results = futures::future::join_all(metadata_tasks).await;
1325                // Only fall back when every path is definitely absent; real
1326                // filesystem errors should not be treated as "missing".
1327                let all_paths_missing = !paths.is_empty()
1328                    && metadata_results
1329                        .into_iter()
1330                        // Ok(None) means the path is definitely absent
1331                        .all(|result| matches!(result, Ok(None)));
1332
1333                if all_paths_missing {
1334                    project_group.path_list().clone()
1335                } else {
1336                    PathList::new(&paths)
1337                }
1338            } else {
1339                PathList::new(&paths)
1340            };
1341
1342            if let Some(requesting_window) = requesting_window
1343                && let Some(workspace) = requesting_window
1344                    .update(cx, |multi_workspace, window, cx| {
1345                        multi_workspace
1346                            .workspace_for_paths_excluding(
1347                                &effective_path_list,
1348                                None,
1349                                &excluding,
1350                                cx,
1351                            )
1352                            .inspect(|workspace| {
1353                                multi_workspace.activate(
1354                                    workspace.clone(),
1355                                    source_workspace.clone(),
1356                                    window,
1357                                    cx,
1358                                );
1359                            })
1360                    })
1361                    .ok()
1362                    .flatten()
1363            {
1364                return Ok(workspace);
1365            }
1366
1367            let result = cx
1368                .update(|cx| {
1369                    Workspace::new_local(
1370                        effective_path_list.paths().to_vec(),
1371                        app_state,
1372                        requesting_window,
1373                        None,
1374                        init,
1375                        open_mode,
1376                        cx,
1377                    )
1378                })
1379                .await?;
1380            Ok(result.workspace)
1381        })
1382    }
1383
1384    pub fn workspace(&self) -> &Entity<Workspace> {
1385        &self.active_workspace
1386    }
1387
1388    pub fn workspaces(&self) -> impl Iterator<Item = &Entity<Workspace>> {
1389        let active_is_retained = self.is_workspace_retained(&self.active_workspace);
1390        self.retained_workspaces
1391            .iter()
1392            .chain(std::iter::once(&self.active_workspace).filter(move |_| !active_is_retained))
1393    }
1394
1395    /// Adds a workspace to this window as persistent without changing which
1396    /// workspace is active. Unlike `activate()`, this always inserts into the
1397    /// persistent list regardless of sidebar state — it's used for system-
1398    /// initiated additions like deserialization and worktree discovery.
1399    pub fn add(&mut self, workspace: Entity<Workspace>, window: &Window, cx: &mut Context<Self>) {
1400        if self.is_workspace_retained(&workspace) {
1401            return;
1402        }
1403
1404        if workspace != self.active_workspace {
1405            self.register_workspace(&workspace, window, cx);
1406        }
1407
1408        let key = workspace.read(cx).project_group_key(cx);
1409        self.retain_workspace(workspace, key, cx);
1410        telemetry::event!(
1411            "Workspace Added",
1412            workspace_count = self.retained_workspaces.len()
1413        );
1414        cx.notify();
1415    }
1416
1417    /// Ensures the workspace is in the multiworkspace and makes it the active one.
1418    pub fn activate(
1419        &mut self,
1420        workspace: Entity<Workspace>,
1421        source_workspace: Option<WeakEntity<Workspace>>,
1422        window: &mut Window,
1423        cx: &mut Context<Self>,
1424    ) {
1425        if self.workspace() == &workspace {
1426            self.focus_active_workspace(window, cx);
1427            return;
1428        }
1429
1430        let old_active_workspace = self.active_workspace.clone();
1431        let old_active_was_retained = self.active_workspace_is_retained();
1432        let workspace_was_retained = self.is_workspace_retained(&workspace);
1433
1434        if !workspace_was_retained {
1435            self.register_workspace(&workspace, window, cx);
1436
1437            if self.sidebar_open {
1438                let key = workspace.read(cx).project_group_key(cx);
1439                self.retain_workspace(workspace.clone(), key, cx);
1440            }
1441        }
1442
1443        self.active_workspace = workspace;
1444
1445        let active_key = self.active_workspace.read(cx).project_group_key(cx);
1446        if let Some(group) = self.project_groups.iter_mut().find(|g| g.key == active_key) {
1447            group.last_active_workspace = Some(self.active_workspace.downgrade());
1448        }
1449
1450        if !self.sidebar_open && !old_active_was_retained {
1451            self.detach_workspace(&old_active_workspace, cx);
1452        }
1453
1454        cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged { source_workspace });
1455        self.serialize(cx);
1456        self.focus_active_workspace(window, cx);
1457        cx.notify();
1458    }
1459
1460    /// Promotes the currently active workspace to persistent if it is
1461    /// transient, so it is retained across workspace switches even when
1462    /// the sidebar is closed. No-op if the workspace is already persistent.
1463    pub fn retain_active_workspace(&mut self, cx: &mut Context<Self>) {
1464        let workspace = self.active_workspace.clone();
1465        if self.is_workspace_retained(&workspace) {
1466            return;
1467        }
1468
1469        let key = workspace.read(cx).project_group_key(cx);
1470        self.retain_workspace(workspace, key, cx);
1471        self.serialize(cx);
1472        cx.notify();
1473    }
1474
1475    /// Collapses to a single workspace, discarding all groups.
1476    /// Used when multi-workspace is disabled (e.g. disable_ai).
1477    fn collapse_to_single_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1478        if self.sidebar_open {
1479            self.close_sidebar(window, cx);
1480        }
1481
1482        let active_workspace = self.active_workspace.clone();
1483        for workspace in self.retained_workspaces.clone() {
1484            if workspace != active_workspace {
1485                self.detach_workspace(&workspace, cx);
1486            }
1487        }
1488
1489        self.retained_workspaces.clear();
1490        self.project_groups.clear();
1491        cx.notify();
1492    }
1493
1494    /// Detaches a workspace: clears session state, DB binding, cached
1495    /// group key, and emits `WorkspaceRemoved`. The DB row is preserved
1496    /// so the workspace still appears in the recent-projects list.
1497    fn detach_workspace(&mut self, workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
1498        self.retained_workspaces
1499            .retain(|retained| retained != workspace);
1500        for group in &mut self.project_groups {
1501            if group
1502                .last_active_workspace
1503                .as_ref()
1504                .and_then(WeakEntity::upgrade)
1505                .as_ref()
1506                == Some(workspace)
1507            {
1508                group.last_active_workspace = None;
1509            }
1510        }
1511        cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(workspace.entity_id()));
1512        workspace.update(cx, |workspace, _cx| {
1513            workspace.session_id.take();
1514            workspace._schedule_serialize_workspace.take();
1515            workspace._serialize_workspace_task.take();
1516        });
1517
1518        if let Some(workspace_id) = workspace.read(cx).database_id() {
1519            let db = crate::persistence::WorkspaceDb::global(cx);
1520            self.pending_removal_tasks.retain(|task| !task.is_ready());
1521            self.pending_removal_tasks
1522                .push(cx.background_spawn(async move {
1523                    db.set_session_binding(workspace_id, None, None)
1524                        .await
1525                        .log_err();
1526                }));
1527        }
1528    }
1529
1530    fn sync_sidebar_to_workspace(&self, workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
1531        if self.sidebar_open() {
1532            let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
1533            workspace.update(cx, |workspace, _| {
1534                workspace.set_sidebar_focus_handle(sidebar_focus_handle);
1535            });
1536        }
1537    }
1538
1539    pub fn serialize(&mut self, cx: &mut Context<Self>) {
1540        self._serialize_task = Some(cx.spawn(async move |this, cx| {
1541            let Some((window_id, state)) = this
1542                .read_with(cx, |this, cx| {
1543                    let state = MultiWorkspaceState {
1544                        active_workspace_id: this.workspace().read(cx).database_id(),
1545                        project_groups: this
1546                            .project_groups
1547                            .iter()
1548                            .map(|group| {
1549                                crate::persistence::model::SerializedProjectGroup::from_group(
1550                                    &group.key,
1551                                    group.expanded,
1552                                )
1553                            })
1554                            .collect::<Vec<_>>(),
1555                        sidebar_open: this.sidebar_open,
1556                        sidebar_state: this.sidebar.as_ref().and_then(|s| s.serialized_state(cx)),
1557                    };
1558                    (this.window_id, state)
1559                })
1560                .ok()
1561            else {
1562                return;
1563            };
1564            let kvp = cx.update(|cx| db::kvp::KeyValueStore::global(cx));
1565            crate::persistence::write_multi_workspace_state(&kvp, window_id, state).await;
1566        }));
1567    }
1568
1569    /// Returns the in-flight serialization task (if any) so the caller can
1570    /// await it. Used by the quit handler to ensure pending DB writes
1571    /// complete before the process exits.
1572    pub fn flush_serialization(&mut self) -> Task<()> {
1573        self._serialize_task.take().unwrap_or(Task::ready(()))
1574    }
1575
1576    fn app_will_quit(&mut self, _cx: &mut Context<Self>) -> impl Future<Output = ()> + use<> {
1577        let mut tasks: Vec<Task<()>> = Vec::new();
1578        if let Some(task) = self._serialize_task.take() {
1579            tasks.push(task);
1580        }
1581        tasks.extend(std::mem::take(&mut self.pending_removal_tasks));
1582
1583        async move {
1584            futures::future::join_all(tasks).await;
1585        }
1586    }
1587
1588    pub fn focus_active_workspace(&self, window: &mut Window, cx: &mut App) {
1589        // If a dock panel is zoomed, focus it instead of the center pane.
1590        // Otherwise, focusing the center pane triggers dismiss_zoomed_items_to_reveal
1591        // which closes the zoomed dock.
1592        let focus_handle = {
1593            let workspace = self.workspace().read(cx);
1594            let mut target = None;
1595            for dock in workspace.all_docks() {
1596                let dock = dock.read(cx);
1597                if dock.is_open() {
1598                    if let Some(panel) = dock.active_panel() {
1599                        if panel.is_zoomed(window, cx) {
1600                            target = Some(panel.panel_focus_handle(cx));
1601                            break;
1602                        }
1603                    }
1604                }
1605            }
1606            target.unwrap_or_else(|| {
1607                let pane = workspace.active_pane().clone();
1608                pane.read(cx).focus_handle(cx)
1609            })
1610        };
1611        window.focus(&focus_handle, cx);
1612    }
1613
1614    pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
1615        self.workspace().read(cx).panel::<T>(cx)
1616    }
1617
1618    pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
1619        self.workspace().read(cx).active_modal::<V>(cx)
1620    }
1621
1622    pub fn add_panel<T: Panel>(
1623        &mut self,
1624        panel: Entity<T>,
1625        window: &mut Window,
1626        cx: &mut Context<Self>,
1627    ) {
1628        self.workspace().update(cx, |workspace, cx| {
1629            workspace.add_panel(panel, window, cx);
1630        });
1631    }
1632
1633    pub fn focus_panel<T: Panel>(
1634        &mut self,
1635        window: &mut Window,
1636        cx: &mut Context<Self>,
1637    ) -> Option<Entity<T>> {
1638        self.workspace()
1639            .update(cx, |workspace, cx| workspace.focus_panel::<T>(window, cx))
1640    }
1641
1642    // used in a test
1643    pub fn toggle_modal<V: ModalView, B>(
1644        &mut self,
1645        window: &mut Window,
1646        cx: &mut Context<Self>,
1647        build: B,
1648    ) where
1649        B: FnOnce(&mut Window, &mut gpui::Context<V>) -> V,
1650    {
1651        self.workspace().update(cx, |workspace, cx| {
1652            workspace.toggle_modal(window, cx, build);
1653        });
1654    }
1655
1656    pub fn toggle_dock(
1657        &mut self,
1658        dock_side: DockPosition,
1659        window: &mut Window,
1660        cx: &mut Context<Self>,
1661    ) {
1662        self.workspace().update(cx, |workspace, cx| {
1663            workspace.toggle_dock(dock_side, window, cx);
1664        });
1665    }
1666
1667    pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
1668        self.workspace().read(cx).active_item_as::<I>(cx)
1669    }
1670
1671    pub fn items_of_type<'a, T: Item>(
1672        &'a self,
1673        cx: &'a App,
1674    ) -> impl 'a + Iterator<Item = Entity<T>> {
1675        self.workspace().read(cx).items_of_type::<T>(cx)
1676    }
1677
1678    pub fn database_id(&self, cx: &App) -> Option<WorkspaceId> {
1679        self.workspace().read(cx).database_id()
1680    }
1681
1682    pub fn take_pending_removal_tasks(&mut self) -> Vec<Task<()>> {
1683        let tasks: Vec<Task<()>> = std::mem::take(&mut self.pending_removal_tasks)
1684            .into_iter()
1685            .filter(|task| !task.is_ready())
1686            .collect();
1687        tasks
1688    }
1689
1690    #[cfg(any(test, feature = "test-support"))]
1691    pub fn test_expand_all_groups(&mut self) {
1692        self.set_all_groups_expanded(true);
1693    }
1694
1695    #[cfg(any(test, feature = "test-support"))]
1696    pub fn assert_project_group_key_integrity(&self, cx: &App) -> anyhow::Result<()> {
1697        let mut retained_ids: collections::HashSet<EntityId> = Default::default();
1698        for workspace in &self.retained_workspaces {
1699            anyhow::ensure!(
1700                retained_ids.insert(workspace.entity_id()),
1701                "workspace {:?} is retained more than once",
1702                workspace.entity_id(),
1703            );
1704
1705            let live_key = workspace.read(cx).project_group_key(cx);
1706            anyhow::ensure!(
1707                self.project_groups
1708                    .iter()
1709                    .any(|group| group.key == live_key),
1710                "workspace {:?} has live key {:?} but no project-group metadata",
1711                workspace.entity_id(),
1712                live_key,
1713            );
1714        }
1715        Ok(())
1716    }
1717
1718    #[cfg(any(test, feature = "test-support"))]
1719    pub fn set_random_database_id(&mut self, cx: &mut Context<Self>) {
1720        self.workspace().update(cx, |workspace, _cx| {
1721            workspace.set_random_database_id();
1722        });
1723    }
1724
1725    #[cfg(any(test, feature = "test-support"))]
1726    pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
1727        let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
1728        Self::new(workspace, window, cx)
1729    }
1730
1731    #[cfg(any(test, feature = "test-support"))]
1732    pub fn test_add_workspace(
1733        &mut self,
1734        project: Entity<Project>,
1735        window: &mut Window,
1736        cx: &mut Context<Self>,
1737    ) -> Entity<Workspace> {
1738        let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
1739        self.activate(workspace.clone(), None, window, cx);
1740        workspace
1741    }
1742
1743    #[cfg(any(test, feature = "test-support"))]
1744    pub fn test_add_project_group(&mut self, group: ProjectGroup) {
1745        self.project_groups.push(ProjectGroupState {
1746            key: group.key,
1747            expanded: group.expanded,
1748            last_active_workspace: None,
1749        });
1750    }
1751
1752    #[cfg(any(test, feature = "test-support"))]
1753    pub fn create_test_workspace(
1754        &mut self,
1755        window: &mut Window,
1756        cx: &mut Context<Self>,
1757    ) -> Task<()> {
1758        let app_state = self.workspace().read(cx).app_state().clone();
1759        let project = Project::local(
1760            app_state.client.clone(),
1761            app_state.node_runtime.clone(),
1762            app_state.user_store.clone(),
1763            app_state.languages.clone(),
1764            app_state.fs.clone(),
1765            None,
1766            project::LocalProjectFlags::default(),
1767            cx,
1768        );
1769        let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
1770        self.activate(new_workspace.clone(), None, window, cx);
1771
1772        let weak_workspace = new_workspace.downgrade();
1773        let db = crate::persistence::WorkspaceDb::global(cx);
1774        cx.spawn_in(window, async move |this, cx| {
1775            let workspace_id = db.next_id().await.unwrap();
1776            let workspace = weak_workspace.upgrade().unwrap();
1777            let task: Task<()> = this
1778                .update_in(cx, |this, window, cx| {
1779                    let session_id = workspace.read(cx).session_id();
1780                    let window_id = window.window_handle().window_id().as_u64();
1781                    workspace.update(cx, |workspace, _cx| {
1782                        workspace.set_database_id(workspace_id);
1783                    });
1784                    this.serialize(cx);
1785                    let db = db.clone();
1786                    cx.background_spawn(async move {
1787                        db.set_session_binding(workspace_id, session_id, Some(window_id))
1788                            .await
1789                            .log_err();
1790                    })
1791                })
1792                .unwrap();
1793            task.await
1794        })
1795    }
1796
1797    /// Assigns random database IDs to all retained workspaces, flushes
1798    /// workspace serialization (SQLite) and multi-workspace state (KVP),
1799    /// and writes session bindings so the serialized data can be read
1800    /// back by `last_session_workspace_locations` +
1801    /// `read_serialized_multi_workspaces`.
1802    #[cfg(any(test, feature = "test-support"))]
1803    pub fn flush_all_serialization(
1804        &mut self,
1805        window: &mut Window,
1806        cx: &mut Context<Self>,
1807    ) -> Vec<Task<()>> {
1808        for workspace in self.workspaces() {
1809            workspace.update(cx, |ws, _cx| {
1810                if ws.database_id().is_none() {
1811                    ws.set_random_database_id();
1812                }
1813            });
1814        }
1815
1816        let session_id = self.workspace().read(cx).session_id();
1817        let window_id_u64 = window.window_handle().window_id().as_u64();
1818
1819        let mut tasks: Vec<Task<()>> = Vec::new();
1820        for workspace in self.workspaces() {
1821            tasks.push(workspace.update(cx, |ws, cx| ws.flush_serialization(window, cx)));
1822            if let Some(db_id) = workspace.read(cx).database_id() {
1823                let db = crate::persistence::WorkspaceDb::global(cx);
1824                let session_id = session_id.clone();
1825                tasks.push(cx.background_spawn(async move {
1826                    db.set_session_binding(db_id, session_id, Some(window_id_u64))
1827                        .await
1828                        .log_err();
1829                }));
1830            }
1831        }
1832        self.serialize(cx);
1833        tasks
1834    }
1835
1836    /// Removes one or more workspaces from this multi-workspace.
1837    ///
1838    /// If the active workspace is among those being removed,
1839    /// `fallback_workspace` is called **synchronously before the removal
1840    /// begins** to produce a `Task` that resolves to the workspace that
1841    /// should become active. The fallback must not be one of the
1842    /// workspaces being removed.
1843    ///
1844    /// Returns `true` if any workspaces were actually removed.
1845    pub fn remove(
1846        &mut self,
1847        workspaces: impl IntoIterator<Item = Entity<Workspace>>,
1848        fallback_workspace: impl FnOnce(
1849            &mut Self,
1850            &mut Window,
1851            &mut Context<Self>,
1852        ) -> Task<Result<Entity<Workspace>>>,
1853        window: &mut Window,
1854        cx: &mut Context<Self>,
1855    ) -> Task<Result<bool>> {
1856        let workspaces: Vec<_> = workspaces.into_iter().collect();
1857
1858        if workspaces.is_empty() {
1859            return Task::ready(Ok(false));
1860        }
1861
1862        let removing_active = workspaces.iter().any(|ws| ws == self.workspace());
1863        let original_active = self.workspace().clone();
1864
1865        let fallback_task = removing_active.then(|| fallback_workspace(self, window, cx));
1866
1867        cx.spawn_in(window, async move |this, cx| {
1868            // Run the standard workspace close lifecycle for every workspace
1869            // being removed from this window. This handles save prompting and
1870            // session cleanup consistently with other replace-in-window flows.
1871            for workspace in &workspaces {
1872                let should_continue = workspace
1873                    .update_in(cx, |workspace, window, cx| {
1874                        workspace.prepare_to_close(CloseIntent::ReplaceWindow, window, cx)
1875                    })?
1876                    .await?;
1877
1878                if !should_continue {
1879                    return Ok(false);
1880                }
1881            }
1882
1883            // If we're removing the active workspace, await the
1884            // fallback and switch to it before tearing anything down.
1885            // Otherwise restore the original active workspace in case
1886            // prompting switched away from it.
1887            if let Some(fallback_task) = fallback_task {
1888                let new_active = fallback_task.await?;
1889
1890                this.update_in(cx, |this, window, cx| {
1891                    assert!(
1892                        !workspaces.contains(&new_active),
1893                        "fallback workspace must not be one of the workspaces being removed"
1894                    );
1895                    this.activate(new_active, None, window, cx);
1896                })?;
1897            } else {
1898                this.update_in(cx, |this, window, cx| {
1899                    if *this.workspace() != original_active {
1900                        this.activate(original_active, None, window, cx);
1901                    }
1902                })?;
1903            }
1904
1905            // Actually remove the workspaces.
1906            this.update_in(cx, |this, _, cx| {
1907                let mut removed_any = false;
1908
1909                for workspace in &workspaces {
1910                    let was_retained = this.is_workspace_retained(workspace);
1911                    if was_retained {
1912                        this.detach_workspace(workspace, cx);
1913                        removed_any = true;
1914                    }
1915                }
1916
1917                if removed_any {
1918                    this.serialize(cx);
1919                    cx.notify();
1920                }
1921
1922                Ok(removed_any)
1923            })?
1924        })
1925    }
1926
1927    pub fn open_project(
1928        &mut self,
1929        paths: Vec<PathBuf>,
1930        open_mode: OpenMode,
1931        window: &mut Window,
1932        cx: &mut Context<Self>,
1933    ) -> Task<Result<Entity<Workspace>>> {
1934        if self.multi_workspace_enabled(cx) {
1935            self.find_or_create_local_workspace(
1936                PathList::new(&paths),
1937                None,
1938                &[],
1939                None,
1940                OpenMode::Activate,
1941                window,
1942                cx,
1943            )
1944        } else {
1945            let workspace = self.workspace().clone();
1946            cx.spawn_in(window, async move |_this, cx| {
1947                let should_continue = workspace
1948                    .update_in(cx, |workspace, window, cx| {
1949                        workspace.prepare_to_close(crate::CloseIntent::ReplaceWindow, window, cx)
1950                    })?
1951                    .await?;
1952                if should_continue {
1953                    workspace
1954                        .update_in(cx, |workspace, window, cx| {
1955                            workspace.open_workspace_for_paths(open_mode, paths, window, cx)
1956                        })?
1957                        .await
1958                } else {
1959                    Ok(workspace)
1960                }
1961            })
1962        }
1963    }
1964}
1965
1966impl Render for MultiWorkspace {
1967    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1968        let multi_workspace_enabled = self.multi_workspace_enabled(cx);
1969        let sidebar_side = self.sidebar_side(cx);
1970        let sidebar_on_right = sidebar_side == SidebarSide::Right;
1971
1972        let sidebar: Option<AnyElement> = if multi_workspace_enabled && self.sidebar_open() {
1973            self.sidebar.as_ref().map(|sidebar_handle| {
1974                let weak = cx.weak_entity();
1975
1976                let sidebar_width = sidebar_handle.width(cx);
1977                let resize_handle = deferred(
1978                    div()
1979                        .id("sidebar-resize-handle")
1980                        .absolute()
1981                        .when(!sidebar_on_right, |el| {
1982                            el.right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
1983                        })
1984                        .when(sidebar_on_right, |el| {
1985                            el.left(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
1986                        })
1987                        .top(px(0.))
1988                        .h_full()
1989                        .w(SIDEBAR_RESIZE_HANDLE_SIZE)
1990                        .cursor_col_resize()
1991                        .on_drag(DraggedSidebar, |dragged, _, _, cx| {
1992                            cx.stop_propagation();
1993                            cx.new(|_| dragged.clone())
1994                        })
1995                        .on_mouse_down(MouseButton::Left, |_, _, cx| {
1996                            cx.stop_propagation();
1997                        })
1998                        .on_mouse_up(MouseButton::Left, move |event, _, cx| {
1999                            if event.click_count == 2 {
2000                                weak.update(cx, |this, cx| {
2001                                    if let Some(sidebar) = this.sidebar.as_mut() {
2002                                        sidebar.set_width(None, cx);
2003                                    }
2004                                    this.serialize(cx);
2005                                })
2006                                .ok();
2007                                cx.stop_propagation();
2008                            } else {
2009                                weak.update(cx, |this, cx| {
2010                                    this.serialize(cx);
2011                                })
2012                                .ok();
2013                            }
2014                        })
2015                        .occlude(),
2016                );
2017
2018                div()
2019                    .id("sidebar-container")
2020                    .relative()
2021                    .h_full()
2022                    .w(sidebar_width)
2023                    .flex_shrink_0()
2024                    .child(sidebar_handle.to_any())
2025                    .child(resize_handle)
2026                    .into_any_element()
2027            })
2028        } else {
2029            None
2030        };
2031
2032        let (left_sidebar, right_sidebar) = if sidebar_on_right {
2033            (None, sidebar)
2034        } else {
2035            (sidebar, None)
2036        };
2037
2038        let ui_font = theme_settings::setup_ui_font(window, cx);
2039        let text_color = cx.theme().colors().text;
2040
2041        let workspace = self.workspace().clone();
2042        let workspace_key_context = workspace.update(cx, |workspace, cx| workspace.key_context(cx));
2043        let root = workspace.update(cx, |workspace, cx| workspace.actions(h_flex(), window, cx));
2044
2045        client_side_decorations(
2046            root.key_context(workspace_key_context)
2047                .relative()
2048                .size_full()
2049                .font(ui_font)
2050                .text_color(text_color)
2051                .on_action(cx.listener(Self::close_window))
2052                .when(self.multi_workspace_enabled(cx), |this| {
2053                    this.on_action(cx.listener(
2054                        |this: &mut Self, _: &ToggleWorkspaceSidebar, window, cx| {
2055                            this.toggle_sidebar(window, cx);
2056                        },
2057                    ))
2058                    .on_action(cx.listener(
2059                        |this: &mut Self, _: &CloseWorkspaceSidebar, window, cx| {
2060                            this.close_sidebar_action(window, cx);
2061                        },
2062                    ))
2063                    .on_action(cx.listener(
2064                        |this: &mut Self, _: &FocusWorkspaceSidebar, window, cx| {
2065                            this.focus_sidebar(window, cx);
2066                        },
2067                    ))
2068                    .on_action(cx.listener(
2069                        |this: &mut Self, action: &ToggleThreadSwitcher, window, cx| {
2070                            if let Some(sidebar) = &this.sidebar {
2071                                sidebar.toggle_thread_switcher(action.select_last, window, cx);
2072                            }
2073                        },
2074                    ))
2075                    .on_action(cx.listener(|this: &mut Self, _: &NextProject, window, cx| {
2076                        if let Some(sidebar) = &this.sidebar {
2077                            sidebar.cycle_project(true, window, cx);
2078                        }
2079                    }))
2080                    .on_action(
2081                        cx.listener(|this: &mut Self, _: &PreviousProject, window, cx| {
2082                            if let Some(sidebar) = &this.sidebar {
2083                                sidebar.cycle_project(false, window, cx);
2084                            }
2085                        }),
2086                    )
2087                    .on_action(cx.listener(|this: &mut Self, _: &NextThread, window, cx| {
2088                        if let Some(sidebar) = &this.sidebar {
2089                            sidebar.cycle_thread(true, window, cx);
2090                        }
2091                    }))
2092                    .on_action(
2093                        cx.listener(|this: &mut Self, _: &PreviousThread, window, cx| {
2094                            if let Some(sidebar) = &this.sidebar {
2095                                sidebar.cycle_thread(false, window, cx);
2096                            }
2097                        }),
2098                    )
2099                    .when(self.project_group_keys().len() >= 2, |el| {
2100                        el.on_action(cx.listener(
2101                            |this: &mut Self, _: &MoveProjectToNewWindow, window, cx| {
2102                                let key =
2103                                    this.project_group_key_for_workspace(this.workspace(), cx);
2104                                this.open_project_group_in_new_window(&key, window, cx)
2105                                    .detach_and_log_err(cx);
2106                            },
2107                        ))
2108                    })
2109                })
2110                .when(
2111                    self.sidebar_open() && self.multi_workspace_enabled(cx),
2112                    |this| {
2113                        this.on_drag_move(cx.listener(
2114                            move |this: &mut Self,
2115                                  e: &DragMoveEvent<DraggedSidebar>,
2116                                  window,
2117                                  cx| {
2118                                if let Some(sidebar) = &this.sidebar {
2119                                    let new_width = if sidebar_on_right {
2120                                        window.bounds().size.width - e.event.position.x
2121                                    } else {
2122                                        e.event.position.x
2123                                    };
2124                                    sidebar.set_width(Some(new_width), cx);
2125                                }
2126                            },
2127                        ))
2128                    },
2129                )
2130                .children(left_sidebar)
2131                .child(
2132                    div()
2133                        .flex()
2134                        .flex_1()
2135                        .size_full()
2136                        .overflow_hidden()
2137                        .child(self.workspace().clone()),
2138                )
2139                .children(right_sidebar)
2140                .child(self.workspace().read(cx).modal_layer.clone())
2141                .children(self.sidebar_overlay.as_ref().map(|view| {
2142                    deferred(div().absolute().size_full().inset_0().occlude().child(
2143                        v_flex().h(px(0.0)).top_20().items_center().child(
2144                            h_flex().occlude().child(view.clone()).on_mouse_down(
2145                                MouseButton::Left,
2146                                |_, _, cx| {
2147                                    cx.stop_propagation();
2148                                },
2149                            ),
2150                        ),
2151                    ))
2152                    .with_priority(2)
2153                })),
2154            window,
2155            cx,
2156            Tiling {
2157                left: !sidebar_on_right && multi_workspace_enabled && self.sidebar_open(),
2158                right: sidebar_on_right && multi_workspace_enabled && self.sidebar_open(),
2159                ..Tiling::default()
2160            },
2161        )
2162    }
2163}