multi_workspace.rs

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