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