multi_workspace.rs

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