multi_workspace.rs

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