workspace.rs

   1pub mod dock;
   2pub mod history_manager;
   3pub mod item;
   4mod modal_layer;
   5pub mod notifications;
   6pub mod pane;
   7pub mod pane_group;
   8mod persistence;
   9pub mod searchable;
  10pub mod shared_screen;
  11mod status_bar;
  12pub mod tasks;
  13mod theme_preview;
  14mod toast_layer;
  15mod toolbar;
  16mod workspace_settings;
  17
  18pub use toast_layer::{RunAction, ToastAction, ToastLayer, ToastView};
  19
  20use anyhow::{Context as _, Result, anyhow};
  21use call::{ActiveCall, call_settings::CallSettings};
  22use client::{
  23    ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore,
  24    proto::{self, ErrorCode, PanelId, PeerId},
  25};
  26use collections::{HashMap, HashSet, hash_map};
  27pub use dock::Panel;
  28use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
  29use futures::{
  30    Future, FutureExt, StreamExt,
  31    channel::{
  32        mpsc::{self, UnboundedReceiver, UnboundedSender},
  33        oneshot,
  34    },
  35    future::try_join_all,
  36};
  37use gpui::{
  38    Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context,
  39    CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
  40    Focusable, Global, Hsla, KeyContext, Keystroke, ManagedView, MouseButton, PathPromptOptions,
  41    Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, Task, Tiling, WeakEntity,
  42    WindowBounds, WindowHandle, WindowId, WindowOptions, action_as, actions, canvas,
  43    impl_action_as, impl_actions, point, relative, size, transparent_black,
  44};
  45pub use history_manager::*;
  46pub use item::{
  47    FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
  48    ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
  49};
  50use itertools::Itertools;
  51use language::{Buffer, LanguageRegistry, Rope};
  52pub use modal_layer::*;
  53use node_runtime::NodeRuntime;
  54use notifications::{
  55    DetachAndPromptErr, Notifications, dismiss_app_notification,
  56    simple_message_notification::MessageNotification,
  57};
  58pub use pane::*;
  59pub use pane_group::*;
  60use persistence::{
  61    DB, SerializedWindowBounds,
  62    model::{SerializedSshProject, SerializedWorkspace},
  63};
  64pub use persistence::{
  65    DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items,
  66    model::{ItemId, LocalPaths, SerializedWorkspaceLocation},
  67};
  68use postage::stream::Stream;
  69use project::{
  70    DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
  71    debugger::breakpoint_store::BreakpointStoreEvent,
  72};
  73use remote::{SshClientDelegate, SshConnectionOptions, ssh_session::ConnectionIdentifier};
  74use schemars::JsonSchema;
  75use serde::Deserialize;
  76use session::AppSession;
  77use settings::Settings;
  78use shared_screen::SharedScreen;
  79use sqlez::{
  80    bindable::{Bind, Column, StaticColumnCount},
  81    statement::Statement,
  82};
  83use status_bar::StatusBar;
  84pub use status_bar::StatusItemView;
  85use std::{
  86    any::TypeId,
  87    borrow::Cow,
  88    cell::RefCell,
  89    cmp,
  90    collections::hash_map::DefaultHasher,
  91    env,
  92    hash::{Hash, Hasher},
  93    path::{Path, PathBuf},
  94    process::ExitStatus,
  95    rc::Rc,
  96    sync::{Arc, LazyLock, Weak, atomic::AtomicUsize},
  97    time::Duration,
  98};
  99use task::{DebugScenario, SpawnInTerminal, TaskContext};
 100use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
 101pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 102pub use ui;
 103use ui::prelude::*;
 104use util::{ResultExt, TryFutureExt, paths::SanitizedPath, serde::default_true};
 105use uuid::Uuid;
 106pub use workspace_settings::{
 107    AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
 108};
 109use zed_actions::feedback::FileBugReport;
 110
 111use crate::notifications::NotificationId;
 112use crate::persistence::{
 113    SerializedAxis,
 114    model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
 115};
 116
 117pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200);
 118
 119static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {
 120    env::var("ZED_WINDOW_SIZE")
 121        .ok()
 122        .as_deref()
 123        .and_then(parse_pixel_size_env_var)
 124});
 125
 126static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
 127    env::var("ZED_WINDOW_POSITION")
 128        .ok()
 129        .as_deref()
 130        .and_then(parse_pixel_position_env_var)
 131});
 132
 133pub trait TerminalProvider {
 134    fn spawn(
 135        &self,
 136        task: SpawnInTerminal,
 137        window: &mut Window,
 138        cx: &mut App,
 139    ) -> Task<Option<Result<ExitStatus>>>;
 140}
 141
 142pub trait DebuggerProvider {
 143    // `active_buffer` is used to resolve build task's name against language-specific tasks.
 144    fn start_session(
 145        &self,
 146        definition: DebugScenario,
 147        task_context: TaskContext,
 148        active_buffer: Option<Entity<Buffer>>,
 149        window: &mut Window,
 150        cx: &mut App,
 151    );
 152}
 153
 154actions!(
 155    workspace,
 156    [
 157        ActivateNextPane,
 158        ActivatePreviousPane,
 159        ActivateNextWindow,
 160        ActivatePreviousWindow,
 161        AddFolderToProject,
 162        ClearAllNotifications,
 163        CloseActiveDock,
 164        CloseAllDocks,
 165        CloseWindow,
 166        Feedback,
 167        FollowNextCollaborator,
 168        MoveFocusedPanelToNextPosition,
 169        NewCenterTerminal,
 170        NewFile,
 171        NewFileSplitVertical,
 172        NewFileSplitHorizontal,
 173        NewSearch,
 174        NewTerminal,
 175        NewWindow,
 176        Open,
 177        OpenFiles,
 178        OpenInTerminal,
 179        OpenComponentPreview,
 180        ReloadActiveItem,
 181        SaveAs,
 182        SaveWithoutFormat,
 183        ShutdownDebugAdapters,
 184        SuppressNotification,
 185        ToggleBottomDock,
 186        ToggleCenteredLayout,
 187        ToggleLeftDock,
 188        ToggleRightDock,
 189        ToggleZoom,
 190        Unfollow,
 191        Welcome,
 192        RestoreBanner,
 193    ]
 194);
 195
 196#[derive(Clone, PartialEq)]
 197pub struct OpenPaths {
 198    pub paths: Vec<PathBuf>,
 199}
 200
 201#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 202pub struct ActivatePane(pub usize);
 203
 204#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 205#[serde(deny_unknown_fields)]
 206pub struct MoveItemToPane {
 207    pub destination: usize,
 208    #[serde(default = "default_true")]
 209    pub focus: bool,
 210}
 211
 212#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 213#[serde(deny_unknown_fields)]
 214pub struct MoveItemToPaneInDirection {
 215    pub direction: SplitDirection,
 216    #[serde(default = "default_true")]
 217    pub focus: bool,
 218}
 219
 220#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
 221#[serde(deny_unknown_fields)]
 222pub struct SaveAll {
 223    pub save_intent: Option<SaveIntent>,
 224}
 225
 226#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
 227#[serde(deny_unknown_fields)]
 228pub struct Save {
 229    pub save_intent: Option<SaveIntent>,
 230}
 231
 232#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema)]
 233#[serde(deny_unknown_fields)]
 234pub struct CloseAllItemsAndPanes {
 235    pub save_intent: Option<SaveIntent>,
 236}
 237
 238#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema)]
 239#[serde(deny_unknown_fields)]
 240pub struct CloseInactiveTabsAndPanes {
 241    pub save_intent: Option<SaveIntent>,
 242}
 243
 244#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 245pub struct SendKeystrokes(pub String);
 246
 247#[derive(Clone, Deserialize, PartialEq, Default, JsonSchema)]
 248#[serde(deny_unknown_fields)]
 249pub struct Reload {
 250    pub binary_path: Option<PathBuf>,
 251}
 252
 253action_as!(project_symbols, ToggleProjectSymbols as Toggle);
 254
 255#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema)]
 256pub struct ToggleFileFinder {
 257    #[serde(default)]
 258    pub separate_history: bool,
 259}
 260
 261impl_action_as!(file_finder, ToggleFileFinder as Toggle);
 262
 263impl_actions!(
 264    workspace,
 265    [
 266        ActivatePane,
 267        CloseAllItemsAndPanes,
 268        CloseInactiveTabsAndPanes,
 269        MoveItemToPane,
 270        MoveItemToPaneInDirection,
 271        OpenTerminal,
 272        Reload,
 273        Save,
 274        SaveAll,
 275        SendKeystrokes,
 276    ]
 277);
 278
 279actions!(
 280    workspace,
 281    [
 282        ActivatePaneLeft,
 283        ActivatePaneRight,
 284        ActivatePaneUp,
 285        ActivatePaneDown,
 286        SwapPaneLeft,
 287        SwapPaneRight,
 288        SwapPaneUp,
 289        SwapPaneDown,
 290    ]
 291);
 292
 293#[derive(PartialEq, Eq, Debug)]
 294pub enum CloseIntent {
 295    /// Quit the program entirely.
 296    Quit,
 297    /// Close a window.
 298    CloseWindow,
 299    /// Replace the workspace in an existing window.
 300    ReplaceWindow,
 301}
 302
 303#[derive(Clone)]
 304pub struct Toast {
 305    id: NotificationId,
 306    msg: Cow<'static, str>,
 307    autohide: bool,
 308    on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut Window, &mut App)>)>,
 309}
 310
 311impl Toast {
 312    pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
 313        Toast {
 314            id,
 315            msg: msg.into(),
 316            on_click: None,
 317            autohide: false,
 318        }
 319    }
 320
 321    pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
 322    where
 323        M: Into<Cow<'static, str>>,
 324        F: Fn(&mut Window, &mut App) + 'static,
 325    {
 326        self.on_click = Some((message.into(), Arc::new(on_click)));
 327        self
 328    }
 329
 330    pub fn autohide(mut self) -> Self {
 331        self.autohide = true;
 332        self
 333    }
 334}
 335
 336impl PartialEq for Toast {
 337    fn eq(&self, other: &Self) -> bool {
 338        self.id == other.id
 339            && self.msg == other.msg
 340            && self.on_click.is_some() == other.on_click.is_some()
 341    }
 342}
 343
 344#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema)]
 345#[serde(deny_unknown_fields)]
 346pub struct OpenTerminal {
 347    pub working_directory: PathBuf,
 348}
 349
 350#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
 351pub struct WorkspaceId(i64);
 352
 353impl StaticColumnCount for WorkspaceId {}
 354impl Bind for WorkspaceId {
 355    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
 356        self.0.bind(statement, start_index)
 357    }
 358}
 359impl Column for WorkspaceId {
 360    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 361        i64::column(statement, start_index)
 362            .map(|(i, next_index)| (Self(i), next_index))
 363            .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
 364    }
 365}
 366impl From<WorkspaceId> for i64 {
 367    fn from(val: WorkspaceId) -> Self {
 368        val.0
 369    }
 370}
 371
 372pub fn init_settings(cx: &mut App) {
 373    WorkspaceSettings::register(cx);
 374    ItemSettings::register(cx);
 375    PreviewTabsSettings::register(cx);
 376    TabBarSettings::register(cx);
 377}
 378
 379fn prompt_and_open_paths(app_state: Arc<AppState>, options: PathPromptOptions, cx: &mut App) {
 380    let paths = cx.prompt_for_paths(options);
 381    cx.spawn(
 382        async move |cx| match paths.await.anyhow().and_then(|res| res) {
 383            Ok(Some(paths)) => {
 384                cx.update(|cx| {
 385                    open_paths(&paths, app_state, OpenOptions::default(), cx).detach_and_log_err(cx)
 386                })
 387                .ok();
 388            }
 389            Ok(None) => {}
 390            Err(err) => {
 391                util::log_err(&err);
 392                cx.update(|cx| {
 393                    if let Some(workspace_window) = cx
 394                        .active_window()
 395                        .and_then(|window| window.downcast::<Workspace>())
 396                    {
 397                        workspace_window
 398                            .update(cx, |workspace, _, cx| {
 399                                workspace.show_portal_error(err.to_string(), cx);
 400                            })
 401                            .ok();
 402                    }
 403                })
 404                .ok();
 405            }
 406        },
 407    )
 408    .detach();
 409}
 410
 411pub fn init(app_state: Arc<AppState>, cx: &mut App) {
 412    init_settings(cx);
 413    component::init();
 414    theme_preview::init(cx);
 415    toast_layer::init(cx);
 416    history_manager::init(cx);
 417
 418    cx.on_action(Workspace::close_global);
 419    cx.on_action(reload);
 420
 421    cx.on_action({
 422        let app_state = Arc::downgrade(&app_state);
 423        move |_: &Open, cx: &mut App| {
 424            if let Some(app_state) = app_state.upgrade() {
 425                prompt_and_open_paths(
 426                    app_state,
 427                    PathPromptOptions {
 428                        files: true,
 429                        directories: true,
 430                        multiple: true,
 431                    },
 432                    cx,
 433                );
 434            }
 435        }
 436    });
 437    cx.on_action({
 438        let app_state = Arc::downgrade(&app_state);
 439        move |_: &OpenFiles, cx: &mut App| {
 440            let directories = cx.can_select_mixed_files_and_dirs();
 441            if let Some(app_state) = app_state.upgrade() {
 442                prompt_and_open_paths(
 443                    app_state,
 444                    PathPromptOptions {
 445                        files: true,
 446                        directories,
 447                        multiple: true,
 448                    },
 449                    cx,
 450                );
 451            }
 452        }
 453    });
 454}
 455
 456type BuildProjectItemFn =
 457    fn(AnyEntity, Entity<Project>, Option<&Pane>, &mut Window, &mut App) -> Box<dyn ItemHandle>;
 458
 459type BuildProjectItemForPathFn =
 460    fn(
 461        &Entity<Project>,
 462        &ProjectPath,
 463        &mut Window,
 464        &mut App,
 465    ) -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
 466
 467#[derive(Clone, Default)]
 468struct ProjectItemRegistry {
 469    build_project_item_fns_by_type: HashMap<TypeId, BuildProjectItemFn>,
 470    build_project_item_for_path_fns: Vec<BuildProjectItemForPathFn>,
 471}
 472
 473impl ProjectItemRegistry {
 474    fn register<T: ProjectItem>(&mut self) {
 475        self.build_project_item_fns_by_type.insert(
 476            TypeId::of::<T::Item>(),
 477            |item, project, pane, window, cx| {
 478                let item = item.downcast().unwrap();
 479                Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx)))
 480                    as Box<dyn ItemHandle>
 481            },
 482        );
 483        self.build_project_item_for_path_fns
 484            .push(|project, project_path, window, cx| {
 485                let project_item =
 486                    <T::Item as project::ProjectItem>::try_open(project, project_path, cx)?;
 487                let project = project.clone();
 488                Some(window.spawn(cx, async move |cx| {
 489                    let project_item = project_item.await?;
 490                    let project_entry_id: Option<ProjectEntryId> =
 491                        project_item.read_with(cx, project::ProjectItem::entry_id)?;
 492                    let build_workspace_item = Box::new(
 493                        |pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
 494                            Box::new(cx.new(|cx| {
 495                                T::for_project_item(project, Some(pane), project_item, window, cx)
 496                            })) as Box<dyn ItemHandle>
 497                        },
 498                    ) as Box<_>;
 499                    Ok((project_entry_id, build_workspace_item))
 500                }))
 501            });
 502    }
 503
 504    fn open_path(
 505        &self,
 506        project: &Entity<Project>,
 507        path: &ProjectPath,
 508        window: &mut Window,
 509        cx: &mut App,
 510    ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
 511        let Some(open_project_item) = self
 512            .build_project_item_for_path_fns
 513            .iter()
 514            .rev()
 515            .find_map(|open_project_item| open_project_item(&project, &path, window, cx))
 516        else {
 517            return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
 518        };
 519        open_project_item
 520    }
 521
 522    fn build_item<T: project::ProjectItem>(
 523        &self,
 524        item: Entity<T>,
 525        project: Entity<Project>,
 526        pane: Option<&Pane>,
 527        window: &mut Window,
 528        cx: &mut App,
 529    ) -> Option<Box<dyn ItemHandle>> {
 530        let build = self
 531            .build_project_item_fns_by_type
 532            .get(&TypeId::of::<T>())?;
 533        Some(build(item.into_any(), project, pane, window, cx))
 534    }
 535}
 536
 537type WorkspaceItemBuilder =
 538    Box<dyn FnOnce(&mut Pane, &mut Window, &mut Context<Pane>) -> Box<dyn ItemHandle>>;
 539
 540impl Global for ProjectItemRegistry {}
 541
 542/// Registers a [ProjectItem] for the app. When opening a file, all the registered
 543/// items will get a chance to open the file, starting from the project item that
 544/// was added last.
 545pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
 546    cx.default_global::<ProjectItemRegistry>().register::<I>();
 547}
 548
 549#[derive(Default)]
 550pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
 551
 552struct FollowableViewDescriptor {
 553    from_state_proto: fn(
 554        Entity<Workspace>,
 555        ViewId,
 556        &mut Option<proto::view::Variant>,
 557        &mut Window,
 558        &mut App,
 559    ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
 560    to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
 561}
 562
 563impl Global for FollowableViewRegistry {}
 564
 565impl FollowableViewRegistry {
 566    pub fn register<I: FollowableItem>(cx: &mut App) {
 567        cx.default_global::<Self>().0.insert(
 568            TypeId::of::<I>(),
 569            FollowableViewDescriptor {
 570                from_state_proto: |workspace, id, state, window, cx| {
 571                    I::from_state_proto(workspace, id, state, window, cx).map(|task| {
 572                        cx.foreground_executor()
 573                            .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
 574                    })
 575                },
 576                to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
 577            },
 578        );
 579    }
 580
 581    pub fn from_state_proto(
 582        workspace: Entity<Workspace>,
 583        view_id: ViewId,
 584        mut state: Option<proto::view::Variant>,
 585        window: &mut Window,
 586        cx: &mut App,
 587    ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
 588        cx.update_default_global(|this: &mut Self, cx| {
 589            this.0.values().find_map(|descriptor| {
 590                (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, window, cx)
 591            })
 592        })
 593    }
 594
 595    pub fn to_followable_view(
 596        view: impl Into<AnyView>,
 597        cx: &App,
 598    ) -> Option<Box<dyn FollowableItemHandle>> {
 599        let this = cx.try_global::<Self>()?;
 600        let view = view.into();
 601        let descriptor = this.0.get(&view.entity_type())?;
 602        Some((descriptor.to_followable_view)(&view))
 603    }
 604}
 605
 606#[derive(Copy, Clone)]
 607struct SerializableItemDescriptor {
 608    deserialize: fn(
 609        Entity<Project>,
 610        WeakEntity<Workspace>,
 611        WorkspaceId,
 612        ItemId,
 613        &mut Window,
 614        &mut Context<Pane>,
 615    ) -> Task<Result<Box<dyn ItemHandle>>>,
 616    cleanup: fn(WorkspaceId, Vec<ItemId>, &mut Window, &mut App) -> Task<Result<()>>,
 617    view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
 618}
 619
 620#[derive(Default)]
 621struct SerializableItemRegistry {
 622    descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
 623    descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
 624}
 625
 626impl Global for SerializableItemRegistry {}
 627
 628impl SerializableItemRegistry {
 629    fn deserialize(
 630        item_kind: &str,
 631        project: Entity<Project>,
 632        workspace: WeakEntity<Workspace>,
 633        workspace_id: WorkspaceId,
 634        item_item: ItemId,
 635        window: &mut Window,
 636        cx: &mut Context<Pane>,
 637    ) -> Task<Result<Box<dyn ItemHandle>>> {
 638        let Some(descriptor) = Self::descriptor(item_kind, cx) else {
 639            return Task::ready(Err(anyhow!(
 640                "cannot deserialize {}, descriptor not found",
 641                item_kind
 642            )));
 643        };
 644
 645        (descriptor.deserialize)(project, workspace, workspace_id, item_item, window, cx)
 646    }
 647
 648    fn cleanup(
 649        item_kind: &str,
 650        workspace_id: WorkspaceId,
 651        loaded_items: Vec<ItemId>,
 652        window: &mut Window,
 653        cx: &mut App,
 654    ) -> Task<Result<()>> {
 655        let Some(descriptor) = Self::descriptor(item_kind, cx) else {
 656            return Task::ready(Err(anyhow!(
 657                "cannot cleanup {}, descriptor not found",
 658                item_kind
 659            )));
 660        };
 661
 662        (descriptor.cleanup)(workspace_id, loaded_items, window, cx)
 663    }
 664
 665    fn view_to_serializable_item_handle(
 666        view: AnyView,
 667        cx: &App,
 668    ) -> Option<Box<dyn SerializableItemHandle>> {
 669        let this = cx.try_global::<Self>()?;
 670        let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
 671        Some((descriptor.view_to_serializable_item)(view))
 672    }
 673
 674    fn descriptor(item_kind: &str, cx: &App) -> Option<SerializableItemDescriptor> {
 675        let this = cx.try_global::<Self>()?;
 676        this.descriptors_by_kind.get(item_kind).copied()
 677    }
 678}
 679
 680pub fn register_serializable_item<I: SerializableItem>(cx: &mut App) {
 681    let serialized_item_kind = I::serialized_item_kind();
 682
 683    let registry = cx.default_global::<SerializableItemRegistry>();
 684    let descriptor = SerializableItemDescriptor {
 685        deserialize: |project, workspace, workspace_id, item_id, window, cx| {
 686            let task = I::deserialize(project, workspace, workspace_id, item_id, window, cx);
 687            cx.foreground_executor()
 688                .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
 689        },
 690        cleanup: |workspace_id, loaded_items, window, cx| {
 691            I::cleanup(workspace_id, loaded_items, window, cx)
 692        },
 693        view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
 694    };
 695    registry
 696        .descriptors_by_kind
 697        .insert(Arc::from(serialized_item_kind), descriptor);
 698    registry
 699        .descriptors_by_type
 700        .insert(TypeId::of::<I>(), descriptor);
 701}
 702
 703pub struct AppState {
 704    pub languages: Arc<LanguageRegistry>,
 705    pub client: Arc<Client>,
 706    pub user_store: Entity<UserStore>,
 707    pub workspace_store: Entity<WorkspaceStore>,
 708    pub fs: Arc<dyn fs::Fs>,
 709    pub build_window_options: fn(Option<Uuid>, &mut App) -> WindowOptions,
 710    pub node_runtime: NodeRuntime,
 711    pub session: Entity<AppSession>,
 712}
 713
 714struct GlobalAppState(Weak<AppState>);
 715
 716impl Global for GlobalAppState {}
 717
 718pub struct WorkspaceStore {
 719    workspaces: HashSet<WindowHandle<Workspace>>,
 720    client: Arc<Client>,
 721    _subscriptions: Vec<client::Subscription>,
 722}
 723
 724#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
 725pub enum CollaboratorId {
 726    PeerId(PeerId),
 727    Agent,
 728}
 729
 730impl From<PeerId> for CollaboratorId {
 731    fn from(peer_id: PeerId) -> Self {
 732        CollaboratorId::PeerId(peer_id)
 733    }
 734}
 735
 736impl From<&PeerId> for CollaboratorId {
 737    fn from(peer_id: &PeerId) -> Self {
 738        CollaboratorId::PeerId(*peer_id)
 739    }
 740}
 741
 742#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
 743struct Follower {
 744    project_id: Option<u64>,
 745    peer_id: PeerId,
 746}
 747
 748impl AppState {
 749    #[track_caller]
 750    pub fn global(cx: &App) -> Weak<Self> {
 751        cx.global::<GlobalAppState>().0.clone()
 752    }
 753    pub fn try_global(cx: &App) -> Option<Weak<Self>> {
 754        cx.try_global::<GlobalAppState>()
 755            .map(|state| state.0.clone())
 756    }
 757    pub fn set_global(state: Weak<AppState>, cx: &mut App) {
 758        cx.set_global(GlobalAppState(state));
 759    }
 760
 761    #[cfg(any(test, feature = "test-support"))]
 762    pub fn test(cx: &mut App) -> Arc<Self> {
 763        use node_runtime::NodeRuntime;
 764        use session::Session;
 765        use settings::SettingsStore;
 766
 767        if !cx.has_global::<SettingsStore>() {
 768            let settings_store = SettingsStore::test(cx);
 769            cx.set_global(settings_store);
 770        }
 771
 772        let fs = fs::FakeFs::new(cx.background_executor().clone());
 773        let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
 774        let clock = Arc::new(clock::FakeSystemClock::new());
 775        let http_client = http_client::FakeHttpClient::with_404_response();
 776        let client = Client::new(clock, http_client.clone(), cx);
 777        let session = cx.new(|cx| AppSession::new(Session::test(), cx));
 778        let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
 779        let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
 780
 781        theme::init(theme::LoadThemes::JustBase, cx);
 782        client::init(&client, cx);
 783        crate::init_settings(cx);
 784
 785        Arc::new(Self {
 786            client,
 787            fs,
 788            languages,
 789            user_store,
 790            workspace_store,
 791            node_runtime: NodeRuntime::unavailable(),
 792            build_window_options: |_, _| Default::default(),
 793            session,
 794        })
 795    }
 796}
 797
 798struct DelayedDebouncedEditAction {
 799    task: Option<Task<()>>,
 800    cancel_channel: Option<oneshot::Sender<()>>,
 801}
 802
 803impl DelayedDebouncedEditAction {
 804    fn new() -> DelayedDebouncedEditAction {
 805        DelayedDebouncedEditAction {
 806            task: None,
 807            cancel_channel: None,
 808        }
 809    }
 810
 811    fn fire_new<F>(
 812        &mut self,
 813        delay: Duration,
 814        window: &mut Window,
 815        cx: &mut Context<Workspace>,
 816        func: F,
 817    ) where
 818        F: 'static
 819            + Send
 820            + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> Task<Result<()>>,
 821    {
 822        if let Some(channel) = self.cancel_channel.take() {
 823            _ = channel.send(());
 824        }
 825
 826        let (sender, mut receiver) = oneshot::channel::<()>();
 827        self.cancel_channel = Some(sender);
 828
 829        let previous_task = self.task.take();
 830        self.task = Some(cx.spawn_in(window, async move |workspace, cx| {
 831            let mut timer = cx.background_executor().timer(delay).fuse();
 832            if let Some(previous_task) = previous_task {
 833                previous_task.await;
 834            }
 835
 836            futures::select_biased! {
 837                _ = receiver => return,
 838                    _ = timer => {}
 839            }
 840
 841            if let Some(result) = workspace
 842                .update_in(cx, |workspace, window, cx| (func)(workspace, window, cx))
 843                .log_err()
 844            {
 845                result.await.log_err();
 846            }
 847        }));
 848    }
 849}
 850
 851pub enum Event {
 852    PaneAdded(Entity<Pane>),
 853    PaneRemoved,
 854    ItemAdded {
 855        item: Box<dyn ItemHandle>,
 856    },
 857    ItemRemoved,
 858    ActiveItemChanged,
 859    UserSavedItem {
 860        pane: WeakEntity<Pane>,
 861        item: Box<dyn WeakItemHandle>,
 862        save_intent: SaveIntent,
 863    },
 864    ContactRequestedJoin(u64),
 865    WorkspaceCreated(WeakEntity<Workspace>),
 866    OpenBundledFile {
 867        text: Cow<'static, str>,
 868        title: &'static str,
 869        language: &'static str,
 870    },
 871    ZoomChanged,
 872    ModalOpened,
 873    ClearActivityIndicator,
 874}
 875
 876#[derive(Debug)]
 877pub enum OpenVisible {
 878    All,
 879    None,
 880    OnlyFiles,
 881    OnlyDirectories,
 882}
 883
 884type PromptForNewPath = Box<
 885    dyn Fn(
 886        &mut Workspace,
 887        &mut Window,
 888        &mut Context<Workspace>,
 889    ) -> oneshot::Receiver<Option<ProjectPath>>,
 890>;
 891
 892type PromptForOpenPath = Box<
 893    dyn Fn(
 894        &mut Workspace,
 895        DirectoryLister,
 896        &mut Window,
 897        &mut Context<Workspace>,
 898    ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
 899>;
 900
 901/// Collects everything project-related for a certain window opened.
 902/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
 903///
 904/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
 905/// The `Workspace` owns everybody's state and serves as a default, "global context",
 906/// that can be used to register a global action to be triggered from any place in the window.
 907pub struct Workspace {
 908    weak_self: WeakEntity<Self>,
 909    workspace_actions: Vec<Box<dyn Fn(Div, &mut Window, &mut Context<Self>) -> Div>>,
 910    zoomed: Option<AnyWeakView>,
 911    previous_dock_drag_coordinates: Option<Point<Pixels>>,
 912    zoomed_position: Option<DockPosition>,
 913    center: PaneGroup,
 914    left_dock: Entity<Dock>,
 915    bottom_dock: Entity<Dock>,
 916    bottom_dock_layout: BottomDockLayout,
 917    right_dock: Entity<Dock>,
 918    panes: Vec<Entity<Pane>>,
 919    panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
 920    active_pane: Entity<Pane>,
 921    last_active_center_pane: Option<WeakEntity<Pane>>,
 922    last_active_view_id: Option<proto::ViewId>,
 923    status_bar: Entity<StatusBar>,
 924    modal_layer: Entity<ModalLayer>,
 925    toast_layer: Entity<ToastLayer>,
 926    titlebar_item: Option<AnyView>,
 927    notifications: Notifications,
 928    suppressed_notifications: HashSet<NotificationId>,
 929    project: Entity<Project>,
 930    follower_states: HashMap<CollaboratorId, FollowerState>,
 931    last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
 932    window_edited: bool,
 933    dirty_items: HashMap<EntityId, Subscription>,
 934    active_call: Option<(Entity<ActiveCall>, Vec<Subscription>)>,
 935    leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
 936    database_id: Option<WorkspaceId>,
 937    app_state: Arc<AppState>,
 938    dispatching_keystrokes: Rc<RefCell<(HashSet<String>, Vec<Keystroke>)>>,
 939    _subscriptions: Vec<Subscription>,
 940    _apply_leader_updates: Task<Result<()>>,
 941    _observe_current_user: Task<Result<()>>,
 942    _schedule_serialize: Option<Task<()>>,
 943    pane_history_timestamp: Arc<AtomicUsize>,
 944    bounds: Bounds<Pixels>,
 945    pub centered_layout: bool,
 946    bounds_save_task_queued: Option<Task<()>>,
 947    on_prompt_for_new_path: Option<PromptForNewPath>,
 948    on_prompt_for_open_path: Option<PromptForOpenPath>,
 949    terminal_provider: Option<Box<dyn TerminalProvider>>,
 950    debugger_provider: Option<Box<dyn DebuggerProvider>>,
 951    serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
 952    serialized_ssh_project: Option<SerializedSshProject>,
 953    _items_serializer: Task<Result<()>>,
 954    session_id: Option<String>,
 955}
 956
 957impl EventEmitter<Event> for Workspace {}
 958
 959#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 960pub struct ViewId {
 961    pub creator: CollaboratorId,
 962    pub id: u64,
 963}
 964
 965pub struct FollowerState {
 966    center_pane: Entity<Pane>,
 967    dock_pane: Option<Entity<Pane>>,
 968    active_view_id: Option<ViewId>,
 969    items_by_leader_view_id: HashMap<ViewId, FollowerView>,
 970}
 971
 972struct FollowerView {
 973    view: Box<dyn FollowableItemHandle>,
 974    location: Option<proto::PanelId>,
 975}
 976
 977impl Workspace {
 978    const DEFAULT_PADDING: f32 = 0.2;
 979    const MAX_PADDING: f32 = 0.4;
 980
 981    pub fn new(
 982        workspace_id: Option<WorkspaceId>,
 983        project: Entity<Project>,
 984        app_state: Arc<AppState>,
 985        window: &mut Window,
 986        cx: &mut Context<Self>,
 987    ) -> Self {
 988        cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
 989            match event {
 990                project::Event::RemoteIdChanged(_) => {
 991                    this.update_window_title(window, cx);
 992                }
 993
 994                project::Event::CollaboratorLeft(peer_id) => {
 995                    this.collaborator_left(*peer_id, window, cx);
 996                }
 997
 998                project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
 999                    this.update_window_title(window, cx);
1000                    this.serialize_workspace(window, cx);
1001                    // This event could be triggered by `AddFolderToProject` or `RemoveFromProject`.
1002                    // So we need to update the history.
1003                    this.update_history(cx);
1004                }
1005
1006                project::Event::DisconnectedFromHost => {
1007                    this.update_window_edited(window, cx);
1008                    let leaders_to_unfollow =
1009                        this.follower_states.keys().copied().collect::<Vec<_>>();
1010                    for leader_id in leaders_to_unfollow {
1011                        this.unfollow(leader_id, window, cx);
1012                    }
1013                }
1014
1015                project::Event::DisconnectedFromSshRemote => {
1016                    this.update_window_edited(window, cx);
1017                }
1018
1019                project::Event::Closed => {
1020                    window.remove_window();
1021                }
1022
1023                project::Event::DeletedEntry(_, entry_id) => {
1024                    for pane in this.panes.iter() {
1025                        pane.update(cx, |pane, cx| {
1026                            pane.handle_deleted_project_item(*entry_id, window, cx)
1027                        });
1028                    }
1029                }
1030
1031                project::Event::Toast {
1032                    notification_id,
1033                    message,
1034                } => this.show_notification(
1035                    NotificationId::named(notification_id.clone()),
1036                    cx,
1037                    |cx| cx.new(|cx| MessageNotification::new(message.clone(), cx)),
1038                ),
1039
1040                project::Event::HideToast { notification_id } => {
1041                    this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
1042                }
1043
1044                project::Event::LanguageServerPrompt(request) => {
1045                    struct LanguageServerPrompt;
1046
1047                    let mut hasher = DefaultHasher::new();
1048                    request.lsp_name.as_str().hash(&mut hasher);
1049                    let id = hasher.finish();
1050
1051                    this.show_notification(
1052                        NotificationId::composite::<LanguageServerPrompt>(id as usize),
1053                        cx,
1054                        |cx| {
1055                            cx.new(|cx| {
1056                                notifications::LanguageServerPrompt::new(request.clone(), cx)
1057                            })
1058                        },
1059                    );
1060                }
1061
1062                project::Event::AgentLocationChanged => {
1063                    this.handle_agent_location_changed(window, cx)
1064                }
1065
1066                _ => {}
1067            }
1068            cx.notify()
1069        })
1070        .detach();
1071
1072        cx.subscribe_in(
1073            &project.read(cx).breakpoint_store(),
1074            window,
1075            |workspace, _, event, window, cx| match event {
1076                BreakpointStoreEvent::BreakpointsUpdated(_, _)
1077                | BreakpointStoreEvent::BreakpointsCleared(_) => {
1078                    workspace.serialize_workspace(window, cx);
1079                }
1080                BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {}
1081            },
1082        )
1083        .detach();
1084
1085        cx.on_focus_lost(window, |this, window, cx| {
1086            let focus_handle = this.focus_handle(cx);
1087            window.focus(&focus_handle);
1088        })
1089        .detach();
1090
1091        let weak_handle = cx.entity().downgrade();
1092        let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
1093
1094        let center_pane = cx.new(|cx| {
1095            let mut center_pane = Pane::new(
1096                weak_handle.clone(),
1097                project.clone(),
1098                pane_history_timestamp.clone(),
1099                None,
1100                NewFile.boxed_clone(),
1101                window,
1102                cx,
1103            );
1104            center_pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
1105            center_pane
1106        });
1107        cx.subscribe_in(&center_pane, window, Self::handle_pane_event)
1108            .detach();
1109
1110        window.focus(&center_pane.focus_handle(cx));
1111
1112        cx.emit(Event::PaneAdded(center_pane.clone()));
1113
1114        let window_handle = window.window_handle().downcast::<Workspace>().unwrap();
1115        app_state.workspace_store.update(cx, |store, _| {
1116            store.workspaces.insert(window_handle);
1117        });
1118
1119        let mut current_user = app_state.user_store.read(cx).watch_current_user();
1120        let mut connection_status = app_state.client.status();
1121        let _observe_current_user = cx.spawn_in(window, async move |this, cx| {
1122            current_user.next().await;
1123            connection_status.next().await;
1124            let mut stream =
1125                Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
1126
1127            while stream.recv().await.is_some() {
1128                this.update(cx, |_, cx| cx.notify())?;
1129            }
1130            anyhow::Ok(())
1131        });
1132
1133        // All leader updates are enqueued and then processed in a single task, so
1134        // that each asynchronous operation can be run in order.
1135        let (leader_updates_tx, mut leader_updates_rx) =
1136            mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
1137        let _apply_leader_updates = cx.spawn_in(window, async move |this, cx| {
1138            while let Some((leader_id, update)) = leader_updates_rx.next().await {
1139                Self::process_leader_update(&this, leader_id, update, cx)
1140                    .await
1141                    .log_err();
1142            }
1143
1144            Ok(())
1145        });
1146
1147        cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
1148        let modal_layer = cx.new(|_| ModalLayer::new());
1149        let toast_layer = cx.new(|_| ToastLayer::new());
1150        cx.subscribe(
1151            &modal_layer,
1152            |_, _, _: &modal_layer::ModalOpenedEvent, cx| {
1153                cx.emit(Event::ModalOpened);
1154            },
1155        )
1156        .detach();
1157
1158        let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout;
1159        let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx);
1160        let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx);
1161        let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx);
1162        let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx));
1163        let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx));
1164        let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx));
1165        let status_bar = cx.new(|cx| {
1166            let mut status_bar = StatusBar::new(&center_pane.clone(), window, cx);
1167            status_bar.add_left_item(left_dock_buttons, window, cx);
1168            status_bar.add_right_item(right_dock_buttons, window, cx);
1169            status_bar.add_right_item(bottom_dock_buttons, window, cx);
1170            status_bar
1171        });
1172
1173        let session_id = app_state.session.read(cx).id().to_owned();
1174
1175        let mut active_call = None;
1176        if let Some(call) = ActiveCall::try_global(cx) {
1177            let call = call.clone();
1178            let subscriptions = vec![cx.subscribe_in(&call, window, Self::on_active_call_event)];
1179            active_call = Some((call, subscriptions));
1180        }
1181
1182        let (serializable_items_tx, serializable_items_rx) =
1183            mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
1184        let _items_serializer = cx.spawn_in(window, async move |this, cx| {
1185            Self::serialize_items(&this, serializable_items_rx, cx).await
1186        });
1187
1188        let subscriptions = vec![
1189            cx.observe_window_activation(window, Self::on_window_activation_changed),
1190            cx.observe_window_bounds(window, move |this, window, cx| {
1191                if this.bounds_save_task_queued.is_some() {
1192                    return;
1193                }
1194                this.bounds_save_task_queued = Some(cx.spawn_in(window, async move |this, cx| {
1195                    cx.background_executor()
1196                        .timer(Duration::from_millis(100))
1197                        .await;
1198                    this.update_in(cx, |this, window, cx| {
1199                        if let Some(display) = window.display(cx) {
1200                            if let Ok(display_uuid) = display.uuid() {
1201                                let window_bounds = window.inner_window_bounds();
1202                                if let Some(database_id) = workspace_id {
1203                                    cx.background_executor()
1204                                        .spawn(DB.set_window_open_status(
1205                                            database_id,
1206                                            SerializedWindowBounds(window_bounds),
1207                                            display_uuid,
1208                                        ))
1209                                        .detach_and_log_err(cx);
1210                                }
1211                            }
1212                        }
1213                        this.bounds_save_task_queued.take();
1214                    })
1215                    .ok();
1216                }));
1217                cx.notify();
1218            }),
1219            cx.observe_window_appearance(window, |_, window, cx| {
1220                let window_appearance = window.appearance();
1221
1222                *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
1223
1224                ThemeSettings::reload_current_theme(cx);
1225                ThemeSettings::reload_current_icon_theme(cx);
1226            }),
1227            cx.on_release(move |this, cx| {
1228                this.app_state.workspace_store.update(cx, move |store, _| {
1229                    store.workspaces.remove(&window_handle.clone());
1230                })
1231            }),
1232        ];
1233
1234        cx.defer_in(window, |this, window, cx| {
1235            this.update_window_title(window, cx);
1236            this.show_initial_notifications(cx);
1237        });
1238        Workspace {
1239            weak_self: weak_handle.clone(),
1240            zoomed: None,
1241            zoomed_position: None,
1242            previous_dock_drag_coordinates: None,
1243            center: PaneGroup::new(center_pane.clone()),
1244            panes: vec![center_pane.clone()],
1245            panes_by_item: Default::default(),
1246            active_pane: center_pane.clone(),
1247            last_active_center_pane: Some(center_pane.downgrade()),
1248            last_active_view_id: None,
1249            status_bar,
1250            modal_layer,
1251            toast_layer,
1252            titlebar_item: None,
1253            notifications: Notifications::default(),
1254            suppressed_notifications: HashSet::default(),
1255            left_dock,
1256            bottom_dock,
1257            bottom_dock_layout,
1258            right_dock,
1259            project: project.clone(),
1260            follower_states: Default::default(),
1261            last_leaders_by_pane: Default::default(),
1262            dispatching_keystrokes: Default::default(),
1263            window_edited: false,
1264            dirty_items: Default::default(),
1265            active_call,
1266            database_id: workspace_id,
1267            app_state,
1268            _observe_current_user,
1269            _apply_leader_updates,
1270            _schedule_serialize: None,
1271            leader_updates_tx,
1272            _subscriptions: subscriptions,
1273            pane_history_timestamp,
1274            workspace_actions: Default::default(),
1275            // This data will be incorrect, but it will be overwritten by the time it needs to be used.
1276            bounds: Default::default(),
1277            centered_layout: false,
1278            bounds_save_task_queued: None,
1279            on_prompt_for_new_path: None,
1280            on_prompt_for_open_path: None,
1281            terminal_provider: None,
1282            debugger_provider: None,
1283            serializable_items_tx,
1284            _items_serializer,
1285            session_id: Some(session_id),
1286            serialized_ssh_project: None,
1287        }
1288    }
1289
1290    pub fn new_local(
1291        abs_paths: Vec<PathBuf>,
1292        app_state: Arc<AppState>,
1293        requesting_window: Option<WindowHandle<Workspace>>,
1294        env: Option<HashMap<String, String>>,
1295        cx: &mut App,
1296    ) -> Task<
1297        anyhow::Result<(
1298            WindowHandle<Workspace>,
1299            Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
1300        )>,
1301    > {
1302        let project_handle = Project::local(
1303            app_state.client.clone(),
1304            app_state.node_runtime.clone(),
1305            app_state.user_store.clone(),
1306            app_state.languages.clone(),
1307            app_state.fs.clone(),
1308            env,
1309            cx,
1310        );
1311
1312        cx.spawn(async move |cx| {
1313            let mut paths_to_open = Vec::with_capacity(abs_paths.len());
1314            for path in abs_paths.into_iter() {
1315                if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
1316                    paths_to_open.push(canonical)
1317                } else {
1318                    paths_to_open.push(path)
1319                }
1320            }
1321
1322            let serialized_workspace =
1323                persistence::DB.workspace_for_roots(paths_to_open.as_slice());
1324
1325            let workspace_location = serialized_workspace
1326                .as_ref()
1327                .map(|ws| &ws.location)
1328                .and_then(|loc| match loc {
1329                    SerializedWorkspaceLocation::Local(_, order) => {
1330                        Some((loc.sorted_paths(), order.order()))
1331                    }
1332                    _ => None,
1333                });
1334
1335            if let Some((paths, order)) = workspace_location {
1336                paths_to_open = paths.iter().cloned().collect();
1337
1338                if order.iter().enumerate().any(|(i, &j)| i != j) {
1339                    project_handle
1340                        .update(cx, |project, cx| {
1341                            project.set_worktrees_reordered(true, cx);
1342                        })
1343                        .log_err();
1344                }
1345            }
1346
1347            // Get project paths for all of the abs_paths
1348            let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
1349                Vec::with_capacity(paths_to_open.len());
1350
1351            for path in paths_to_open.into_iter() {
1352                if let Some((_, project_entry)) = cx
1353                    .update(|cx| {
1354                        Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
1355                    })?
1356                    .await
1357                    .log_err()
1358                {
1359                    project_paths.push((path, Some(project_entry)));
1360                } else {
1361                    project_paths.push((path, None));
1362                }
1363            }
1364
1365            let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
1366                serialized_workspace.id
1367            } else {
1368                DB.next_id().await.unwrap_or_else(|_| Default::default())
1369            };
1370
1371            let toolchains = DB.toolchains(workspace_id).await?;
1372            for (toolchain, worktree_id, path) in toolchains {
1373                project_handle
1374                    .update(cx, |this, cx| {
1375                        this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
1376                    })?
1377                    .await;
1378            }
1379            let window = if let Some(window) = requesting_window {
1380                cx.update_window(window.into(), |_, window, cx| {
1381                    window.replace_root(cx, |window, cx| {
1382                        Workspace::new(
1383                            Some(workspace_id),
1384                            project_handle.clone(),
1385                            app_state.clone(),
1386                            window,
1387                            cx,
1388                        )
1389                    });
1390                })?;
1391                window
1392            } else {
1393                let window_bounds_override = window_bounds_env_override();
1394
1395                let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
1396                    (Some(WindowBounds::Windowed(bounds)), None)
1397                } else {
1398                    let restorable_bounds = serialized_workspace
1399                        .as_ref()
1400                        .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
1401                        .or_else(|| {
1402                            let (display, window_bounds) = DB.last_window().log_err()?;
1403                            Some((display?, window_bounds?))
1404                        });
1405
1406                    if let Some((serialized_display, serialized_status)) = restorable_bounds {
1407                        (Some(serialized_status.0), Some(serialized_display))
1408                    } else {
1409                        (None, None)
1410                    }
1411                };
1412
1413                // Use the serialized workspace to construct the new window
1414                let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx))?;
1415                options.window_bounds = window_bounds;
1416                let centered_layout = serialized_workspace
1417                    .as_ref()
1418                    .map(|w| w.centered_layout)
1419                    .unwrap_or(false);
1420                cx.open_window(options, {
1421                    let app_state = app_state.clone();
1422                    let project_handle = project_handle.clone();
1423                    move |window, cx| {
1424                        cx.new(|cx| {
1425                            let mut workspace = Workspace::new(
1426                                Some(workspace_id),
1427                                project_handle,
1428                                app_state,
1429                                window,
1430                                cx,
1431                            );
1432                            workspace.centered_layout = centered_layout;
1433                            workspace
1434                        })
1435                    }
1436                })?
1437            };
1438
1439            notify_if_database_failed(window, cx);
1440            let opened_items = window
1441                .update(cx, |_workspace, window, cx| {
1442                    open_items(serialized_workspace, project_paths, window, cx)
1443                })?
1444                .await
1445                .unwrap_or_default();
1446
1447            window
1448                .update(cx, |workspace, window, cx| {
1449                    window.activate_window();
1450                    workspace.update_history(cx);
1451                })
1452                .log_err();
1453            Ok((window, opened_items))
1454        })
1455    }
1456
1457    pub fn weak_handle(&self) -> WeakEntity<Self> {
1458        self.weak_self.clone()
1459    }
1460
1461    pub fn left_dock(&self) -> &Entity<Dock> {
1462        &self.left_dock
1463    }
1464
1465    pub fn bottom_dock(&self) -> &Entity<Dock> {
1466        &self.bottom_dock
1467    }
1468
1469    pub fn bottom_dock_layout(&self) -> BottomDockLayout {
1470        self.bottom_dock_layout
1471    }
1472
1473    pub fn set_bottom_dock_layout(
1474        &mut self,
1475        layout: BottomDockLayout,
1476        window: &mut Window,
1477        cx: &mut Context<Self>,
1478    ) {
1479        let fs = self.project().read(cx).fs();
1480        settings::update_settings_file::<WorkspaceSettings>(fs.clone(), cx, move |content, _cx| {
1481            content.bottom_dock_layout = Some(layout);
1482        });
1483
1484        self.bottom_dock_layout = layout;
1485        cx.notify();
1486        self.serialize_workspace(window, cx);
1487    }
1488
1489    pub fn right_dock(&self) -> &Entity<Dock> {
1490        &self.right_dock
1491    }
1492
1493    pub fn all_docks(&self) -> [&Entity<Dock>; 3] {
1494        [&self.left_dock, &self.bottom_dock, &self.right_dock]
1495    }
1496
1497    pub fn dock_at_position(&self, position: DockPosition) -> &Entity<Dock> {
1498        match position {
1499            DockPosition::Left => &self.left_dock,
1500            DockPosition::Bottom => &self.bottom_dock,
1501            DockPosition::Right => &self.right_dock,
1502        }
1503    }
1504
1505    pub fn is_edited(&self) -> bool {
1506        self.window_edited
1507    }
1508
1509    pub fn add_panel<T: Panel>(
1510        &mut self,
1511        panel: Entity<T>,
1512        window: &mut Window,
1513        cx: &mut Context<Self>,
1514    ) {
1515        let focus_handle = panel.panel_focus_handle(cx);
1516        cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused)
1517            .detach();
1518
1519        let dock_position = panel.position(window, cx);
1520        let dock = self.dock_at_position(dock_position);
1521
1522        dock.update(cx, |dock, cx| {
1523            dock.add_panel(panel, self.weak_self.clone(), window, cx)
1524        });
1525    }
1526
1527    pub fn status_bar(&self) -> &Entity<StatusBar> {
1528        &self.status_bar
1529    }
1530
1531    pub fn app_state(&self) -> &Arc<AppState> {
1532        &self.app_state
1533    }
1534
1535    pub fn user_store(&self) -> &Entity<UserStore> {
1536        &self.app_state.user_store
1537    }
1538
1539    pub fn project(&self) -> &Entity<Project> {
1540        &self.project
1541    }
1542
1543    pub fn recently_activated_items(&self, cx: &App) -> HashMap<EntityId, usize> {
1544        let mut history: HashMap<EntityId, usize> = HashMap::default();
1545
1546        for pane_handle in &self.panes {
1547            let pane = pane_handle.read(cx);
1548
1549            for entry in pane.activation_history() {
1550                history.insert(
1551                    entry.entity_id,
1552                    history
1553                        .get(&entry.entity_id)
1554                        .cloned()
1555                        .unwrap_or(0)
1556                        .max(entry.timestamp),
1557                );
1558            }
1559        }
1560
1561        history
1562    }
1563
1564    pub fn recent_navigation_history_iter(
1565        &self,
1566        cx: &App,
1567    ) -> impl Iterator<Item = (ProjectPath, Option<PathBuf>)> {
1568        let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
1569        let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
1570
1571        for pane in &self.panes {
1572            let pane = pane.read(cx);
1573
1574            pane.nav_history()
1575                .for_each_entry(cx, |entry, (project_path, fs_path)| {
1576                    if let Some(fs_path) = &fs_path {
1577                        abs_paths_opened
1578                            .entry(fs_path.clone())
1579                            .or_default()
1580                            .insert(project_path.clone());
1581                    }
1582                    let timestamp = entry.timestamp;
1583                    match history.entry(project_path) {
1584                        hash_map::Entry::Occupied(mut entry) => {
1585                            let (_, old_timestamp) = entry.get();
1586                            if &timestamp > old_timestamp {
1587                                entry.insert((fs_path, timestamp));
1588                            }
1589                        }
1590                        hash_map::Entry::Vacant(entry) => {
1591                            entry.insert((fs_path, timestamp));
1592                        }
1593                    }
1594                });
1595
1596            if let Some(item) = pane.active_item() {
1597                if let Some(project_path) = item.project_path(cx) {
1598                    let fs_path = self.project.read(cx).absolute_path(&project_path, cx);
1599
1600                    if let Some(fs_path) = &fs_path {
1601                        abs_paths_opened
1602                            .entry(fs_path.clone())
1603                            .or_default()
1604                            .insert(project_path.clone());
1605                    }
1606
1607                    history.insert(project_path, (fs_path, std::usize::MAX));
1608                }
1609            }
1610        }
1611
1612        history
1613            .into_iter()
1614            .sorted_by_key(|(_, (_, order))| *order)
1615            .map(|(project_path, (fs_path, _))| (project_path, fs_path))
1616            .rev()
1617            .filter(move |(history_path, abs_path)| {
1618                let latest_project_path_opened = abs_path
1619                    .as_ref()
1620                    .and_then(|abs_path| abs_paths_opened.get(abs_path))
1621                    .and_then(|project_paths| {
1622                        project_paths
1623                            .iter()
1624                            .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
1625                    });
1626
1627                match latest_project_path_opened {
1628                    Some(latest_project_path_opened) => latest_project_path_opened == history_path,
1629                    None => true,
1630                }
1631            })
1632    }
1633
1634    pub fn recent_navigation_history(
1635        &self,
1636        limit: Option<usize>,
1637        cx: &App,
1638    ) -> Vec<(ProjectPath, Option<PathBuf>)> {
1639        self.recent_navigation_history_iter(cx)
1640            .take(limit.unwrap_or(usize::MAX))
1641            .collect()
1642    }
1643
1644    fn navigate_history(
1645        &mut self,
1646        pane: WeakEntity<Pane>,
1647        mode: NavigationMode,
1648        window: &mut Window,
1649        cx: &mut Context<Workspace>,
1650    ) -> Task<Result<()>> {
1651        let to_load = if let Some(pane) = pane.upgrade() {
1652            pane.update(cx, |pane, cx| {
1653                window.focus(&pane.focus_handle(cx));
1654                loop {
1655                    // Retrieve the weak item handle from the history.
1656                    let entry = pane.nav_history_mut().pop(mode, cx)?;
1657
1658                    // If the item is still present in this pane, then activate it.
1659                    if let Some(index) = entry
1660                        .item
1661                        .upgrade()
1662                        .and_then(|v| pane.index_for_item(v.as_ref()))
1663                    {
1664                        let prev_active_item_index = pane.active_item_index();
1665                        pane.nav_history_mut().set_mode(mode);
1666                        pane.activate_item(index, true, true, window, cx);
1667                        pane.nav_history_mut().set_mode(NavigationMode::Normal);
1668
1669                        let mut navigated = prev_active_item_index != pane.active_item_index();
1670                        if let Some(data) = entry.data {
1671                            navigated |= pane.active_item()?.navigate(data, window, cx);
1672                        }
1673
1674                        if navigated {
1675                            break None;
1676                        }
1677                    } else {
1678                        // If the item is no longer present in this pane, then retrieve its
1679                        // path info in order to reopen it.
1680                        break pane
1681                            .nav_history()
1682                            .path_for_item(entry.item.id())
1683                            .map(|(project_path, abs_path)| (project_path, abs_path, entry));
1684                    }
1685                }
1686            })
1687        } else {
1688            None
1689        };
1690
1691        if let Some((project_path, abs_path, entry)) = to_load {
1692            // If the item was no longer present, then load it again from its previous path, first try the local path
1693            let open_by_project_path = self.load_path(project_path.clone(), window, cx);
1694
1695            cx.spawn_in(window, async move  |workspace, cx| {
1696                let open_by_project_path = open_by_project_path.await;
1697                let mut navigated = false;
1698                match open_by_project_path
1699                    .with_context(|| format!("Navigating to {project_path:?}"))
1700                {
1701                    Ok((project_entry_id, build_item)) => {
1702                        let prev_active_item_id = pane.update(cx, |pane, _| {
1703                            pane.nav_history_mut().set_mode(mode);
1704                            pane.active_item().map(|p| p.item_id())
1705                        })?;
1706
1707                        pane.update_in(cx, |pane, window, cx| {
1708                            let item = pane.open_item(
1709                                project_entry_id,
1710                                project_path,
1711                                true,
1712                                entry.is_preview,
1713                                true,
1714                                None,
1715                                window, cx,
1716                                build_item,
1717                            );
1718                            navigated |= Some(item.item_id()) != prev_active_item_id;
1719                            pane.nav_history_mut().set_mode(NavigationMode::Normal);
1720                            if let Some(data) = entry.data {
1721                                navigated |= item.navigate(data, window, cx);
1722                            }
1723                        })?;
1724                    }
1725                    Err(open_by_project_path_e) => {
1726                        // Fall back to opening by abs path, in case an external file was opened and closed,
1727                        // and its worktree is now dropped
1728                        if let Some(abs_path) = abs_path {
1729                            let prev_active_item_id = pane.update(cx, |pane, _| {
1730                                pane.nav_history_mut().set_mode(mode);
1731                                pane.active_item().map(|p| p.item_id())
1732                            })?;
1733                            let open_by_abs_path = workspace.update_in(cx, |workspace, window, cx| {
1734                                workspace.open_abs_path(abs_path.clone(), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
1735                            })?;
1736                            match open_by_abs_path
1737                                .await
1738                                .with_context(|| format!("Navigating to {abs_path:?}"))
1739                            {
1740                                Ok(item) => {
1741                                    pane.update_in(cx, |pane, window, cx| {
1742                                        navigated |= Some(item.item_id()) != prev_active_item_id;
1743                                        pane.nav_history_mut().set_mode(NavigationMode::Normal);
1744                                        if let Some(data) = entry.data {
1745                                            navigated |= item.navigate(data, window, cx);
1746                                        }
1747                                    })?;
1748                                }
1749                                Err(open_by_abs_path_e) => {
1750                                    log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
1751                                }
1752                            }
1753                        }
1754                    }
1755                }
1756
1757                if !navigated {
1758                    workspace
1759                        .update_in(cx, |workspace, window, cx| {
1760                            Self::navigate_history(workspace, pane, mode, window, cx)
1761                        })?
1762                        .await?;
1763                }
1764
1765                Ok(())
1766            })
1767        } else {
1768            Task::ready(Ok(()))
1769        }
1770    }
1771
1772    pub fn go_back(
1773        &mut self,
1774        pane: WeakEntity<Pane>,
1775        window: &mut Window,
1776        cx: &mut Context<Workspace>,
1777    ) -> Task<Result<()>> {
1778        self.navigate_history(pane, NavigationMode::GoingBack, window, cx)
1779    }
1780
1781    pub fn go_forward(
1782        &mut self,
1783        pane: WeakEntity<Pane>,
1784        window: &mut Window,
1785        cx: &mut Context<Workspace>,
1786    ) -> Task<Result<()>> {
1787        self.navigate_history(pane, NavigationMode::GoingForward, window, cx)
1788    }
1789
1790    pub fn reopen_closed_item(
1791        &mut self,
1792        window: &mut Window,
1793        cx: &mut Context<Workspace>,
1794    ) -> Task<Result<()>> {
1795        self.navigate_history(
1796            self.active_pane().downgrade(),
1797            NavigationMode::ReopeningClosedItem,
1798            window,
1799            cx,
1800        )
1801    }
1802
1803    pub fn client(&self) -> &Arc<Client> {
1804        &self.app_state.client
1805    }
1806
1807    pub fn set_titlebar_item(&mut self, item: AnyView, _: &mut Window, cx: &mut Context<Self>) {
1808        self.titlebar_item = Some(item);
1809        cx.notify();
1810    }
1811
1812    pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
1813        self.on_prompt_for_new_path = Some(prompt)
1814    }
1815
1816    pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
1817        self.on_prompt_for_open_path = Some(prompt)
1818    }
1819
1820    pub fn set_terminal_provider(&mut self, provider: impl TerminalProvider + 'static) {
1821        self.terminal_provider = Some(Box::new(provider));
1822    }
1823
1824    pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) {
1825        self.debugger_provider = Some(Box::new(provider));
1826    }
1827
1828    pub fn serialized_ssh_project(&self) -> Option<SerializedSshProject> {
1829        self.serialized_ssh_project.clone()
1830    }
1831
1832    pub fn set_serialized_ssh_project(&mut self, serialized_ssh_project: SerializedSshProject) {
1833        self.serialized_ssh_project = Some(serialized_ssh_project);
1834    }
1835
1836    pub fn prompt_for_open_path(
1837        &mut self,
1838        path_prompt_options: PathPromptOptions,
1839        lister: DirectoryLister,
1840        window: &mut Window,
1841        cx: &mut Context<Self>,
1842    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
1843        if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
1844            let prompt = self.on_prompt_for_open_path.take().unwrap();
1845            let rx = prompt(self, lister, window, cx);
1846            self.on_prompt_for_open_path = Some(prompt);
1847            rx
1848        } else {
1849            let (tx, rx) = oneshot::channel();
1850            let abs_path = cx.prompt_for_paths(path_prompt_options);
1851
1852            cx.spawn_in(window, async move |this, cx| {
1853                let Ok(result) = abs_path.await else {
1854                    return Ok(());
1855                };
1856
1857                match result {
1858                    Ok(result) => {
1859                        tx.send(result).log_err();
1860                    }
1861                    Err(err) => {
1862                        let rx = this.update_in(cx, |this, window, cx| {
1863                            this.show_portal_error(err.to_string(), cx);
1864                            let prompt = this.on_prompt_for_open_path.take().unwrap();
1865                            let rx = prompt(this, lister, window, cx);
1866                            this.on_prompt_for_open_path = Some(prompt);
1867                            rx
1868                        })?;
1869                        if let Ok(path) = rx.await {
1870                            tx.send(path).log_err();
1871                        }
1872                    }
1873                };
1874                anyhow::Ok(())
1875            })
1876            .detach();
1877
1878            rx
1879        }
1880    }
1881
1882    pub fn prompt_for_new_path(
1883        &mut self,
1884        window: &mut Window,
1885        cx: &mut Context<Self>,
1886    ) -> oneshot::Receiver<Option<ProjectPath>> {
1887        if (self.project.read(cx).is_via_collab() || self.project.read(cx).is_via_ssh())
1888            || !WorkspaceSettings::get_global(cx).use_system_path_prompts
1889        {
1890            let prompt = self.on_prompt_for_new_path.take().unwrap();
1891            let rx = prompt(self, window, cx);
1892            self.on_prompt_for_new_path = Some(prompt);
1893            return rx;
1894        }
1895
1896        let (tx, rx) = oneshot::channel();
1897        cx.spawn_in(window, async move |this, cx| {
1898            let abs_path = this.update(cx, |this, cx| {
1899                let mut relative_to = this
1900                    .most_recent_active_path(cx)
1901                    .and_then(|p| p.parent().map(|p| p.to_path_buf()));
1902                if relative_to.is_none() {
1903                    let project = this.project.read(cx);
1904                    relative_to = project
1905                        .visible_worktrees(cx)
1906                        .filter_map(|worktree| {
1907                            Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
1908                        })
1909                        .next()
1910                };
1911
1912                cx.prompt_for_new_path(&relative_to.unwrap_or_else(|| PathBuf::from("")))
1913            })?;
1914            let abs_path = match abs_path.await? {
1915                Ok(path) => path,
1916                Err(err) => {
1917                    let rx = this.update_in(cx, |this, window, cx| {
1918                        this.show_portal_error(err.to_string(), cx);
1919
1920                        let prompt = this.on_prompt_for_new_path.take().unwrap();
1921                        let rx = prompt(this, window, cx);
1922                        this.on_prompt_for_new_path = Some(prompt);
1923                        rx
1924                    })?;
1925                    if let Ok(path) = rx.await {
1926                        tx.send(path).log_err();
1927                    }
1928                    return anyhow::Ok(());
1929                }
1930            };
1931
1932            let project_path = abs_path.and_then(|abs_path| {
1933                this.update(cx, |this, cx| {
1934                    this.project.update(cx, |project, cx| {
1935                        project.find_or_create_worktree(abs_path, true, cx)
1936                    })
1937                })
1938                .ok()
1939            });
1940
1941            if let Some(project_path) = project_path {
1942                let (worktree, path) = project_path.await?;
1943                let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
1944                tx.send(Some(ProjectPath {
1945                    worktree_id,
1946                    path: path.into(),
1947                }))
1948                .ok();
1949            } else {
1950                tx.send(None).ok();
1951            }
1952            anyhow::Ok(())
1953        })
1954        .detach_and_log_err(cx);
1955
1956        rx
1957    }
1958
1959    pub fn titlebar_item(&self) -> Option<AnyView> {
1960        self.titlebar_item.clone()
1961    }
1962
1963    /// Call the given callback with a workspace whose project is local.
1964    ///
1965    /// If the given workspace has a local project, then it will be passed
1966    /// to the callback. Otherwise, a new empty window will be created.
1967    pub fn with_local_workspace<T, F>(
1968        &mut self,
1969        window: &mut Window,
1970        cx: &mut Context<Self>,
1971        callback: F,
1972    ) -> Task<Result<T>>
1973    where
1974        T: 'static,
1975        F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
1976    {
1977        if self.project.read(cx).is_local() {
1978            Task::ready(Ok(callback(self, window, cx)))
1979        } else {
1980            let env = self.project.read(cx).cli_environment(cx);
1981            let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, cx);
1982            cx.spawn_in(window, async move |_vh, cx| {
1983                let (workspace, _) = task.await?;
1984                workspace.update(cx, callback)
1985            })
1986        }
1987    }
1988
1989    pub fn worktrees<'a>(&self, cx: &'a App) -> impl 'a + Iterator<Item = Entity<Worktree>> {
1990        self.project.read(cx).worktrees(cx)
1991    }
1992
1993    pub fn visible_worktrees<'a>(
1994        &self,
1995        cx: &'a App,
1996    ) -> impl 'a + Iterator<Item = Entity<Worktree>> {
1997        self.project.read(cx).visible_worktrees(cx)
1998    }
1999
2000    #[cfg(any(test, feature = "test-support"))]
2001    pub fn worktree_scans_complete(&self, cx: &App) -> impl Future<Output = ()> + 'static + use<> {
2002        let futures = self
2003            .worktrees(cx)
2004            .filter_map(|worktree| worktree.read(cx).as_local())
2005            .map(|worktree| worktree.scan_complete())
2006            .collect::<Vec<_>>();
2007        async move {
2008            for future in futures {
2009                future.await;
2010            }
2011        }
2012    }
2013
2014    pub fn close_global(_: &CloseWindow, cx: &mut App) {
2015        cx.defer(|cx| {
2016            cx.windows().iter().find(|window| {
2017                window
2018                    .update(cx, |_, window, _| {
2019                        if window.is_window_active() {
2020                            //This can only get called when the window's project connection has been lost
2021                            //so we don't need to prompt the user for anything and instead just close the window
2022                            window.remove_window();
2023                            true
2024                        } else {
2025                            false
2026                        }
2027                    })
2028                    .unwrap_or(false)
2029            });
2030        });
2031    }
2032
2033    pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
2034        let prepare = self.prepare_to_close(CloseIntent::CloseWindow, window, cx);
2035        cx.spawn_in(window, async move |_, cx| {
2036            if prepare.await? {
2037                cx.update(|window, _cx| window.remove_window())?;
2038            }
2039            anyhow::Ok(())
2040        })
2041        .detach_and_log_err(cx)
2042    }
2043
2044    pub fn move_focused_panel_to_next_position(
2045        &mut self,
2046        _: &MoveFocusedPanelToNextPosition,
2047        window: &mut Window,
2048        cx: &mut Context<Self>,
2049    ) {
2050        let docks = self.all_docks();
2051        let active_dock = docks
2052            .into_iter()
2053            .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
2054
2055        if let Some(dock) = active_dock {
2056            dock.update(cx, |dock, cx| {
2057                let active_panel = dock
2058                    .active_panel()
2059                    .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx));
2060
2061                if let Some(panel) = active_panel {
2062                    panel.move_to_next_position(window, cx);
2063                }
2064            })
2065        }
2066    }
2067
2068    pub fn prepare_to_close(
2069        &mut self,
2070        close_intent: CloseIntent,
2071        window: &mut Window,
2072        cx: &mut Context<Self>,
2073    ) -> Task<Result<bool>> {
2074        let active_call = self.active_call().cloned();
2075
2076        // On Linux and Windows, closing the last window should restore the last workspace.
2077        let save_last_workspace = cfg!(not(target_os = "macos"))
2078            && close_intent != CloseIntent::ReplaceWindow
2079            && cx.windows().len() == 1;
2080
2081        cx.spawn_in(window, async move |this, cx| {
2082            let workspace_count = cx.update(|_window, cx| {
2083                cx.windows()
2084                    .iter()
2085                    .filter(|window| window.downcast::<Workspace>().is_some())
2086                    .count()
2087            })?;
2088
2089            if let Some(active_call) = active_call {
2090                if close_intent != CloseIntent::Quit
2091                    && workspace_count == 1
2092                    && active_call.read_with(cx, |call, _| call.room().is_some())?
2093                {
2094                    let answer = cx.update(|window, cx| {
2095                        window.prompt(
2096                            PromptLevel::Warning,
2097                            "Do you want to leave the current call?",
2098                            None,
2099                            &["Close window and hang up", "Cancel"],
2100                            cx,
2101                        )
2102                    })?;
2103
2104                    if answer.await.log_err() == Some(1) {
2105                        return anyhow::Ok(false);
2106                    } else {
2107                        active_call
2108                            .update(cx, |call, cx| call.hang_up(cx))?
2109                            .await
2110                            .log_err();
2111                    }
2112                }
2113            }
2114
2115            let save_result = this
2116                .update_in(cx, |this, window, cx| {
2117                    this.save_all_internal(SaveIntent::Close, window, cx)
2118                })?
2119                .await;
2120
2121            // If we're not quitting, but closing, we remove the workspace from
2122            // the current session.
2123            if close_intent != CloseIntent::Quit
2124                && !save_last_workspace
2125                && save_result.as_ref().map_or(false, |&res| res)
2126            {
2127                this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx))?
2128                    .await;
2129            }
2130
2131            save_result
2132        })
2133    }
2134
2135    fn save_all(&mut self, action: &SaveAll, window: &mut Window, cx: &mut Context<Self>) {
2136        self.save_all_internal(
2137            action.save_intent.unwrap_or(SaveIntent::SaveAll),
2138            window,
2139            cx,
2140        )
2141        .detach_and_log_err(cx);
2142    }
2143
2144    fn send_keystrokes(
2145        &mut self,
2146        action: &SendKeystrokes,
2147        window: &mut Window,
2148        cx: &mut Context<Self>,
2149    ) {
2150        let mut state = self.dispatching_keystrokes.borrow_mut();
2151        if !state.0.insert(action.0.clone()) {
2152            cx.propagate();
2153            return;
2154        }
2155        let mut keystrokes: Vec<Keystroke> = action
2156            .0
2157            .split(' ')
2158            .flat_map(|k| Keystroke::parse(k).log_err())
2159            .collect();
2160        keystrokes.reverse();
2161
2162        state.1.append(&mut keystrokes);
2163        drop(state);
2164
2165        let keystrokes = self.dispatching_keystrokes.clone();
2166        window
2167            .spawn(cx, async move |cx| {
2168                // limit to 100 keystrokes to avoid infinite recursion.
2169                for _ in 0..100 {
2170                    let Some(keystroke) = keystrokes.borrow_mut().1.pop() else {
2171                        keystrokes.borrow_mut().0.clear();
2172                        return Ok(());
2173                    };
2174                    cx.update(|window, cx| {
2175                        let focused = window.focused(cx);
2176                        window.dispatch_keystroke(keystroke.clone(), cx);
2177                        if window.focused(cx) != focused {
2178                            // dispatch_keystroke may cause the focus to change.
2179                            // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
2180                            // And we need that to happen before the next keystroke to keep vim mode happy...
2181                            // (Note that the tests always do this implicitly, so you must manually test with something like:
2182                            //   "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
2183                            // )
2184                            window.draw(cx);
2185                        }
2186                    })?;
2187                }
2188
2189                *keystrokes.borrow_mut() = Default::default();
2190                anyhow::bail!("over 100 keystrokes passed to send_keystrokes");
2191            })
2192            .detach_and_log_err(cx);
2193    }
2194
2195    fn save_all_internal(
2196        &mut self,
2197        mut save_intent: SaveIntent,
2198        window: &mut Window,
2199        cx: &mut Context<Self>,
2200    ) -> Task<Result<bool>> {
2201        if self.project.read(cx).is_disconnected(cx) {
2202            return Task::ready(Ok(true));
2203        }
2204        let dirty_items = self
2205            .panes
2206            .iter()
2207            .flat_map(|pane| {
2208                pane.read(cx).items().filter_map(|item| {
2209                    if item.is_dirty(cx) {
2210                        item.tab_content_text(0, cx);
2211                        Some((pane.downgrade(), item.boxed_clone()))
2212                    } else {
2213                        None
2214                    }
2215                })
2216            })
2217            .collect::<Vec<_>>();
2218
2219        let project = self.project.clone();
2220        cx.spawn_in(window, async move |workspace, cx| {
2221            let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() {
2222                let (serialize_tasks, remaining_dirty_items) =
2223                    workspace.update_in(cx, |workspace, window, cx| {
2224                        let mut remaining_dirty_items = Vec::new();
2225                        let mut serialize_tasks = Vec::new();
2226                        for (pane, item) in dirty_items {
2227                            if let Some(task) = item
2228                                .to_serializable_item_handle(cx)
2229                                .and_then(|handle| handle.serialize(workspace, true, window, cx))
2230                            {
2231                                serialize_tasks.push(task);
2232                            } else {
2233                                remaining_dirty_items.push((pane, item));
2234                            }
2235                        }
2236                        (serialize_tasks, remaining_dirty_items)
2237                    })?;
2238
2239                futures::future::try_join_all(serialize_tasks).await?;
2240
2241                if remaining_dirty_items.len() > 1 {
2242                    let answer = workspace.update_in(cx, |_, window, cx| {
2243                        let detail = Pane::file_names_for_prompt(
2244                            &mut remaining_dirty_items.iter().map(|(_, handle)| handle),
2245                            cx,
2246                        );
2247                        window.prompt(
2248                            PromptLevel::Warning,
2249                            &"Do you want to save all changes in the following files?",
2250                            Some(&detail),
2251                            &["Save all", "Discard all", "Cancel"],
2252                            cx,
2253                        )
2254                    })?;
2255                    match answer.await.log_err() {
2256                        Some(0) => save_intent = SaveIntent::SaveAll,
2257                        Some(1) => save_intent = SaveIntent::Skip,
2258                        Some(2) => return Ok(false),
2259                        _ => {}
2260                    }
2261                }
2262
2263                remaining_dirty_items
2264            } else {
2265                dirty_items
2266            };
2267
2268            for (pane, item) in dirty_items {
2269                let (singleton, project_entry_ids) =
2270                    cx.update(|_, cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?;
2271                if singleton || !project_entry_ids.is_empty() {
2272                    if !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await? {
2273                        return Ok(false);
2274                    }
2275                }
2276            }
2277            Ok(true)
2278        })
2279    }
2280
2281    pub fn open_workspace_for_paths(
2282        &mut self,
2283        replace_current_window: bool,
2284        paths: Vec<PathBuf>,
2285        window: &mut Window,
2286        cx: &mut Context<Self>,
2287    ) -> Task<Result<()>> {
2288        let window_handle = window.window_handle().downcast::<Self>();
2289        let is_remote = self.project.read(cx).is_via_collab();
2290        let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
2291        let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
2292
2293        let window_to_replace = if replace_current_window {
2294            window_handle
2295        } else if is_remote || has_worktree || has_dirty_items {
2296            None
2297        } else {
2298            window_handle
2299        };
2300        let app_state = self.app_state.clone();
2301
2302        cx.spawn(async move |_, cx| {
2303            cx.update(|cx| {
2304                open_paths(
2305                    &paths,
2306                    app_state,
2307                    OpenOptions {
2308                        replace_window: window_to_replace,
2309                        ..Default::default()
2310                    },
2311                    cx,
2312                )
2313            })?
2314            .await?;
2315            Ok(())
2316        })
2317    }
2318
2319    #[allow(clippy::type_complexity)]
2320    pub fn open_paths(
2321        &mut self,
2322        mut abs_paths: Vec<PathBuf>,
2323        options: OpenOptions,
2324        pane: Option<WeakEntity<Pane>>,
2325        window: &mut Window,
2326        cx: &mut Context<Self>,
2327    ) -> Task<Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>> {
2328        log::info!("open paths {abs_paths:?}");
2329
2330        let fs = self.app_state.fs.clone();
2331
2332        // Sort the paths to ensure we add worktrees for parents before their children.
2333        abs_paths.sort_unstable();
2334        cx.spawn_in(window, async move |this, cx| {
2335            let mut tasks = Vec::with_capacity(abs_paths.len());
2336
2337            for abs_path in &abs_paths {
2338                let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
2339                    OpenVisible::All => Some(true),
2340                    OpenVisible::None => Some(false),
2341                    OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
2342                        Some(Some(metadata)) => Some(!metadata.is_dir),
2343                        Some(None) => Some(true),
2344                        None => None,
2345                    },
2346                    OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
2347                        Some(Some(metadata)) => Some(metadata.is_dir),
2348                        Some(None) => Some(false),
2349                        None => None,
2350                    },
2351                };
2352                let project_path = match visible {
2353                    Some(visible) => match this
2354                        .update(cx, |this, cx| {
2355                            Workspace::project_path_for_path(
2356                                this.project.clone(),
2357                                abs_path,
2358                                visible,
2359                                cx,
2360                            )
2361                        })
2362                        .log_err()
2363                    {
2364                        Some(project_path) => project_path.await.log_err(),
2365                        None => None,
2366                    },
2367                    None => None,
2368                };
2369
2370                let this = this.clone();
2371                let abs_path: Arc<Path> = SanitizedPath::from(abs_path.clone()).into();
2372                let fs = fs.clone();
2373                let pane = pane.clone();
2374                let task = cx.spawn(async move |cx| {
2375                    let (worktree, project_path) = project_path?;
2376                    if fs.is_dir(&abs_path).await {
2377                        this.update(cx, |workspace, cx| {
2378                            let worktree = worktree.read(cx);
2379                            let worktree_abs_path = worktree.abs_path();
2380                            let entry_id = if abs_path.as_ref() == worktree_abs_path.as_ref() {
2381                                worktree.root_entry()
2382                            } else {
2383                                abs_path
2384                                    .strip_prefix(worktree_abs_path.as_ref())
2385                                    .ok()
2386                                    .and_then(|relative_path| {
2387                                        worktree.entry_for_path(relative_path)
2388                                    })
2389                            }
2390                            .map(|entry| entry.id);
2391                            if let Some(entry_id) = entry_id {
2392                                workspace.project.update(cx, |_, cx| {
2393                                    cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
2394                                })
2395                            }
2396                        })
2397                        .log_err()?;
2398                        None
2399                    } else {
2400                        Some(
2401                            this.update_in(cx, |this, window, cx| {
2402                                this.open_path(
2403                                    project_path,
2404                                    pane,
2405                                    options.focus.unwrap_or(true),
2406                                    window,
2407                                    cx,
2408                                )
2409                            })
2410                            .log_err()?
2411                            .await,
2412                        )
2413                    }
2414                });
2415                tasks.push(task);
2416            }
2417
2418            futures::future::join_all(tasks).await
2419        })
2420    }
2421
2422    pub fn open_resolved_path(
2423        &mut self,
2424        path: ResolvedPath,
2425        window: &mut Window,
2426        cx: &mut Context<Self>,
2427    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2428        match path {
2429            ResolvedPath::ProjectPath { project_path, .. } => {
2430                self.open_path(project_path, None, true, window, cx)
2431            }
2432            ResolvedPath::AbsPath { path, .. } => self.open_abs_path(
2433                path,
2434                OpenOptions {
2435                    visible: Some(OpenVisible::None),
2436                    ..Default::default()
2437                },
2438                window,
2439                cx,
2440            ),
2441        }
2442    }
2443
2444    pub fn absolute_path_of_worktree(
2445        &self,
2446        worktree_id: WorktreeId,
2447        cx: &mut Context<Self>,
2448    ) -> Option<PathBuf> {
2449        self.project
2450            .read(cx)
2451            .worktree_for_id(worktree_id, cx)
2452            // TODO: use `abs_path` or `root_dir`
2453            .map(|wt| wt.read(cx).abs_path().as_ref().to_path_buf())
2454    }
2455
2456    fn add_folder_to_project(
2457        &mut self,
2458        _: &AddFolderToProject,
2459        window: &mut Window,
2460        cx: &mut Context<Self>,
2461    ) {
2462        let project = self.project.read(cx);
2463        if project.is_via_collab() {
2464            self.show_error(
2465                &anyhow!("You cannot add folders to someone else's project"),
2466                cx,
2467            );
2468            return;
2469        }
2470        let paths = self.prompt_for_open_path(
2471            PathPromptOptions {
2472                files: false,
2473                directories: true,
2474                multiple: true,
2475            },
2476            DirectoryLister::Project(self.project.clone()),
2477            window,
2478            cx,
2479        );
2480        cx.spawn_in(window, async move |this, cx| {
2481            if let Some(paths) = paths.await.log_err().flatten() {
2482                let results = this
2483                    .update_in(cx, |this, window, cx| {
2484                        this.open_paths(
2485                            paths,
2486                            OpenOptions {
2487                                visible: Some(OpenVisible::All),
2488                                ..Default::default()
2489                            },
2490                            None,
2491                            window,
2492                            cx,
2493                        )
2494                    })?
2495                    .await;
2496                for result in results.into_iter().flatten() {
2497                    result.log_err();
2498                }
2499            }
2500            anyhow::Ok(())
2501        })
2502        .detach_and_log_err(cx);
2503    }
2504
2505    pub fn project_path_for_path(
2506        project: Entity<Project>,
2507        abs_path: &Path,
2508        visible: bool,
2509        cx: &mut App,
2510    ) -> Task<Result<(Entity<Worktree>, ProjectPath)>> {
2511        let entry = project.update(cx, |project, cx| {
2512            project.find_or_create_worktree(abs_path, visible, cx)
2513        });
2514        cx.spawn(async move |cx| {
2515            let (worktree, path) = entry.await?;
2516            let worktree_id = worktree.update(cx, |t, _| t.id())?;
2517            Ok((
2518                worktree,
2519                ProjectPath {
2520                    worktree_id,
2521                    path: path.into(),
2522                },
2523            ))
2524        })
2525    }
2526
2527    pub fn items<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = &'a Box<dyn ItemHandle>> {
2528        self.panes.iter().flat_map(|pane| pane.read(cx).items())
2529    }
2530
2531    pub fn item_of_type<T: Item>(&self, cx: &App) -> Option<Entity<T>> {
2532        self.items_of_type(cx).max_by_key(|item| item.item_id())
2533    }
2534
2535    pub fn items_of_type<'a, T: Item>(
2536        &'a self,
2537        cx: &'a App,
2538    ) -> impl 'a + Iterator<Item = Entity<T>> {
2539        self.panes
2540            .iter()
2541            .flat_map(|pane| pane.read(cx).items_of_type())
2542    }
2543
2544    pub fn active_item(&self, cx: &App) -> Option<Box<dyn ItemHandle>> {
2545        self.active_pane().read(cx).active_item()
2546    }
2547
2548    pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
2549        let item = self.active_item(cx)?;
2550        item.to_any().downcast::<I>().ok()
2551    }
2552
2553    fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
2554        self.active_item(cx).and_then(|item| item.project_path(cx))
2555    }
2556
2557    pub fn most_recent_active_path(&self, cx: &App) -> Option<PathBuf> {
2558        self.recent_navigation_history_iter(cx)
2559            .filter_map(|(path, abs_path)| {
2560                let worktree = self
2561                    .project
2562                    .read(cx)
2563                    .worktree_for_id(path.worktree_id, cx)?;
2564                if worktree.read(cx).is_visible() {
2565                    abs_path
2566                } else {
2567                    None
2568                }
2569            })
2570            .next()
2571    }
2572
2573    pub fn save_active_item(
2574        &mut self,
2575        save_intent: SaveIntent,
2576        window: &mut Window,
2577        cx: &mut App,
2578    ) -> Task<Result<()>> {
2579        let project = self.project.clone();
2580        let pane = self.active_pane();
2581        let item = pane.read(cx).active_item();
2582        let pane = pane.downgrade();
2583
2584        window.spawn(cx, async move |mut cx| {
2585            if let Some(item) = item {
2586                Pane::save_item(project, &pane, item.as_ref(), save_intent, &mut cx)
2587                    .await
2588                    .map(|_| ())
2589            } else {
2590                Ok(())
2591            }
2592        })
2593    }
2594
2595    pub fn close_inactive_items_and_panes(
2596        &mut self,
2597        action: &CloseInactiveTabsAndPanes,
2598        window: &mut Window,
2599        cx: &mut Context<Self>,
2600    ) {
2601        if let Some(task) = self.close_all_internal(
2602            true,
2603            action.save_intent.unwrap_or(SaveIntent::Close),
2604            window,
2605            cx,
2606        ) {
2607            task.detach_and_log_err(cx)
2608        }
2609    }
2610
2611    pub fn close_all_items_and_panes(
2612        &mut self,
2613        action: &CloseAllItemsAndPanes,
2614        window: &mut Window,
2615        cx: &mut Context<Self>,
2616    ) {
2617        if let Some(task) = self.close_all_internal(
2618            false,
2619            action.save_intent.unwrap_or(SaveIntent::Close),
2620            window,
2621            cx,
2622        ) {
2623            task.detach_and_log_err(cx)
2624        }
2625    }
2626
2627    fn close_all_internal(
2628        &mut self,
2629        retain_active_pane: bool,
2630        save_intent: SaveIntent,
2631        window: &mut Window,
2632        cx: &mut Context<Self>,
2633    ) -> Option<Task<Result<()>>> {
2634        let current_pane = self.active_pane();
2635
2636        let mut tasks = Vec::new();
2637
2638        if retain_active_pane {
2639            if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
2640                pane.close_inactive_items(
2641                    &CloseInactiveItems {
2642                        save_intent: None,
2643                        close_pinned: false,
2644                    },
2645                    window,
2646                    cx,
2647                )
2648            }) {
2649                tasks.push(current_pane_close);
2650            };
2651        }
2652
2653        for pane in self.panes() {
2654            if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
2655                continue;
2656            }
2657
2658            if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
2659                pane.close_all_items(
2660                    &CloseAllItems {
2661                        save_intent: Some(save_intent),
2662                        close_pinned: false,
2663                    },
2664                    window,
2665                    cx,
2666                )
2667            }) {
2668                tasks.push(close_pane_items)
2669            }
2670        }
2671
2672        if tasks.is_empty() {
2673            None
2674        } else {
2675            Some(cx.spawn_in(window, async move |_, _| {
2676                for task in tasks {
2677                    task.await?
2678                }
2679                Ok(())
2680            }))
2681        }
2682    }
2683
2684    pub fn is_dock_at_position_open(&self, position: DockPosition, cx: &mut Context<Self>) -> bool {
2685        self.dock_at_position(position).read(cx).is_open()
2686    }
2687
2688    pub fn toggle_dock(
2689        &mut self,
2690        dock_side: DockPosition,
2691        window: &mut Window,
2692        cx: &mut Context<Self>,
2693    ) {
2694        let dock = self.dock_at_position(dock_side);
2695        let mut focus_center = false;
2696        let mut reveal_dock = false;
2697        dock.update(cx, |dock, cx| {
2698            let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
2699            let was_visible = dock.is_open() && !other_is_zoomed;
2700            dock.set_open(!was_visible, window, cx);
2701
2702            if dock.active_panel().is_none() {
2703                let Some(panel_ix) = dock
2704                    .first_enabled_panel_idx(cx)
2705                    .log_with_level(log::Level::Info)
2706                else {
2707                    return;
2708                };
2709                dock.activate_panel(panel_ix, window, cx);
2710            }
2711
2712            if let Some(active_panel) = dock.active_panel() {
2713                if was_visible {
2714                    if active_panel
2715                        .panel_focus_handle(cx)
2716                        .contains_focused(window, cx)
2717                    {
2718                        focus_center = true;
2719                    }
2720                } else {
2721                    let focus_handle = &active_panel.panel_focus_handle(cx);
2722                    window.focus(focus_handle);
2723                    reveal_dock = true;
2724                }
2725            }
2726        });
2727
2728        if reveal_dock {
2729            self.dismiss_zoomed_items_to_reveal(Some(dock_side), window, cx);
2730        }
2731
2732        if focus_center {
2733            self.active_pane
2734                .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)))
2735        }
2736
2737        cx.notify();
2738        self.serialize_workspace(window, cx);
2739    }
2740
2741    fn active_dock(&self, window: &Window, cx: &Context<Self>) -> Option<&Entity<Dock>> {
2742        self.all_docks().into_iter().find(|&dock| {
2743            dock.read(cx).is_open() && dock.focus_handle(cx).contains_focused(window, cx)
2744        })
2745    }
2746
2747    fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2748        if let Some(dock) = self.active_dock(window, cx) {
2749            dock.update(cx, |dock, cx| {
2750                dock.set_open(false, window, cx);
2751            });
2752        }
2753    }
2754
2755    pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2756        for dock in self.all_docks() {
2757            dock.update(cx, |dock, cx| {
2758                dock.set_open(false, window, cx);
2759            });
2760        }
2761
2762        cx.focus_self(window);
2763        cx.notify();
2764        self.serialize_workspace(window, cx);
2765    }
2766
2767    /// Transfer focus to the panel of the given type.
2768    pub fn focus_panel<T: Panel>(
2769        &mut self,
2770        window: &mut Window,
2771        cx: &mut Context<Self>,
2772    ) -> Option<Entity<T>> {
2773        let panel = self.focus_or_unfocus_panel::<T>(window, cx, |_, _, _| true)?;
2774        panel.to_any().downcast().ok()
2775    }
2776
2777    /// Focus the panel of the given type if it isn't already focused. If it is
2778    /// already focused, then transfer focus back to the workspace center.
2779    pub fn toggle_panel_focus<T: Panel>(
2780        &mut self,
2781        window: &mut Window,
2782        cx: &mut Context<Self>,
2783    ) -> bool {
2784        let mut did_focus_panel = false;
2785        self.focus_or_unfocus_panel::<T>(window, cx, |panel, window, cx| {
2786            did_focus_panel = !panel.panel_focus_handle(cx).contains_focused(window, cx);
2787            did_focus_panel
2788        });
2789        did_focus_panel
2790    }
2791
2792    pub fn activate_panel_for_proto_id(
2793        &mut self,
2794        panel_id: PanelId,
2795        window: &mut Window,
2796        cx: &mut Context<Self>,
2797    ) -> Option<Arc<dyn PanelHandle>> {
2798        let mut panel = None;
2799        for dock in self.all_docks() {
2800            if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
2801                panel = dock.update(cx, |dock, cx| {
2802                    dock.activate_panel(panel_index, window, cx);
2803                    dock.set_open(true, window, cx);
2804                    dock.active_panel().cloned()
2805                });
2806                break;
2807            }
2808        }
2809
2810        if panel.is_some() {
2811            cx.notify();
2812            self.serialize_workspace(window, cx);
2813        }
2814
2815        panel
2816    }
2817
2818    /// Focus or unfocus the given panel type, depending on the given callback.
2819    fn focus_or_unfocus_panel<T: Panel>(
2820        &mut self,
2821        window: &mut Window,
2822        cx: &mut Context<Self>,
2823        mut should_focus: impl FnMut(&dyn PanelHandle, &mut Window, &mut Context<Dock>) -> bool,
2824    ) -> Option<Arc<dyn PanelHandle>> {
2825        let mut result_panel = None;
2826        let mut serialize = false;
2827        for dock in self.all_docks() {
2828            if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2829                let mut focus_center = false;
2830                let panel = dock.update(cx, |dock, cx| {
2831                    dock.activate_panel(panel_index, window, cx);
2832
2833                    let panel = dock.active_panel().cloned();
2834                    if let Some(panel) = panel.as_ref() {
2835                        if should_focus(&**panel, window, cx) {
2836                            dock.set_open(true, window, cx);
2837                            panel.panel_focus_handle(cx).focus(window);
2838                        } else {
2839                            focus_center = true;
2840                        }
2841                    }
2842                    panel
2843                });
2844
2845                if focus_center {
2846                    self.active_pane
2847                        .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)))
2848                }
2849
2850                result_panel = panel;
2851                serialize = true;
2852                break;
2853            }
2854        }
2855
2856        if serialize {
2857            self.serialize_workspace(window, cx);
2858        }
2859
2860        cx.notify();
2861        result_panel
2862    }
2863
2864    /// Open the panel of the given type
2865    pub fn open_panel<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2866        for dock in self.all_docks() {
2867            if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2868                dock.update(cx, |dock, cx| {
2869                    dock.activate_panel(panel_index, window, cx);
2870                    dock.set_open(true, window, cx);
2871                });
2872            }
2873        }
2874    }
2875
2876    pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
2877        self.all_docks()
2878            .iter()
2879            .find_map(|dock| dock.read(cx).panel::<T>())
2880    }
2881
2882    fn dismiss_zoomed_items_to_reveal(
2883        &mut self,
2884        dock_to_reveal: Option<DockPosition>,
2885        window: &mut Window,
2886        cx: &mut Context<Self>,
2887    ) {
2888        // If a center pane is zoomed, unzoom it.
2889        for pane in &self.panes {
2890            if pane != &self.active_pane || dock_to_reveal.is_some() {
2891                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2892            }
2893        }
2894
2895        // If another dock is zoomed, hide it.
2896        let mut focus_center = false;
2897        for dock in self.all_docks() {
2898            dock.update(cx, |dock, cx| {
2899                if Some(dock.position()) != dock_to_reveal {
2900                    if let Some(panel) = dock.active_panel() {
2901                        if panel.is_zoomed(window, cx) {
2902                            focus_center |=
2903                                panel.panel_focus_handle(cx).contains_focused(window, cx);
2904                            dock.set_open(false, window, cx);
2905                        }
2906                    }
2907                }
2908            });
2909        }
2910
2911        if focus_center {
2912            self.active_pane
2913                .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)))
2914        }
2915
2916        if self.zoomed_position != dock_to_reveal {
2917            self.zoomed = None;
2918            self.zoomed_position = None;
2919            cx.emit(Event::ZoomChanged);
2920        }
2921
2922        cx.notify();
2923    }
2924
2925    fn add_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
2926        let pane = cx.new(|cx| {
2927            let mut pane = Pane::new(
2928                self.weak_handle(),
2929                self.project.clone(),
2930                self.pane_history_timestamp.clone(),
2931                None,
2932                NewFile.boxed_clone(),
2933                window,
2934                cx,
2935            );
2936            pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
2937            pane
2938        });
2939        cx.subscribe_in(&pane, window, Self::handle_pane_event)
2940            .detach();
2941        self.panes.push(pane.clone());
2942
2943        window.focus(&pane.focus_handle(cx));
2944
2945        cx.emit(Event::PaneAdded(pane.clone()));
2946        pane
2947    }
2948
2949    pub fn add_item_to_center(
2950        &mut self,
2951        item: Box<dyn ItemHandle>,
2952        window: &mut Window,
2953        cx: &mut Context<Self>,
2954    ) -> bool {
2955        if let Some(center_pane) = self.last_active_center_pane.clone() {
2956            if let Some(center_pane) = center_pane.upgrade() {
2957                center_pane.update(cx, |pane, cx| {
2958                    pane.add_item(item, true, true, None, window, cx)
2959                });
2960                true
2961            } else {
2962                false
2963            }
2964        } else {
2965            false
2966        }
2967    }
2968
2969    pub fn add_item_to_active_pane(
2970        &mut self,
2971        item: Box<dyn ItemHandle>,
2972        destination_index: Option<usize>,
2973        focus_item: bool,
2974        window: &mut Window,
2975        cx: &mut App,
2976    ) {
2977        self.add_item(
2978            self.active_pane.clone(),
2979            item,
2980            destination_index,
2981            false,
2982            focus_item,
2983            window,
2984            cx,
2985        )
2986    }
2987
2988    pub fn add_item(
2989        &mut self,
2990        pane: Entity<Pane>,
2991        item: Box<dyn ItemHandle>,
2992        destination_index: Option<usize>,
2993        activate_pane: bool,
2994        focus_item: bool,
2995        window: &mut Window,
2996        cx: &mut App,
2997    ) {
2998        if let Some(text) = item.telemetry_event_text(cx) {
2999            telemetry::event!(text);
3000        }
3001
3002        pane.update(cx, |pane, cx| {
3003            pane.add_item(
3004                item,
3005                activate_pane,
3006                focus_item,
3007                destination_index,
3008                window,
3009                cx,
3010            )
3011        });
3012    }
3013
3014    pub fn split_item(
3015        &mut self,
3016        split_direction: SplitDirection,
3017        item: Box<dyn ItemHandle>,
3018        window: &mut Window,
3019        cx: &mut Context<Self>,
3020    ) {
3021        let new_pane = self.split_pane(self.active_pane.clone(), split_direction, window, cx);
3022        self.add_item(new_pane, item, None, true, true, window, cx);
3023    }
3024
3025    pub fn open_abs_path(
3026        &mut self,
3027        abs_path: PathBuf,
3028        options: OpenOptions,
3029        window: &mut Window,
3030        cx: &mut Context<Self>,
3031    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3032        cx.spawn_in(window, async move |workspace, cx| {
3033            let open_paths_task_result = workspace
3034                .update_in(cx, |workspace, window, cx| {
3035                    workspace.open_paths(vec![abs_path.clone()], options, None, window, cx)
3036                })
3037                .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
3038                .await;
3039            anyhow::ensure!(
3040                open_paths_task_result.len() == 1,
3041                "open abs path {abs_path:?} task returned incorrect number of results"
3042            );
3043            match open_paths_task_result
3044                .into_iter()
3045                .next()
3046                .expect("ensured single task result")
3047            {
3048                Some(open_result) => {
3049                    open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
3050                }
3051                None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
3052            }
3053        })
3054    }
3055
3056    pub fn split_abs_path(
3057        &mut self,
3058        abs_path: PathBuf,
3059        visible: bool,
3060        window: &mut Window,
3061        cx: &mut Context<Self>,
3062    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3063        let project_path_task =
3064            Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
3065        cx.spawn_in(window, async move |this, cx| {
3066            let (_, path) = project_path_task.await?;
3067            this.update_in(cx, |this, window, cx| this.split_path(path, window, cx))?
3068                .await
3069        })
3070    }
3071
3072    pub fn open_path(
3073        &mut self,
3074        path: impl Into<ProjectPath>,
3075        pane: Option<WeakEntity<Pane>>,
3076        focus_item: bool,
3077        window: &mut Window,
3078        cx: &mut App,
3079    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3080        self.open_path_preview(path, pane, focus_item, false, true, window, cx)
3081    }
3082
3083    pub fn open_path_preview(
3084        &mut self,
3085        path: impl Into<ProjectPath>,
3086        pane: Option<WeakEntity<Pane>>,
3087        focus_item: bool,
3088        allow_preview: bool,
3089        activate: bool,
3090        window: &mut Window,
3091        cx: &mut App,
3092    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3093        let pane = pane.unwrap_or_else(|| {
3094            self.last_active_center_pane.clone().unwrap_or_else(|| {
3095                self.panes
3096                    .first()
3097                    .expect("There must be an active pane")
3098                    .downgrade()
3099            })
3100        });
3101
3102        let project_path = path.into();
3103        let task = self.load_path(project_path.clone(), window, cx);
3104        window.spawn(cx, async move |cx| {
3105            let (project_entry_id, build_item) = task.await?;
3106            let result = pane.update_in(cx, |pane, window, cx| {
3107                let result = pane.open_item(
3108                    project_entry_id,
3109                    project_path,
3110                    focus_item,
3111                    allow_preview,
3112                    activate,
3113                    None,
3114                    window,
3115                    cx,
3116                    build_item,
3117                );
3118
3119                result
3120            });
3121            result
3122        })
3123    }
3124
3125    pub fn split_path(
3126        &mut self,
3127        path: impl Into<ProjectPath>,
3128        window: &mut Window,
3129        cx: &mut Context<Self>,
3130    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3131        self.split_path_preview(path, false, None, window, cx)
3132    }
3133
3134    pub fn split_path_preview(
3135        &mut self,
3136        path: impl Into<ProjectPath>,
3137        allow_preview: bool,
3138        split_direction: Option<SplitDirection>,
3139        window: &mut Window,
3140        cx: &mut Context<Self>,
3141    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3142        let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
3143            self.panes
3144                .first()
3145                .expect("There must be an active pane")
3146                .downgrade()
3147        });
3148
3149        if let Member::Pane(center_pane) = &self.center.root {
3150            if center_pane.read(cx).items_len() == 0 {
3151                return self.open_path(path, Some(pane), true, window, cx);
3152            }
3153        }
3154
3155        let project_path = path.into();
3156        let task = self.load_path(project_path.clone(), window, cx);
3157        cx.spawn_in(window, async move |this, cx| {
3158            let (project_entry_id, build_item) = task.await?;
3159            this.update_in(cx, move |this, window, cx| -> Option<_> {
3160                let pane = pane.upgrade()?;
3161                let new_pane = this.split_pane(
3162                    pane,
3163                    split_direction.unwrap_or(SplitDirection::Right),
3164                    window,
3165                    cx,
3166                );
3167                new_pane.update(cx, |new_pane, cx| {
3168                    Some(new_pane.open_item(
3169                        project_entry_id,
3170                        project_path,
3171                        true,
3172                        allow_preview,
3173                        true,
3174                        None,
3175                        window,
3176                        cx,
3177                        build_item,
3178                    ))
3179                })
3180            })
3181            .map(|option| option.context("pane was dropped"))?
3182        })
3183    }
3184
3185    fn load_path(
3186        &mut self,
3187        path: ProjectPath,
3188        window: &mut Window,
3189        cx: &mut App,
3190    ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
3191        let project = self.project().clone();
3192        let registry = cx.default_global::<ProjectItemRegistry>().clone();
3193        registry.open_path(&project, &path, window, cx)
3194    }
3195
3196    pub fn find_project_item<T>(
3197        &self,
3198        pane: &Entity<Pane>,
3199        project_item: &Entity<T::Item>,
3200        cx: &App,
3201    ) -> Option<Entity<T>>
3202    where
3203        T: ProjectItem,
3204    {
3205        use project::ProjectItem as _;
3206        let project_item = project_item.read(cx);
3207        let entry_id = project_item.entry_id(cx);
3208        let project_path = project_item.project_path(cx);
3209
3210        let mut item = None;
3211        if let Some(entry_id) = entry_id {
3212            item = pane.read(cx).item_for_entry(entry_id, cx);
3213        }
3214        if item.is_none() {
3215            if let Some(project_path) = project_path {
3216                item = pane.read(cx).item_for_path(project_path, cx);
3217            }
3218        }
3219
3220        item.and_then(|item| item.downcast::<T>())
3221    }
3222
3223    pub fn is_project_item_open<T>(
3224        &self,
3225        pane: &Entity<Pane>,
3226        project_item: &Entity<T::Item>,
3227        cx: &App,
3228    ) -> bool
3229    where
3230        T: ProjectItem,
3231    {
3232        self.find_project_item::<T>(pane, project_item, cx)
3233            .is_some()
3234    }
3235
3236    pub fn open_project_item<T>(
3237        &mut self,
3238        pane: Entity<Pane>,
3239        project_item: Entity<T::Item>,
3240        activate_pane: bool,
3241        focus_item: bool,
3242        window: &mut Window,
3243        cx: &mut Context<Self>,
3244    ) -> Entity<T>
3245    where
3246        T: ProjectItem,
3247    {
3248        if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
3249            self.activate_item(&item, activate_pane, focus_item, window, cx);
3250            return item;
3251        }
3252
3253        let item = pane.update(cx, |pane, cx| {
3254            cx.new(|cx| {
3255                T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx)
3256            })
3257        });
3258        let item_id = item.item_id();
3259        let mut destination_index = None;
3260        pane.update(cx, |pane, cx| {
3261            if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
3262                if let Some(preview_item_id) = pane.preview_item_id() {
3263                    if preview_item_id != item_id {
3264                        destination_index = pane.close_current_preview_item(window, cx);
3265                    }
3266                }
3267            }
3268            pane.set_preview_item_id(Some(item.item_id()), cx)
3269        });
3270
3271        self.add_item(
3272            pane,
3273            Box::new(item.clone()),
3274            destination_index,
3275            activate_pane,
3276            focus_item,
3277            window,
3278            cx,
3279        );
3280        item
3281    }
3282
3283    pub fn open_shared_screen(
3284        &mut self,
3285        peer_id: PeerId,
3286        window: &mut Window,
3287        cx: &mut Context<Self>,
3288    ) {
3289        if let Some(shared_screen) =
3290            self.shared_screen_for_peer(peer_id, &self.active_pane, window, cx)
3291        {
3292            self.active_pane.update(cx, |pane, cx| {
3293                pane.add_item(Box::new(shared_screen), false, true, None, window, cx)
3294            });
3295        }
3296    }
3297
3298    pub fn activate_item(
3299        &mut self,
3300        item: &dyn ItemHandle,
3301        activate_pane: bool,
3302        focus_item: bool,
3303        window: &mut Window,
3304        cx: &mut App,
3305    ) -> bool {
3306        let result = self.panes.iter().find_map(|pane| {
3307            pane.read(cx)
3308                .index_for_item(item)
3309                .map(|ix| (pane.clone(), ix))
3310        });
3311        if let Some((pane, ix)) = result {
3312            pane.update(cx, |pane, cx| {
3313                pane.activate_item(ix, activate_pane, focus_item, window, cx)
3314            });
3315            true
3316        } else {
3317            false
3318        }
3319    }
3320
3321    fn activate_pane_at_index(
3322        &mut self,
3323        action: &ActivatePane,
3324        window: &mut Window,
3325        cx: &mut Context<Self>,
3326    ) {
3327        let panes = self.center.panes();
3328        if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
3329            window.focus(&pane.focus_handle(cx));
3330        } else {
3331            self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx);
3332        }
3333    }
3334
3335    fn move_item_to_pane_at_index(
3336        &mut self,
3337        action: &MoveItemToPane,
3338        window: &mut Window,
3339        cx: &mut Context<Self>,
3340    ) {
3341        let Some(&target_pane) = self.center.panes().get(action.destination) else {
3342            return;
3343        };
3344        move_active_item(
3345            &self.active_pane,
3346            target_pane,
3347            action.focus,
3348            true,
3349            window,
3350            cx,
3351        );
3352    }
3353
3354    pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) {
3355        let panes = self.center.panes();
3356        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
3357            let next_ix = (ix + 1) % panes.len();
3358            let next_pane = panes[next_ix].clone();
3359            window.focus(&next_pane.focus_handle(cx));
3360        }
3361    }
3362
3363    pub fn activate_previous_pane(&mut self, window: &mut Window, cx: &mut App) {
3364        let panes = self.center.panes();
3365        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
3366            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
3367            let prev_pane = panes[prev_ix].clone();
3368            window.focus(&prev_pane.focus_handle(cx));
3369        }
3370    }
3371
3372    pub fn activate_pane_in_direction(
3373        &mut self,
3374        direction: SplitDirection,
3375        window: &mut Window,
3376        cx: &mut App,
3377    ) {
3378        use ActivateInDirectionTarget as Target;
3379        enum Origin {
3380            LeftDock,
3381            RightDock,
3382            BottomDock,
3383            Center,
3384        }
3385
3386        let origin: Origin = [
3387            (&self.left_dock, Origin::LeftDock),
3388            (&self.right_dock, Origin::RightDock),
3389            (&self.bottom_dock, Origin::BottomDock),
3390        ]
3391        .into_iter()
3392        .find_map(|(dock, origin)| {
3393            if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() {
3394                Some(origin)
3395            } else {
3396                None
3397            }
3398        })
3399        .unwrap_or(Origin::Center);
3400
3401        let get_last_active_pane = || {
3402            let pane = self
3403                .last_active_center_pane
3404                .clone()
3405                .unwrap_or_else(|| {
3406                    self.panes
3407                        .first()
3408                        .expect("There must be an active pane")
3409                        .downgrade()
3410                })
3411                .upgrade()?;
3412            (pane.read(cx).items_len() != 0).then_some(pane)
3413        };
3414
3415        let try_dock =
3416            |dock: &Entity<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
3417
3418        let target = match (origin, direction) {
3419            // We're in the center, so we first try to go to a different pane,
3420            // otherwise try to go to a dock.
3421            (Origin::Center, direction) => {
3422                if let Some(pane) = self.find_pane_in_direction(direction, cx) {
3423                    Some(Target::Pane(pane))
3424                } else {
3425                    match direction {
3426                        SplitDirection::Up => None,
3427                        SplitDirection::Down => try_dock(&self.bottom_dock),
3428                        SplitDirection::Left => try_dock(&self.left_dock),
3429                        SplitDirection::Right => try_dock(&self.right_dock),
3430                    }
3431                }
3432            }
3433
3434            (Origin::LeftDock, SplitDirection::Right) => {
3435                if let Some(last_active_pane) = get_last_active_pane() {
3436                    Some(Target::Pane(last_active_pane))
3437                } else {
3438                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
3439                }
3440            }
3441
3442            (Origin::LeftDock, SplitDirection::Down)
3443            | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
3444
3445            (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
3446            (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
3447            (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
3448
3449            (Origin::RightDock, SplitDirection::Left) => {
3450                if let Some(last_active_pane) = get_last_active_pane() {
3451                    Some(Target::Pane(last_active_pane))
3452                } else {
3453                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
3454                }
3455            }
3456
3457            _ => None,
3458        };
3459
3460        match target {
3461            Some(ActivateInDirectionTarget::Pane(pane)) => {
3462                window.focus(&pane.focus_handle(cx));
3463            }
3464            Some(ActivateInDirectionTarget::Dock(dock)) => {
3465                // Defer this to avoid a panic when the dock's active panel is already on the stack.
3466                window.defer(cx, move |window, cx| {
3467                    let dock = dock.read(cx);
3468                    if let Some(panel) = dock.active_panel() {
3469                        panel.panel_focus_handle(cx).focus(window);
3470                    } else {
3471                        log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
3472                    }
3473                })
3474            }
3475            None => {}
3476        }
3477    }
3478
3479    pub fn move_item_to_pane_in_direction(
3480        &mut self,
3481        action: &MoveItemToPaneInDirection,
3482        window: &mut Window,
3483        cx: &mut App,
3484    ) {
3485        if let Some(destination) = self.find_pane_in_direction(action.direction, cx) {
3486            move_active_item(
3487                &self.active_pane,
3488                &destination,
3489                action.focus,
3490                true,
3491                window,
3492                cx,
3493            );
3494        }
3495    }
3496
3497    pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
3498        self.center.bounding_box_for_pane(pane)
3499    }
3500
3501    pub fn find_pane_in_direction(
3502        &mut self,
3503        direction: SplitDirection,
3504        cx: &App,
3505    ) -> Option<Entity<Pane>> {
3506        self.center
3507            .find_pane_in_direction(&self.active_pane, direction, cx)
3508            .cloned()
3509    }
3510
3511    pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
3512        if let Some(to) = self.find_pane_in_direction(direction, cx) {
3513            self.center.swap(&self.active_pane, &to);
3514            cx.notify();
3515        }
3516    }
3517
3518    pub fn resize_pane(
3519        &mut self,
3520        axis: gpui::Axis,
3521        amount: Pixels,
3522        window: &mut Window,
3523        cx: &mut Context<Self>,
3524    ) {
3525        let docks = self.all_docks();
3526        let active_dock = docks
3527            .into_iter()
3528            .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
3529
3530        if let Some(dock) = active_dock {
3531            let Some(panel_size) = dock.read(cx).active_panel_size(window, cx) else {
3532                return;
3533            };
3534            match dock.read(cx).position() {
3535                DockPosition::Left => resize_left_dock(panel_size + amount, self, window, cx),
3536                DockPosition::Bottom => resize_bottom_dock(panel_size + amount, self, window, cx),
3537                DockPosition::Right => resize_right_dock(panel_size + amount, self, window, cx),
3538            }
3539        } else {
3540            self.center
3541                .resize(&self.active_pane, axis, amount, &self.bounds);
3542        }
3543        cx.notify();
3544    }
3545
3546    pub fn reset_pane_sizes(&mut self, cx: &mut Context<Self>) {
3547        self.center.reset_pane_sizes();
3548        cx.notify();
3549    }
3550
3551    fn handle_pane_focused(
3552        &mut self,
3553        pane: Entity<Pane>,
3554        window: &mut Window,
3555        cx: &mut Context<Self>,
3556    ) {
3557        // This is explicitly hoisted out of the following check for pane identity as
3558        // terminal panel panes are not registered as a center panes.
3559        self.status_bar.update(cx, |status_bar, cx| {
3560            status_bar.set_active_pane(&pane, window, cx);
3561        });
3562        if self.active_pane != pane {
3563            self.set_active_pane(&pane, window, cx);
3564        }
3565
3566        if self.last_active_center_pane.is_none() {
3567            self.last_active_center_pane = Some(pane.downgrade());
3568        }
3569
3570        self.dismiss_zoomed_items_to_reveal(None, window, cx);
3571        if pane.read(cx).is_zoomed() {
3572            self.zoomed = Some(pane.downgrade().into());
3573        } else {
3574            self.zoomed = None;
3575        }
3576        self.zoomed_position = None;
3577        cx.emit(Event::ZoomChanged);
3578        self.update_active_view_for_followers(window, cx);
3579        pane.update(cx, |pane, _| {
3580            pane.track_alternate_file_items();
3581        });
3582
3583        cx.notify();
3584    }
3585
3586    fn set_active_pane(
3587        &mut self,
3588        pane: &Entity<Pane>,
3589        window: &mut Window,
3590        cx: &mut Context<Self>,
3591    ) {
3592        self.active_pane = pane.clone();
3593        self.active_item_path_changed(window, cx);
3594        self.last_active_center_pane = Some(pane.downgrade());
3595    }
3596
3597    fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3598        self.update_active_view_for_followers(window, cx);
3599    }
3600
3601    fn handle_pane_event(
3602        &mut self,
3603        pane: &Entity<Pane>,
3604        event: &pane::Event,
3605        window: &mut Window,
3606        cx: &mut Context<Self>,
3607    ) {
3608        let mut serialize_workspace = true;
3609        match event {
3610            pane::Event::AddItem { item } => {
3611                item.added_to_pane(self, pane.clone(), window, cx);
3612                cx.emit(Event::ItemAdded {
3613                    item: item.boxed_clone(),
3614                });
3615            }
3616            pane::Event::Split(direction) => {
3617                self.split_and_clone(pane.clone(), *direction, window, cx);
3618            }
3619            pane::Event::JoinIntoNext => {
3620                self.join_pane_into_next(pane.clone(), window, cx);
3621            }
3622            pane::Event::JoinAll => {
3623                self.join_all_panes(window, cx);
3624            }
3625            pane::Event::Remove { focus_on_pane } => {
3626                self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx);
3627            }
3628            pane::Event::ActivateItem {
3629                local,
3630                focus_changed,
3631            } => {
3632                cx.on_next_frame(window, |_, window, _| {
3633                    window.invalidate_character_coordinates();
3634                });
3635
3636                pane.update(cx, |pane, _| {
3637                    pane.track_alternate_file_items();
3638                });
3639                if *local {
3640                    self.unfollow_in_pane(&pane, window, cx);
3641                }
3642                if pane == self.active_pane() {
3643                    self.active_item_path_changed(window, cx);
3644                    self.update_active_view_for_followers(window, cx);
3645                }
3646                serialize_workspace = *focus_changed || pane != self.active_pane();
3647            }
3648            pane::Event::UserSavedItem { item, save_intent } => {
3649                cx.emit(Event::UserSavedItem {
3650                    pane: pane.downgrade(),
3651                    item: item.boxed_clone(),
3652                    save_intent: *save_intent,
3653                });
3654                serialize_workspace = false;
3655            }
3656            pane::Event::ChangeItemTitle => {
3657                if *pane == self.active_pane {
3658                    self.active_item_path_changed(window, cx);
3659                }
3660                serialize_workspace = false;
3661            }
3662            pane::Event::RemoveItem { .. } => {}
3663            pane::Event::RemovedItem { item } => {
3664                cx.emit(Event::ActiveItemChanged);
3665                self.update_window_edited(window, cx);
3666                if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id()) {
3667                    if entry.get().entity_id() == pane.entity_id() {
3668                        entry.remove();
3669                    }
3670                }
3671            }
3672            pane::Event::Focus => {
3673                cx.on_next_frame(window, |_, window, _| {
3674                    window.invalidate_character_coordinates();
3675                });
3676                self.handle_pane_focused(pane.clone(), window, cx);
3677            }
3678            pane::Event::ZoomIn => {
3679                if *pane == self.active_pane {
3680                    pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
3681                    if pane.read(cx).has_focus(window, cx) {
3682                        self.zoomed = Some(pane.downgrade().into());
3683                        self.zoomed_position = None;
3684                        cx.emit(Event::ZoomChanged);
3685                    }
3686                    cx.notify();
3687                }
3688            }
3689            pane::Event::ZoomOut => {
3690                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3691                if self.zoomed_position.is_none() {
3692                    self.zoomed = None;
3693                    cx.emit(Event::ZoomChanged);
3694                }
3695                cx.notify();
3696            }
3697        }
3698
3699        if serialize_workspace {
3700            self.serialize_workspace(window, cx);
3701        }
3702    }
3703
3704    pub fn unfollow_in_pane(
3705        &mut self,
3706        pane: &Entity<Pane>,
3707        window: &mut Window,
3708        cx: &mut Context<Workspace>,
3709    ) -> Option<CollaboratorId> {
3710        let leader_id = self.leader_for_pane(pane)?;
3711        self.unfollow(leader_id, window, cx);
3712        Some(leader_id)
3713    }
3714
3715    pub fn split_pane(
3716        &mut self,
3717        pane_to_split: Entity<Pane>,
3718        split_direction: SplitDirection,
3719        window: &mut Window,
3720        cx: &mut Context<Self>,
3721    ) -> Entity<Pane> {
3722        let new_pane = self.add_pane(window, cx);
3723        self.center
3724            .split(&pane_to_split, &new_pane, split_direction)
3725            .unwrap();
3726        cx.notify();
3727        new_pane
3728    }
3729
3730    pub fn split_and_clone(
3731        &mut self,
3732        pane: Entity<Pane>,
3733        direction: SplitDirection,
3734        window: &mut Window,
3735        cx: &mut Context<Self>,
3736    ) -> Option<Entity<Pane>> {
3737        let item = pane.read(cx).active_item()?;
3738        let maybe_pane_handle =
3739            if let Some(clone) = item.clone_on_split(self.database_id(), window, cx) {
3740                let new_pane = self.add_pane(window, cx);
3741                new_pane.update(cx, |pane, cx| {
3742                    pane.add_item(clone, true, true, None, window, cx)
3743                });
3744                self.center.split(&pane, &new_pane, direction).unwrap();
3745                Some(new_pane)
3746            } else {
3747                None
3748            };
3749        cx.notify();
3750        maybe_pane_handle
3751    }
3752
3753    pub fn split_pane_with_item(
3754        &mut self,
3755        pane_to_split: WeakEntity<Pane>,
3756        split_direction: SplitDirection,
3757        from: WeakEntity<Pane>,
3758        item_id_to_move: EntityId,
3759        window: &mut Window,
3760        cx: &mut Context<Self>,
3761    ) {
3762        let Some(pane_to_split) = pane_to_split.upgrade() else {
3763            return;
3764        };
3765        let Some(from) = from.upgrade() else {
3766            return;
3767        };
3768
3769        let new_pane = self.add_pane(window, cx);
3770        move_item(&from, &new_pane, item_id_to_move, 0, window, cx);
3771        self.center
3772            .split(&pane_to_split, &new_pane, split_direction)
3773            .unwrap();
3774        cx.notify();
3775    }
3776
3777    pub fn split_pane_with_project_entry(
3778        &mut self,
3779        pane_to_split: WeakEntity<Pane>,
3780        split_direction: SplitDirection,
3781        project_entry: ProjectEntryId,
3782        window: &mut Window,
3783        cx: &mut Context<Self>,
3784    ) -> Option<Task<Result<()>>> {
3785        let pane_to_split = pane_to_split.upgrade()?;
3786        let new_pane = self.add_pane(window, cx);
3787        self.center
3788            .split(&pane_to_split, &new_pane, split_direction)
3789            .unwrap();
3790
3791        let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
3792        let task = self.open_path(path, Some(new_pane.downgrade()), true, window, cx);
3793        Some(cx.foreground_executor().spawn(async move {
3794            task.await?;
3795            Ok(())
3796        }))
3797    }
3798
3799    pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3800        let active_item = self.active_pane.read(cx).active_item();
3801        for pane in &self.panes {
3802            join_pane_into_active(&self.active_pane, pane, window, cx);
3803        }
3804        if let Some(active_item) = active_item {
3805            self.activate_item(active_item.as_ref(), true, true, window, cx);
3806        }
3807        cx.notify();
3808    }
3809
3810    pub fn join_pane_into_next(
3811        &mut self,
3812        pane: Entity<Pane>,
3813        window: &mut Window,
3814        cx: &mut Context<Self>,
3815    ) {
3816        let next_pane = self
3817            .find_pane_in_direction(SplitDirection::Right, cx)
3818            .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
3819            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3820            .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
3821        let Some(next_pane) = next_pane else {
3822            return;
3823        };
3824        move_all_items(&pane, &next_pane, window, cx);
3825        cx.notify();
3826    }
3827
3828    fn remove_pane(
3829        &mut self,
3830        pane: Entity<Pane>,
3831        focus_on: Option<Entity<Pane>>,
3832        window: &mut Window,
3833        cx: &mut Context<Self>,
3834    ) {
3835        if self.center.remove(&pane).unwrap() {
3836            self.force_remove_pane(&pane, &focus_on, window, cx);
3837            self.unfollow_in_pane(&pane, window, cx);
3838            self.last_leaders_by_pane.remove(&pane.downgrade());
3839            for removed_item in pane.read(cx).items() {
3840                self.panes_by_item.remove(&removed_item.item_id());
3841            }
3842
3843            cx.notify();
3844        } else {
3845            self.active_item_path_changed(window, cx);
3846        }
3847        cx.emit(Event::PaneRemoved);
3848    }
3849
3850    pub fn panes(&self) -> &[Entity<Pane>] {
3851        &self.panes
3852    }
3853
3854    pub fn active_pane(&self) -> &Entity<Pane> {
3855        &self.active_pane
3856    }
3857
3858    pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> {
3859        for dock in self.all_docks() {
3860            if dock.focus_handle(cx).contains_focused(window, cx) {
3861                if let Some(pane) = dock
3862                    .read(cx)
3863                    .active_panel()
3864                    .and_then(|panel| panel.pane(cx))
3865                {
3866                    return pane;
3867                }
3868            }
3869        }
3870        self.active_pane().clone()
3871    }
3872
3873    pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
3874        self.find_pane_in_direction(SplitDirection::Right, cx)
3875            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3876            .unwrap_or_else(|| {
3877                self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx)
3878            })
3879            .clone()
3880    }
3881
3882    pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> {
3883        let weak_pane = self.panes_by_item.get(&handle.item_id())?;
3884        weak_pane.upgrade()
3885    }
3886
3887    fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
3888        self.follower_states.retain(|leader_id, state| {
3889            if *leader_id == CollaboratorId::PeerId(peer_id) {
3890                for item in state.items_by_leader_view_id.values() {
3891                    item.view.set_leader_id(None, window, cx);
3892                }
3893                false
3894            } else {
3895                true
3896            }
3897        });
3898        cx.notify();
3899    }
3900
3901    pub fn start_following(
3902        &mut self,
3903        leader_id: impl Into<CollaboratorId>,
3904        window: &mut Window,
3905        cx: &mut Context<Self>,
3906    ) -> Option<Task<Result<()>>> {
3907        let leader_id = leader_id.into();
3908        let pane = self.active_pane().clone();
3909
3910        self.last_leaders_by_pane
3911            .insert(pane.downgrade(), leader_id);
3912        self.unfollow(leader_id, window, cx);
3913        self.unfollow_in_pane(&pane, window, cx);
3914        self.follower_states.insert(
3915            leader_id,
3916            FollowerState {
3917                center_pane: pane.clone(),
3918                dock_pane: None,
3919                active_view_id: None,
3920                items_by_leader_view_id: Default::default(),
3921            },
3922        );
3923        cx.notify();
3924
3925        match leader_id {
3926            CollaboratorId::PeerId(leader_peer_id) => {
3927                let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3928                let project_id = self.project.read(cx).remote_id();
3929                let request = self.app_state.client.request(proto::Follow {
3930                    room_id,
3931                    project_id,
3932                    leader_id: Some(leader_peer_id),
3933                });
3934
3935                Some(cx.spawn_in(window, async move |this, cx| {
3936                    let response = request.await?;
3937                    this.update(cx, |this, _| {
3938                        let state = this
3939                            .follower_states
3940                            .get_mut(&leader_id)
3941                            .context("following interrupted")?;
3942                        state.active_view_id = response
3943                            .active_view
3944                            .as_ref()
3945                            .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3946                        anyhow::Ok(())
3947                    })??;
3948                    if let Some(view) = response.active_view {
3949                        Self::add_view_from_leader(this.clone(), leader_peer_id, &view, cx).await?;
3950                    }
3951                    this.update_in(cx, |this, window, cx| {
3952                        this.leader_updated(leader_id, window, cx)
3953                    })?;
3954                    Ok(())
3955                }))
3956            }
3957            CollaboratorId::Agent => {
3958                self.leader_updated(leader_id, window, cx)?;
3959                Some(Task::ready(Ok(())))
3960            }
3961        }
3962    }
3963
3964    pub fn follow_next_collaborator(
3965        &mut self,
3966        _: &FollowNextCollaborator,
3967        window: &mut Window,
3968        cx: &mut Context<Self>,
3969    ) {
3970        let collaborators = self.project.read(cx).collaborators();
3971        let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
3972            let mut collaborators = collaborators.keys().copied();
3973            for peer_id in collaborators.by_ref() {
3974                if CollaboratorId::PeerId(peer_id) == leader_id {
3975                    break;
3976                }
3977            }
3978            collaborators.next().map(CollaboratorId::PeerId)
3979        } else if let Some(last_leader_id) =
3980            self.last_leaders_by_pane.get(&self.active_pane.downgrade())
3981        {
3982            match last_leader_id {
3983                CollaboratorId::PeerId(peer_id) => {
3984                    if collaborators.contains_key(peer_id) {
3985                        Some(*last_leader_id)
3986                    } else {
3987                        None
3988                    }
3989                }
3990                CollaboratorId::Agent => Some(CollaboratorId::Agent),
3991            }
3992        } else {
3993            None
3994        };
3995
3996        let pane = self.active_pane.clone();
3997        let Some(leader_id) = next_leader_id.or_else(|| {
3998            Some(CollaboratorId::PeerId(
3999                collaborators.keys().copied().next()?,
4000            ))
4001        }) else {
4002            return;
4003        };
4004        if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
4005            return;
4006        }
4007        if let Some(task) = self.start_following(leader_id, window, cx) {
4008            task.detach_and_log_err(cx)
4009        }
4010    }
4011
4012    pub fn follow(
4013        &mut self,
4014        leader_id: impl Into<CollaboratorId>,
4015        window: &mut Window,
4016        cx: &mut Context<Self>,
4017    ) {
4018        let leader_id = leader_id.into();
4019
4020        if let CollaboratorId::PeerId(peer_id) = leader_id {
4021            let Some(room) = ActiveCall::global(cx).read(cx).room() else {
4022                return;
4023            };
4024            let room = room.read(cx);
4025            let Some(remote_participant) = room.remote_participant_for_peer_id(peer_id) else {
4026                return;
4027            };
4028
4029            let project = self.project.read(cx);
4030
4031            let other_project_id = match remote_participant.location {
4032                call::ParticipantLocation::External => None,
4033                call::ParticipantLocation::UnsharedProject => None,
4034                call::ParticipantLocation::SharedProject { project_id } => {
4035                    if Some(project_id) == project.remote_id() {
4036                        None
4037                    } else {
4038                        Some(project_id)
4039                    }
4040                }
4041            };
4042
4043            // if they are active in another project, follow there.
4044            if let Some(project_id) = other_project_id {
4045                let app_state = self.app_state.clone();
4046                crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
4047                    .detach_and_log_err(cx);
4048            }
4049        }
4050
4051        // if you're already following, find the right pane and focus it.
4052        if let Some(follower_state) = self.follower_states.get(&leader_id) {
4053            window.focus(&follower_state.pane().focus_handle(cx));
4054
4055            return;
4056        }
4057
4058        // Otherwise, follow.
4059        if let Some(task) = self.start_following(leader_id, window, cx) {
4060            task.detach_and_log_err(cx)
4061        }
4062    }
4063
4064    pub fn unfollow(
4065        &mut self,
4066        leader_id: impl Into<CollaboratorId>,
4067        window: &mut Window,
4068        cx: &mut Context<Self>,
4069    ) -> Option<()> {
4070        cx.notify();
4071
4072        let leader_id = leader_id.into();
4073        let state = self.follower_states.remove(&leader_id)?;
4074        for (_, item) in state.items_by_leader_view_id {
4075            item.view.set_leader_id(None, window, cx);
4076        }
4077
4078        if let CollaboratorId::PeerId(leader_peer_id) = leader_id {
4079            let project_id = self.project.read(cx).remote_id();
4080            let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
4081            self.app_state
4082                .client
4083                .send(proto::Unfollow {
4084                    room_id,
4085                    project_id,
4086                    leader_id: Some(leader_peer_id),
4087                })
4088                .log_err();
4089        }
4090
4091        Some(())
4092    }
4093
4094    pub fn is_being_followed(&self, id: impl Into<CollaboratorId>) -> bool {
4095        self.follower_states.contains_key(&id.into())
4096    }
4097
4098    fn active_item_path_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4099        cx.emit(Event::ActiveItemChanged);
4100        let active_entry = self.active_project_path(cx);
4101        self.project
4102            .update(cx, |project, cx| project.set_active_path(active_entry, cx));
4103
4104        self.update_window_title(window, cx);
4105    }
4106
4107    fn update_window_title(&mut self, window: &mut Window, cx: &mut App) {
4108        let project = self.project().read(cx);
4109        let mut title = String::new();
4110
4111        for (i, name) in project.worktree_root_names(cx).enumerate() {
4112            if i > 0 {
4113                title.push_str(", ");
4114            }
4115            title.push_str(name);
4116        }
4117
4118        if title.is_empty() {
4119            title = "empty project".to_string();
4120        }
4121
4122        if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
4123            let filename = path
4124                .path
4125                .file_name()
4126                .map(|s| s.to_string_lossy())
4127                .or_else(|| {
4128                    Some(Cow::Borrowed(
4129                        project
4130                            .worktree_for_id(path.worktree_id, cx)?
4131                            .read(cx)
4132                            .root_name(),
4133                    ))
4134                });
4135
4136            if let Some(filename) = filename {
4137                title.push_str("");
4138                title.push_str(filename.as_ref());
4139            }
4140        }
4141
4142        if project.is_via_collab() {
4143            title.push_str("");
4144        } else if project.is_shared() {
4145            title.push_str("");
4146        }
4147
4148        window.set_window_title(&title);
4149    }
4150
4151    fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
4152        let is_edited = !self.project.read(cx).is_disconnected(cx) && !self.dirty_items.is_empty();
4153        if is_edited != self.window_edited {
4154            self.window_edited = is_edited;
4155            window.set_window_edited(self.window_edited)
4156        }
4157    }
4158
4159    fn update_item_dirty_state(
4160        &mut self,
4161        item: &dyn ItemHandle,
4162        window: &mut Window,
4163        cx: &mut App,
4164    ) {
4165        let is_dirty = item.is_dirty(cx);
4166        let item_id = item.item_id();
4167        let was_dirty = self.dirty_items.contains_key(&item_id);
4168        if is_dirty == was_dirty {
4169            return;
4170        }
4171        if was_dirty {
4172            self.dirty_items.remove(&item_id);
4173            self.update_window_edited(window, cx);
4174            return;
4175        }
4176        if let Some(window_handle) = window.window_handle().downcast::<Self>() {
4177            let s = item.on_release(
4178                cx,
4179                Box::new(move |cx| {
4180                    window_handle
4181                        .update(cx, |this, window, cx| {
4182                            this.dirty_items.remove(&item_id);
4183                            this.update_window_edited(window, cx)
4184                        })
4185                        .ok();
4186                }),
4187            );
4188            self.dirty_items.insert(item_id, s);
4189            self.update_window_edited(window, cx);
4190        }
4191    }
4192
4193    fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
4194        if self.notifications.is_empty() {
4195            None
4196        } else {
4197            Some(
4198                div()
4199                    .absolute()
4200                    .right_3()
4201                    .bottom_3()
4202                    .w_112()
4203                    .h_full()
4204                    .flex()
4205                    .flex_col()
4206                    .justify_end()
4207                    .gap_2()
4208                    .children(
4209                        self.notifications
4210                            .iter()
4211                            .map(|(_, notification)| notification.clone().into_any()),
4212                    ),
4213            )
4214        }
4215    }
4216
4217    // RPC handlers
4218
4219    fn active_view_for_follower(
4220        &self,
4221        follower_project_id: Option<u64>,
4222        window: &mut Window,
4223        cx: &mut Context<Self>,
4224    ) -> Option<proto::View> {
4225        let (item, panel_id) = self.active_item_for_followers(window, cx);
4226        let item = item?;
4227        let leader_id = self
4228            .pane_for(&*item)
4229            .and_then(|pane| self.leader_for_pane(&pane));
4230        let leader_peer_id = match leader_id {
4231            Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
4232            Some(CollaboratorId::Agent) | None => None,
4233        };
4234
4235        let item_handle = item.to_followable_item_handle(cx)?;
4236        let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
4237        let variant = item_handle.to_state_proto(window, cx)?;
4238
4239        if item_handle.is_project_item(window, cx)
4240            && (follower_project_id.is_none()
4241                || follower_project_id != self.project.read(cx).remote_id())
4242        {
4243            return None;
4244        }
4245
4246        Some(proto::View {
4247            id: id.to_proto(),
4248            leader_id: leader_peer_id,
4249            variant: Some(variant),
4250            panel_id: panel_id.map(|id| id as i32),
4251        })
4252    }
4253
4254    fn handle_follow(
4255        &mut self,
4256        follower_project_id: Option<u64>,
4257        window: &mut Window,
4258        cx: &mut Context<Self>,
4259    ) -> proto::FollowResponse {
4260        let active_view = self.active_view_for_follower(follower_project_id, window, cx);
4261
4262        cx.notify();
4263        proto::FollowResponse {
4264            // TODO: Remove after version 0.145.x stabilizes.
4265            active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
4266            views: active_view.iter().cloned().collect(),
4267            active_view,
4268        }
4269    }
4270
4271    fn handle_update_followers(
4272        &mut self,
4273        leader_id: PeerId,
4274        message: proto::UpdateFollowers,
4275        _window: &mut Window,
4276        _cx: &mut Context<Self>,
4277    ) {
4278        self.leader_updates_tx
4279            .unbounded_send((leader_id, message))
4280            .ok();
4281    }
4282
4283    async fn process_leader_update(
4284        this: &WeakEntity<Self>,
4285        leader_id: PeerId,
4286        update: proto::UpdateFollowers,
4287        cx: &mut AsyncWindowContext,
4288    ) -> Result<()> {
4289        match update.variant.context("invalid update")? {
4290            proto::update_followers::Variant::CreateView(view) => {
4291                let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
4292                let should_add_view = this.update(cx, |this, _| {
4293                    if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
4294                        anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
4295                    } else {
4296                        anyhow::Ok(false)
4297                    }
4298                })??;
4299
4300                if should_add_view {
4301                    Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
4302                }
4303            }
4304            proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
4305                let should_add_view = this.update(cx, |this, _| {
4306                    if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
4307                        state.active_view_id = update_active_view
4308                            .view
4309                            .as_ref()
4310                            .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
4311
4312                        if state.active_view_id.is_some_and(|view_id| {
4313                            !state.items_by_leader_view_id.contains_key(&view_id)
4314                        }) {
4315                            anyhow::Ok(true)
4316                        } else {
4317                            anyhow::Ok(false)
4318                        }
4319                    } else {
4320                        anyhow::Ok(false)
4321                    }
4322                })??;
4323
4324                if should_add_view {
4325                    if let Some(view) = update_active_view.view {
4326                        Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
4327                    }
4328                }
4329            }
4330            proto::update_followers::Variant::UpdateView(update_view) => {
4331                let variant = update_view.variant.context("missing update view variant")?;
4332                let id = update_view.id.context("missing update view id")?;
4333                let mut tasks = Vec::new();
4334                this.update_in(cx, |this, window, cx| {
4335                    let project = this.project.clone();
4336                    if let Some(state) = this.follower_states.get(&leader_id.into()) {
4337                        let view_id = ViewId::from_proto(id.clone())?;
4338                        if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
4339                            tasks.push(item.view.apply_update_proto(
4340                                &project,
4341                                variant.clone(),
4342                                window,
4343                                cx,
4344                            ));
4345                        }
4346                    }
4347                    anyhow::Ok(())
4348                })??;
4349                try_join_all(tasks).await.log_err();
4350            }
4351        }
4352        this.update_in(cx, |this, window, cx| {
4353            this.leader_updated(leader_id, window, cx)
4354        })?;
4355        Ok(())
4356    }
4357
4358    async fn add_view_from_leader(
4359        this: WeakEntity<Self>,
4360        leader_id: PeerId,
4361        view: &proto::View,
4362        cx: &mut AsyncWindowContext,
4363    ) -> Result<()> {
4364        let this = this.upgrade().context("workspace dropped")?;
4365
4366        let Some(id) = view.id.clone() else {
4367            anyhow::bail!("no id for view");
4368        };
4369        let id = ViewId::from_proto(id)?;
4370        let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
4371
4372        let pane = this.update(cx, |this, _cx| {
4373            let state = this
4374                .follower_states
4375                .get(&leader_id.into())
4376                .context("stopped following")?;
4377            anyhow::Ok(state.pane().clone())
4378        })??;
4379        let existing_item = pane.update_in(cx, |pane, window, cx| {
4380            let client = this.read(cx).client().clone();
4381            pane.items().find_map(|item| {
4382                let item = item.to_followable_item_handle(cx)?;
4383                if item.remote_id(&client, window, cx) == Some(id) {
4384                    Some(item)
4385                } else {
4386                    None
4387                }
4388            })
4389        })?;
4390        let item = if let Some(existing_item) = existing_item {
4391            existing_item
4392        } else {
4393            let variant = view.variant.clone();
4394            anyhow::ensure!(variant.is_some(), "missing view variant");
4395
4396            let task = cx.update(|window, cx| {
4397                FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
4398            })?;
4399
4400            let Some(task) = task else {
4401                anyhow::bail!(
4402                    "failed to construct view from leader (maybe from a different version of zed?)"
4403                );
4404            };
4405
4406            let mut new_item = task.await?;
4407            pane.update_in(cx, |pane, window, cx| {
4408                let mut item_to_remove = None;
4409                for (ix, item) in pane.items().enumerate() {
4410                    if let Some(item) = item.to_followable_item_handle(cx) {
4411                        match new_item.dedup(item.as_ref(), window, cx) {
4412                            Some(item::Dedup::KeepExisting) => {
4413                                new_item =
4414                                    item.boxed_clone().to_followable_item_handle(cx).unwrap();
4415                                break;
4416                            }
4417                            Some(item::Dedup::ReplaceExisting) => {
4418                                item_to_remove = Some((ix, item.item_id()));
4419                                break;
4420                            }
4421                            None => {}
4422                        }
4423                    }
4424                }
4425
4426                if let Some((ix, id)) = item_to_remove {
4427                    pane.remove_item(id, false, false, window, cx);
4428                    pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
4429                }
4430            })?;
4431
4432            new_item
4433        };
4434
4435        this.update_in(cx, |this, window, cx| {
4436            let state = this.follower_states.get_mut(&leader_id.into())?;
4437            item.set_leader_id(Some(leader_id.into()), window, cx);
4438            state.items_by_leader_view_id.insert(
4439                id,
4440                FollowerView {
4441                    view: item,
4442                    location: panel_id,
4443                },
4444            );
4445
4446            Some(())
4447        })?;
4448
4449        Ok(())
4450    }
4451
4452    fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4453        let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else {
4454            return;
4455        };
4456
4457        if let Some(agent_location) = self.project.read(cx).agent_location() {
4458            let buffer_entity_id = agent_location.buffer.entity_id();
4459            let view_id = ViewId {
4460                creator: CollaboratorId::Agent,
4461                id: buffer_entity_id.as_u64(),
4462            };
4463            follower_state.active_view_id = Some(view_id);
4464
4465            let item = match follower_state.items_by_leader_view_id.entry(view_id) {
4466                hash_map::Entry::Occupied(entry) => Some(entry.into_mut()),
4467                hash_map::Entry::Vacant(entry) => {
4468                    let existing_view =
4469                        follower_state
4470                            .center_pane
4471                            .read(cx)
4472                            .items()
4473                            .find_map(|item| {
4474                                let item = item.to_followable_item_handle(cx)?;
4475                                if item.is_singleton(cx)
4476                                    && item.project_item_model_ids(cx).as_slice()
4477                                        == [buffer_entity_id]
4478                                {
4479                                    Some(item)
4480                                } else {
4481                                    None
4482                                }
4483                            });
4484                    let view = existing_view.or_else(|| {
4485                        agent_location.buffer.upgrade().and_then(|buffer| {
4486                            cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| {
4487                                registry.build_item(buffer, self.project.clone(), None, window, cx)
4488                            })?
4489                            .to_followable_item_handle(cx)
4490                        })
4491                    });
4492
4493                    if let Some(view) = view {
4494                        Some(entry.insert(FollowerView {
4495                            view,
4496                            location: None,
4497                        }))
4498                    } else {
4499                        None
4500                    }
4501                }
4502            };
4503
4504            if let Some(item) = item {
4505                item.view
4506                    .set_leader_id(Some(CollaboratorId::Agent), window, cx);
4507                item.view
4508                    .update_agent_location(agent_location.position, window, cx);
4509            }
4510        } else {
4511            follower_state.active_view_id = None;
4512        }
4513
4514        self.leader_updated(CollaboratorId::Agent, window, cx);
4515    }
4516
4517    pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
4518        let mut is_project_item = true;
4519        let mut update = proto::UpdateActiveView::default();
4520        if window.is_window_active() {
4521            let (active_item, panel_id) = self.active_item_for_followers(window, cx);
4522
4523            if let Some(item) = active_item {
4524                if item.item_focus_handle(cx).contains_focused(window, cx) {
4525                    let leader_id = self
4526                        .pane_for(&*item)
4527                        .and_then(|pane| self.leader_for_pane(&pane));
4528                    let leader_peer_id = match leader_id {
4529                        Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
4530                        Some(CollaboratorId::Agent) | None => None,
4531                    };
4532
4533                    if let Some(item) = item.to_followable_item_handle(cx) {
4534                        let id = item
4535                            .remote_id(&self.app_state.client, window, cx)
4536                            .map(|id| id.to_proto());
4537
4538                        if let Some(id) = id.clone() {
4539                            if let Some(variant) = item.to_state_proto(window, cx) {
4540                                let view = Some(proto::View {
4541                                    id: id.clone(),
4542                                    leader_id: leader_peer_id,
4543                                    variant: Some(variant),
4544                                    panel_id: panel_id.map(|id| id as i32),
4545                                });
4546
4547                                is_project_item = item.is_project_item(window, cx);
4548                                update = proto::UpdateActiveView {
4549                                    view,
4550                                    // TODO: Remove after version 0.145.x stabilizes.
4551                                    id: id.clone(),
4552                                    leader_id: leader_peer_id,
4553                                };
4554                            }
4555                        };
4556                    }
4557                }
4558            }
4559        }
4560
4561        let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
4562        if active_view_id != self.last_active_view_id.as_ref() {
4563            self.last_active_view_id = active_view_id.cloned();
4564            self.update_followers(
4565                is_project_item,
4566                proto::update_followers::Variant::UpdateActiveView(update),
4567                window,
4568                cx,
4569            );
4570        }
4571    }
4572
4573    fn active_item_for_followers(
4574        &self,
4575        window: &mut Window,
4576        cx: &mut App,
4577    ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
4578        let mut active_item = None;
4579        let mut panel_id = None;
4580        for dock in self.all_docks() {
4581            if dock.focus_handle(cx).contains_focused(window, cx) {
4582                if let Some(panel) = dock.read(cx).active_panel() {
4583                    if let Some(pane) = panel.pane(cx) {
4584                        if let Some(item) = pane.read(cx).active_item() {
4585                            active_item = Some(item);
4586                            panel_id = panel.remote_id();
4587                            break;
4588                        }
4589                    }
4590                }
4591            }
4592        }
4593
4594        if active_item.is_none() {
4595            active_item = self.active_pane().read(cx).active_item();
4596        }
4597        (active_item, panel_id)
4598    }
4599
4600    fn update_followers(
4601        &self,
4602        project_only: bool,
4603        update: proto::update_followers::Variant,
4604        _: &mut Window,
4605        cx: &mut App,
4606    ) -> Option<()> {
4607        // If this update only applies to for followers in the current project,
4608        // then skip it unless this project is shared. If it applies to all
4609        // followers, regardless of project, then set `project_id` to none,
4610        // indicating that it goes to all followers.
4611        let project_id = if project_only {
4612            Some(self.project.read(cx).remote_id()?)
4613        } else {
4614            None
4615        };
4616        self.app_state().workspace_store.update(cx, |store, cx| {
4617            store.update_followers(project_id, update, cx)
4618        })
4619    }
4620
4621    pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<CollaboratorId> {
4622        self.follower_states.iter().find_map(|(leader_id, state)| {
4623            if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
4624                Some(*leader_id)
4625            } else {
4626                None
4627            }
4628        })
4629    }
4630
4631    fn leader_updated(
4632        &mut self,
4633        leader_id: impl Into<CollaboratorId>,
4634        window: &mut Window,
4635        cx: &mut Context<Self>,
4636    ) -> Option<Box<dyn ItemHandle>> {
4637        cx.notify();
4638
4639        let leader_id = leader_id.into();
4640        let (panel_id, item) = match leader_id {
4641            CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?,
4642            CollaboratorId::Agent => (None, self.active_item_for_agent()?),
4643        };
4644
4645        let state = self.follower_states.get(&leader_id)?;
4646        let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
4647        let pane;
4648        if let Some(panel_id) = panel_id {
4649            pane = self
4650                .activate_panel_for_proto_id(panel_id, window, cx)?
4651                .pane(cx)?;
4652            let state = self.follower_states.get_mut(&leader_id)?;
4653            state.dock_pane = Some(pane.clone());
4654        } else {
4655            pane = state.center_pane.clone();
4656            let state = self.follower_states.get_mut(&leader_id)?;
4657            if let Some(dock_pane) = state.dock_pane.take() {
4658                transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
4659            }
4660        }
4661
4662        pane.update(cx, |pane, cx| {
4663            let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
4664            if let Some(index) = pane.index_for_item(item.as_ref()) {
4665                pane.activate_item(index, false, false, window, cx);
4666            } else {
4667                pane.add_item(item.boxed_clone(), false, false, None, window, cx)
4668            }
4669
4670            if focus_active_item {
4671                pane.focus_active_item(window, cx)
4672            }
4673        });
4674
4675        Some(item)
4676    }
4677
4678    fn active_item_for_agent(&self) -> Option<Box<dyn ItemHandle>> {
4679        let state = self.follower_states.get(&CollaboratorId::Agent)?;
4680        let active_view_id = state.active_view_id?;
4681        Some(
4682            state
4683                .items_by_leader_view_id
4684                .get(&active_view_id)?
4685                .view
4686                .boxed_clone(),
4687        )
4688    }
4689
4690    fn active_item_for_peer(
4691        &self,
4692        peer_id: PeerId,
4693        window: &mut Window,
4694        cx: &mut Context<Self>,
4695    ) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
4696        let call = self.active_call()?;
4697        let room = call.read(cx).room()?.read(cx);
4698        let participant = room.remote_participant_for_peer_id(peer_id)?;
4699        let leader_in_this_app;
4700        let leader_in_this_project;
4701        match participant.location {
4702            call::ParticipantLocation::SharedProject { project_id } => {
4703                leader_in_this_app = true;
4704                leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
4705            }
4706            call::ParticipantLocation::UnsharedProject => {
4707                leader_in_this_app = true;
4708                leader_in_this_project = false;
4709            }
4710            call::ParticipantLocation::External => {
4711                leader_in_this_app = false;
4712                leader_in_this_project = false;
4713            }
4714        };
4715        let state = self.follower_states.get(&peer_id.into())?;
4716        let mut item_to_activate = None;
4717        if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
4718            if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
4719                if leader_in_this_project || !item.view.is_project_item(window, cx) {
4720                    item_to_activate = Some((item.location, item.view.boxed_clone()));
4721                }
4722            }
4723        } else if let Some(shared_screen) =
4724            self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx)
4725        {
4726            item_to_activate = Some((None, Box::new(shared_screen)));
4727        }
4728        item_to_activate
4729    }
4730
4731    fn shared_screen_for_peer(
4732        &self,
4733        peer_id: PeerId,
4734        pane: &Entity<Pane>,
4735        window: &mut Window,
4736        cx: &mut App,
4737    ) -> Option<Entity<SharedScreen>> {
4738        let call = self.active_call()?;
4739        let room = call.read(cx).room()?.clone();
4740        let participant = room.read(cx).remote_participant_for_peer_id(peer_id)?;
4741        let track = participant.video_tracks.values().next()?.clone();
4742        let user = participant.user.clone();
4743
4744        for item in pane.read(cx).items_of_type::<SharedScreen>() {
4745            if item.read(cx).peer_id == peer_id {
4746                return Some(item);
4747            }
4748        }
4749
4750        Some(cx.new(|cx| SharedScreen::new(track, peer_id, user.clone(), room.clone(), window, cx)))
4751    }
4752
4753    pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4754        if window.is_window_active() {
4755            self.update_active_view_for_followers(window, cx);
4756
4757            if let Some(database_id) = self.database_id {
4758                cx.background_spawn(persistence::DB.update_timestamp(database_id))
4759                    .detach();
4760            }
4761        } else {
4762            for pane in &self.panes {
4763                pane.update(cx, |pane, cx| {
4764                    if let Some(item) = pane.active_item() {
4765                        item.workspace_deactivated(window, cx);
4766                    }
4767                    for item in pane.items() {
4768                        if matches!(
4769                            item.workspace_settings(cx).autosave,
4770                            AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
4771                        ) {
4772                            Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
4773                                .detach_and_log_err(cx);
4774                        }
4775                    }
4776                });
4777            }
4778        }
4779    }
4780
4781    pub fn active_call(&self) -> Option<&Entity<ActiveCall>> {
4782        self.active_call.as_ref().map(|(call, _)| call)
4783    }
4784
4785    fn on_active_call_event(
4786        &mut self,
4787        _: &Entity<ActiveCall>,
4788        event: &call::room::Event,
4789        window: &mut Window,
4790        cx: &mut Context<Self>,
4791    ) {
4792        match event {
4793            call::room::Event::ParticipantLocationChanged { participant_id }
4794            | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4795                self.leader_updated(participant_id, window, cx);
4796            }
4797            _ => {}
4798        }
4799    }
4800
4801    pub fn database_id(&self) -> Option<WorkspaceId> {
4802        self.database_id
4803    }
4804
4805    pub fn session_id(&self) -> Option<String> {
4806        self.session_id.clone()
4807    }
4808
4809    fn local_paths(&self, cx: &App) -> Option<Vec<Arc<Path>>> {
4810        let project = self.project().read(cx);
4811
4812        if project.is_local() {
4813            Some(
4814                project
4815                    .visible_worktrees(cx)
4816                    .map(|worktree| worktree.read(cx).abs_path())
4817                    .collect::<Vec<_>>(),
4818            )
4819        } else {
4820            None
4821        }
4822    }
4823
4824    fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
4825        match member {
4826            Member::Axis(PaneAxis { members, .. }) => {
4827                for child in members.iter() {
4828                    self.remove_panes(child.clone(), window, cx)
4829                }
4830            }
4831            Member::Pane(pane) => {
4832                self.force_remove_pane(&pane, &None, window, cx);
4833            }
4834        }
4835    }
4836
4837    fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
4838        self.session_id.take();
4839        self.serialize_workspace_internal(window, cx)
4840    }
4841
4842    fn force_remove_pane(
4843        &mut self,
4844        pane: &Entity<Pane>,
4845        focus_on: &Option<Entity<Pane>>,
4846        window: &mut Window,
4847        cx: &mut Context<Workspace>,
4848    ) {
4849        self.panes.retain(|p| p != pane);
4850        if let Some(focus_on) = focus_on {
4851            focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4852        } else {
4853            if self.active_pane() == pane {
4854                self.panes
4855                    .last()
4856                    .unwrap()
4857                    .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4858            }
4859        }
4860        if self.last_active_center_pane == Some(pane.downgrade()) {
4861            self.last_active_center_pane = None;
4862        }
4863        cx.notify();
4864    }
4865
4866    fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4867        if self._schedule_serialize.is_none() {
4868            self._schedule_serialize = Some(cx.spawn_in(window, async move |this, cx| {
4869                cx.background_executor()
4870                    .timer(Duration::from_millis(100))
4871                    .await;
4872                this.update_in(cx, |this, window, cx| {
4873                    this.serialize_workspace_internal(window, cx).detach();
4874                    this._schedule_serialize.take();
4875                })
4876                .log_err();
4877            }));
4878        }
4879    }
4880
4881    fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
4882        let Some(database_id) = self.database_id() else {
4883            return Task::ready(());
4884        };
4885
4886        fn serialize_pane_handle(
4887            pane_handle: &Entity<Pane>,
4888            window: &mut Window,
4889            cx: &mut App,
4890        ) -> SerializedPane {
4891            let (items, active, pinned_count) = {
4892                let pane = pane_handle.read(cx);
4893                let active_item_id = pane.active_item().map(|item| item.item_id());
4894                (
4895                    pane.items()
4896                        .filter_map(|handle| {
4897                            let handle = handle.to_serializable_item_handle(cx)?;
4898
4899                            Some(SerializedItem {
4900                                kind: Arc::from(handle.serialized_item_kind()),
4901                                item_id: handle.item_id().as_u64(),
4902                                active: Some(handle.item_id()) == active_item_id,
4903                                preview: pane.is_active_preview_item(handle.item_id()),
4904                            })
4905                        })
4906                        .collect::<Vec<_>>(),
4907                    pane.has_focus(window, cx),
4908                    pane.pinned_count(),
4909                )
4910            };
4911
4912            SerializedPane::new(items, active, pinned_count)
4913        }
4914
4915        fn build_serialized_pane_group(
4916            pane_group: &Member,
4917            window: &mut Window,
4918            cx: &mut App,
4919        ) -> SerializedPaneGroup {
4920            match pane_group {
4921                Member::Axis(PaneAxis {
4922                    axis,
4923                    members,
4924                    flexes,
4925                    bounding_boxes: _,
4926                }) => SerializedPaneGroup::Group {
4927                    axis: SerializedAxis(*axis),
4928                    children: members
4929                        .iter()
4930                        .map(|member| build_serialized_pane_group(member, window, cx))
4931                        .collect::<Vec<_>>(),
4932                    flexes: Some(flexes.lock().clone()),
4933                },
4934                Member::Pane(pane_handle) => {
4935                    SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
4936                }
4937            }
4938        }
4939
4940        fn build_serialized_docks(
4941            this: &Workspace,
4942            window: &mut Window,
4943            cx: &mut App,
4944        ) -> DockStructure {
4945            let left_dock = this.left_dock.read(cx);
4946            let left_visible = left_dock.is_open();
4947            let left_active_panel = left_dock
4948                .active_panel()
4949                .map(|panel| panel.persistent_name().to_string());
4950            let left_dock_zoom = left_dock
4951                .active_panel()
4952                .map(|panel| panel.is_zoomed(window, cx))
4953                .unwrap_or(false);
4954
4955            let right_dock = this.right_dock.read(cx);
4956            let right_visible = right_dock.is_open();
4957            let right_active_panel = right_dock
4958                .active_panel()
4959                .map(|panel| panel.persistent_name().to_string());
4960            let right_dock_zoom = right_dock
4961                .active_panel()
4962                .map(|panel| panel.is_zoomed(window, cx))
4963                .unwrap_or(false);
4964
4965            let bottom_dock = this.bottom_dock.read(cx);
4966            let bottom_visible = bottom_dock.is_open();
4967            let bottom_active_panel = bottom_dock
4968                .active_panel()
4969                .map(|panel| panel.persistent_name().to_string());
4970            let bottom_dock_zoom = bottom_dock
4971                .active_panel()
4972                .map(|panel| panel.is_zoomed(window, cx))
4973                .unwrap_or(false);
4974
4975            DockStructure {
4976                left: DockData {
4977                    visible: left_visible,
4978                    active_panel: left_active_panel,
4979                    zoom: left_dock_zoom,
4980                },
4981                right: DockData {
4982                    visible: right_visible,
4983                    active_panel: right_active_panel,
4984                    zoom: right_dock_zoom,
4985                },
4986                bottom: DockData {
4987                    visible: bottom_visible,
4988                    active_panel: bottom_active_panel,
4989                    zoom: bottom_dock_zoom,
4990                },
4991            }
4992        }
4993
4994        if let Some(location) = self.serialize_workspace_location(cx) {
4995            let breakpoints = self.project.update(cx, |project, cx| {
4996                project
4997                    .breakpoint_store()
4998                    .read(cx)
4999                    .all_source_breakpoints(cx)
5000            });
5001
5002            let center_group = build_serialized_pane_group(&self.center.root, window, cx);
5003            let docks = build_serialized_docks(self, window, cx);
5004            let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
5005            let serialized_workspace = SerializedWorkspace {
5006                id: database_id,
5007                location,
5008                center_group,
5009                window_bounds,
5010                display: Default::default(),
5011                docks,
5012                centered_layout: self.centered_layout,
5013                session_id: self.session_id.clone(),
5014                breakpoints,
5015                window_id: Some(window.window_handle().window_id().as_u64()),
5016            };
5017
5018            return window.spawn(cx, async move |_| {
5019                persistence::DB.save_workspace(serialized_workspace).await;
5020            });
5021        }
5022        Task::ready(())
5023    }
5024
5025    fn serialize_workspace_location(&self, cx: &App) -> Option<SerializedWorkspaceLocation> {
5026        if let Some(ssh_project) = &self.serialized_ssh_project {
5027            Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
5028        } else if let Some(local_paths) = self.local_paths(cx) {
5029            if !local_paths.is_empty() {
5030                Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
5031            } else {
5032                None
5033            }
5034        } else {
5035            None
5036        }
5037    }
5038
5039    fn update_history(&self, cx: &mut App) {
5040        let Some(id) = self.database_id() else {
5041            return;
5042        };
5043        let Some(location) = self.serialize_workspace_location(cx) else {
5044            return;
5045        };
5046        if let Some(manager) = HistoryManager::global(cx) {
5047            manager.update(cx, |this, cx| {
5048                this.update_history(id, HistoryManagerEntry::new(id, &location), cx);
5049            });
5050        }
5051    }
5052
5053    async fn serialize_items(
5054        this: &WeakEntity<Self>,
5055        items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
5056        cx: &mut AsyncWindowContext,
5057    ) -> Result<()> {
5058        const CHUNK_SIZE: usize = 200;
5059
5060        let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
5061
5062        while let Some(items_received) = serializable_items.next().await {
5063            let unique_items =
5064                items_received
5065                    .into_iter()
5066                    .fold(HashMap::default(), |mut acc, item| {
5067                        acc.entry(item.item_id()).or_insert(item);
5068                        acc
5069                    });
5070
5071            // We use into_iter() here so that the references to the items are moved into
5072            // the tasks and not kept alive while we're sleeping.
5073            for (_, item) in unique_items.into_iter() {
5074                if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
5075                    item.serialize(workspace, false, window, cx)
5076                }) {
5077                    cx.background_spawn(async move { task.await.log_err() })
5078                        .detach();
5079                }
5080            }
5081
5082            cx.background_executor()
5083                .timer(SERIALIZATION_THROTTLE_TIME)
5084                .await;
5085        }
5086
5087        Ok(())
5088    }
5089
5090    pub(crate) fn enqueue_item_serialization(
5091        &mut self,
5092        item: Box<dyn SerializableItemHandle>,
5093    ) -> Result<()> {
5094        self.serializable_items_tx
5095            .unbounded_send(item)
5096            .map_err(|err| anyhow!("failed to send serializable item over channel: {err}"))
5097    }
5098
5099    pub(crate) fn load_workspace(
5100        serialized_workspace: SerializedWorkspace,
5101        paths_to_open: Vec<Option<ProjectPath>>,
5102        window: &mut Window,
5103        cx: &mut Context<Workspace>,
5104    ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
5105        cx.spawn_in(window, async move |workspace, cx| {
5106            let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
5107
5108            let mut center_group = None;
5109            let mut center_items = None;
5110
5111            // Traverse the splits tree and add to things
5112            if let Some((group, active_pane, items)) = serialized_workspace
5113                .center_group
5114                .deserialize(&project, serialized_workspace.id, workspace.clone(), cx)
5115                .await
5116            {
5117                center_items = Some(items);
5118                center_group = Some((group, active_pane))
5119            }
5120
5121            let mut items_by_project_path = HashMap::default();
5122            let mut item_ids_by_kind = HashMap::default();
5123            let mut all_deserialized_items = Vec::default();
5124            cx.update(|_, cx| {
5125                for item in center_items.unwrap_or_default().into_iter().flatten() {
5126                    if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
5127                        item_ids_by_kind
5128                            .entry(serializable_item_handle.serialized_item_kind())
5129                            .or_insert(Vec::new())
5130                            .push(item.item_id().as_u64() as ItemId);
5131                    }
5132
5133                    if let Some(project_path) = item.project_path(cx) {
5134                        items_by_project_path.insert(project_path, item.clone());
5135                    }
5136                    all_deserialized_items.push(item);
5137                }
5138            })?;
5139
5140            let opened_items = paths_to_open
5141                .into_iter()
5142                .map(|path_to_open| {
5143                    path_to_open
5144                        .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
5145                })
5146                .collect::<Vec<_>>();
5147
5148            // Remove old panes from workspace panes list
5149            workspace.update_in(cx, |workspace, window, cx| {
5150                if let Some((center_group, active_pane)) = center_group {
5151                    workspace.remove_panes(workspace.center.root.clone(), window, cx);
5152
5153                    // Swap workspace center group
5154                    workspace.center = PaneGroup::with_root(center_group);
5155                    if let Some(active_pane) = active_pane {
5156                        workspace.set_active_pane(&active_pane, window, cx);
5157                        cx.focus_self(window);
5158                    } else {
5159                        workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
5160                    }
5161                }
5162
5163                let docks = serialized_workspace.docks;
5164
5165                for (dock, serialized_dock) in [
5166                    (&mut workspace.right_dock, docks.right),
5167                    (&mut workspace.left_dock, docks.left),
5168                    (&mut workspace.bottom_dock, docks.bottom),
5169                ]
5170                .iter_mut()
5171                {
5172                    dock.update(cx, |dock, cx| {
5173                        dock.serialized_dock = Some(serialized_dock.clone());
5174                        dock.restore_state(window, cx);
5175                    });
5176                }
5177
5178                cx.notify();
5179            })?;
5180
5181            let _ = project
5182                .update(cx, |project, cx| {
5183                    project
5184                        .breakpoint_store()
5185                        .update(cx, |breakpoint_store, cx| {
5186                            breakpoint_store
5187                                .with_serialized_breakpoints(serialized_workspace.breakpoints, cx)
5188                        })
5189                })?
5190                .await;
5191
5192            // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
5193            // after loading the items, we might have different items and in order to avoid
5194            // the database filling up, we delete items that haven't been loaded now.
5195            //
5196            // The items that have been loaded, have been saved after they've been added to the workspace.
5197            let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
5198                item_ids_by_kind
5199                    .into_iter()
5200                    .map(|(item_kind, loaded_items)| {
5201                        SerializableItemRegistry::cleanup(
5202                            item_kind,
5203                            serialized_workspace.id,
5204                            loaded_items,
5205                            window,
5206                            cx,
5207                        )
5208                        .log_err()
5209                    })
5210                    .collect::<Vec<_>>()
5211            })?;
5212
5213            futures::future::join_all(clean_up_tasks).await;
5214
5215            workspace
5216                .update_in(cx, |workspace, window, cx| {
5217                    // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
5218                    workspace.serialize_workspace_internal(window, cx).detach();
5219
5220                    // Ensure that we mark the window as edited if we did load dirty items
5221                    workspace.update_window_edited(window, cx);
5222                })
5223                .ok();
5224
5225            Ok(opened_items)
5226        })
5227    }
5228
5229    fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
5230        self.add_workspace_actions_listeners(div, window, cx)
5231            .on_action(cx.listener(Self::close_inactive_items_and_panes))
5232            .on_action(cx.listener(Self::close_all_items_and_panes))
5233            .on_action(cx.listener(Self::save_all))
5234            .on_action(cx.listener(Self::send_keystrokes))
5235            .on_action(cx.listener(Self::add_folder_to_project))
5236            .on_action(cx.listener(Self::follow_next_collaborator))
5237            .on_action(cx.listener(Self::close_window))
5238            .on_action(cx.listener(Self::activate_pane_at_index))
5239            .on_action(cx.listener(Self::move_item_to_pane_at_index))
5240            .on_action(cx.listener(Self::move_focused_panel_to_next_position))
5241            .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
5242                let pane = workspace.active_pane().clone();
5243                workspace.unfollow_in_pane(&pane, window, cx);
5244            }))
5245            .on_action(cx.listener(|workspace, action: &Save, window, cx| {
5246                workspace
5247                    .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
5248                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
5249            }))
5250            .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
5251                workspace
5252                    .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
5253                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
5254            }))
5255            .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
5256                workspace
5257                    .save_active_item(SaveIntent::SaveAs, window, cx)
5258                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
5259            }))
5260            .on_action(
5261                cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
5262                    workspace.activate_previous_pane(window, cx)
5263                }),
5264            )
5265            .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
5266                workspace.activate_next_pane(window, cx)
5267            }))
5268            .on_action(
5269                cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
5270                    workspace.activate_next_window(cx)
5271                }),
5272            )
5273            .on_action(
5274                cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
5275                    workspace.activate_previous_window(cx)
5276                }),
5277            )
5278            .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
5279                workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
5280            }))
5281            .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
5282                workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
5283            }))
5284            .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
5285                workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
5286            }))
5287            .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
5288                workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
5289            }))
5290            .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
5291                workspace.activate_next_pane(window, cx)
5292            }))
5293            .on_action(cx.listener(
5294                |workspace, action: &MoveItemToPaneInDirection, window, cx| {
5295                    workspace.move_item_to_pane_in_direction(action, window, cx)
5296                },
5297            ))
5298            .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
5299                workspace.swap_pane_in_direction(SplitDirection::Left, cx)
5300            }))
5301            .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
5302                workspace.swap_pane_in_direction(SplitDirection::Right, cx)
5303            }))
5304            .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
5305                workspace.swap_pane_in_direction(SplitDirection::Up, cx)
5306            }))
5307            .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
5308                workspace.swap_pane_in_direction(SplitDirection::Down, cx)
5309            }))
5310            .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
5311                this.toggle_dock(DockPosition::Left, window, cx);
5312            }))
5313            .on_action(cx.listener(
5314                |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
5315                    workspace.toggle_dock(DockPosition::Right, window, cx);
5316                },
5317            ))
5318            .on_action(cx.listener(
5319                |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
5320                    workspace.toggle_dock(DockPosition::Bottom, window, cx);
5321                },
5322            ))
5323            .on_action(cx.listener(
5324                |workspace: &mut Workspace, _: &CloseActiveDock, window, cx| {
5325                    workspace.close_active_dock(window, cx);
5326                },
5327            ))
5328            .on_action(
5329                cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
5330                    workspace.close_all_docks(window, cx);
5331                }),
5332            )
5333            .on_action(cx.listener(
5334                |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
5335                    workspace.clear_all_notifications(cx);
5336                },
5337            ))
5338            .on_action(cx.listener(
5339                |workspace: &mut Workspace, _: &SuppressNotification, _, cx| {
5340                    if let Some((notification_id, _)) = workspace.notifications.pop() {
5341                        workspace.suppress_notification(&notification_id, cx);
5342                    }
5343                },
5344            ))
5345            .on_action(cx.listener(
5346                |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
5347                    workspace.reopen_closed_item(window, cx).detach();
5348                },
5349            ))
5350            .on_action(cx.listener(Workspace::toggle_centered_layout))
5351            .on_action(cx.listener(Workspace::cancel))
5352    }
5353
5354    #[cfg(any(test, feature = "test-support"))]
5355    pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
5356        use node_runtime::NodeRuntime;
5357        use session::Session;
5358
5359        let client = project.read(cx).client();
5360        let user_store = project.read(cx).user_store();
5361
5362        let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
5363        let session = cx.new(|cx| AppSession::new(Session::test(), cx));
5364        window.activate_window();
5365        let app_state = Arc::new(AppState {
5366            languages: project.read(cx).languages().clone(),
5367            workspace_store,
5368            client,
5369            user_store,
5370            fs: project.read(cx).fs().clone(),
5371            build_window_options: |_, _| Default::default(),
5372            node_runtime: NodeRuntime::unavailable(),
5373            session,
5374        });
5375        let workspace = Self::new(Default::default(), project, app_state, window, cx);
5376        workspace
5377            .active_pane
5378            .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
5379        workspace
5380    }
5381
5382    pub fn register_action<A: Action>(
5383        &mut self,
5384        callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
5385    ) -> &mut Self {
5386        let callback = Arc::new(callback);
5387
5388        self.workspace_actions.push(Box::new(move |div, _, cx| {
5389            let callback = callback.clone();
5390            div.on_action(cx.listener(move |workspace, event, window, cx| {
5391                (callback)(workspace, event, window, cx)
5392            }))
5393        }));
5394        self
5395    }
5396
5397    fn add_workspace_actions_listeners(
5398        &self,
5399        mut div: Div,
5400        window: &mut Window,
5401        cx: &mut Context<Self>,
5402    ) -> Div {
5403        for action in self.workspace_actions.iter() {
5404            div = (action)(div, window, cx)
5405        }
5406        div
5407    }
5408
5409    pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
5410        self.modal_layer.read(cx).has_active_modal()
5411    }
5412
5413    pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
5414        self.modal_layer.read(cx).active_modal()
5415    }
5416
5417    pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
5418    where
5419        B: FnOnce(&mut Window, &mut Context<V>) -> V,
5420    {
5421        self.modal_layer.update(cx, |modal_layer, cx| {
5422            modal_layer.toggle_modal(window, cx, build)
5423        })
5424    }
5425
5426    pub fn toggle_status_toast<V: ToastView>(&mut self, entity: Entity<V>, cx: &mut App) {
5427        self.toast_layer
5428            .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity))
5429    }
5430
5431    pub fn toggle_centered_layout(
5432        &mut self,
5433        _: &ToggleCenteredLayout,
5434        _: &mut Window,
5435        cx: &mut Context<Self>,
5436    ) {
5437        self.centered_layout = !self.centered_layout;
5438        if let Some(database_id) = self.database_id() {
5439            cx.background_spawn(DB.set_centered_layout(database_id, self.centered_layout))
5440                .detach_and_log_err(cx);
5441        }
5442        cx.notify();
5443    }
5444
5445    fn adjust_padding(padding: Option<f32>) -> f32 {
5446        padding
5447            .unwrap_or(Self::DEFAULT_PADDING)
5448            .clamp(0.0, Self::MAX_PADDING)
5449    }
5450
5451    fn render_dock(
5452        &self,
5453        position: DockPosition,
5454        dock: &Entity<Dock>,
5455        window: &mut Window,
5456        cx: &mut App,
5457    ) -> Option<Div> {
5458        if self.zoomed_position == Some(position) {
5459            return None;
5460        }
5461
5462        let leader_border = dock.read(cx).active_panel().and_then(|panel| {
5463            let pane = panel.pane(cx)?;
5464            let follower_states = &self.follower_states;
5465            leader_border_for_pane(follower_states, &pane, window, cx)
5466        });
5467
5468        Some(
5469            div()
5470                .flex()
5471                .flex_none()
5472                .overflow_hidden()
5473                .child(dock.clone())
5474                .children(leader_border),
5475        )
5476    }
5477
5478    pub fn for_window(window: &mut Window, _: &mut App) -> Option<Entity<Workspace>> {
5479        window.root().flatten()
5480    }
5481
5482    pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
5483        self.zoomed.as_ref()
5484    }
5485
5486    pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
5487        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5488            return;
5489        };
5490        let windows = cx.windows();
5491        let Some(next_window) = windows
5492            .iter()
5493            .cycle()
5494            .skip_while(|window| window.window_id() != current_window_id)
5495            .nth(1)
5496        else {
5497            return;
5498        };
5499        next_window
5500            .update(cx, |_, window, _| window.activate_window())
5501            .ok();
5502    }
5503
5504    pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
5505        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5506            return;
5507        };
5508        let windows = cx.windows();
5509        let Some(prev_window) = windows
5510            .iter()
5511            .rev()
5512            .cycle()
5513            .skip_while(|window| window.window_id() != current_window_id)
5514            .nth(1)
5515        else {
5516            return;
5517        };
5518        prev_window
5519            .update(cx, |_, window, _| window.activate_window())
5520            .ok();
5521    }
5522
5523    pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
5524        if cx.stop_active_drag(window) {
5525            return;
5526        } else if let Some((notification_id, _)) = self.notifications.pop() {
5527            dismiss_app_notification(&notification_id, cx);
5528        } else {
5529            cx.emit(Event::ClearActivityIndicator);
5530            cx.propagate();
5531        }
5532    }
5533}
5534
5535fn leader_border_for_pane(
5536    follower_states: &HashMap<CollaboratorId, FollowerState>,
5537    pane: &Entity<Pane>,
5538    _: &Window,
5539    cx: &App,
5540) -> Option<Div> {
5541    let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
5542        if state.pane() == pane {
5543            Some((*leader_id, state))
5544        } else {
5545            None
5546        }
5547    })?;
5548
5549    let mut leader_color = match leader_id {
5550        CollaboratorId::PeerId(leader_peer_id) => {
5551            let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
5552            let leader = room.remote_participant_for_peer_id(leader_peer_id)?;
5553
5554            cx.theme()
5555                .players()
5556                .color_for_participant(leader.participant_index.0)
5557                .cursor
5558        }
5559        CollaboratorId::Agent => cx.theme().players().agent().cursor,
5560    };
5561    leader_color.fade_out(0.3);
5562    Some(
5563        div()
5564            .absolute()
5565            .size_full()
5566            .left_0()
5567            .top_0()
5568            .border_2()
5569            .border_color(leader_color),
5570    )
5571}
5572
5573fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
5574    ZED_WINDOW_POSITION
5575        .zip(*ZED_WINDOW_SIZE)
5576        .map(|(position, size)| Bounds {
5577            origin: position,
5578            size,
5579        })
5580}
5581
5582fn open_items(
5583    serialized_workspace: Option<SerializedWorkspace>,
5584    mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
5585    window: &mut Window,
5586    cx: &mut Context<Workspace>,
5587) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
5588    let restored_items = serialized_workspace.map(|serialized_workspace| {
5589        Workspace::load_workspace(
5590            serialized_workspace,
5591            project_paths_to_open
5592                .iter()
5593                .map(|(_, project_path)| project_path)
5594                .cloned()
5595                .collect(),
5596            window,
5597            cx,
5598        )
5599    });
5600
5601    cx.spawn_in(window, async move |workspace, cx| {
5602        let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
5603
5604        if let Some(restored_items) = restored_items {
5605            let restored_items = restored_items.await?;
5606
5607            let restored_project_paths = restored_items
5608                .iter()
5609                .filter_map(|item| {
5610                    cx.update(|_, cx| item.as_ref()?.project_path(cx))
5611                        .ok()
5612                        .flatten()
5613                })
5614                .collect::<HashSet<_>>();
5615
5616            for restored_item in restored_items {
5617                opened_items.push(restored_item.map(Ok));
5618            }
5619
5620            project_paths_to_open
5621                .iter_mut()
5622                .for_each(|(_, project_path)| {
5623                    if let Some(project_path_to_open) = project_path {
5624                        if restored_project_paths.contains(project_path_to_open) {
5625                            *project_path = None;
5626                        }
5627                    }
5628                });
5629        } else {
5630            for _ in 0..project_paths_to_open.len() {
5631                opened_items.push(None);
5632            }
5633        }
5634        assert!(opened_items.len() == project_paths_to_open.len());
5635
5636        let tasks =
5637            project_paths_to_open
5638                .into_iter()
5639                .enumerate()
5640                .map(|(ix, (abs_path, project_path))| {
5641                    let workspace = workspace.clone();
5642                    cx.spawn(async move |cx| {
5643                        let file_project_path = project_path?;
5644                        let abs_path_task = workspace.update(cx, |workspace, cx| {
5645                            workspace.project().update(cx, |project, cx| {
5646                                project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
5647                            })
5648                        });
5649
5650                        // We only want to open file paths here. If one of the items
5651                        // here is a directory, it was already opened further above
5652                        // with a `find_or_create_worktree`.
5653                        if let Ok(task) = abs_path_task {
5654                            if task.await.map_or(true, |p| p.is_file()) {
5655                                return Some((
5656                                    ix,
5657                                    workspace
5658                                        .update_in(cx, |workspace, window, cx| {
5659                                            workspace.open_path(
5660                                                file_project_path,
5661                                                None,
5662                                                true,
5663                                                window,
5664                                                cx,
5665                                            )
5666                                        })
5667                                        .log_err()?
5668                                        .await,
5669                                ));
5670                            }
5671                        }
5672                        None
5673                    })
5674                });
5675
5676        let tasks = tasks.collect::<Vec<_>>();
5677
5678        let tasks = futures::future::join_all(tasks);
5679        for (ix, path_open_result) in tasks.await.into_iter().flatten() {
5680            opened_items[ix] = Some(path_open_result);
5681        }
5682
5683        Ok(opened_items)
5684    })
5685}
5686
5687enum ActivateInDirectionTarget {
5688    Pane(Entity<Pane>),
5689    Dock(Entity<Dock>),
5690}
5691
5692fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
5693    workspace
5694        .update(cx, |workspace, _, cx| {
5695            if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
5696                struct DatabaseFailedNotification;
5697
5698                workspace.show_notification(
5699                    NotificationId::unique::<DatabaseFailedNotification>(),
5700                    cx,
5701                    |cx| {
5702                        cx.new(|cx| {
5703                            MessageNotification::new("Failed to load the database file.", cx)
5704                                .primary_message("File an Issue")
5705                                .primary_icon(IconName::Plus)
5706                                .primary_on_click(|window, cx| {
5707                                    window.dispatch_action(Box::new(FileBugReport), cx)
5708                                })
5709                        })
5710                    },
5711                );
5712            }
5713        })
5714        .log_err();
5715}
5716
5717impl Focusable for Workspace {
5718    fn focus_handle(&self, cx: &App) -> FocusHandle {
5719        self.active_pane.focus_handle(cx)
5720    }
5721}
5722
5723#[derive(Clone)]
5724struct DraggedDock(DockPosition);
5725
5726impl Render for DraggedDock {
5727    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5728        gpui::Empty
5729    }
5730}
5731
5732impl Render for Workspace {
5733    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5734        let mut context = KeyContext::new_with_defaults();
5735        context.add("Workspace");
5736        context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
5737        let centered_layout = self.centered_layout
5738            && self.center.panes().len() == 1
5739            && self.active_item(cx).is_some();
5740        let render_padding = |size| {
5741            (size > 0.0).then(|| {
5742                div()
5743                    .h_full()
5744                    .w(relative(size))
5745                    .bg(cx.theme().colors().editor_background)
5746                    .border_color(cx.theme().colors().pane_group_border)
5747            })
5748        };
5749        let paddings = if centered_layout {
5750            let settings = WorkspaceSettings::get_global(cx).centered_layout;
5751            (
5752                render_padding(Self::adjust_padding(settings.left_padding)),
5753                render_padding(Self::adjust_padding(settings.right_padding)),
5754            )
5755        } else {
5756            (None, None)
5757        };
5758        let ui_font = theme::setup_ui_font(window, cx);
5759
5760        let theme = cx.theme().clone();
5761        let colors = theme.colors();
5762        let notification_entities = self
5763            .notifications
5764            .iter()
5765            .map(|(_, notification)| notification.entity_id())
5766            .collect::<Vec<_>>();
5767
5768        client_side_decorations(
5769            self.actions(div(), window, cx)
5770                .key_context(context)
5771                .relative()
5772                .size_full()
5773                .flex()
5774                .flex_col()
5775                .font(ui_font)
5776                .gap_0()
5777                .justify_start()
5778                .items_start()
5779                .text_color(colors.text)
5780                .overflow_hidden()
5781                .children(self.titlebar_item.clone())
5782                .on_modifiers_changed(move |_, _, cx| {
5783                    for &id in &notification_entities {
5784                        cx.notify(id);
5785                    }
5786                })
5787                .child(
5788                    div()
5789                        .size_full()
5790                        .relative()
5791                        .flex_1()
5792                        .flex()
5793                        .flex_col()
5794                        .child(
5795                            div()
5796                                .id("workspace")
5797                                .bg(colors.background)
5798                                .relative()
5799                                .flex_1()
5800                                .w_full()
5801                                .flex()
5802                                .flex_col()
5803                                .overflow_hidden()
5804                                .border_t_1()
5805                                .border_b_1()
5806                                .border_color(colors.border)
5807                                .child({
5808                                    let this = cx.entity().clone();
5809                                    canvas(
5810                                        move |bounds, window, cx| {
5811                                            this.update(cx, |this, cx| {
5812                                                let bounds_changed = this.bounds != bounds;
5813                                                this.bounds = bounds;
5814
5815                                                if bounds_changed {
5816                                                    this.left_dock.update(cx, |dock, cx| {
5817                                                        dock.clamp_panel_size(
5818                                                            bounds.size.width,
5819                                                            window,
5820                                                            cx,
5821                                                        )
5822                                                    });
5823
5824                                                    this.right_dock.update(cx, |dock, cx| {
5825                                                        dock.clamp_panel_size(
5826                                                            bounds.size.width,
5827                                                            window,
5828                                                            cx,
5829                                                        )
5830                                                    });
5831
5832                                                    this.bottom_dock.update(cx, |dock, cx| {
5833                                                        dock.clamp_panel_size(
5834                                                            bounds.size.height,
5835                                                            window,
5836                                                            cx,
5837                                                        )
5838                                                    });
5839                                                }
5840                                            })
5841                                        },
5842                                        |_, _, _, _| {},
5843                                    )
5844                                    .absolute()
5845                                    .size_full()
5846                                })
5847                                .when(self.zoomed.is_none(), |this| {
5848                                    this.on_drag_move(cx.listener(
5849                                        move |workspace,
5850                                              e: &DragMoveEvent<DraggedDock>,
5851                                              window,
5852                                              cx| {
5853                                            if workspace.previous_dock_drag_coordinates
5854                                                != Some(e.event.position)
5855                                            {
5856                                                workspace.previous_dock_drag_coordinates =
5857                                                    Some(e.event.position);
5858                                                match e.drag(cx).0 {
5859                                                    DockPosition::Left => {
5860                                                        resize_left_dock(
5861                                                            e.event.position.x
5862                                                                - workspace.bounds.left(),
5863                                                            workspace,
5864                                                            window,
5865                                                            cx,
5866                                                        );
5867                                                    }
5868                                                    DockPosition::Right => {
5869                                                        resize_right_dock(
5870                                                            workspace.bounds.right()
5871                                                                - e.event.position.x,
5872                                                            workspace,
5873                                                            window,
5874                                                            cx,
5875                                                        );
5876                                                    }
5877                                                    DockPosition::Bottom => {
5878                                                        resize_bottom_dock(
5879                                                            workspace.bounds.bottom()
5880                                                                - e.event.position.y,
5881                                                            workspace,
5882                                                            window,
5883                                                            cx,
5884                                                        );
5885                                                    }
5886                                                };
5887                                                workspace.serialize_workspace(window, cx);
5888                                            }
5889                                        },
5890                                    ))
5891                                })
5892                                .child({
5893                                    match self.bottom_dock_layout {
5894                                        BottomDockLayout::Full => div()
5895                                            .flex()
5896                                            .flex_col()
5897                                            .h_full()
5898                                            .child(
5899                                                div()
5900                                                    .flex()
5901                                                    .flex_row()
5902                                                    .flex_1()
5903                                                    .overflow_hidden()
5904                                                    .children(self.render_dock(
5905                                                        DockPosition::Left,
5906                                                        &self.left_dock,
5907                                                        window,
5908                                                        cx,
5909                                                    ))
5910                                                    .child(
5911                                                        div()
5912                                                            .flex()
5913                                                            .flex_col()
5914                                                            .flex_1()
5915                                                            .overflow_hidden()
5916                                                            .child(
5917                                                                h_flex()
5918                                                                    .flex_1()
5919                                                                    .when_some(
5920                                                                        paddings.0,
5921                                                                        |this, p| {
5922                                                                            this.child(
5923                                                                                p.border_r_1(),
5924                                                                            )
5925                                                                        },
5926                                                                    )
5927                                                                    .child(self.center.render(
5928                                                                        self.zoomed.as_ref(),
5929                                                                        &PaneRenderContext {
5930                                                                            follower_states:
5931                                                                                &self.follower_states,
5932                                                                            active_call: self.active_call(),
5933                                                                            active_pane: &self.active_pane,
5934                                                                            app_state: &self.app_state,
5935                                                                            project: &self.project,
5936                                                                            workspace: &self.weak_self,
5937                                                                        },
5938                                                                        window,
5939                                                                        cx,
5940                                                                    ))
5941                                                                    .when_some(
5942                                                                        paddings.1,
5943                                                                        |this, p| {
5944                                                                            this.child(
5945                                                                                p.border_l_1(),
5946                                                                            )
5947                                                                        },
5948                                                                    ),
5949                                                            ),
5950                                                    )
5951                                                    .children(self.render_dock(
5952                                                        DockPosition::Right,
5953                                                        &self.right_dock,
5954                                                        window,
5955                                                        cx,
5956                                                    )),
5957                                            )
5958                                            .child(div().w_full().children(self.render_dock(
5959                                                DockPosition::Bottom,
5960                                                &self.bottom_dock,
5961                                                window,
5962                                                cx
5963                                            ))),
5964
5965                                        BottomDockLayout::LeftAligned => div()
5966                                            .flex()
5967                                            .flex_row()
5968                                            .h_full()
5969                                            .child(
5970                                                div()
5971                                                    .flex()
5972                                                    .flex_col()
5973                                                    .flex_1()
5974                                                    .h_full()
5975                                                    .child(
5976                                                        div()
5977                                                            .flex()
5978                                                            .flex_row()
5979                                                            .flex_1()
5980                                                            .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx))
5981                                                            .child(
5982                                                                div()
5983                                                                    .flex()
5984                                                                    .flex_col()
5985                                                                    .flex_1()
5986                                                                    .overflow_hidden()
5987                                                                    .child(
5988                                                                        h_flex()
5989                                                                            .flex_1()
5990                                                                            .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
5991                                                                            .child(self.center.render(
5992                                                                                self.zoomed.as_ref(),
5993                                                                                &PaneRenderContext {
5994                                                                                    follower_states:
5995                                                                                        &self.follower_states,
5996                                                                                    active_call: self.active_call(),
5997                                                                                    active_pane: &self.active_pane,
5998                                                                                    app_state: &self.app_state,
5999                                                                                    project: &self.project,
6000                                                                                    workspace: &self.weak_self,
6001                                                                                },
6002                                                                                window,
6003                                                                                cx,
6004                                                                            ))
6005                                                                            .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
6006                                                                    )
6007                                                            )
6008                                                    )
6009                                                    .child(
6010                                                        div()
6011                                                            .w_full()
6012                                                            .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
6013                                                    ),
6014                                            )
6015                                            .children(self.render_dock(
6016                                                DockPosition::Right,
6017                                                &self.right_dock,
6018                                                window,
6019                                                cx,
6020                                            )),
6021
6022                                        BottomDockLayout::RightAligned => div()
6023                                            .flex()
6024                                            .flex_row()
6025                                            .h_full()
6026                                            .children(self.render_dock(
6027                                                DockPosition::Left,
6028                                                &self.left_dock,
6029                                                window,
6030                                                cx,
6031                                            ))
6032                                            .child(
6033                                                div()
6034                                                    .flex()
6035                                                    .flex_col()
6036                                                    .flex_1()
6037                                                    .h_full()
6038                                                    .child(
6039                                                        div()
6040                                                            .flex()
6041                                                            .flex_row()
6042                                                            .flex_1()
6043                                                            .child(
6044                                                                div()
6045                                                                    .flex()
6046                                                                    .flex_col()
6047                                                                    .flex_1()
6048                                                                    .overflow_hidden()
6049                                                                    .child(
6050                                                                        h_flex()
6051                                                                            .flex_1()
6052                                                                            .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
6053                                                                            .child(self.center.render(
6054                                                                                self.zoomed.as_ref(),
6055                                                                                &PaneRenderContext {
6056                                                                                    follower_states:
6057                                                                                        &self.follower_states,
6058                                                                                    active_call: self.active_call(),
6059                                                                                    active_pane: &self.active_pane,
6060                                                                                    app_state: &self.app_state,
6061                                                                                    project: &self.project,
6062                                                                                    workspace: &self.weak_self,
6063                                                                                },
6064                                                                                window,
6065                                                                                cx,
6066                                                                            ))
6067                                                                            .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
6068                                                                    )
6069                                                            )
6070                                                            .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx))
6071                                                    )
6072                                                    .child(
6073                                                        div()
6074                                                            .w_full()
6075                                                            .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
6076                                                    ),
6077                                            ),
6078
6079                                        BottomDockLayout::Contained => div()
6080                                            .flex()
6081                                            .flex_row()
6082                                            .h_full()
6083                                            .children(self.render_dock(
6084                                                DockPosition::Left,
6085                                                &self.left_dock,
6086                                                window,
6087                                                cx,
6088                                            ))
6089                                            .child(
6090                                                div()
6091                                                    .flex()
6092                                                    .flex_col()
6093                                                    .flex_1()
6094                                                    .overflow_hidden()
6095                                                    .child(
6096                                                        h_flex()
6097                                                            .flex_1()
6098                                                            .when_some(paddings.0, |this, p| {
6099                                                                this.child(p.border_r_1())
6100                                                            })
6101                                                            .child(self.center.render(
6102                                                                self.zoomed.as_ref(),
6103                                                                &PaneRenderContext {
6104                                                                    follower_states:
6105                                                                        &self.follower_states,
6106                                                                    active_call: self.active_call(),
6107                                                                    active_pane: &self.active_pane,
6108                                                                    app_state: &self.app_state,
6109                                                                    project: &self.project,
6110                                                                    workspace: &self.weak_self,
6111                                                                },
6112                                                                window,
6113                                                                cx,
6114                                                            ))
6115                                                            .when_some(paddings.1, |this, p| {
6116                                                                this.child(p.border_l_1())
6117                                                            }),
6118                                                    )
6119                                                    .children(self.render_dock(
6120                                                        DockPosition::Bottom,
6121                                                        &self.bottom_dock,
6122                                                        window,
6123                                                        cx,
6124                                                    )),
6125                                            )
6126                                            .children(self.render_dock(
6127                                                DockPosition::Right,
6128                                                &self.right_dock,
6129                                                window,
6130                                                cx,
6131                                            )),
6132                                    }
6133                                })
6134                                .children(self.zoomed.as_ref().and_then(|view| {
6135                                    let zoomed_view = view.upgrade()?;
6136                                    let div = div()
6137                                        .occlude()
6138                                        .absolute()
6139                                        .overflow_hidden()
6140                                        .border_color(colors.border)
6141                                        .bg(colors.background)
6142                                        .child(zoomed_view)
6143                                        .inset_0()
6144                                        .shadow_lg();
6145
6146                                    Some(match self.zoomed_position {
6147                                        Some(DockPosition::Left) => div.right_2().border_r_1(),
6148                                        Some(DockPosition::Right) => div.left_2().border_l_1(),
6149                                        Some(DockPosition::Bottom) => div.top_2().border_t_1(),
6150                                        None => {
6151                                            div.top_2().bottom_2().left_2().right_2().border_1()
6152                                        }
6153                                    })
6154                                }))
6155                                .children(self.render_notifications(window, cx)),
6156                        )
6157                        .child(self.status_bar.clone())
6158                        .child(self.modal_layer.clone())
6159                        .child(self.toast_layer.clone()),
6160                ),
6161            window,
6162            cx,
6163        )
6164    }
6165}
6166
6167fn resize_bottom_dock(
6168    new_size: Pixels,
6169    workspace: &mut Workspace,
6170    window: &mut Window,
6171    cx: &mut App,
6172) {
6173    let size =
6174        new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE - workspace.bounds.top());
6175    workspace.bottom_dock.update(cx, |bottom_dock, cx| {
6176        bottom_dock.resize_active_panel(Some(size), window, cx);
6177    });
6178}
6179
6180fn resize_right_dock(
6181    new_size: Pixels,
6182    workspace: &mut Workspace,
6183    window: &mut Window,
6184    cx: &mut App,
6185) {
6186    let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
6187    workspace.right_dock.update(cx, |right_dock, cx| {
6188        right_dock.resize_active_panel(Some(size), window, cx);
6189    });
6190}
6191
6192fn resize_left_dock(
6193    new_size: Pixels,
6194    workspace: &mut Workspace,
6195    window: &mut Window,
6196    cx: &mut App,
6197) {
6198    let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
6199
6200    workspace.left_dock.update(cx, |left_dock, cx| {
6201        left_dock.resize_active_panel(Some(size), window, cx);
6202    });
6203}
6204
6205impl WorkspaceStore {
6206    pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
6207        Self {
6208            workspaces: Default::default(),
6209            _subscriptions: vec![
6210                client.add_request_handler(cx.weak_entity(), Self::handle_follow),
6211                client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
6212            ],
6213            client,
6214        }
6215    }
6216
6217    pub fn update_followers(
6218        &self,
6219        project_id: Option<u64>,
6220        update: proto::update_followers::Variant,
6221        cx: &App,
6222    ) -> Option<()> {
6223        let active_call = ActiveCall::try_global(cx)?;
6224        let room_id = active_call.read(cx).room()?.read(cx).id();
6225        self.client
6226            .send(proto::UpdateFollowers {
6227                room_id,
6228                project_id,
6229                variant: Some(update),
6230            })
6231            .log_err()
6232    }
6233
6234    pub async fn handle_follow(
6235        this: Entity<Self>,
6236        envelope: TypedEnvelope<proto::Follow>,
6237        mut cx: AsyncApp,
6238    ) -> Result<proto::FollowResponse> {
6239        this.update(&mut cx, |this, cx| {
6240            let follower = Follower {
6241                project_id: envelope.payload.project_id,
6242                peer_id: envelope.original_sender_id()?,
6243            };
6244
6245            let mut response = proto::FollowResponse::default();
6246            this.workspaces.retain(|workspace| {
6247                workspace
6248                    .update(cx, |workspace, window, cx| {
6249                        let handler_response =
6250                            workspace.handle_follow(follower.project_id, window, cx);
6251                        if let Some(active_view) = handler_response.active_view.clone() {
6252                            if workspace.project.read(cx).remote_id() == follower.project_id {
6253                                response.active_view = Some(active_view)
6254                            }
6255                        }
6256                    })
6257                    .is_ok()
6258            });
6259
6260            Ok(response)
6261        })?
6262    }
6263
6264    async fn handle_update_followers(
6265        this: Entity<Self>,
6266        envelope: TypedEnvelope<proto::UpdateFollowers>,
6267        mut cx: AsyncApp,
6268    ) -> Result<()> {
6269        let leader_id = envelope.original_sender_id()?;
6270        let update = envelope.payload;
6271
6272        this.update(&mut cx, |this, cx| {
6273            this.workspaces.retain(|workspace| {
6274                workspace
6275                    .update(cx, |workspace, window, cx| {
6276                        let project_id = workspace.project.read(cx).remote_id();
6277                        if update.project_id != project_id && update.project_id.is_some() {
6278                            return;
6279                        }
6280                        workspace.handle_update_followers(leader_id, update.clone(), window, cx);
6281                    })
6282                    .is_ok()
6283            });
6284            Ok(())
6285        })?
6286    }
6287}
6288
6289impl ViewId {
6290    pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
6291        Ok(Self {
6292            creator: message
6293                .creator
6294                .map(CollaboratorId::PeerId)
6295                .context("creator is missing")?,
6296            id: message.id,
6297        })
6298    }
6299
6300    pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
6301        if let CollaboratorId::PeerId(peer_id) = self.creator {
6302            Some(proto::ViewId {
6303                creator: Some(peer_id),
6304                id: self.id,
6305            })
6306        } else {
6307            None
6308        }
6309    }
6310}
6311
6312impl FollowerState {
6313    fn pane(&self) -> &Entity<Pane> {
6314        self.dock_pane.as_ref().unwrap_or(&self.center_pane)
6315    }
6316}
6317
6318pub trait WorkspaceHandle {
6319    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
6320}
6321
6322impl WorkspaceHandle for Entity<Workspace> {
6323    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
6324        self.read(cx)
6325            .worktrees(cx)
6326            .flat_map(|worktree| {
6327                let worktree_id = worktree.read(cx).id();
6328                worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
6329                    worktree_id,
6330                    path: f.path.clone(),
6331                })
6332            })
6333            .collect::<Vec<_>>()
6334    }
6335}
6336
6337impl std::fmt::Debug for OpenPaths {
6338    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6339        f.debug_struct("OpenPaths")
6340            .field("paths", &self.paths)
6341            .finish()
6342    }
6343}
6344
6345pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
6346    DB.last_workspace().await.log_err().flatten()
6347}
6348
6349pub fn last_session_workspace_locations(
6350    last_session_id: &str,
6351    last_session_window_stack: Option<Vec<WindowId>>,
6352) -> Option<Vec<SerializedWorkspaceLocation>> {
6353    DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
6354        .log_err()
6355}
6356
6357actions!(
6358    collab,
6359    [
6360        OpenChannelNotes,
6361        Mute,
6362        Deafen,
6363        LeaveCall,
6364        ShareProject,
6365        ScreenShare
6366    ]
6367);
6368actions!(zed, [OpenLog]);
6369
6370async fn join_channel_internal(
6371    channel_id: ChannelId,
6372    app_state: &Arc<AppState>,
6373    requesting_window: Option<WindowHandle<Workspace>>,
6374    active_call: &Entity<ActiveCall>,
6375    cx: &mut AsyncApp,
6376) -> Result<bool> {
6377    let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
6378        let Some(room) = active_call.room().map(|room| room.read(cx)) else {
6379            return (false, None);
6380        };
6381
6382        let already_in_channel = room.channel_id() == Some(channel_id);
6383        let should_prompt = room.is_sharing_project()
6384            && !room.remote_participants().is_empty()
6385            && !already_in_channel;
6386        let open_room = if already_in_channel {
6387            active_call.room().cloned()
6388        } else {
6389            None
6390        };
6391        (should_prompt, open_room)
6392    })?;
6393
6394    if let Some(room) = open_room {
6395        let task = room.update(cx, |room, cx| {
6396            if let Some((project, host)) = room.most_active_project(cx) {
6397                return Some(join_in_room_project(project, host, app_state.clone(), cx));
6398            }
6399
6400            None
6401        })?;
6402        if let Some(task) = task {
6403            task.await?;
6404        }
6405        return anyhow::Ok(true);
6406    }
6407
6408    if should_prompt {
6409        if let Some(workspace) = requesting_window {
6410            let answer = workspace
6411                .update(cx, |_, window, cx| {
6412                    window.prompt(
6413                        PromptLevel::Warning,
6414                        "Do you want to switch channels?",
6415                        Some("Leaving this call will unshare your current project."),
6416                        &["Yes, Join Channel", "Cancel"],
6417                        cx,
6418                    )
6419                })?
6420                .await;
6421
6422            if answer == Ok(1) {
6423                return Ok(false);
6424            }
6425        } else {
6426            return Ok(false); // unreachable!() hopefully
6427        }
6428    }
6429
6430    let client = cx.update(|cx| active_call.read(cx).client())?;
6431
6432    let mut client_status = client.status();
6433
6434    // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
6435    'outer: loop {
6436        let Some(status) = client_status.recv().await else {
6437            anyhow::bail!("error connecting");
6438        };
6439
6440        match status {
6441            Status::Connecting
6442            | Status::Authenticating
6443            | Status::Reconnecting
6444            | Status::Reauthenticating => continue,
6445            Status::Connected { .. } => break 'outer,
6446            Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
6447            Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
6448            Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
6449                return Err(ErrorCode::Disconnected.into());
6450            }
6451        }
6452    }
6453
6454    let room = active_call
6455        .update(cx, |active_call, cx| {
6456            active_call.join_channel(channel_id, cx)
6457        })?
6458        .await?;
6459
6460    let Some(room) = room else {
6461        return anyhow::Ok(true);
6462    };
6463
6464    room.update(cx, |room, _| room.room_update_completed())?
6465        .await;
6466
6467    let task = room.update(cx, |room, cx| {
6468        if let Some((project, host)) = room.most_active_project(cx) {
6469            return Some(join_in_room_project(project, host, app_state.clone(), cx));
6470        }
6471
6472        // If you are the first to join a channel, see if you should share your project.
6473        if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
6474            if let Some(workspace) = requesting_window {
6475                let project = workspace.update(cx, |workspace, _, cx| {
6476                    let project = workspace.project.read(cx);
6477
6478                    if !CallSettings::get_global(cx).share_on_join {
6479                        return None;
6480                    }
6481
6482                    if (project.is_local() || project.is_via_ssh())
6483                        && project.visible_worktrees(cx).any(|tree| {
6484                            tree.read(cx)
6485                                .root_entry()
6486                                .map_or(false, |entry| entry.is_dir())
6487                        })
6488                    {
6489                        Some(workspace.project.clone())
6490                    } else {
6491                        None
6492                    }
6493                });
6494                if let Ok(Some(project)) = project {
6495                    return Some(cx.spawn(async move |room, cx| {
6496                        room.update(cx, |room, cx| room.share_project(project, cx))?
6497                            .await?;
6498                        Ok(())
6499                    }));
6500                }
6501            }
6502        }
6503
6504        None
6505    })?;
6506    if let Some(task) = task {
6507        task.await?;
6508        return anyhow::Ok(true);
6509    }
6510    anyhow::Ok(false)
6511}
6512
6513pub fn join_channel(
6514    channel_id: ChannelId,
6515    app_state: Arc<AppState>,
6516    requesting_window: Option<WindowHandle<Workspace>>,
6517    cx: &mut App,
6518) -> Task<Result<()>> {
6519    let active_call = ActiveCall::global(cx);
6520    cx.spawn(async move |cx| {
6521        let result = join_channel_internal(
6522            channel_id,
6523            &app_state,
6524            requesting_window,
6525            &active_call,
6526             cx,
6527        )
6528            .await;
6529
6530        // join channel succeeded, and opened a window
6531        if matches!(result, Ok(true)) {
6532            return anyhow::Ok(());
6533        }
6534
6535        // find an existing workspace to focus and show call controls
6536        let mut active_window =
6537            requesting_window.or_else(|| activate_any_workspace_window( cx));
6538        if active_window.is_none() {
6539            // no open workspaces, make one to show the error in (blergh)
6540            let (window_handle, _) = cx
6541                .update(|cx| {
6542                    Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
6543                })?
6544                .await?;
6545
6546            if result.is_ok() {
6547                cx.update(|cx| {
6548                    cx.dispatch_action(&OpenChannelNotes);
6549                }).log_err();
6550            }
6551
6552            active_window = Some(window_handle);
6553        }
6554
6555        if let Err(err) = result {
6556            log::error!("failed to join channel: {}", err);
6557            if let Some(active_window) = active_window {
6558                active_window
6559                    .update(cx, |_, window, cx| {
6560                        let detail: SharedString = match err.error_code() {
6561                            ErrorCode::SignedOut => {
6562                                "Please sign in to continue.".into()
6563                            }
6564                            ErrorCode::UpgradeRequired => {
6565                                "Your are running an unsupported version of Zed. Please update to continue.".into()
6566                            }
6567                            ErrorCode::NoSuchChannel => {
6568                                "No matching channel was found. Please check the link and try again.".into()
6569                            }
6570                            ErrorCode::Forbidden => {
6571                                "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
6572                            }
6573                            ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
6574                            _ => format!("{}\n\nPlease try again.", err).into(),
6575                        };
6576                        window.prompt(
6577                            PromptLevel::Critical,
6578                            "Failed to join channel",
6579                            Some(&detail),
6580                            &["Ok"],
6581                        cx)
6582                    })?
6583                    .await
6584                    .ok();
6585            }
6586        }
6587
6588        // return ok, we showed the error to the user.
6589        anyhow::Ok(())
6590    })
6591}
6592
6593pub async fn get_any_active_workspace(
6594    app_state: Arc<AppState>,
6595    mut cx: AsyncApp,
6596) -> anyhow::Result<WindowHandle<Workspace>> {
6597    // find an existing workspace to focus and show call controls
6598    let active_window = activate_any_workspace_window(&mut cx);
6599    if active_window.is_none() {
6600        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
6601            .await?;
6602    }
6603    activate_any_workspace_window(&mut cx).context("could not open zed")
6604}
6605
6606fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
6607    cx.update(|cx| {
6608        if let Some(workspace_window) = cx
6609            .active_window()
6610            .and_then(|window| window.downcast::<Workspace>())
6611        {
6612            return Some(workspace_window);
6613        }
6614
6615        for window in cx.windows() {
6616            if let Some(workspace_window) = window.downcast::<Workspace>() {
6617                workspace_window
6618                    .update(cx, |_, window, _| window.activate_window())
6619                    .ok();
6620                return Some(workspace_window);
6621            }
6622        }
6623        None
6624    })
6625    .ok()
6626    .flatten()
6627}
6628
6629pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
6630    cx.windows()
6631        .into_iter()
6632        .filter_map(|window| window.downcast::<Workspace>())
6633        .filter(|workspace| {
6634            workspace
6635                .read(cx)
6636                .is_ok_and(|workspace| workspace.project.read(cx).is_local())
6637        })
6638        .collect()
6639}
6640
6641#[derive(Default)]
6642pub struct OpenOptions {
6643    pub visible: Option<OpenVisible>,
6644    pub focus: Option<bool>,
6645    pub open_new_workspace: Option<bool>,
6646    pub replace_window: Option<WindowHandle<Workspace>>,
6647    pub env: Option<HashMap<String, String>>,
6648}
6649
6650#[allow(clippy::type_complexity)]
6651pub fn open_paths(
6652    abs_paths: &[PathBuf],
6653    app_state: Arc<AppState>,
6654    open_options: OpenOptions,
6655    cx: &mut App,
6656) -> Task<
6657    anyhow::Result<(
6658        WindowHandle<Workspace>,
6659        Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
6660    )>,
6661> {
6662    let abs_paths = abs_paths.to_vec();
6663    let mut existing = None;
6664    let mut best_match = None;
6665    let mut open_visible = OpenVisible::All;
6666
6667    cx.spawn(async move |cx| {
6668        if open_options.open_new_workspace != Some(true) {
6669            let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
6670            let all_metadatas = futures::future::join_all(all_paths)
6671                .await
6672                .into_iter()
6673                .filter_map(|result| result.ok().flatten())
6674                .collect::<Vec<_>>();
6675
6676            cx.update(|cx| {
6677                for window in local_workspace_windows(&cx) {
6678                    if let Ok(workspace) = window.read(&cx) {
6679                        let m = workspace.project.read(&cx).visibility_for_paths(
6680                            &abs_paths,
6681                            &all_metadatas,
6682                            open_options.open_new_workspace == None,
6683                            cx,
6684                        );
6685                        if m > best_match {
6686                            existing = Some(window);
6687                            best_match = m;
6688                        } else if best_match.is_none()
6689                            && open_options.open_new_workspace == Some(false)
6690                        {
6691                            existing = Some(window)
6692                        }
6693                    }
6694                }
6695            })?;
6696
6697            if open_options.open_new_workspace.is_none() && existing.is_none() {
6698                if all_metadatas.iter().all(|file| !file.is_dir) {
6699                    cx.update(|cx| {
6700                        if let Some(window) = cx
6701                            .active_window()
6702                            .and_then(|window| window.downcast::<Workspace>())
6703                        {
6704                            if let Ok(workspace) = window.read(cx) {
6705                                let project = workspace.project().read(cx);
6706                                if project.is_local() && !project.is_via_collab() {
6707                                    existing = Some(window);
6708                                    open_visible = OpenVisible::None;
6709                                    return;
6710                                }
6711                            }
6712                        }
6713                        for window in local_workspace_windows(cx) {
6714                            if let Ok(workspace) = window.read(cx) {
6715                                let project = workspace.project().read(cx);
6716                                if project.is_via_collab() {
6717                                    continue;
6718                                }
6719                                existing = Some(window);
6720                                open_visible = OpenVisible::None;
6721                                break;
6722                            }
6723                        }
6724                    })?;
6725                }
6726            }
6727        }
6728
6729        if let Some(existing) = existing {
6730            let open_task = existing
6731                .update(cx, |workspace, window, cx| {
6732                    window.activate_window();
6733                    workspace.open_paths(
6734                        abs_paths,
6735                        OpenOptions {
6736                            visible: Some(open_visible),
6737                            ..Default::default()
6738                        },
6739                        None,
6740                        window,
6741                        cx,
6742                    )
6743                })?
6744                .await;
6745
6746            _ = existing.update(cx, |workspace, _, cx| {
6747                for item in open_task.iter().flatten() {
6748                    if let Err(e) = item {
6749                        workspace.show_error(&e, cx);
6750                    }
6751                }
6752            });
6753
6754            Ok((existing, open_task))
6755        } else {
6756            cx.update(move |cx| {
6757                Workspace::new_local(
6758                    abs_paths,
6759                    app_state.clone(),
6760                    open_options.replace_window,
6761                    open_options.env,
6762                    cx,
6763                )
6764            })?
6765            .await
6766        }
6767    })
6768}
6769
6770pub fn open_new(
6771    open_options: OpenOptions,
6772    app_state: Arc<AppState>,
6773    cx: &mut App,
6774    init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6775) -> Task<anyhow::Result<()>> {
6776    let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6777    cx.spawn(async move |cx| {
6778        let (workspace, opened_paths) = task.await?;
6779        workspace.update(cx, |workspace, window, cx| {
6780            if opened_paths.is_empty() {
6781                init(workspace, window, cx)
6782            }
6783        })?;
6784        Ok(())
6785    })
6786}
6787
6788pub fn create_and_open_local_file(
6789    path: &'static Path,
6790    window: &mut Window,
6791    cx: &mut Context<Workspace>,
6792    default_content: impl 'static + Send + FnOnce() -> Rope,
6793) -> Task<Result<Box<dyn ItemHandle>>> {
6794    cx.spawn_in(window, async move |workspace, cx| {
6795        let fs = workspace.update(cx, |workspace, _| workspace.app_state().fs.clone())?;
6796        if !fs.is_file(path).await {
6797            fs.create_file(path, Default::default()).await?;
6798            fs.save(path, &default_content(), Default::default())
6799                .await?;
6800        }
6801
6802        let mut items = workspace
6803            .update_in(cx, |workspace, window, cx| {
6804                workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6805                    workspace.open_paths(
6806                        vec![path.to_path_buf()],
6807                        OpenOptions {
6808                            visible: Some(OpenVisible::None),
6809                            ..Default::default()
6810                        },
6811                        None,
6812                        window,
6813                        cx,
6814                    )
6815                })
6816            })?
6817            .await?
6818            .await;
6819
6820        let item = items.pop().flatten();
6821        item.with_context(|| format!("path {path:?} is not a file"))?
6822    })
6823}
6824
6825pub fn open_ssh_project_with_new_connection(
6826    window: WindowHandle<Workspace>,
6827    connection_options: SshConnectionOptions,
6828    cancel_rx: oneshot::Receiver<()>,
6829    delegate: Arc<dyn SshClientDelegate>,
6830    app_state: Arc<AppState>,
6831    paths: Vec<PathBuf>,
6832    cx: &mut App,
6833) -> Task<Result<()>> {
6834    cx.spawn(async move |cx| {
6835        let (serialized_ssh_project, workspace_id, serialized_workspace) =
6836            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6837
6838        let session = match cx
6839            .update(|cx| {
6840                remote::SshRemoteClient::new(
6841                    ConnectionIdentifier::Workspace(workspace_id.0),
6842                    connection_options,
6843                    cancel_rx,
6844                    delegate,
6845                    cx,
6846                )
6847            })?
6848            .await?
6849        {
6850            Some(result) => result,
6851            None => return Ok(()),
6852        };
6853
6854        let project = cx.update(|cx| {
6855            project::Project::ssh(
6856                session,
6857                app_state.client.clone(),
6858                app_state.node_runtime.clone(),
6859                app_state.user_store.clone(),
6860                app_state.languages.clone(),
6861                app_state.fs.clone(),
6862                cx,
6863            )
6864        })?;
6865
6866        open_ssh_project_inner(
6867            project,
6868            paths,
6869            serialized_ssh_project,
6870            workspace_id,
6871            serialized_workspace,
6872            app_state,
6873            window,
6874            cx,
6875        )
6876        .await
6877    })
6878}
6879
6880pub fn open_ssh_project_with_existing_connection(
6881    connection_options: SshConnectionOptions,
6882    project: Entity<Project>,
6883    paths: Vec<PathBuf>,
6884    app_state: Arc<AppState>,
6885    window: WindowHandle<Workspace>,
6886    cx: &mut AsyncApp,
6887) -> Task<Result<()>> {
6888    cx.spawn(async move |cx| {
6889        let (serialized_ssh_project, workspace_id, serialized_workspace) =
6890            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6891
6892        open_ssh_project_inner(
6893            project,
6894            paths,
6895            serialized_ssh_project,
6896            workspace_id,
6897            serialized_workspace,
6898            app_state,
6899            window,
6900            cx,
6901        )
6902        .await
6903    })
6904}
6905
6906async fn open_ssh_project_inner(
6907    project: Entity<Project>,
6908    paths: Vec<PathBuf>,
6909    serialized_ssh_project: SerializedSshProject,
6910    workspace_id: WorkspaceId,
6911    serialized_workspace: Option<SerializedWorkspace>,
6912    app_state: Arc<AppState>,
6913    window: WindowHandle<Workspace>,
6914    cx: &mut AsyncApp,
6915) -> Result<()> {
6916    let toolchains = DB.toolchains(workspace_id).await?;
6917    for (toolchain, worktree_id, path) in toolchains {
6918        project
6919            .update(cx, |this, cx| {
6920                this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
6921            })?
6922            .await;
6923    }
6924    let mut project_paths_to_open = vec![];
6925    let mut project_path_errors = vec![];
6926
6927    for path in paths {
6928        let result = cx
6929            .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6930            .await;
6931        match result {
6932            Ok((_, project_path)) => {
6933                project_paths_to_open.push((path.clone(), Some(project_path)));
6934            }
6935            Err(error) => {
6936                project_path_errors.push(error);
6937            }
6938        };
6939    }
6940
6941    if project_paths_to_open.is_empty() {
6942        return Err(project_path_errors.pop().context("no paths given")?);
6943    }
6944
6945    cx.update_window(window.into(), |_, window, cx| {
6946        window.replace_root(cx, |window, cx| {
6947            telemetry::event!("SSH Project Opened");
6948
6949            let mut workspace =
6950                Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6951            workspace.set_serialized_ssh_project(serialized_ssh_project);
6952            workspace.update_history(cx);
6953            workspace
6954        });
6955    })?;
6956
6957    window
6958        .update(cx, |_, window, cx| {
6959            window.activate_window();
6960            open_items(serialized_workspace, project_paths_to_open, window, cx)
6961        })?
6962        .await?;
6963
6964    window.update(cx, |workspace, _, cx| {
6965        for error in project_path_errors {
6966            if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6967                if let Some(path) = error.error_tag("path") {
6968                    workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6969                }
6970            } else {
6971                workspace.show_error(&error, cx)
6972            }
6973        }
6974    })?;
6975
6976    Ok(())
6977}
6978
6979fn serialize_ssh_project(
6980    connection_options: SshConnectionOptions,
6981    paths: Vec<PathBuf>,
6982    cx: &AsyncApp,
6983) -> Task<
6984    Result<(
6985        SerializedSshProject,
6986        WorkspaceId,
6987        Option<SerializedWorkspace>,
6988    )>,
6989> {
6990    cx.background_spawn(async move {
6991        let serialized_ssh_project = persistence::DB
6992            .get_or_create_ssh_project(
6993                connection_options.host.clone(),
6994                connection_options.port,
6995                paths
6996                    .iter()
6997                    .map(|path| path.to_string_lossy().to_string())
6998                    .collect::<Vec<_>>(),
6999                connection_options.username.clone(),
7000            )
7001            .await?;
7002
7003        let serialized_workspace =
7004            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
7005
7006        let workspace_id = if let Some(workspace_id) =
7007            serialized_workspace.as_ref().map(|workspace| workspace.id)
7008        {
7009            workspace_id
7010        } else {
7011            persistence::DB.next_id().await?
7012        };
7013
7014        Ok((serialized_ssh_project, workspace_id, serialized_workspace))
7015    })
7016}
7017
7018pub fn join_in_room_project(
7019    project_id: u64,
7020    follow_user_id: u64,
7021    app_state: Arc<AppState>,
7022    cx: &mut App,
7023) -> Task<Result<()>> {
7024    let windows = cx.windows();
7025    cx.spawn(async move |cx| {
7026        let existing_workspace = windows.into_iter().find_map(|window_handle| {
7027            window_handle
7028                .downcast::<Workspace>()
7029                .and_then(|window_handle| {
7030                    window_handle
7031                        .update(cx, |workspace, _window, cx| {
7032                            if workspace.project().read(cx).remote_id() == Some(project_id) {
7033                                Some(window_handle)
7034                            } else {
7035                                None
7036                            }
7037                        })
7038                        .unwrap_or(None)
7039                })
7040        });
7041
7042        let workspace = if let Some(existing_workspace) = existing_workspace {
7043            existing_workspace
7044        } else {
7045            let active_call = cx.update(|cx| ActiveCall::global(cx))?;
7046            let room = active_call
7047                .read_with(cx, |call, _| call.room().cloned())?
7048                .context("not in a call")?;
7049            let project = room
7050                .update(cx, |room, cx| {
7051                    room.join_project(
7052                        project_id,
7053                        app_state.languages.clone(),
7054                        app_state.fs.clone(),
7055                        cx,
7056                    )
7057                })?
7058                .await?;
7059
7060            let window_bounds_override = window_bounds_env_override();
7061            cx.update(|cx| {
7062                let mut options = (app_state.build_window_options)(None, cx);
7063                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
7064                cx.open_window(options, |window, cx| {
7065                    cx.new(|cx| {
7066                        Workspace::new(Default::default(), project, app_state.clone(), window, cx)
7067                    })
7068                })
7069            })??
7070        };
7071
7072        workspace.update(cx, |workspace, window, cx| {
7073            cx.activate(true);
7074            window.activate_window();
7075
7076            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
7077                let follow_peer_id = room
7078                    .read(cx)
7079                    .remote_participants()
7080                    .iter()
7081                    .find(|(_, participant)| participant.user.id == follow_user_id)
7082                    .map(|(_, p)| p.peer_id)
7083                    .or_else(|| {
7084                        // If we couldn't follow the given user, follow the host instead.
7085                        let collaborator = workspace
7086                            .project()
7087                            .read(cx)
7088                            .collaborators()
7089                            .values()
7090                            .find(|collaborator| collaborator.is_host)?;
7091                        Some(collaborator.peer_id)
7092                    });
7093
7094                if let Some(follow_peer_id) = follow_peer_id {
7095                    workspace.follow(follow_peer_id, window, cx);
7096                }
7097            }
7098        })?;
7099
7100        anyhow::Ok(())
7101    })
7102}
7103
7104pub fn reload(reload: &Reload, cx: &mut App) {
7105    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
7106    let mut workspace_windows = cx
7107        .windows()
7108        .into_iter()
7109        .filter_map(|window| window.downcast::<Workspace>())
7110        .collect::<Vec<_>>();
7111
7112    // If multiple windows have unsaved changes, and need a save prompt,
7113    // prompt in the active window before switching to a different window.
7114    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
7115
7116    let mut prompt = None;
7117    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
7118        prompt = window
7119            .update(cx, |_, window, cx| {
7120                window.prompt(
7121                    PromptLevel::Info,
7122                    "Are you sure you want to restart?",
7123                    None,
7124                    &["Restart", "Cancel"],
7125                    cx,
7126                )
7127            })
7128            .ok();
7129    }
7130
7131    let binary_path = reload.binary_path.clone();
7132    cx.spawn(async move |cx| {
7133        if let Some(prompt) = prompt {
7134            let answer = prompt.await?;
7135            if answer != 0 {
7136                return Ok(());
7137            }
7138        }
7139
7140        // If the user cancels any save prompt, then keep the app open.
7141        for window in workspace_windows {
7142            if let Ok(should_close) = window.update(cx, |workspace, window, cx| {
7143                workspace.prepare_to_close(CloseIntent::Quit, window, cx)
7144            }) {
7145                if !should_close.await? {
7146                    return Ok(());
7147                }
7148            }
7149        }
7150
7151        cx.update(|cx| cx.restart(binary_path))
7152    })
7153    .detach_and_log_err(cx);
7154}
7155
7156fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
7157    let mut parts = value.split(',');
7158    let x: usize = parts.next()?.parse().ok()?;
7159    let y: usize = parts.next()?.parse().ok()?;
7160    Some(point(px(x as f32), px(y as f32)))
7161}
7162
7163fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
7164    let mut parts = value.split(',');
7165    let width: usize = parts.next()?.parse().ok()?;
7166    let height: usize = parts.next()?.parse().ok()?;
7167    Some(size(px(width as f32), px(height as f32)))
7168}
7169
7170pub fn client_side_decorations(
7171    element: impl IntoElement,
7172    window: &mut Window,
7173    cx: &mut App,
7174) -> Stateful<Div> {
7175    const BORDER_SIZE: Pixels = px(1.0);
7176    let decorations = window.window_decorations();
7177
7178    if matches!(decorations, Decorations::Client { .. }) {
7179        window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
7180    }
7181
7182    struct GlobalResizeEdge(ResizeEdge);
7183    impl Global for GlobalResizeEdge {}
7184
7185    div()
7186        .id("window-backdrop")
7187        .bg(transparent_black())
7188        .map(|div| match decorations {
7189            Decorations::Server => div,
7190            Decorations::Client { tiling, .. } => div
7191                .when(!(tiling.top || tiling.right), |div| {
7192                    div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7193                })
7194                .when(!(tiling.top || tiling.left), |div| {
7195                    div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7196                })
7197                .when(!(tiling.bottom || tiling.right), |div| {
7198                    div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7199                })
7200                .when(!(tiling.bottom || tiling.left), |div| {
7201                    div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7202                })
7203                .when(!tiling.top, |div| {
7204                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
7205                })
7206                .when(!tiling.bottom, |div| {
7207                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
7208                })
7209                .when(!tiling.left, |div| {
7210                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
7211                })
7212                .when(!tiling.right, |div| {
7213                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
7214                })
7215                .on_mouse_move(move |e, window, cx| {
7216                    let size = window.window_bounds().get_bounds().size;
7217                    let pos = e.position;
7218
7219                    let new_edge =
7220                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
7221
7222                    let edge = cx.try_global::<GlobalResizeEdge>();
7223                    if new_edge != edge.map(|edge| edge.0) {
7224                        window
7225                            .window_handle()
7226                            .update(cx, |workspace, _, cx| {
7227                                cx.notify(workspace.entity_id());
7228                            })
7229                            .ok();
7230                    }
7231                })
7232                .on_mouse_down(MouseButton::Left, move |e, window, _| {
7233                    let size = window.window_bounds().get_bounds().size;
7234                    let pos = e.position;
7235
7236                    let edge = match resize_edge(
7237                        pos,
7238                        theme::CLIENT_SIDE_DECORATION_SHADOW,
7239                        size,
7240                        tiling,
7241                    ) {
7242                        Some(value) => value,
7243                        None => return,
7244                    };
7245
7246                    window.start_window_resize(edge);
7247                }),
7248        })
7249        .size_full()
7250        .child(
7251            div()
7252                .cursor(CursorStyle::Arrow)
7253                .map(|div| match decorations {
7254                    Decorations::Server => div,
7255                    Decorations::Client { tiling } => div
7256                        .border_color(cx.theme().colors().border)
7257                        .when(!(tiling.top || tiling.right), |div| {
7258                            div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7259                        })
7260                        .when(!(tiling.top || tiling.left), |div| {
7261                            div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7262                        })
7263                        .when(!(tiling.bottom || tiling.right), |div| {
7264                            div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7265                        })
7266                        .when(!(tiling.bottom || tiling.left), |div| {
7267                            div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7268                        })
7269                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
7270                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
7271                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
7272                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
7273                        .when(!tiling.is_tiled(), |div| {
7274                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
7275                                color: Hsla {
7276                                    h: 0.,
7277                                    s: 0.,
7278                                    l: 0.,
7279                                    a: 0.4,
7280                                },
7281                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
7282                                spread_radius: px(0.),
7283                                offset: point(px(0.0), px(0.0)),
7284                            }])
7285                        }),
7286                })
7287                .on_mouse_move(|_e, _, cx| {
7288                    cx.stop_propagation();
7289                })
7290                .size_full()
7291                .child(element),
7292        )
7293        .map(|div| match decorations {
7294            Decorations::Server => div,
7295            Decorations::Client { tiling, .. } => div.child(
7296                canvas(
7297                    |_bounds, window, _| {
7298                        window.insert_hitbox(
7299                            Bounds::new(
7300                                point(px(0.0), px(0.0)),
7301                                window.window_bounds().get_bounds().size,
7302                            ),
7303                            false,
7304                        )
7305                    },
7306                    move |_bounds, hitbox, window, cx| {
7307                        let mouse = window.mouse_position();
7308                        let size = window.window_bounds().get_bounds().size;
7309                        let Some(edge) =
7310                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
7311                        else {
7312                            return;
7313                        };
7314                        cx.set_global(GlobalResizeEdge(edge));
7315                        window.set_cursor_style(
7316                            match edge {
7317                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
7318                                ResizeEdge::Left | ResizeEdge::Right => {
7319                                    CursorStyle::ResizeLeftRight
7320                                }
7321                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
7322                                    CursorStyle::ResizeUpLeftDownRight
7323                                }
7324                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
7325                                    CursorStyle::ResizeUpRightDownLeft
7326                                }
7327                            },
7328                            Some(&hitbox),
7329                        );
7330                    },
7331                )
7332                .size_full()
7333                .absolute(),
7334            ),
7335        })
7336}
7337
7338fn resize_edge(
7339    pos: Point<Pixels>,
7340    shadow_size: Pixels,
7341    window_size: Size<Pixels>,
7342    tiling: Tiling,
7343) -> Option<ResizeEdge> {
7344    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
7345    if bounds.contains(&pos) {
7346        return None;
7347    }
7348
7349    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
7350    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
7351    if !tiling.top && top_left_bounds.contains(&pos) {
7352        return Some(ResizeEdge::TopLeft);
7353    }
7354
7355    let top_right_bounds = Bounds::new(
7356        Point::new(window_size.width - corner_size.width, px(0.)),
7357        corner_size,
7358    );
7359    if !tiling.top && top_right_bounds.contains(&pos) {
7360        return Some(ResizeEdge::TopRight);
7361    }
7362
7363    let bottom_left_bounds = Bounds::new(
7364        Point::new(px(0.), window_size.height - corner_size.height),
7365        corner_size,
7366    );
7367    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
7368        return Some(ResizeEdge::BottomLeft);
7369    }
7370
7371    let bottom_right_bounds = Bounds::new(
7372        Point::new(
7373            window_size.width - corner_size.width,
7374            window_size.height - corner_size.height,
7375        ),
7376        corner_size,
7377    );
7378    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
7379        return Some(ResizeEdge::BottomRight);
7380    }
7381
7382    if !tiling.top && pos.y < shadow_size {
7383        Some(ResizeEdge::Top)
7384    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
7385        Some(ResizeEdge::Bottom)
7386    } else if !tiling.left && pos.x < shadow_size {
7387        Some(ResizeEdge::Left)
7388    } else if !tiling.right && pos.x > window_size.width - shadow_size {
7389        Some(ResizeEdge::Right)
7390    } else {
7391        None
7392    }
7393}
7394
7395fn join_pane_into_active(
7396    active_pane: &Entity<Pane>,
7397    pane: &Entity<Pane>,
7398    window: &mut Window,
7399    cx: &mut App,
7400) {
7401    if pane == active_pane {
7402        return;
7403    } else if pane.read(cx).items_len() == 0 {
7404        pane.update(cx, |_, cx| {
7405            cx.emit(pane::Event::Remove {
7406                focus_on_pane: None,
7407            });
7408        })
7409    } else {
7410        move_all_items(pane, active_pane, window, cx);
7411    }
7412}
7413
7414fn move_all_items(
7415    from_pane: &Entity<Pane>,
7416    to_pane: &Entity<Pane>,
7417    window: &mut Window,
7418    cx: &mut App,
7419) {
7420    let destination_is_different = from_pane != to_pane;
7421    let mut moved_items = 0;
7422    for (item_ix, item_handle) in from_pane
7423        .read(cx)
7424        .items()
7425        .enumerate()
7426        .map(|(ix, item)| (ix, item.clone()))
7427        .collect::<Vec<_>>()
7428    {
7429        let ix = item_ix - moved_items;
7430        if destination_is_different {
7431            // Close item from previous pane
7432            from_pane.update(cx, |source, cx| {
7433                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
7434            });
7435            moved_items += 1;
7436        }
7437
7438        // This automatically removes duplicate items in the pane
7439        to_pane.update(cx, |destination, cx| {
7440            destination.add_item(item_handle, true, true, None, window, cx);
7441            window.focus(&destination.focus_handle(cx))
7442        });
7443    }
7444}
7445
7446pub fn move_item(
7447    source: &Entity<Pane>,
7448    destination: &Entity<Pane>,
7449    item_id_to_move: EntityId,
7450    destination_index: usize,
7451    window: &mut Window,
7452    cx: &mut App,
7453) {
7454    let Some((item_ix, item_handle)) = source
7455        .read(cx)
7456        .items()
7457        .enumerate()
7458        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
7459        .map(|(ix, item)| (ix, item.clone()))
7460    else {
7461        // Tab was closed during drag
7462        return;
7463    };
7464
7465    if source != destination {
7466        // Close item from previous pane
7467        source.update(cx, |source, cx| {
7468            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
7469        });
7470    }
7471
7472    // This automatically removes duplicate items in the pane
7473    destination.update(cx, |destination, cx| {
7474        destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
7475        window.focus(&destination.focus_handle(cx))
7476    });
7477}
7478
7479pub fn move_active_item(
7480    source: &Entity<Pane>,
7481    destination: &Entity<Pane>,
7482    focus_destination: bool,
7483    close_if_empty: bool,
7484    window: &mut Window,
7485    cx: &mut App,
7486) {
7487    if source == destination {
7488        return;
7489    }
7490    let Some(active_item) = source.read(cx).active_item() else {
7491        return;
7492    };
7493    source.update(cx, |source_pane, cx| {
7494        let item_id = active_item.item_id();
7495        source_pane.remove_item(item_id, false, close_if_empty, window, cx);
7496        destination.update(cx, |target_pane, cx| {
7497            target_pane.add_item(
7498                active_item,
7499                focus_destination,
7500                focus_destination,
7501                Some(target_pane.items_len()),
7502                window,
7503                cx,
7504            );
7505        });
7506    });
7507}
7508
7509#[derive(Debug)]
7510pub struct WorkspacePosition {
7511    pub window_bounds: Option<WindowBounds>,
7512    pub display: Option<Uuid>,
7513    pub centered_layout: bool,
7514}
7515
7516pub fn ssh_workspace_position_from_db(
7517    host: String,
7518    port: Option<u16>,
7519    user: Option<String>,
7520    paths_to_open: &[PathBuf],
7521    cx: &App,
7522) -> Task<Result<WorkspacePosition>> {
7523    let paths = paths_to_open
7524        .iter()
7525        .map(|path| path.to_string_lossy().to_string())
7526        .collect::<Vec<_>>();
7527
7528    cx.background_spawn(async move {
7529        let serialized_ssh_project = persistence::DB
7530            .get_or_create_ssh_project(host, port, paths, user)
7531            .await
7532            .context("fetching serialized ssh project")?;
7533        let serialized_workspace =
7534            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
7535
7536        let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
7537            (Some(WindowBounds::Windowed(bounds)), None)
7538        } else {
7539            let restorable_bounds = serialized_workspace
7540                .as_ref()
7541                .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
7542                .or_else(|| {
7543                    let (display, window_bounds) = DB.last_window().log_err()?;
7544                    Some((display?, window_bounds?))
7545                });
7546
7547            if let Some((serialized_display, serialized_status)) = restorable_bounds {
7548                (Some(serialized_status.0), Some(serialized_display))
7549            } else {
7550                (None, None)
7551            }
7552        };
7553
7554        let centered_layout = serialized_workspace
7555            .as_ref()
7556            .map(|w| w.centered_layout)
7557            .unwrap_or(false);
7558
7559        Ok(WorkspacePosition {
7560            window_bounds,
7561            display,
7562            centered_layout,
7563        })
7564    })
7565}
7566
7567#[cfg(test)]
7568mod tests {
7569    use std::{cell::RefCell, rc::Rc};
7570
7571    use super::*;
7572    use crate::{
7573        dock::{PanelEvent, test::TestPanel},
7574        item::{
7575            ItemEvent,
7576            test::{TestItem, TestProjectItem},
7577        },
7578    };
7579    use fs::FakeFs;
7580    use gpui::{
7581        DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
7582        UpdateGlobal, VisualTestContext, px,
7583    };
7584    use project::{Project, ProjectEntryId};
7585    use serde_json::json;
7586    use settings::SettingsStore;
7587
7588    #[gpui::test]
7589    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
7590        init_test(cx);
7591
7592        let fs = FakeFs::new(cx.executor());
7593        let project = Project::test(fs, [], cx).await;
7594        let (workspace, cx) =
7595            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7596
7597        // Adding an item with no ambiguity renders the tab without detail.
7598        let item1 = cx.new(|cx| {
7599            let mut item = TestItem::new(cx);
7600            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
7601            item
7602        });
7603        workspace.update_in(cx, |workspace, window, cx| {
7604            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7605        });
7606        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
7607
7608        // Adding an item that creates ambiguity increases the level of detail on
7609        // both tabs.
7610        let item2 = cx.new_window_entity(|_window, cx| {
7611            let mut item = TestItem::new(cx);
7612            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7613            item
7614        });
7615        workspace.update_in(cx, |workspace, window, cx| {
7616            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7617        });
7618        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7619        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7620
7621        // Adding an item that creates ambiguity increases the level of detail only
7622        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
7623        // we stop at the highest detail available.
7624        let item3 = cx.new(|cx| {
7625            let mut item = TestItem::new(cx);
7626            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7627            item
7628        });
7629        workspace.update_in(cx, |workspace, window, cx| {
7630            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7631        });
7632        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7633        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7634        item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7635    }
7636
7637    #[gpui::test]
7638    async fn test_tracking_active_path(cx: &mut TestAppContext) {
7639        init_test(cx);
7640
7641        let fs = FakeFs::new(cx.executor());
7642        fs.insert_tree(
7643            "/root1",
7644            json!({
7645                "one.txt": "",
7646                "two.txt": "",
7647            }),
7648        )
7649        .await;
7650        fs.insert_tree(
7651            "/root2",
7652            json!({
7653                "three.txt": "",
7654            }),
7655        )
7656        .await;
7657
7658        let project = Project::test(fs, ["root1".as_ref()], cx).await;
7659        let (workspace, cx) =
7660            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7661        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7662        let worktree_id = project.update(cx, |project, cx| {
7663            project.worktrees(cx).next().unwrap().read(cx).id()
7664        });
7665
7666        let item1 = cx.new(|cx| {
7667            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
7668        });
7669        let item2 = cx.new(|cx| {
7670            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
7671        });
7672
7673        // Add an item to an empty pane
7674        workspace.update_in(cx, |workspace, window, cx| {
7675            workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
7676        });
7677        project.update(cx, |project, cx| {
7678            assert_eq!(
7679                project.active_entry(),
7680                project
7681                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7682                    .map(|e| e.id)
7683            );
7684        });
7685        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7686
7687        // Add a second item to a non-empty pane
7688        workspace.update_in(cx, |workspace, window, cx| {
7689            workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
7690        });
7691        assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
7692        project.update(cx, |project, cx| {
7693            assert_eq!(
7694                project.active_entry(),
7695                project
7696                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
7697                    .map(|e| e.id)
7698            );
7699        });
7700
7701        // Close the active item
7702        pane.update_in(cx, |pane, window, cx| {
7703            pane.close_active_item(&Default::default(), window, cx)
7704                .unwrap()
7705        })
7706        .await
7707        .unwrap();
7708        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7709        project.update(cx, |project, cx| {
7710            assert_eq!(
7711                project.active_entry(),
7712                project
7713                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7714                    .map(|e| e.id)
7715            );
7716        });
7717
7718        // Add a project folder
7719        project
7720            .update(cx, |project, cx| {
7721                project.find_or_create_worktree("root2", true, cx)
7722            })
7723            .await
7724            .unwrap();
7725        assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
7726
7727        // Remove a project folder
7728        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
7729        assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
7730    }
7731
7732    #[gpui::test]
7733    async fn test_close_window(cx: &mut TestAppContext) {
7734        init_test(cx);
7735
7736        let fs = FakeFs::new(cx.executor());
7737        fs.insert_tree("/root", json!({ "one": "" })).await;
7738
7739        let project = Project::test(fs, ["root".as_ref()], cx).await;
7740        let (workspace, cx) =
7741            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7742
7743        // When there are no dirty items, there's nothing to do.
7744        let item1 = cx.new(TestItem::new);
7745        workspace.update_in(cx, |w, window, cx| {
7746            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
7747        });
7748        let task = workspace.update_in(cx, |w, window, cx| {
7749            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7750        });
7751        assert!(task.await.unwrap());
7752
7753        // When there are dirty untitled items, prompt to save each one. If the user
7754        // cancels any prompt, then abort.
7755        let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
7756        let item3 = cx.new(|cx| {
7757            TestItem::new(cx)
7758                .with_dirty(true)
7759                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7760        });
7761        workspace.update_in(cx, |w, window, cx| {
7762            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7763            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7764        });
7765        let task = workspace.update_in(cx, |w, window, cx| {
7766            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7767        });
7768        cx.executor().run_until_parked();
7769        cx.simulate_prompt_answer("Cancel"); // cancel save all
7770        cx.executor().run_until_parked();
7771        assert!(!cx.has_pending_prompt());
7772        assert!(!task.await.unwrap());
7773    }
7774
7775    #[gpui::test]
7776    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
7777        init_test(cx);
7778
7779        // Register TestItem as a serializable item
7780        cx.update(|cx| {
7781            register_serializable_item::<TestItem>(cx);
7782        });
7783
7784        let fs = FakeFs::new(cx.executor());
7785        fs.insert_tree("/root", json!({ "one": "" })).await;
7786
7787        let project = Project::test(fs, ["root".as_ref()], cx).await;
7788        let (workspace, cx) =
7789            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7790
7791        // When there are dirty untitled items, but they can serialize, then there is no prompt.
7792        let item1 = cx.new(|cx| {
7793            TestItem::new(cx)
7794                .with_dirty(true)
7795                .with_serialize(|| Some(Task::ready(Ok(()))))
7796        });
7797        let item2 = cx.new(|cx| {
7798            TestItem::new(cx)
7799                .with_dirty(true)
7800                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7801                .with_serialize(|| Some(Task::ready(Ok(()))))
7802        });
7803        workspace.update_in(cx, |w, window, cx| {
7804            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7805            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7806        });
7807        let task = workspace.update_in(cx, |w, window, cx| {
7808            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7809        });
7810        assert!(task.await.unwrap());
7811    }
7812
7813    #[gpui::test]
7814    async fn test_close_pane_items(cx: &mut TestAppContext) {
7815        init_test(cx);
7816
7817        let fs = FakeFs::new(cx.executor());
7818
7819        let project = Project::test(fs, None, cx).await;
7820        let (workspace, cx) =
7821            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7822
7823        let item1 = cx.new(|cx| {
7824            TestItem::new(cx)
7825                .with_dirty(true)
7826                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7827        });
7828        let item2 = cx.new(|cx| {
7829            TestItem::new(cx)
7830                .with_dirty(true)
7831                .with_conflict(true)
7832                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7833        });
7834        let item3 = cx.new(|cx| {
7835            TestItem::new(cx)
7836                .with_dirty(true)
7837                .with_conflict(true)
7838                .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7839        });
7840        let item4 = cx.new(|cx| {
7841            TestItem::new(cx).with_dirty(true).with_project_items(&[{
7842                let project_item = TestProjectItem::new_untitled(cx);
7843                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7844                project_item
7845            }])
7846        });
7847        let pane = workspace.update_in(cx, |workspace, window, cx| {
7848            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7849            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7850            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7851            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7852            workspace.active_pane().clone()
7853        });
7854
7855        let close_items = pane.update_in(cx, |pane, window, cx| {
7856            pane.activate_item(1, true, true, window, cx);
7857            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7858            let item1_id = item1.item_id();
7859            let item3_id = item3.item_id();
7860            let item4_id = item4.item_id();
7861            pane.close_items(window, cx, SaveIntent::Close, move |id| {
7862                [item1_id, item3_id, item4_id].contains(&id)
7863            })
7864        });
7865        cx.executor().run_until_parked();
7866
7867        assert!(cx.has_pending_prompt());
7868        cx.simulate_prompt_answer("Save all");
7869
7870        cx.executor().run_until_parked();
7871
7872        // Item 1 is saved. There's a prompt to save item 3.
7873        pane.update(cx, |pane, cx| {
7874            assert_eq!(item1.read(cx).save_count, 1);
7875            assert_eq!(item1.read(cx).save_as_count, 0);
7876            assert_eq!(item1.read(cx).reload_count, 0);
7877            assert_eq!(pane.items_len(), 3);
7878            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7879        });
7880        assert!(cx.has_pending_prompt());
7881
7882        // Cancel saving item 3.
7883        cx.simulate_prompt_answer("Discard");
7884        cx.executor().run_until_parked();
7885
7886        // Item 3 is reloaded. There's a prompt to save item 4.
7887        pane.update(cx, |pane, cx| {
7888            assert_eq!(item3.read(cx).save_count, 0);
7889            assert_eq!(item3.read(cx).save_as_count, 0);
7890            assert_eq!(item3.read(cx).reload_count, 1);
7891            assert_eq!(pane.items_len(), 2);
7892            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7893        });
7894
7895        // There's a prompt for a path for item 4.
7896        cx.simulate_new_path_selection(|_| Some(Default::default()));
7897        close_items.await.unwrap();
7898
7899        // The requested items are closed.
7900        pane.update(cx, |pane, cx| {
7901            assert_eq!(item4.read(cx).save_count, 0);
7902            assert_eq!(item4.read(cx).save_as_count, 1);
7903            assert_eq!(item4.read(cx).reload_count, 0);
7904            assert_eq!(pane.items_len(), 1);
7905            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7906        });
7907    }
7908
7909    #[gpui::test]
7910    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7911        init_test(cx);
7912
7913        let fs = FakeFs::new(cx.executor());
7914        let project = Project::test(fs, [], cx).await;
7915        let (workspace, cx) =
7916            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7917
7918        // Create several workspace items with single project entries, and two
7919        // workspace items with multiple project entries.
7920        let single_entry_items = (0..=4)
7921            .map(|project_entry_id| {
7922                cx.new(|cx| {
7923                    TestItem::new(cx)
7924                        .with_dirty(true)
7925                        .with_project_items(&[dirty_project_item(
7926                            project_entry_id,
7927                            &format!("{project_entry_id}.txt"),
7928                            cx,
7929                        )])
7930                })
7931            })
7932            .collect::<Vec<_>>();
7933        let item_2_3 = cx.new(|cx| {
7934            TestItem::new(cx)
7935                .with_dirty(true)
7936                .with_singleton(false)
7937                .with_project_items(&[
7938                    single_entry_items[2].read(cx).project_items[0].clone(),
7939                    single_entry_items[3].read(cx).project_items[0].clone(),
7940                ])
7941        });
7942        let item_3_4 = cx.new(|cx| {
7943            TestItem::new(cx)
7944                .with_dirty(true)
7945                .with_singleton(false)
7946                .with_project_items(&[
7947                    single_entry_items[3].read(cx).project_items[0].clone(),
7948                    single_entry_items[4].read(cx).project_items[0].clone(),
7949                ])
7950        });
7951
7952        // Create two panes that contain the following project entries:
7953        //   left pane:
7954        //     multi-entry items:   (2, 3)
7955        //     single-entry items:  0, 2, 3, 4
7956        //   right pane:
7957        //     single-entry items:  4, 1
7958        //     multi-entry items:   (3, 4)
7959        let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
7960            let left_pane = workspace.active_pane().clone();
7961            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7962            workspace.add_item_to_active_pane(
7963                single_entry_items[0].boxed_clone(),
7964                None,
7965                true,
7966                window,
7967                cx,
7968            );
7969            workspace.add_item_to_active_pane(
7970                single_entry_items[2].boxed_clone(),
7971                None,
7972                true,
7973                window,
7974                cx,
7975            );
7976            workspace.add_item_to_active_pane(
7977                single_entry_items[3].boxed_clone(),
7978                None,
7979                true,
7980                window,
7981                cx,
7982            );
7983            workspace.add_item_to_active_pane(
7984                single_entry_items[4].boxed_clone(),
7985                None,
7986                true,
7987                window,
7988                cx,
7989            );
7990
7991            let right_pane = workspace
7992                .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7993                .unwrap();
7994
7995            right_pane.update(cx, |pane, cx| {
7996                pane.add_item(
7997                    single_entry_items[1].boxed_clone(),
7998                    true,
7999                    true,
8000                    None,
8001                    window,
8002                    cx,
8003                );
8004                pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
8005            });
8006
8007            (left_pane, right_pane)
8008        });
8009
8010        cx.focus(&right_pane);
8011
8012        let mut close = right_pane.update_in(cx, |pane, window, cx| {
8013            pane.close_all_items(&CloseAllItems::default(), window, cx)
8014                .unwrap()
8015        });
8016        cx.executor().run_until_parked();
8017
8018        let msg = cx.pending_prompt().unwrap().0;
8019        assert!(msg.contains("1.txt"));
8020        assert!(!msg.contains("2.txt"));
8021        assert!(!msg.contains("3.txt"));
8022        assert!(!msg.contains("4.txt"));
8023
8024        cx.simulate_prompt_answer("Cancel");
8025        close.await.unwrap();
8026
8027        left_pane
8028            .update_in(cx, |left_pane, window, cx| {
8029                left_pane.close_item_by_id(
8030                    single_entry_items[3].entity_id(),
8031                    SaveIntent::Skip,
8032                    window,
8033                    cx,
8034                )
8035            })
8036            .await
8037            .unwrap();
8038
8039        close = right_pane.update_in(cx, |pane, window, cx| {
8040            pane.close_all_items(&CloseAllItems::default(), window, cx)
8041                .unwrap()
8042        });
8043        cx.executor().run_until_parked();
8044
8045        let details = cx.pending_prompt().unwrap().1;
8046        assert!(details.contains("1.txt"));
8047        assert!(!details.contains("2.txt"));
8048        assert!(details.contains("3.txt"));
8049        // ideally this assertion could be made, but today we can only
8050        // save whole items not project items, so the orphaned item 3 causes
8051        // 4 to be saved too.
8052        // assert!(!details.contains("4.txt"));
8053
8054        cx.simulate_prompt_answer("Save all");
8055
8056        cx.executor().run_until_parked();
8057        close.await.unwrap();
8058        right_pane.update(cx, |pane, _| {
8059            assert_eq!(pane.items_len(), 0);
8060        });
8061    }
8062
8063    #[gpui::test]
8064    async fn test_autosave(cx: &mut gpui::TestAppContext) {
8065        init_test(cx);
8066
8067        let fs = FakeFs::new(cx.executor());
8068        let project = Project::test(fs, [], cx).await;
8069        let (workspace, cx) =
8070            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8071        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8072
8073        let item = cx.new(|cx| {
8074            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
8075        });
8076        let item_id = item.entity_id();
8077        workspace.update_in(cx, |workspace, window, cx| {
8078            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8079        });
8080
8081        // Autosave on window change.
8082        item.update(cx, |item, cx| {
8083            SettingsStore::update_global(cx, |settings, cx| {
8084                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8085                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
8086                })
8087            });
8088            item.is_dirty = true;
8089        });
8090
8091        // Deactivating the window saves the file.
8092        cx.deactivate_window();
8093        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
8094
8095        // Re-activating the window doesn't save the file.
8096        cx.update(|window, _| window.activate_window());
8097        cx.executor().run_until_parked();
8098        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
8099
8100        // Autosave on focus change.
8101        item.update_in(cx, |item, window, cx| {
8102            cx.focus_self(window);
8103            SettingsStore::update_global(cx, |settings, cx| {
8104                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8105                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
8106                })
8107            });
8108            item.is_dirty = true;
8109        });
8110
8111        // Blurring the item saves the file.
8112        item.update_in(cx, |_, window, _| window.blur());
8113        cx.executor().run_until_parked();
8114        item.update(cx, |item, _| assert_eq!(item.save_count, 2));
8115
8116        // Deactivating the window still saves the file.
8117        item.update_in(cx, |item, window, cx| {
8118            cx.focus_self(window);
8119            item.is_dirty = true;
8120        });
8121        cx.deactivate_window();
8122        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
8123
8124        // Autosave after delay.
8125        item.update(cx, |item, cx| {
8126            SettingsStore::update_global(cx, |settings, cx| {
8127                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8128                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
8129                })
8130            });
8131            item.is_dirty = true;
8132            cx.emit(ItemEvent::Edit);
8133        });
8134
8135        // Delay hasn't fully expired, so the file is still dirty and unsaved.
8136        cx.executor().advance_clock(Duration::from_millis(250));
8137        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
8138
8139        // After delay expires, the file is saved.
8140        cx.executor().advance_clock(Duration::from_millis(250));
8141        item.update(cx, |item, _| assert_eq!(item.save_count, 4));
8142
8143        // Autosave on focus change, ensuring closing the tab counts as such.
8144        item.update(cx, |item, cx| {
8145            SettingsStore::update_global(cx, |settings, cx| {
8146                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8147                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
8148                })
8149            });
8150            item.is_dirty = true;
8151            for project_item in &mut item.project_items {
8152                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
8153            }
8154        });
8155
8156        pane.update_in(cx, |pane, window, cx| {
8157            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
8158        })
8159        .await
8160        .unwrap();
8161        assert!(!cx.has_pending_prompt());
8162        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
8163
8164        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
8165        workspace.update_in(cx, |workspace, window, cx| {
8166            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8167        });
8168        item.update_in(cx, |item, window, cx| {
8169            item.project_items[0].update(cx, |item, _| {
8170                item.entry_id = None;
8171            });
8172            item.is_dirty = true;
8173            window.blur();
8174        });
8175        cx.run_until_parked();
8176        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
8177
8178        // Ensure autosave is prevented for deleted files also when closing the buffer.
8179        let _close_items = pane.update_in(cx, |pane, window, cx| {
8180            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
8181        });
8182        cx.run_until_parked();
8183        assert!(cx.has_pending_prompt());
8184        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
8185    }
8186
8187    #[gpui::test]
8188    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
8189        init_test(cx);
8190
8191        let fs = FakeFs::new(cx.executor());
8192
8193        let project = Project::test(fs, [], cx).await;
8194        let (workspace, cx) =
8195            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8196
8197        let item = cx.new(|cx| {
8198            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
8199        });
8200        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8201        let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
8202        let toolbar_notify_count = Rc::new(RefCell::new(0));
8203
8204        workspace.update_in(cx, |workspace, window, cx| {
8205            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8206            let toolbar_notification_count = toolbar_notify_count.clone();
8207            cx.observe_in(&toolbar, window, move |_, _, _, _| {
8208                *toolbar_notification_count.borrow_mut() += 1
8209            })
8210            .detach();
8211        });
8212
8213        pane.update(cx, |pane, _| {
8214            assert!(!pane.can_navigate_backward());
8215            assert!(!pane.can_navigate_forward());
8216        });
8217
8218        item.update_in(cx, |item, _, cx| {
8219            item.set_state("one".to_string(), cx);
8220        });
8221
8222        // Toolbar must be notified to re-render the navigation buttons
8223        assert_eq!(*toolbar_notify_count.borrow(), 1);
8224
8225        pane.update(cx, |pane, _| {
8226            assert!(pane.can_navigate_backward());
8227            assert!(!pane.can_navigate_forward());
8228        });
8229
8230        workspace
8231            .update_in(cx, |workspace, window, cx| {
8232                workspace.go_back(pane.downgrade(), window, cx)
8233            })
8234            .await
8235            .unwrap();
8236
8237        assert_eq!(*toolbar_notify_count.borrow(), 2);
8238        pane.update(cx, |pane, _| {
8239            assert!(!pane.can_navigate_backward());
8240            assert!(pane.can_navigate_forward());
8241        });
8242    }
8243
8244    #[gpui::test]
8245    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
8246        init_test(cx);
8247        let fs = FakeFs::new(cx.executor());
8248
8249        let project = Project::test(fs, [], cx).await;
8250        let (workspace, cx) =
8251            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8252
8253        let panel = workspace.update_in(cx, |workspace, window, cx| {
8254            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8255            workspace.add_panel(panel.clone(), window, cx);
8256
8257            workspace
8258                .right_dock()
8259                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8260
8261            panel
8262        });
8263
8264        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8265        pane.update_in(cx, |pane, window, cx| {
8266            let item = cx.new(TestItem::new);
8267            pane.add_item(Box::new(item), true, true, None, window, cx);
8268        });
8269
8270        // Transfer focus from center to panel
8271        workspace.update_in(cx, |workspace, window, cx| {
8272            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8273        });
8274
8275        workspace.update_in(cx, |workspace, window, cx| {
8276            assert!(workspace.right_dock().read(cx).is_open());
8277            assert!(!panel.is_zoomed(window, cx));
8278            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8279        });
8280
8281        // Transfer focus from panel to center
8282        workspace.update_in(cx, |workspace, window, cx| {
8283            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8284        });
8285
8286        workspace.update_in(cx, |workspace, window, cx| {
8287            assert!(workspace.right_dock().read(cx).is_open());
8288            assert!(!panel.is_zoomed(window, cx));
8289            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8290        });
8291
8292        // Close the dock
8293        workspace.update_in(cx, |workspace, window, cx| {
8294            workspace.toggle_dock(DockPosition::Right, window, cx);
8295        });
8296
8297        workspace.update_in(cx, |workspace, window, cx| {
8298            assert!(!workspace.right_dock().read(cx).is_open());
8299            assert!(!panel.is_zoomed(window, cx));
8300            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8301        });
8302
8303        // Open the dock
8304        workspace.update_in(cx, |workspace, window, cx| {
8305            workspace.toggle_dock(DockPosition::Right, window, cx);
8306        });
8307
8308        workspace.update_in(cx, |workspace, window, cx| {
8309            assert!(workspace.right_dock().read(cx).is_open());
8310            assert!(!panel.is_zoomed(window, cx));
8311            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8312        });
8313
8314        // Focus and zoom panel
8315        panel.update_in(cx, |panel, window, cx| {
8316            cx.focus_self(window);
8317            panel.set_zoomed(true, window, cx)
8318        });
8319
8320        workspace.update_in(cx, |workspace, window, cx| {
8321            assert!(workspace.right_dock().read(cx).is_open());
8322            assert!(panel.is_zoomed(window, cx));
8323            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8324        });
8325
8326        // Transfer focus to the center closes the dock
8327        workspace.update_in(cx, |workspace, window, cx| {
8328            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8329        });
8330
8331        workspace.update_in(cx, |workspace, window, cx| {
8332            assert!(!workspace.right_dock().read(cx).is_open());
8333            assert!(panel.is_zoomed(window, cx));
8334            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8335        });
8336
8337        // Transferring focus back to the panel keeps it zoomed
8338        workspace.update_in(cx, |workspace, window, cx| {
8339            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8340        });
8341
8342        workspace.update_in(cx, |workspace, window, cx| {
8343            assert!(workspace.right_dock().read(cx).is_open());
8344            assert!(panel.is_zoomed(window, cx));
8345            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8346        });
8347
8348        // Close the dock while it is zoomed
8349        workspace.update_in(cx, |workspace, window, cx| {
8350            workspace.toggle_dock(DockPosition::Right, window, cx)
8351        });
8352
8353        workspace.update_in(cx, |workspace, window, cx| {
8354            assert!(!workspace.right_dock().read(cx).is_open());
8355            assert!(panel.is_zoomed(window, cx));
8356            assert!(workspace.zoomed.is_none());
8357            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8358        });
8359
8360        // Opening the dock, when it's zoomed, retains focus
8361        workspace.update_in(cx, |workspace, window, cx| {
8362            workspace.toggle_dock(DockPosition::Right, window, cx)
8363        });
8364
8365        workspace.update_in(cx, |workspace, window, cx| {
8366            assert!(workspace.right_dock().read(cx).is_open());
8367            assert!(panel.is_zoomed(window, cx));
8368            assert!(workspace.zoomed.is_some());
8369            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8370        });
8371
8372        // Unzoom and close the panel, zoom the active pane.
8373        panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
8374        workspace.update_in(cx, |workspace, window, cx| {
8375            workspace.toggle_dock(DockPosition::Right, window, cx)
8376        });
8377        pane.update_in(cx, |pane, window, cx| {
8378            pane.toggle_zoom(&Default::default(), window, cx)
8379        });
8380
8381        // Opening a dock unzooms the pane.
8382        workspace.update_in(cx, |workspace, window, cx| {
8383            workspace.toggle_dock(DockPosition::Right, window, cx)
8384        });
8385        workspace.update_in(cx, |workspace, window, cx| {
8386            let pane = pane.read(cx);
8387            assert!(!pane.is_zoomed());
8388            assert!(!pane.focus_handle(cx).is_focused(window));
8389            assert!(workspace.right_dock().read(cx).is_open());
8390            assert!(workspace.zoomed.is_none());
8391        });
8392    }
8393
8394    #[gpui::test]
8395    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
8396        init_test(cx);
8397
8398        let fs = FakeFs::new(cx.executor());
8399
8400        let project = Project::test(fs, None, cx).await;
8401        let (workspace, cx) =
8402            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8403
8404        // Let's arrange the panes like this:
8405        //
8406        // +-----------------------+
8407        // |         top           |
8408        // +------+--------+-------+
8409        // | left | center | right |
8410        // +------+--------+-------+
8411        // |        bottom         |
8412        // +-----------------------+
8413
8414        let top_item = cx.new(|cx| {
8415            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
8416        });
8417        let bottom_item = cx.new(|cx| {
8418            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
8419        });
8420        let left_item = cx.new(|cx| {
8421            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
8422        });
8423        let right_item = cx.new(|cx| {
8424            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
8425        });
8426        let center_item = cx.new(|cx| {
8427            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
8428        });
8429
8430        let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8431            let top_pane_id = workspace.active_pane().entity_id();
8432            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
8433            workspace.split_pane(
8434                workspace.active_pane().clone(),
8435                SplitDirection::Down,
8436                window,
8437                cx,
8438            );
8439            top_pane_id
8440        });
8441        let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8442            let bottom_pane_id = workspace.active_pane().entity_id();
8443            workspace.add_item_to_active_pane(
8444                Box::new(bottom_item.clone()),
8445                None,
8446                false,
8447                window,
8448                cx,
8449            );
8450            workspace.split_pane(
8451                workspace.active_pane().clone(),
8452                SplitDirection::Up,
8453                window,
8454                cx,
8455            );
8456            bottom_pane_id
8457        });
8458        let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8459            let left_pane_id = workspace.active_pane().entity_id();
8460            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
8461            workspace.split_pane(
8462                workspace.active_pane().clone(),
8463                SplitDirection::Right,
8464                window,
8465                cx,
8466            );
8467            left_pane_id
8468        });
8469        let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8470            let right_pane_id = workspace.active_pane().entity_id();
8471            workspace.add_item_to_active_pane(
8472                Box::new(right_item.clone()),
8473                None,
8474                false,
8475                window,
8476                cx,
8477            );
8478            workspace.split_pane(
8479                workspace.active_pane().clone(),
8480                SplitDirection::Left,
8481                window,
8482                cx,
8483            );
8484            right_pane_id
8485        });
8486        let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8487            let center_pane_id = workspace.active_pane().entity_id();
8488            workspace.add_item_to_active_pane(
8489                Box::new(center_item.clone()),
8490                None,
8491                false,
8492                window,
8493                cx,
8494            );
8495            center_pane_id
8496        });
8497        cx.executor().run_until_parked();
8498
8499        workspace.update_in(cx, |workspace, window, cx| {
8500            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
8501
8502            // Join into next from center pane into right
8503            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8504        });
8505
8506        workspace.update_in(cx, |workspace, window, cx| {
8507            let active_pane = workspace.active_pane();
8508            assert_eq!(right_pane_id, active_pane.entity_id());
8509            assert_eq!(2, active_pane.read(cx).items_len());
8510            let item_ids_in_pane =
8511                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8512            assert!(item_ids_in_pane.contains(&center_item.item_id()));
8513            assert!(item_ids_in_pane.contains(&right_item.item_id()));
8514
8515            // Join into next from right pane into bottom
8516            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8517        });
8518
8519        workspace.update_in(cx, |workspace, window, cx| {
8520            let active_pane = workspace.active_pane();
8521            assert_eq!(bottom_pane_id, active_pane.entity_id());
8522            assert_eq!(3, active_pane.read(cx).items_len());
8523            let item_ids_in_pane =
8524                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8525            assert!(item_ids_in_pane.contains(&center_item.item_id()));
8526            assert!(item_ids_in_pane.contains(&right_item.item_id()));
8527            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8528
8529            // Join into next from bottom pane into left
8530            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8531        });
8532
8533        workspace.update_in(cx, |workspace, window, cx| {
8534            let active_pane = workspace.active_pane();
8535            assert_eq!(left_pane_id, active_pane.entity_id());
8536            assert_eq!(4, active_pane.read(cx).items_len());
8537            let item_ids_in_pane =
8538                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8539            assert!(item_ids_in_pane.contains(&center_item.item_id()));
8540            assert!(item_ids_in_pane.contains(&right_item.item_id()));
8541            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8542            assert!(item_ids_in_pane.contains(&left_item.item_id()));
8543
8544            // Join into next from left pane into top
8545            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8546        });
8547
8548        workspace.update_in(cx, |workspace, window, cx| {
8549            let active_pane = workspace.active_pane();
8550            assert_eq!(top_pane_id, active_pane.entity_id());
8551            assert_eq!(5, active_pane.read(cx).items_len());
8552            let item_ids_in_pane =
8553                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8554            assert!(item_ids_in_pane.contains(&center_item.item_id()));
8555            assert!(item_ids_in_pane.contains(&right_item.item_id()));
8556            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8557            assert!(item_ids_in_pane.contains(&left_item.item_id()));
8558            assert!(item_ids_in_pane.contains(&top_item.item_id()));
8559
8560            // Single pane left: no-op
8561            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
8562        });
8563
8564        workspace.update(cx, |workspace, _cx| {
8565            let active_pane = workspace.active_pane();
8566            assert_eq!(top_pane_id, active_pane.entity_id());
8567        });
8568    }
8569
8570    fn add_an_item_to_active_pane(
8571        cx: &mut VisualTestContext,
8572        workspace: &Entity<Workspace>,
8573        item_id: u64,
8574    ) -> Entity<TestItem> {
8575        let item = cx.new(|cx| {
8576            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
8577                item_id,
8578                "item{item_id}.txt",
8579                cx,
8580            )])
8581        });
8582        workspace.update_in(cx, |workspace, window, cx| {
8583            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
8584        });
8585        return item;
8586    }
8587
8588    fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
8589        return workspace.update_in(cx, |workspace, window, cx| {
8590            let new_pane = workspace.split_pane(
8591                workspace.active_pane().clone(),
8592                SplitDirection::Right,
8593                window,
8594                cx,
8595            );
8596            new_pane
8597        });
8598    }
8599
8600    #[gpui::test]
8601    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
8602        init_test(cx);
8603        let fs = FakeFs::new(cx.executor());
8604        let project = Project::test(fs, None, cx).await;
8605        let (workspace, cx) =
8606            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8607
8608        add_an_item_to_active_pane(cx, &workspace, 1);
8609        split_pane(cx, &workspace);
8610        add_an_item_to_active_pane(cx, &workspace, 2);
8611        split_pane(cx, &workspace); // empty pane
8612        split_pane(cx, &workspace);
8613        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
8614
8615        cx.executor().run_until_parked();
8616
8617        workspace.update(cx, |workspace, cx| {
8618            let num_panes = workspace.panes().len();
8619            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8620            let active_item = workspace
8621                .active_pane()
8622                .read(cx)
8623                .active_item()
8624                .expect("item is in focus");
8625
8626            assert_eq!(num_panes, 4);
8627            assert_eq!(num_items_in_current_pane, 1);
8628            assert_eq!(active_item.item_id(), last_item.item_id());
8629        });
8630
8631        workspace.update_in(cx, |workspace, window, cx| {
8632            workspace.join_all_panes(window, cx);
8633        });
8634
8635        workspace.update(cx, |workspace, cx| {
8636            let num_panes = workspace.panes().len();
8637            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8638            let active_item = workspace
8639                .active_pane()
8640                .read(cx)
8641                .active_item()
8642                .expect("item is in focus");
8643
8644            assert_eq!(num_panes, 1);
8645            assert_eq!(num_items_in_current_pane, 3);
8646            assert_eq!(active_item.item_id(), last_item.item_id());
8647        });
8648    }
8649    struct TestModal(FocusHandle);
8650
8651    impl TestModal {
8652        fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
8653            Self(cx.focus_handle())
8654        }
8655    }
8656
8657    impl EventEmitter<DismissEvent> for TestModal {}
8658
8659    impl Focusable for TestModal {
8660        fn focus_handle(&self, _cx: &App) -> FocusHandle {
8661            self.0.clone()
8662        }
8663    }
8664
8665    impl ModalView for TestModal {}
8666
8667    impl Render for TestModal {
8668        fn render(
8669            &mut self,
8670            _window: &mut Window,
8671            _cx: &mut Context<TestModal>,
8672        ) -> impl IntoElement {
8673            div().track_focus(&self.0)
8674        }
8675    }
8676
8677    #[gpui::test]
8678    async fn test_panels(cx: &mut gpui::TestAppContext) {
8679        init_test(cx);
8680        let fs = FakeFs::new(cx.executor());
8681
8682        let project = Project::test(fs, [], cx).await;
8683        let (workspace, cx) =
8684            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8685
8686        let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
8687            let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
8688            workspace.add_panel(panel_1.clone(), window, cx);
8689            workspace.toggle_dock(DockPosition::Left, window, cx);
8690            let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8691            workspace.add_panel(panel_2.clone(), window, cx);
8692            workspace.toggle_dock(DockPosition::Right, window, cx);
8693
8694            let left_dock = workspace.left_dock();
8695            assert_eq!(
8696                left_dock.read(cx).visible_panel().unwrap().panel_id(),
8697                panel_1.panel_id()
8698            );
8699            assert_eq!(
8700                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8701                panel_1.size(window, cx)
8702            );
8703
8704            left_dock.update(cx, |left_dock, cx| {
8705                left_dock.resize_active_panel(Some(px(1337.)), window, cx)
8706            });
8707            assert_eq!(
8708                workspace
8709                    .right_dock()
8710                    .read(cx)
8711                    .visible_panel()
8712                    .unwrap()
8713                    .panel_id(),
8714                panel_2.panel_id(),
8715            );
8716
8717            (panel_1, panel_2)
8718        });
8719
8720        // Move panel_1 to the right
8721        panel_1.update_in(cx, |panel_1, window, cx| {
8722            panel_1.set_position(DockPosition::Right, window, cx)
8723        });
8724
8725        workspace.update_in(cx, |workspace, window, cx| {
8726            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
8727            // Since it was the only panel on the left, the left dock should now be closed.
8728            assert!(!workspace.left_dock().read(cx).is_open());
8729            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
8730            let right_dock = workspace.right_dock();
8731            assert_eq!(
8732                right_dock.read(cx).visible_panel().unwrap().panel_id(),
8733                panel_1.panel_id()
8734            );
8735            assert_eq!(
8736                right_dock.read(cx).active_panel_size(window, cx).unwrap(),
8737                px(1337.)
8738            );
8739
8740            // Now we move panel_2 to the left
8741            panel_2.set_position(DockPosition::Left, window, cx);
8742        });
8743
8744        workspace.update(cx, |workspace, cx| {
8745            // Since panel_2 was not visible on the right, we don't open the left dock.
8746            assert!(!workspace.left_dock().read(cx).is_open());
8747            // And the right dock is unaffected in its displaying of panel_1
8748            assert!(workspace.right_dock().read(cx).is_open());
8749            assert_eq!(
8750                workspace
8751                    .right_dock()
8752                    .read(cx)
8753                    .visible_panel()
8754                    .unwrap()
8755                    .panel_id(),
8756                panel_1.panel_id(),
8757            );
8758        });
8759
8760        // Move panel_1 back to the left
8761        panel_1.update_in(cx, |panel_1, window, cx| {
8762            panel_1.set_position(DockPosition::Left, window, cx)
8763        });
8764
8765        workspace.update_in(cx, |workspace, window, cx| {
8766            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
8767            let left_dock = workspace.left_dock();
8768            assert!(left_dock.read(cx).is_open());
8769            assert_eq!(
8770                left_dock.read(cx).visible_panel().unwrap().panel_id(),
8771                panel_1.panel_id()
8772            );
8773            assert_eq!(
8774                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8775                px(1337.)
8776            );
8777            // And the right dock should be closed as it no longer has any panels.
8778            assert!(!workspace.right_dock().read(cx).is_open());
8779
8780            // Now we move panel_1 to the bottom
8781            panel_1.set_position(DockPosition::Bottom, window, cx);
8782        });
8783
8784        workspace.update_in(cx, |workspace, window, cx| {
8785            // Since panel_1 was visible on the left, we close the left dock.
8786            assert!(!workspace.left_dock().read(cx).is_open());
8787            // The bottom dock is sized based on the panel's default size,
8788            // since the panel orientation changed from vertical to horizontal.
8789            let bottom_dock = workspace.bottom_dock();
8790            assert_eq!(
8791                bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
8792                panel_1.size(window, cx),
8793            );
8794            // Close bottom dock and move panel_1 back to the left.
8795            bottom_dock.update(cx, |bottom_dock, cx| {
8796                bottom_dock.set_open(false, window, cx)
8797            });
8798            panel_1.set_position(DockPosition::Left, window, cx);
8799        });
8800
8801        // Emit activated event on panel 1
8802        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
8803
8804        // Now the left dock is open and panel_1 is active and focused.
8805        workspace.update_in(cx, |workspace, window, cx| {
8806            let left_dock = workspace.left_dock();
8807            assert!(left_dock.read(cx).is_open());
8808            assert_eq!(
8809                left_dock.read(cx).visible_panel().unwrap().panel_id(),
8810                panel_1.panel_id(),
8811            );
8812            assert!(panel_1.focus_handle(cx).is_focused(window));
8813        });
8814
8815        // Emit closed event on panel 2, which is not active
8816        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8817
8818        // Wo don't close the left dock, because panel_2 wasn't the active panel
8819        workspace.update(cx, |workspace, cx| {
8820            let left_dock = workspace.left_dock();
8821            assert!(left_dock.read(cx).is_open());
8822            assert_eq!(
8823                left_dock.read(cx).visible_panel().unwrap().panel_id(),
8824                panel_1.panel_id(),
8825            );
8826        });
8827
8828        // Emitting a ZoomIn event shows the panel as zoomed.
8829        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
8830        workspace.update(cx, |workspace, _| {
8831            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8832            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
8833        });
8834
8835        // Move panel to another dock while it is zoomed
8836        panel_1.update_in(cx, |panel, window, cx| {
8837            panel.set_position(DockPosition::Right, window, cx)
8838        });
8839        workspace.update(cx, |workspace, _| {
8840            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8841
8842            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8843        });
8844
8845        // This is a helper for getting a:
8846        // - valid focus on an element,
8847        // - that isn't a part of the panes and panels system of the Workspace,
8848        // - and doesn't trigger the 'on_focus_lost' API.
8849        let focus_other_view = {
8850            let workspace = workspace.clone();
8851            move |cx: &mut VisualTestContext| {
8852                workspace.update_in(cx, |workspace, window, cx| {
8853                    if let Some(_) = workspace.active_modal::<TestModal>(cx) {
8854                        workspace.toggle_modal(window, cx, TestModal::new);
8855                        workspace.toggle_modal(window, cx, TestModal::new);
8856                    } else {
8857                        workspace.toggle_modal(window, cx, TestModal::new);
8858                    }
8859                })
8860            }
8861        };
8862
8863        // If focus is transferred to another view that's not a panel or another pane, we still show
8864        // the panel as zoomed.
8865        focus_other_view(cx);
8866        workspace.update(cx, |workspace, _| {
8867            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8868            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8869        });
8870
8871        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8872        workspace.update_in(cx, |_workspace, window, cx| {
8873            cx.focus_self(window);
8874        });
8875        workspace.update(cx, |workspace, _| {
8876            assert_eq!(workspace.zoomed, None);
8877            assert_eq!(workspace.zoomed_position, None);
8878        });
8879
8880        // If focus is transferred again to another view that's not a panel or a pane, we won't
8881        // show the panel as zoomed because it wasn't zoomed before.
8882        focus_other_view(cx);
8883        workspace.update(cx, |workspace, _| {
8884            assert_eq!(workspace.zoomed, None);
8885            assert_eq!(workspace.zoomed_position, None);
8886        });
8887
8888        // When the panel is activated, it is zoomed again.
8889        cx.dispatch_action(ToggleRightDock);
8890        workspace.update(cx, |workspace, _| {
8891            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8892            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8893        });
8894
8895        // Emitting a ZoomOut event unzooms the panel.
8896        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8897        workspace.update(cx, |workspace, _| {
8898            assert_eq!(workspace.zoomed, None);
8899            assert_eq!(workspace.zoomed_position, None);
8900        });
8901
8902        // Emit closed event on panel 1, which is active
8903        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8904
8905        // Now the left dock is closed, because panel_1 was the active panel
8906        workspace.update(cx, |workspace, cx| {
8907            let right_dock = workspace.right_dock();
8908            assert!(!right_dock.read(cx).is_open());
8909        });
8910    }
8911
8912    #[gpui::test]
8913    async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8914        init_test(cx);
8915
8916        let fs = FakeFs::new(cx.background_executor.clone());
8917        let project = Project::test(fs, [], cx).await;
8918        let (workspace, cx) =
8919            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8920        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8921
8922        let dirty_regular_buffer = cx.new(|cx| {
8923            TestItem::new(cx)
8924                .with_dirty(true)
8925                .with_label("1.txt")
8926                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8927        });
8928        let dirty_regular_buffer_2 = cx.new(|cx| {
8929            TestItem::new(cx)
8930                .with_dirty(true)
8931                .with_label("2.txt")
8932                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8933        });
8934        let dirty_multi_buffer_with_both = cx.new(|cx| {
8935            TestItem::new(cx)
8936                .with_dirty(true)
8937                .with_singleton(false)
8938                .with_label("Fake Project Search")
8939                .with_project_items(&[
8940                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8941                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8942                ])
8943        });
8944        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8945        workspace.update_in(cx, |workspace, window, cx| {
8946            workspace.add_item(
8947                pane.clone(),
8948                Box::new(dirty_regular_buffer.clone()),
8949                None,
8950                false,
8951                false,
8952                window,
8953                cx,
8954            );
8955            workspace.add_item(
8956                pane.clone(),
8957                Box::new(dirty_regular_buffer_2.clone()),
8958                None,
8959                false,
8960                false,
8961                window,
8962                cx,
8963            );
8964            workspace.add_item(
8965                pane.clone(),
8966                Box::new(dirty_multi_buffer_with_both.clone()),
8967                None,
8968                false,
8969                false,
8970                window,
8971                cx,
8972            );
8973        });
8974
8975        pane.update_in(cx, |pane, window, cx| {
8976            pane.activate_item(2, true, true, window, cx);
8977            assert_eq!(
8978                pane.active_item().unwrap().item_id(),
8979                multi_buffer_with_both_files_id,
8980                "Should select the multi buffer in the pane"
8981            );
8982        });
8983        let close_all_but_multi_buffer_task = pane
8984            .update_in(cx, |pane, window, cx| {
8985                pane.close_inactive_items(
8986                    &CloseInactiveItems {
8987                        save_intent: Some(SaveIntent::Save),
8988                        close_pinned: true,
8989                    },
8990                    window,
8991                    cx,
8992                )
8993            })
8994            .expect("should have inactive files to close");
8995        cx.background_executor.run_until_parked();
8996        assert!(!cx.has_pending_prompt());
8997        close_all_but_multi_buffer_task
8998            .await
8999            .expect("Closing all buffers but the multi buffer failed");
9000        pane.update(cx, |pane, cx| {
9001            assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
9002            assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
9003            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
9004            assert_eq!(pane.items_len(), 1);
9005            assert_eq!(
9006                pane.active_item().unwrap().item_id(),
9007                multi_buffer_with_both_files_id,
9008                "Should have only the multi buffer left in the pane"
9009            );
9010            assert!(
9011                dirty_multi_buffer_with_both.read(cx).is_dirty,
9012                "The multi buffer containing the unsaved buffer should still be dirty"
9013            );
9014        });
9015
9016        dirty_regular_buffer.update(cx, |buffer, cx| {
9017            buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
9018        });
9019
9020        let close_multi_buffer_task = pane
9021            .update_in(cx, |pane, window, cx| {
9022                pane.close_active_item(
9023                    &CloseActiveItem {
9024                        save_intent: Some(SaveIntent::Close),
9025                        close_pinned: false,
9026                    },
9027                    window,
9028                    cx,
9029                )
9030            })
9031            .expect("should have the multi buffer to close");
9032        cx.background_executor.run_until_parked();
9033        assert!(
9034            cx.has_pending_prompt(),
9035            "Dirty multi buffer should prompt a save dialog"
9036        );
9037        cx.simulate_prompt_answer("Save");
9038        cx.background_executor.run_until_parked();
9039        close_multi_buffer_task
9040            .await
9041            .expect("Closing the multi buffer failed");
9042        pane.update(cx, |pane, cx| {
9043            assert_eq!(
9044                dirty_multi_buffer_with_both.read(cx).save_count,
9045                1,
9046                "Multi buffer item should get be saved"
9047            );
9048            // Test impl does not save inner items, so we do not assert them
9049            assert_eq!(
9050                pane.items_len(),
9051                0,
9052                "No more items should be left in the pane"
9053            );
9054            assert!(pane.active_item().is_none());
9055        });
9056    }
9057
9058    #[gpui::test]
9059    async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
9060        cx: &mut TestAppContext,
9061    ) {
9062        init_test(cx);
9063
9064        let fs = FakeFs::new(cx.background_executor.clone());
9065        let project = Project::test(fs, [], cx).await;
9066        let (workspace, cx) =
9067            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9068        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
9069
9070        let dirty_regular_buffer = cx.new(|cx| {
9071            TestItem::new(cx)
9072                .with_dirty(true)
9073                .with_label("1.txt")
9074                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9075        });
9076        let dirty_regular_buffer_2 = cx.new(|cx| {
9077            TestItem::new(cx)
9078                .with_dirty(true)
9079                .with_label("2.txt")
9080                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9081        });
9082        let clear_regular_buffer = cx.new(|cx| {
9083            TestItem::new(cx)
9084                .with_label("3.txt")
9085                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
9086        });
9087
9088        let dirty_multi_buffer_with_both = cx.new(|cx| {
9089            TestItem::new(cx)
9090                .with_dirty(true)
9091                .with_singleton(false)
9092                .with_label("Fake Project Search")
9093                .with_project_items(&[
9094                    dirty_regular_buffer.read(cx).project_items[0].clone(),
9095                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
9096                    clear_regular_buffer.read(cx).project_items[0].clone(),
9097                ])
9098        });
9099        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
9100        workspace.update_in(cx, |workspace, window, cx| {
9101            workspace.add_item(
9102                pane.clone(),
9103                Box::new(dirty_regular_buffer.clone()),
9104                None,
9105                false,
9106                false,
9107                window,
9108                cx,
9109            );
9110            workspace.add_item(
9111                pane.clone(),
9112                Box::new(dirty_multi_buffer_with_both.clone()),
9113                None,
9114                false,
9115                false,
9116                window,
9117                cx,
9118            );
9119        });
9120
9121        pane.update_in(cx, |pane, window, cx| {
9122            pane.activate_item(1, true, true, window, cx);
9123            assert_eq!(
9124                pane.active_item().unwrap().item_id(),
9125                multi_buffer_with_both_files_id,
9126                "Should select the multi buffer in the pane"
9127            );
9128        });
9129        let _close_multi_buffer_task = pane
9130            .update_in(cx, |pane, window, cx| {
9131                pane.close_active_item(
9132                    &CloseActiveItem {
9133                        save_intent: None,
9134                        close_pinned: false,
9135                    },
9136                    window,
9137                    cx,
9138                )
9139            })
9140            .expect("should have active multi buffer to close");
9141        cx.background_executor.run_until_parked();
9142        assert!(
9143            cx.has_pending_prompt(),
9144            "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
9145        );
9146    }
9147
9148    #[gpui::test]
9149    async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
9150        cx: &mut TestAppContext,
9151    ) {
9152        init_test(cx);
9153
9154        let fs = FakeFs::new(cx.background_executor.clone());
9155        let project = Project::test(fs, [], cx).await;
9156        let (workspace, cx) =
9157            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9158        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
9159
9160        let dirty_regular_buffer = cx.new(|cx| {
9161            TestItem::new(cx)
9162                .with_dirty(true)
9163                .with_label("1.txt")
9164                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9165        });
9166        let dirty_regular_buffer_2 = cx.new(|cx| {
9167            TestItem::new(cx)
9168                .with_dirty(true)
9169                .with_label("2.txt")
9170                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9171        });
9172        let clear_regular_buffer = cx.new(|cx| {
9173            TestItem::new(cx)
9174                .with_label("3.txt")
9175                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
9176        });
9177
9178        let dirty_multi_buffer = cx.new(|cx| {
9179            TestItem::new(cx)
9180                .with_dirty(true)
9181                .with_singleton(false)
9182                .with_label("Fake Project Search")
9183                .with_project_items(&[
9184                    dirty_regular_buffer.read(cx).project_items[0].clone(),
9185                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
9186                    clear_regular_buffer.read(cx).project_items[0].clone(),
9187                ])
9188        });
9189        workspace.update_in(cx, |workspace, window, cx| {
9190            workspace.add_item(
9191                pane.clone(),
9192                Box::new(dirty_regular_buffer.clone()),
9193                None,
9194                false,
9195                false,
9196                window,
9197                cx,
9198            );
9199            workspace.add_item(
9200                pane.clone(),
9201                Box::new(dirty_regular_buffer_2.clone()),
9202                None,
9203                false,
9204                false,
9205                window,
9206                cx,
9207            );
9208            workspace.add_item(
9209                pane.clone(),
9210                Box::new(dirty_multi_buffer.clone()),
9211                None,
9212                false,
9213                false,
9214                window,
9215                cx,
9216            );
9217        });
9218
9219        pane.update_in(cx, |pane, window, cx| {
9220            pane.activate_item(2, true, true, window, cx);
9221            assert_eq!(
9222                pane.active_item().unwrap().item_id(),
9223                dirty_multi_buffer.item_id(),
9224                "Should select the multi buffer in the pane"
9225            );
9226        });
9227        let close_multi_buffer_task = pane
9228            .update_in(cx, |pane, window, cx| {
9229                pane.close_active_item(
9230                    &CloseActiveItem {
9231                        save_intent: None,
9232                        close_pinned: false,
9233                    },
9234                    window,
9235                    cx,
9236                )
9237            })
9238            .expect("should have active multi buffer to close");
9239        cx.background_executor.run_until_parked();
9240        assert!(
9241            !cx.has_pending_prompt(),
9242            "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
9243        );
9244        close_multi_buffer_task
9245            .await
9246            .expect("Closing multi buffer failed");
9247        pane.update(cx, |pane, cx| {
9248            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
9249            assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
9250            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
9251            assert_eq!(
9252                pane.items()
9253                    .map(|item| item.item_id())
9254                    .sorted()
9255                    .collect::<Vec<_>>(),
9256                vec![
9257                    dirty_regular_buffer.item_id(),
9258                    dirty_regular_buffer_2.item_id(),
9259                ],
9260                "Should have no multi buffer left in the pane"
9261            );
9262            assert!(dirty_regular_buffer.read(cx).is_dirty);
9263            assert!(dirty_regular_buffer_2.read(cx).is_dirty);
9264        });
9265    }
9266
9267    #[gpui::test]
9268    async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
9269        init_test(cx);
9270        let fs = FakeFs::new(cx.executor());
9271        let project = Project::test(fs, [], cx).await;
9272        let (workspace, cx) =
9273            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9274
9275        // Add a new panel to the right dock, opening the dock and setting the
9276        // focus to the new panel.
9277        let panel = workspace.update_in(cx, |workspace, window, cx| {
9278            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
9279            workspace.add_panel(panel.clone(), window, cx);
9280
9281            workspace
9282                .right_dock()
9283                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
9284
9285            workspace.toggle_panel_focus::<TestPanel>(window, cx);
9286
9287            panel
9288        });
9289
9290        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
9291        // panel to the next valid position which, in this case, is the left
9292        // dock.
9293        cx.dispatch_action(MoveFocusedPanelToNextPosition);
9294        workspace.update(cx, |workspace, cx| {
9295            assert!(workspace.left_dock().read(cx).is_open());
9296            assert_eq!(panel.read(cx).position, DockPosition::Left);
9297        });
9298
9299        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
9300        // panel to the next valid position which, in this case, is the bottom
9301        // dock.
9302        cx.dispatch_action(MoveFocusedPanelToNextPosition);
9303        workspace.update(cx, |workspace, cx| {
9304            assert!(workspace.bottom_dock().read(cx).is_open());
9305            assert_eq!(panel.read(cx).position, DockPosition::Bottom);
9306        });
9307
9308        // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
9309        // around moving the panel to its initial position, the right dock.
9310        cx.dispatch_action(MoveFocusedPanelToNextPosition);
9311        workspace.update(cx, |workspace, cx| {
9312            assert!(workspace.right_dock().read(cx).is_open());
9313            assert_eq!(panel.read(cx).position, DockPosition::Right);
9314        });
9315
9316        // Remove focus from the panel, ensuring that, if the panel is not
9317        // focused, the `MoveFocusedPanelToNextPosition` action does not update
9318        // the panel's position, so the panel is still in the right dock.
9319        workspace.update_in(cx, |workspace, window, cx| {
9320            workspace.toggle_panel_focus::<TestPanel>(window, cx);
9321        });
9322
9323        cx.dispatch_action(MoveFocusedPanelToNextPosition);
9324        workspace.update(cx, |workspace, cx| {
9325            assert!(workspace.right_dock().read(cx).is_open());
9326            assert_eq!(panel.read(cx).position, DockPosition::Right);
9327        });
9328    }
9329
9330    mod register_project_item_tests {
9331
9332        use super::*;
9333
9334        // View
9335        struct TestPngItemView {
9336            focus_handle: FocusHandle,
9337        }
9338        // Model
9339        struct TestPngItem {}
9340
9341        impl project::ProjectItem for TestPngItem {
9342            fn try_open(
9343                _project: &Entity<Project>,
9344                path: &ProjectPath,
9345                cx: &mut App,
9346            ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
9347                if path.path.extension().unwrap() == "png" {
9348                    Some(cx.spawn(async move |cx| cx.new(|_| TestPngItem {})))
9349                } else {
9350                    None
9351                }
9352            }
9353
9354            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
9355                None
9356            }
9357
9358            fn project_path(&self, _: &App) -> Option<ProjectPath> {
9359                None
9360            }
9361
9362            fn is_dirty(&self) -> bool {
9363                false
9364            }
9365        }
9366
9367        impl Item for TestPngItemView {
9368            type Event = ();
9369            fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
9370                "".into()
9371            }
9372        }
9373        impl EventEmitter<()> for TestPngItemView {}
9374        impl Focusable for TestPngItemView {
9375            fn focus_handle(&self, _cx: &App) -> FocusHandle {
9376                self.focus_handle.clone()
9377            }
9378        }
9379
9380        impl Render for TestPngItemView {
9381            fn render(
9382                &mut self,
9383                _window: &mut Window,
9384                _cx: &mut Context<Self>,
9385            ) -> impl IntoElement {
9386                Empty
9387            }
9388        }
9389
9390        impl ProjectItem for TestPngItemView {
9391            type Item = TestPngItem;
9392
9393            fn for_project_item(
9394                _project: Entity<Project>,
9395                _pane: Option<&Pane>,
9396                _item: Entity<Self::Item>,
9397                _: &mut Window,
9398                cx: &mut Context<Self>,
9399            ) -> Self
9400            where
9401                Self: Sized,
9402            {
9403                Self {
9404                    focus_handle: cx.focus_handle(),
9405                }
9406            }
9407        }
9408
9409        // View
9410        struct TestIpynbItemView {
9411            focus_handle: FocusHandle,
9412        }
9413        // Model
9414        struct TestIpynbItem {}
9415
9416        impl project::ProjectItem for TestIpynbItem {
9417            fn try_open(
9418                _project: &Entity<Project>,
9419                path: &ProjectPath,
9420                cx: &mut App,
9421            ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
9422                if path.path.extension().unwrap() == "ipynb" {
9423                    Some(cx.spawn(async move |cx| cx.new(|_| TestIpynbItem {})))
9424                } else {
9425                    None
9426                }
9427            }
9428
9429            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
9430                None
9431            }
9432
9433            fn project_path(&self, _: &App) -> Option<ProjectPath> {
9434                None
9435            }
9436
9437            fn is_dirty(&self) -> bool {
9438                false
9439            }
9440        }
9441
9442        impl Item for TestIpynbItemView {
9443            type Event = ();
9444            fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
9445                "".into()
9446            }
9447        }
9448        impl EventEmitter<()> for TestIpynbItemView {}
9449        impl Focusable for TestIpynbItemView {
9450            fn focus_handle(&self, _cx: &App) -> FocusHandle {
9451                self.focus_handle.clone()
9452            }
9453        }
9454
9455        impl Render for TestIpynbItemView {
9456            fn render(
9457                &mut self,
9458                _window: &mut Window,
9459                _cx: &mut Context<Self>,
9460            ) -> impl IntoElement {
9461                Empty
9462            }
9463        }
9464
9465        impl ProjectItem for TestIpynbItemView {
9466            type Item = TestIpynbItem;
9467
9468            fn for_project_item(
9469                _project: Entity<Project>,
9470                _pane: Option<&Pane>,
9471                _item: Entity<Self::Item>,
9472                _: &mut Window,
9473                cx: &mut Context<Self>,
9474            ) -> Self
9475            where
9476                Self: Sized,
9477            {
9478                Self {
9479                    focus_handle: cx.focus_handle(),
9480                }
9481            }
9482        }
9483
9484        struct TestAlternatePngItemView {
9485            focus_handle: FocusHandle,
9486        }
9487
9488        impl Item for TestAlternatePngItemView {
9489            type Event = ();
9490            fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
9491                "".into()
9492            }
9493        }
9494
9495        impl EventEmitter<()> for TestAlternatePngItemView {}
9496        impl Focusable for TestAlternatePngItemView {
9497            fn focus_handle(&self, _cx: &App) -> FocusHandle {
9498                self.focus_handle.clone()
9499            }
9500        }
9501
9502        impl Render for TestAlternatePngItemView {
9503            fn render(
9504                &mut self,
9505                _window: &mut Window,
9506                _cx: &mut Context<Self>,
9507            ) -> impl IntoElement {
9508                Empty
9509            }
9510        }
9511
9512        impl ProjectItem for TestAlternatePngItemView {
9513            type Item = TestPngItem;
9514
9515            fn for_project_item(
9516                _project: Entity<Project>,
9517                _pane: Option<&Pane>,
9518                _item: Entity<Self::Item>,
9519                _: &mut Window,
9520                cx: &mut Context<Self>,
9521            ) -> Self
9522            where
9523                Self: Sized,
9524            {
9525                Self {
9526                    focus_handle: cx.focus_handle(),
9527                }
9528            }
9529        }
9530
9531        #[gpui::test]
9532        async fn test_register_project_item(cx: &mut TestAppContext) {
9533            init_test(cx);
9534
9535            cx.update(|cx| {
9536                register_project_item::<TestPngItemView>(cx);
9537                register_project_item::<TestIpynbItemView>(cx);
9538            });
9539
9540            let fs = FakeFs::new(cx.executor());
9541            fs.insert_tree(
9542                "/root1",
9543                json!({
9544                    "one.png": "BINARYDATAHERE",
9545                    "two.ipynb": "{ totally a notebook }",
9546                    "three.txt": "editing text, sure why not?"
9547                }),
9548            )
9549            .await;
9550
9551            let project = Project::test(fs, ["root1".as_ref()], cx).await;
9552            let (workspace, cx) =
9553                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9554
9555            let worktree_id = project.update(cx, |project, cx| {
9556                project.worktrees(cx).next().unwrap().read(cx).id()
9557            });
9558
9559            let handle = workspace
9560                .update_in(cx, |workspace, window, cx| {
9561                    let project_path = (worktree_id, "one.png");
9562                    workspace.open_path(project_path, None, true, window, cx)
9563                })
9564                .await
9565                .unwrap();
9566
9567            // Now we can check if the handle we got back errored or not
9568            assert_eq!(
9569                handle.to_any().entity_type(),
9570                TypeId::of::<TestPngItemView>()
9571            );
9572
9573            let handle = workspace
9574                .update_in(cx, |workspace, window, cx| {
9575                    let project_path = (worktree_id, "two.ipynb");
9576                    workspace.open_path(project_path, None, true, window, cx)
9577                })
9578                .await
9579                .unwrap();
9580
9581            assert_eq!(
9582                handle.to_any().entity_type(),
9583                TypeId::of::<TestIpynbItemView>()
9584            );
9585
9586            let handle = workspace
9587                .update_in(cx, |workspace, window, cx| {
9588                    let project_path = (worktree_id, "three.txt");
9589                    workspace.open_path(project_path, None, true, window, cx)
9590                })
9591                .await;
9592            assert!(handle.is_err());
9593        }
9594
9595        #[gpui::test]
9596        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
9597            init_test(cx);
9598
9599            cx.update(|cx| {
9600                register_project_item::<TestPngItemView>(cx);
9601                register_project_item::<TestAlternatePngItemView>(cx);
9602            });
9603
9604            let fs = FakeFs::new(cx.executor());
9605            fs.insert_tree(
9606                "/root1",
9607                json!({
9608                    "one.png": "BINARYDATAHERE",
9609                    "two.ipynb": "{ totally a notebook }",
9610                    "three.txt": "editing text, sure why not?"
9611                }),
9612            )
9613            .await;
9614            let project = Project::test(fs, ["root1".as_ref()], cx).await;
9615            let (workspace, cx) =
9616                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9617            let worktree_id = project.update(cx, |project, cx| {
9618                project.worktrees(cx).next().unwrap().read(cx).id()
9619            });
9620
9621            let handle = workspace
9622                .update_in(cx, |workspace, window, cx| {
9623                    let project_path = (worktree_id, "one.png");
9624                    workspace.open_path(project_path, None, true, window, cx)
9625                })
9626                .await
9627                .unwrap();
9628
9629            // This _must_ be the second item registered
9630            assert_eq!(
9631                handle.to_any().entity_type(),
9632                TypeId::of::<TestAlternatePngItemView>()
9633            );
9634
9635            let handle = workspace
9636                .update_in(cx, |workspace, window, cx| {
9637                    let project_path = (worktree_id, "three.txt");
9638                    workspace.open_path(project_path, None, true, window, cx)
9639                })
9640                .await;
9641            assert!(handle.is_err());
9642        }
9643    }
9644
9645    pub fn init_test(cx: &mut TestAppContext) {
9646        cx.update(|cx| {
9647            let settings_store = SettingsStore::test(cx);
9648            cx.set_global(settings_store);
9649            theme::init(theme::LoadThemes::JustBase, cx);
9650            language::init(cx);
9651            crate::init_settings(cx);
9652            Project::init_settings(cx);
9653        });
9654    }
9655
9656    fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
9657        let item = TestProjectItem::new(id, path, cx);
9658        item.update(cx, |item, _| {
9659            item.is_dirty = true;
9660        });
9661        item
9662    }
9663}