workspace.rs

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