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#[allow(clippy::type_complexity)]
5962pub fn open_paths(
5963    abs_paths: &[PathBuf],
5964    app_state: Arc<AppState>,
5965    open_options: OpenOptions,
5966    cx: &mut App,
5967) -> Task<
5968    anyhow::Result<(
5969        WindowHandle<Workspace>,
5970        Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5971    )>,
5972> {
5973    let abs_paths = abs_paths.to_vec();
5974    let mut existing = None;
5975    let mut best_match = None;
5976    let mut open_visible = OpenVisible::All;
5977
5978    cx.spawn(move |mut cx| async move {
5979        if open_options.open_new_workspace != Some(true) {
5980            let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5981            let all_metadatas = futures::future::join_all(all_paths)
5982                .await
5983                .into_iter()
5984                .filter_map(|result| result.ok().flatten())
5985                .collect::<Vec<_>>();
5986
5987            cx.update(|cx| {
5988                for window in local_workspace_windows(&cx) {
5989                    if let Ok(workspace) = window.read(&cx) {
5990                        let m = workspace.project.read(&cx).visibility_for_paths(
5991                            &abs_paths,
5992                            &all_metadatas,
5993                            open_options.open_new_workspace == None,
5994                            cx,
5995                        );
5996                        if m > best_match {
5997                            existing = Some(window);
5998                            best_match = m;
5999                        } else if best_match.is_none()
6000                            && open_options.open_new_workspace == Some(false)
6001                        {
6002                            existing = Some(window)
6003                        }
6004                    }
6005                }
6006            })?;
6007
6008            if open_options.open_new_workspace.is_none() && existing.is_none() {
6009                if all_metadatas.iter().all(|file| !file.is_dir) {
6010                    cx.update(|cx| {
6011                        if let Some(window) = cx
6012                            .active_window()
6013                            .and_then(|window| window.downcast::<Workspace>())
6014                        {
6015                            if let Ok(workspace) = window.read(cx) {
6016                                let project = workspace.project().read(cx);
6017                                if project.is_local() && !project.is_via_collab() {
6018                                    existing = Some(window);
6019                                    open_visible = OpenVisible::None;
6020                                    return;
6021                                }
6022                            }
6023                        }
6024                        for window in local_workspace_windows(cx) {
6025                            if let Ok(workspace) = window.read(cx) {
6026                                let project = workspace.project().read(cx);
6027                                if project.is_via_collab() {
6028                                    continue;
6029                                }
6030                                existing = Some(window);
6031                                open_visible = OpenVisible::None;
6032                                break;
6033                            }
6034                        }
6035                    })?;
6036                }
6037            }
6038        }
6039
6040        if let Some(existing) = existing {
6041            let open_task = existing
6042                .update(&mut cx, |workspace, window, cx| {
6043                    window.activate_window();
6044                    workspace.open_paths(abs_paths, open_visible, None, window, cx)
6045                })?
6046                .await;
6047
6048            _ = existing.update(&mut cx, |workspace, _, cx| {
6049                for item in open_task.iter().flatten() {
6050                    if let Err(e) = item {
6051                        workspace.show_error(&e, cx);
6052                    }
6053                }
6054            });
6055
6056            Ok((existing, open_task))
6057        } else {
6058            cx.update(move |cx| {
6059                Workspace::new_local(
6060                    abs_paths,
6061                    app_state.clone(),
6062                    open_options.replace_window,
6063                    open_options.env,
6064                    cx,
6065                )
6066            })?
6067            .await
6068        }
6069    })
6070}
6071
6072pub fn open_new(
6073    open_options: OpenOptions,
6074    app_state: Arc<AppState>,
6075    cx: &mut App,
6076    init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6077) -> Task<anyhow::Result<()>> {
6078    let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6079    cx.spawn(|mut cx| async move {
6080        let (workspace, opened_paths) = task.await?;
6081        workspace.update(&mut cx, |workspace, window, cx| {
6082            if opened_paths.is_empty() {
6083                init(workspace, window, cx)
6084            }
6085        })?;
6086        Ok(())
6087    })
6088}
6089
6090pub fn create_and_open_local_file(
6091    path: &'static Path,
6092    window: &mut Window,
6093    cx: &mut Context<Workspace>,
6094    default_content: impl 'static + Send + FnOnce() -> Rope,
6095) -> Task<Result<Box<dyn ItemHandle>>> {
6096    cx.spawn_in(window, |workspace, mut cx| async move {
6097        let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
6098        if !fs.is_file(path).await {
6099            fs.create_file(path, Default::default()).await?;
6100            fs.save(path, &default_content(), Default::default())
6101                .await?;
6102        }
6103
6104        let mut items = workspace
6105            .update_in(&mut cx, |workspace, window, cx| {
6106                workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6107                    workspace.open_paths(
6108                        vec![path.to_path_buf()],
6109                        OpenVisible::None,
6110                        None,
6111                        window,
6112                        cx,
6113                    )
6114                })
6115            })?
6116            .await?
6117            .await;
6118
6119        let item = items.pop().flatten();
6120        item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
6121    })
6122}
6123
6124pub fn open_ssh_project(
6125    window: WindowHandle<Workspace>,
6126    connection_options: SshConnectionOptions,
6127    cancel_rx: oneshot::Receiver<()>,
6128    delegate: Arc<dyn SshClientDelegate>,
6129    app_state: Arc<AppState>,
6130    paths: Vec<PathBuf>,
6131    cx: &mut App,
6132) -> Task<Result<()>> {
6133    cx.spawn(|mut cx| async move {
6134        let (serialized_ssh_project, workspace_id, serialized_workspace) =
6135            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6136
6137        let session = match cx
6138            .update(|cx| {
6139                remote::SshRemoteClient::new(
6140                    ConnectionIdentifier::Workspace(workspace_id.0),
6141                    connection_options,
6142                    cancel_rx,
6143                    delegate,
6144                    cx,
6145                )
6146            })?
6147            .await?
6148        {
6149            Some(result) => result,
6150            None => return Ok(()),
6151        };
6152
6153        let project = cx.update(|cx| {
6154            project::Project::ssh(
6155                session,
6156                app_state.client.clone(),
6157                app_state.node_runtime.clone(),
6158                app_state.user_store.clone(),
6159                app_state.languages.clone(),
6160                app_state.fs.clone(),
6161                cx,
6162            )
6163        })?;
6164
6165        let toolchains = DB.toolchains(workspace_id).await?;
6166        for (toolchain, worktree_id) in toolchains {
6167            project
6168                .update(&mut cx, |this, cx| {
6169                    this.activate_toolchain(worktree_id, toolchain, cx)
6170                })?
6171                .await;
6172        }
6173        let mut project_paths_to_open = vec![];
6174        let mut project_path_errors = vec![];
6175
6176        for path in paths {
6177            let result = cx
6178                .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6179                .await;
6180            match result {
6181                Ok((_, project_path)) => {
6182                    project_paths_to_open.push((path.clone(), Some(project_path)));
6183                }
6184                Err(error) => {
6185                    project_path_errors.push(error);
6186                }
6187            };
6188        }
6189
6190        if project_paths_to_open.is_empty() {
6191            return Err(project_path_errors
6192                .pop()
6193                .unwrap_or_else(|| anyhow!("no paths given")));
6194        }
6195
6196        cx.update_window(window.into(), |_, window, cx| {
6197            window.replace_root(cx, |window, cx| {
6198                telemetry::event!("SSH Project Opened");
6199
6200                let mut workspace =
6201                    Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6202                workspace.set_serialized_ssh_project(serialized_ssh_project);
6203                workspace
6204            });
6205        })?;
6206
6207        window
6208            .update(&mut cx, |_, window, cx| {
6209                window.activate_window();
6210
6211                open_items(serialized_workspace, project_paths_to_open, window, cx)
6212            })?
6213            .await?;
6214
6215        window.update(&mut cx, |workspace, _, cx| {
6216            for error in project_path_errors {
6217                if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6218                    if let Some(path) = error.error_tag("path") {
6219                        workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6220                    }
6221                } else {
6222                    workspace.show_error(&error, cx)
6223                }
6224            }
6225        })
6226    })
6227}
6228
6229fn serialize_ssh_project(
6230    connection_options: SshConnectionOptions,
6231    paths: Vec<PathBuf>,
6232    cx: &AsyncApp,
6233) -> Task<
6234    Result<(
6235        SerializedSshProject,
6236        WorkspaceId,
6237        Option<SerializedWorkspace>,
6238    )>,
6239> {
6240    cx.background_executor().spawn(async move {
6241        let serialized_ssh_project = persistence::DB
6242            .get_or_create_ssh_project(
6243                connection_options.host.clone(),
6244                connection_options.port,
6245                paths
6246                    .iter()
6247                    .map(|path| path.to_string_lossy().to_string())
6248                    .collect::<Vec<_>>(),
6249                connection_options.username.clone(),
6250            )
6251            .await?;
6252
6253        let serialized_workspace =
6254            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6255
6256        let workspace_id = if let Some(workspace_id) =
6257            serialized_workspace.as_ref().map(|workspace| workspace.id)
6258        {
6259            workspace_id
6260        } else {
6261            persistence::DB.next_id().await?
6262        };
6263
6264        Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6265    })
6266}
6267
6268pub fn join_in_room_project(
6269    project_id: u64,
6270    follow_user_id: u64,
6271    app_state: Arc<AppState>,
6272    cx: &mut App,
6273) -> Task<Result<()>> {
6274    let windows = cx.windows();
6275    cx.spawn(|mut cx| async move {
6276        let existing_workspace = windows.into_iter().find_map(|window_handle| {
6277            window_handle
6278                .downcast::<Workspace>()
6279                .and_then(|window_handle| {
6280                    window_handle
6281                        .update(&mut cx, |workspace, _window, cx| {
6282                            if workspace.project().read(cx).remote_id() == Some(project_id) {
6283                                Some(window_handle)
6284                            } else {
6285                                None
6286                            }
6287                        })
6288                        .unwrap_or(None)
6289                })
6290        });
6291
6292        let workspace = if let Some(existing_workspace) = existing_workspace {
6293            existing_workspace
6294        } else {
6295            let active_call = cx.update(|cx| ActiveCall::global(cx))?;
6296            let room = active_call
6297                .read_with(&cx, |call, _| call.room().cloned())?
6298                .ok_or_else(|| anyhow!("not in a call"))?;
6299            let project = room
6300                .update(&mut cx, |room, cx| {
6301                    room.join_project(
6302                        project_id,
6303                        app_state.languages.clone(),
6304                        app_state.fs.clone(),
6305                        cx,
6306                    )
6307                })?
6308                .await?;
6309
6310            let window_bounds_override = window_bounds_env_override();
6311            cx.update(|cx| {
6312                let mut options = (app_state.build_window_options)(None, cx);
6313                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
6314                cx.open_window(options, |window, cx| {
6315                    cx.new(|cx| {
6316                        Workspace::new(Default::default(), project, app_state.clone(), window, cx)
6317                    })
6318                })
6319            })??
6320        };
6321
6322        workspace.update(&mut cx, |workspace, window, cx| {
6323            cx.activate(true);
6324            window.activate_window();
6325
6326            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
6327                let follow_peer_id = room
6328                    .read(cx)
6329                    .remote_participants()
6330                    .iter()
6331                    .find(|(_, participant)| participant.user.id == follow_user_id)
6332                    .map(|(_, p)| p.peer_id)
6333                    .or_else(|| {
6334                        // If we couldn't follow the given user, follow the host instead.
6335                        let collaborator = workspace
6336                            .project()
6337                            .read(cx)
6338                            .collaborators()
6339                            .values()
6340                            .find(|collaborator| collaborator.is_host)?;
6341                        Some(collaborator.peer_id)
6342                    });
6343
6344                if let Some(follow_peer_id) = follow_peer_id {
6345                    workspace.follow(follow_peer_id, window, cx);
6346                }
6347            }
6348        })?;
6349
6350        anyhow::Ok(())
6351    })
6352}
6353
6354pub fn reload(reload: &Reload, cx: &mut App) {
6355    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
6356    let mut workspace_windows = cx
6357        .windows()
6358        .into_iter()
6359        .filter_map(|window| window.downcast::<Workspace>())
6360        .collect::<Vec<_>>();
6361
6362    // If multiple windows have unsaved changes, and need a save prompt,
6363    // prompt in the active window before switching to a different window.
6364    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
6365
6366    let mut prompt = None;
6367    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6368        prompt = window
6369            .update(cx, |_, window, cx| {
6370                window.prompt(
6371                    PromptLevel::Info,
6372                    "Are you sure you want to restart?",
6373                    None,
6374                    &["Restart", "Cancel"],
6375                    cx,
6376                )
6377            })
6378            .ok();
6379    }
6380
6381    let binary_path = reload.binary_path.clone();
6382    cx.spawn(|mut cx| async move {
6383        if let Some(prompt) = prompt {
6384            let answer = prompt.await?;
6385            if answer != 0 {
6386                return Ok(());
6387            }
6388        }
6389
6390        // If the user cancels any save prompt, then keep the app open.
6391        for window in workspace_windows {
6392            if let Ok(should_close) = window.update(&mut cx, |workspace, window, cx| {
6393                workspace.prepare_to_close(CloseIntent::Quit, window, cx)
6394            }) {
6395                if !should_close.await? {
6396                    return Ok(());
6397                }
6398            }
6399        }
6400
6401        cx.update(|cx| cx.restart(binary_path))
6402    })
6403    .detach_and_log_err(cx);
6404}
6405
6406fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6407    let mut parts = value.split(',');
6408    let x: usize = parts.next()?.parse().ok()?;
6409    let y: usize = parts.next()?.parse().ok()?;
6410    Some(point(px(x as f32), px(y as f32)))
6411}
6412
6413fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6414    let mut parts = value.split(',');
6415    let width: usize = parts.next()?.parse().ok()?;
6416    let height: usize = parts.next()?.parse().ok()?;
6417    Some(size(px(width as f32), px(height as f32)))
6418}
6419
6420pub fn client_side_decorations(
6421    element: impl IntoElement,
6422    window: &mut Window,
6423    cx: &mut App,
6424) -> Stateful<Div> {
6425    const BORDER_SIZE: Pixels = px(1.0);
6426    let decorations = window.window_decorations();
6427
6428    if matches!(decorations, Decorations::Client { .. }) {
6429        window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6430    }
6431
6432    struct GlobalResizeEdge(ResizeEdge);
6433    impl Global for GlobalResizeEdge {}
6434
6435    div()
6436        .id("window-backdrop")
6437        .bg(transparent_black())
6438        .map(|div| match decorations {
6439            Decorations::Server => div,
6440            Decorations::Client { tiling, .. } => div
6441                .when(!(tiling.top || tiling.right), |div| {
6442                    div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6443                })
6444                .when(!(tiling.top || tiling.left), |div| {
6445                    div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6446                })
6447                .when(!(tiling.bottom || tiling.right), |div| {
6448                    div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6449                })
6450                .when(!(tiling.bottom || tiling.left), |div| {
6451                    div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6452                })
6453                .when(!tiling.top, |div| {
6454                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6455                })
6456                .when(!tiling.bottom, |div| {
6457                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6458                })
6459                .when(!tiling.left, |div| {
6460                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6461                })
6462                .when(!tiling.right, |div| {
6463                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6464                })
6465                .on_mouse_move(move |e, window, cx| {
6466                    let size = window.window_bounds().get_bounds().size;
6467                    let pos = e.position;
6468
6469                    let new_edge =
6470                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6471
6472                    let edge = cx.try_global::<GlobalResizeEdge>();
6473                    if new_edge != edge.map(|edge| edge.0) {
6474                        window
6475                            .window_handle()
6476                            .update(cx, |workspace, _, cx| {
6477                                cx.notify(workspace.entity_id());
6478                            })
6479                            .ok();
6480                    }
6481                })
6482                .on_mouse_down(MouseButton::Left, move |e, window, _| {
6483                    let size = window.window_bounds().get_bounds().size;
6484                    let pos = e.position;
6485
6486                    let edge = match resize_edge(
6487                        pos,
6488                        theme::CLIENT_SIDE_DECORATION_SHADOW,
6489                        size,
6490                        tiling,
6491                    ) {
6492                        Some(value) => value,
6493                        None => return,
6494                    };
6495
6496                    window.start_window_resize(edge);
6497                }),
6498        })
6499        .size_full()
6500        .child(
6501            div()
6502                .cursor(CursorStyle::Arrow)
6503                .map(|div| match decorations {
6504                    Decorations::Server => div,
6505                    Decorations::Client { tiling } => div
6506                        .border_color(cx.theme().colors().border)
6507                        .when(!(tiling.top || tiling.right), |div| {
6508                            div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6509                        })
6510                        .when(!(tiling.top || tiling.left), |div| {
6511                            div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6512                        })
6513                        .when(!(tiling.bottom || tiling.right), |div| {
6514                            div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6515                        })
6516                        .when(!(tiling.bottom || tiling.left), |div| {
6517                            div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6518                        })
6519                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6520                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6521                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6522                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6523                        .when(!tiling.is_tiled(), |div| {
6524                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
6525                                color: Hsla {
6526                                    h: 0.,
6527                                    s: 0.,
6528                                    l: 0.,
6529                                    a: 0.4,
6530                                },
6531                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6532                                spread_radius: px(0.),
6533                                offset: point(px(0.0), px(0.0)),
6534                            }])
6535                        }),
6536                })
6537                .on_mouse_move(|_e, _, cx| {
6538                    cx.stop_propagation();
6539                })
6540                .size_full()
6541                .child(element),
6542        )
6543        .map(|div| match decorations {
6544            Decorations::Server => div,
6545            Decorations::Client { tiling, .. } => div.child(
6546                canvas(
6547                    |_bounds, window, _| {
6548                        window.insert_hitbox(
6549                            Bounds::new(
6550                                point(px(0.0), px(0.0)),
6551                                window.window_bounds().get_bounds().size,
6552                            ),
6553                            false,
6554                        )
6555                    },
6556                    move |_bounds, hitbox, window, cx| {
6557                        let mouse = window.mouse_position();
6558                        let size = window.window_bounds().get_bounds().size;
6559                        let Some(edge) =
6560                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6561                        else {
6562                            return;
6563                        };
6564                        cx.set_global(GlobalResizeEdge(edge));
6565                        window.set_cursor_style(
6566                            match edge {
6567                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6568                                ResizeEdge::Left | ResizeEdge::Right => {
6569                                    CursorStyle::ResizeLeftRight
6570                                }
6571                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6572                                    CursorStyle::ResizeUpLeftDownRight
6573                                }
6574                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6575                                    CursorStyle::ResizeUpRightDownLeft
6576                                }
6577                            },
6578                            &hitbox,
6579                        );
6580                    },
6581                )
6582                .size_full()
6583                .absolute(),
6584            ),
6585        })
6586}
6587
6588fn resize_edge(
6589    pos: Point<Pixels>,
6590    shadow_size: Pixels,
6591    window_size: Size<Pixels>,
6592    tiling: Tiling,
6593) -> Option<ResizeEdge> {
6594    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6595    if bounds.contains(&pos) {
6596        return None;
6597    }
6598
6599    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6600    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6601    if !tiling.top && top_left_bounds.contains(&pos) {
6602        return Some(ResizeEdge::TopLeft);
6603    }
6604
6605    let top_right_bounds = Bounds::new(
6606        Point::new(window_size.width - corner_size.width, px(0.)),
6607        corner_size,
6608    );
6609    if !tiling.top && top_right_bounds.contains(&pos) {
6610        return Some(ResizeEdge::TopRight);
6611    }
6612
6613    let bottom_left_bounds = Bounds::new(
6614        Point::new(px(0.), window_size.height - corner_size.height),
6615        corner_size,
6616    );
6617    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6618        return Some(ResizeEdge::BottomLeft);
6619    }
6620
6621    let bottom_right_bounds = Bounds::new(
6622        Point::new(
6623            window_size.width - corner_size.width,
6624            window_size.height - corner_size.height,
6625        ),
6626        corner_size,
6627    );
6628    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6629        return Some(ResizeEdge::BottomRight);
6630    }
6631
6632    if !tiling.top && pos.y < shadow_size {
6633        Some(ResizeEdge::Top)
6634    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6635        Some(ResizeEdge::Bottom)
6636    } else if !tiling.left && pos.x < shadow_size {
6637        Some(ResizeEdge::Left)
6638    } else if !tiling.right && pos.x > window_size.width - shadow_size {
6639        Some(ResizeEdge::Right)
6640    } else {
6641        None
6642    }
6643}
6644
6645fn join_pane_into_active(
6646    active_pane: &Entity<Pane>,
6647    pane: &Entity<Pane>,
6648    window: &mut Window,
6649    cx: &mut App,
6650) {
6651    if pane == active_pane {
6652        return;
6653    } else if pane.read(cx).items_len() == 0 {
6654        pane.update(cx, |_, cx| {
6655            cx.emit(pane::Event::Remove {
6656                focus_on_pane: None,
6657            });
6658        })
6659    } else {
6660        move_all_items(pane, active_pane, window, cx);
6661    }
6662}
6663
6664fn move_all_items(
6665    from_pane: &Entity<Pane>,
6666    to_pane: &Entity<Pane>,
6667    window: &mut Window,
6668    cx: &mut App,
6669) {
6670    let destination_is_different = from_pane != to_pane;
6671    let mut moved_items = 0;
6672    for (item_ix, item_handle) in from_pane
6673        .read(cx)
6674        .items()
6675        .enumerate()
6676        .map(|(ix, item)| (ix, item.clone()))
6677        .collect::<Vec<_>>()
6678    {
6679        let ix = item_ix - moved_items;
6680        if destination_is_different {
6681            // Close item from previous pane
6682            from_pane.update(cx, |source, cx| {
6683                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
6684            });
6685            moved_items += 1;
6686        }
6687
6688        // This automatically removes duplicate items in the pane
6689        to_pane.update(cx, |destination, cx| {
6690            destination.add_item(item_handle, true, true, None, window, cx);
6691            window.focus(&destination.focus_handle(cx))
6692        });
6693    }
6694}
6695
6696pub fn move_item(
6697    source: &Entity<Pane>,
6698    destination: &Entity<Pane>,
6699    item_id_to_move: EntityId,
6700    destination_index: usize,
6701    window: &mut Window,
6702    cx: &mut App,
6703) {
6704    let Some((item_ix, item_handle)) = source
6705        .read(cx)
6706        .items()
6707        .enumerate()
6708        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6709        .map(|(ix, item)| (ix, item.clone()))
6710    else {
6711        // Tab was closed during drag
6712        return;
6713    };
6714
6715    if source != destination {
6716        // Close item from previous pane
6717        source.update(cx, |source, cx| {
6718            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
6719        });
6720    }
6721
6722    // This automatically removes duplicate items in the pane
6723    destination.update(cx, |destination, cx| {
6724        destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
6725        window.focus(&destination.focus_handle(cx))
6726    });
6727}
6728
6729pub fn move_active_item(
6730    source: &Entity<Pane>,
6731    destination: &Entity<Pane>,
6732    focus_destination: bool,
6733    close_if_empty: bool,
6734    window: &mut Window,
6735    cx: &mut App,
6736) {
6737    if source == destination {
6738        return;
6739    }
6740    let Some(active_item) = source.read(cx).active_item() else {
6741        return;
6742    };
6743    source.update(cx, |source_pane, cx| {
6744        let item_id = active_item.item_id();
6745        source_pane.remove_item(item_id, false, close_if_empty, window, cx);
6746        destination.update(cx, |target_pane, cx| {
6747            target_pane.add_item(
6748                active_item,
6749                focus_destination,
6750                focus_destination,
6751                Some(target_pane.items_len()),
6752                window,
6753                cx,
6754            );
6755        });
6756    });
6757}
6758
6759#[cfg(test)]
6760mod tests {
6761    use std::{cell::RefCell, rc::Rc};
6762
6763    use super::*;
6764    use crate::{
6765        dock::{test::TestPanel, PanelEvent},
6766        item::{
6767            test::{TestItem, TestProjectItem},
6768            ItemEvent,
6769        },
6770    };
6771    use fs::FakeFs;
6772    use gpui::{
6773        px, DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
6774        UpdateGlobal, VisualTestContext,
6775    };
6776    use project::{Project, ProjectEntryId};
6777    use serde_json::json;
6778    use settings::SettingsStore;
6779
6780    #[gpui::test]
6781    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6782        init_test(cx);
6783
6784        let fs = FakeFs::new(cx.executor());
6785        let project = Project::test(fs, [], cx).await;
6786        let (workspace, cx) =
6787            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6788
6789        // Adding an item with no ambiguity renders the tab without detail.
6790        let item1 = cx.new(|cx| {
6791            let mut item = TestItem::new(cx);
6792            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6793            item
6794        });
6795        workspace.update_in(cx, |workspace, window, cx| {
6796            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6797        });
6798        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6799
6800        // Adding an item that creates ambiguity increases the level of detail on
6801        // both tabs.
6802        let item2 = cx.new_window_entity(|_window, cx| {
6803            let mut item = TestItem::new(cx);
6804            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6805            item
6806        });
6807        workspace.update_in(cx, |workspace, window, cx| {
6808            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6809        });
6810        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6811        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6812
6813        // Adding an item that creates ambiguity increases the level of detail only
6814        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6815        // we stop at the highest detail available.
6816        let item3 = cx.new(|cx| {
6817            let mut item = TestItem::new(cx);
6818            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6819            item
6820        });
6821        workspace.update_in(cx, |workspace, window, cx| {
6822            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6823        });
6824        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6825        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6826        item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6827    }
6828
6829    #[gpui::test]
6830    async fn test_tracking_active_path(cx: &mut TestAppContext) {
6831        init_test(cx);
6832
6833        let fs = FakeFs::new(cx.executor());
6834        fs.insert_tree(
6835            "/root1",
6836            json!({
6837                "one.txt": "",
6838                "two.txt": "",
6839            }),
6840        )
6841        .await;
6842        fs.insert_tree(
6843            "/root2",
6844            json!({
6845                "three.txt": "",
6846            }),
6847        )
6848        .await;
6849
6850        let project = Project::test(fs, ["root1".as_ref()], cx).await;
6851        let (workspace, cx) =
6852            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6853        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6854        let worktree_id = project.update(cx, |project, cx| {
6855            project.worktrees(cx).next().unwrap().read(cx).id()
6856        });
6857
6858        let item1 = cx.new(|cx| {
6859            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6860        });
6861        let item2 = cx.new(|cx| {
6862            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6863        });
6864
6865        // Add an item to an empty pane
6866        workspace.update_in(cx, |workspace, window, cx| {
6867            workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
6868        });
6869        project.update(cx, |project, cx| {
6870            assert_eq!(
6871                project.active_entry(),
6872                project
6873                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6874                    .map(|e| e.id)
6875            );
6876        });
6877        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6878
6879        // Add a second item to a non-empty pane
6880        workspace.update_in(cx, |workspace, window, cx| {
6881            workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
6882        });
6883        assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6884        project.update(cx, |project, cx| {
6885            assert_eq!(
6886                project.active_entry(),
6887                project
6888                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6889                    .map(|e| e.id)
6890            );
6891        });
6892
6893        // Close the active item
6894        pane.update_in(cx, |pane, window, cx| {
6895            pane.close_active_item(&Default::default(), window, cx)
6896                .unwrap()
6897        })
6898        .await
6899        .unwrap();
6900        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6901        project.update(cx, |project, cx| {
6902            assert_eq!(
6903                project.active_entry(),
6904                project
6905                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6906                    .map(|e| e.id)
6907            );
6908        });
6909
6910        // Add a project folder
6911        project
6912            .update(cx, |project, cx| {
6913                project.find_or_create_worktree("root2", true, cx)
6914            })
6915            .await
6916            .unwrap();
6917        assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6918
6919        // Remove a project folder
6920        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6921        assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6922    }
6923
6924    #[gpui::test]
6925    async fn test_close_window(cx: &mut TestAppContext) {
6926        init_test(cx);
6927
6928        let fs = FakeFs::new(cx.executor());
6929        fs.insert_tree("/root", json!({ "one": "" })).await;
6930
6931        let project = Project::test(fs, ["root".as_ref()], cx).await;
6932        let (workspace, cx) =
6933            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6934
6935        // When there are no dirty items, there's nothing to do.
6936        let item1 = cx.new(TestItem::new);
6937        workspace.update_in(cx, |w, window, cx| {
6938            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
6939        });
6940        let task = workspace.update_in(cx, |w, window, cx| {
6941            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6942        });
6943        assert!(task.await.unwrap());
6944
6945        // When there are dirty untitled items, prompt to save each one. If the user
6946        // cancels any prompt, then abort.
6947        let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
6948        let item3 = cx.new(|cx| {
6949            TestItem::new(cx)
6950                .with_dirty(true)
6951                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6952        });
6953        workspace.update_in(cx, |w, window, cx| {
6954            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6955            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6956        });
6957        let task = workspace.update_in(cx, |w, window, cx| {
6958            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6959        });
6960        cx.executor().run_until_parked();
6961        cx.simulate_prompt_answer(2); // cancel save all
6962        cx.executor().run_until_parked();
6963        cx.simulate_prompt_answer(2); // cancel save all
6964        cx.executor().run_until_parked();
6965        assert!(!cx.has_pending_prompt());
6966        assert!(!task.await.unwrap());
6967    }
6968
6969    #[gpui::test]
6970    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6971        init_test(cx);
6972
6973        // Register TestItem as a serializable item
6974        cx.update(|cx| {
6975            register_serializable_item::<TestItem>(cx);
6976        });
6977
6978        let fs = FakeFs::new(cx.executor());
6979        fs.insert_tree("/root", json!({ "one": "" })).await;
6980
6981        let project = Project::test(fs, ["root".as_ref()], cx).await;
6982        let (workspace, cx) =
6983            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6984
6985        // When there are dirty untitled items, but they can serialize, then there is no prompt.
6986        let item1 = cx.new(|cx| {
6987            TestItem::new(cx)
6988                .with_dirty(true)
6989                .with_serialize(|| Some(Task::ready(Ok(()))))
6990        });
6991        let item2 = cx.new(|cx| {
6992            TestItem::new(cx)
6993                .with_dirty(true)
6994                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6995                .with_serialize(|| Some(Task::ready(Ok(()))))
6996        });
6997        workspace.update_in(cx, |w, window, cx| {
6998            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6999            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7000        });
7001        let task = workspace.update_in(cx, |w, window, cx| {
7002            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7003        });
7004        assert!(task.await.unwrap());
7005    }
7006
7007    #[gpui::test]
7008    async fn test_close_pane_items(cx: &mut TestAppContext) {
7009        init_test(cx);
7010
7011        let fs = FakeFs::new(cx.executor());
7012
7013        let project = Project::test(fs, None, cx).await;
7014        let (workspace, cx) =
7015            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7016
7017        let item1 = cx.new(|cx| {
7018            TestItem::new(cx)
7019                .with_dirty(true)
7020                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7021        });
7022        let item2 = cx.new(|cx| {
7023            TestItem::new(cx)
7024                .with_dirty(true)
7025                .with_conflict(true)
7026                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7027        });
7028        let item3 = cx.new(|cx| {
7029            TestItem::new(cx)
7030                .with_dirty(true)
7031                .with_conflict(true)
7032                .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7033        });
7034        let item4 = cx.new(|cx| {
7035            TestItem::new(cx).with_dirty(true).with_project_items(&[{
7036                let project_item = TestProjectItem::new_untitled(cx);
7037                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7038                project_item
7039            }])
7040        });
7041        let pane = workspace.update_in(cx, |workspace, window, cx| {
7042            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7043            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7044            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7045            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7046            workspace.active_pane().clone()
7047        });
7048
7049        let close_items = pane.update_in(cx, |pane, window, cx| {
7050            pane.activate_item(1, true, true, window, cx);
7051            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7052            let item1_id = item1.item_id();
7053            let item3_id = item3.item_id();
7054            let item4_id = item4.item_id();
7055            pane.close_items(window, cx, SaveIntent::Close, move |id| {
7056                [item1_id, item3_id, item4_id].contains(&id)
7057            })
7058        });
7059        cx.executor().run_until_parked();
7060
7061        assert!(cx.has_pending_prompt());
7062        // Ignore "Save all" prompt
7063        cx.simulate_prompt_answer(2);
7064        cx.executor().run_until_parked();
7065        // There's a prompt to save item 1.
7066        pane.update(cx, |pane, _| {
7067            assert_eq!(pane.items_len(), 4);
7068            assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
7069        });
7070        // Confirm saving item 1.
7071        cx.simulate_prompt_answer(0);
7072        cx.executor().run_until_parked();
7073
7074        // Item 1 is saved. There's a prompt to save item 3.
7075        pane.update(cx, |pane, cx| {
7076            assert_eq!(item1.read(cx).save_count, 1);
7077            assert_eq!(item1.read(cx).save_as_count, 0);
7078            assert_eq!(item1.read(cx).reload_count, 0);
7079            assert_eq!(pane.items_len(), 3);
7080            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7081        });
7082        assert!(cx.has_pending_prompt());
7083
7084        // Cancel saving item 3.
7085        cx.simulate_prompt_answer(1);
7086        cx.executor().run_until_parked();
7087
7088        // Item 3 is reloaded. There's a prompt to save item 4.
7089        pane.update(cx, |pane, cx| {
7090            assert_eq!(item3.read(cx).save_count, 0);
7091            assert_eq!(item3.read(cx).save_as_count, 0);
7092            assert_eq!(item3.read(cx).reload_count, 1);
7093            assert_eq!(pane.items_len(), 2);
7094            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7095        });
7096        assert!(cx.has_pending_prompt());
7097
7098        // Confirm saving item 4.
7099        cx.simulate_prompt_answer(0);
7100        cx.executor().run_until_parked();
7101
7102        // There's a prompt for a path for item 4.
7103        cx.simulate_new_path_selection(|_| Some(Default::default()));
7104        close_items.await.unwrap();
7105
7106        // The requested items are closed.
7107        pane.update(cx, |pane, cx| {
7108            assert_eq!(item4.read(cx).save_count, 0);
7109            assert_eq!(item4.read(cx).save_as_count, 1);
7110            assert_eq!(item4.read(cx).reload_count, 0);
7111            assert_eq!(pane.items_len(), 1);
7112            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7113        });
7114    }
7115
7116    #[gpui::test]
7117    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7118        init_test(cx);
7119
7120        let fs = FakeFs::new(cx.executor());
7121        let project = Project::test(fs, [], cx).await;
7122        let (workspace, cx) =
7123            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7124
7125        // Create several workspace items with single project entries, and two
7126        // workspace items with multiple project entries.
7127        let single_entry_items = (0..=4)
7128            .map(|project_entry_id| {
7129                cx.new(|cx| {
7130                    TestItem::new(cx)
7131                        .with_dirty(true)
7132                        .with_project_items(&[dirty_project_item(
7133                            project_entry_id,
7134                            &format!("{project_entry_id}.txt"),
7135                            cx,
7136                        )])
7137                })
7138            })
7139            .collect::<Vec<_>>();
7140        let item_2_3 = cx.new(|cx| {
7141            TestItem::new(cx)
7142                .with_dirty(true)
7143                .with_singleton(false)
7144                .with_project_items(&[
7145                    single_entry_items[2].read(cx).project_items[0].clone(),
7146                    single_entry_items[3].read(cx).project_items[0].clone(),
7147                ])
7148        });
7149        let item_3_4 = cx.new(|cx| {
7150            TestItem::new(cx)
7151                .with_dirty(true)
7152                .with_singleton(false)
7153                .with_project_items(&[
7154                    single_entry_items[3].read(cx).project_items[0].clone(),
7155                    single_entry_items[4].read(cx).project_items[0].clone(),
7156                ])
7157        });
7158
7159        // Create two panes that contain the following project entries:
7160        //   left pane:
7161        //     multi-entry items:   (2, 3)
7162        //     single-entry items:  0, 1, 2, 3, 4
7163        //   right pane:
7164        //     single-entry items:  1
7165        //     multi-entry items:   (3, 4)
7166        let left_pane = workspace.update_in(cx, |workspace, window, cx| {
7167            let left_pane = workspace.active_pane().clone();
7168            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7169            for item in single_entry_items {
7170                workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
7171            }
7172            left_pane.update(cx, |pane, cx| {
7173                pane.activate_item(2, true, true, window, cx);
7174            });
7175
7176            let right_pane = workspace
7177                .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7178                .unwrap();
7179
7180            right_pane.update(cx, |pane, cx| {
7181                pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7182            });
7183
7184            left_pane
7185        });
7186
7187        cx.focus(&left_pane);
7188
7189        // When closing all of the items in the left pane, we should be prompted twice:
7190        // once for project entry 0, and once for project entry 2. Project entries 1,
7191        // 3, and 4 are all still open in the other paten. After those two
7192        // prompts, the task should complete.
7193
7194        let close = left_pane.update_in(cx, |pane, window, cx| {
7195            pane.close_all_items(&CloseAllItems::default(), window, cx)
7196                .unwrap()
7197        });
7198        cx.executor().run_until_parked();
7199
7200        // Discard "Save all" prompt
7201        cx.simulate_prompt_answer(2);
7202
7203        cx.executor().run_until_parked();
7204        left_pane.update(cx, |pane, cx| {
7205            assert_eq!(
7206                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
7207                &[ProjectEntryId::from_proto(0)]
7208            );
7209        });
7210        cx.simulate_prompt_answer(0);
7211
7212        cx.executor().run_until_parked();
7213        left_pane.update(cx, |pane, cx| {
7214            assert_eq!(
7215                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
7216                &[ProjectEntryId::from_proto(2)]
7217            );
7218        });
7219        cx.simulate_prompt_answer(0);
7220
7221        cx.executor().run_until_parked();
7222        close.await.unwrap();
7223        left_pane.update(cx, |pane, _| {
7224            assert_eq!(pane.items_len(), 0);
7225        });
7226    }
7227
7228    #[gpui::test]
7229    async fn test_autosave(cx: &mut gpui::TestAppContext) {
7230        init_test(cx);
7231
7232        let fs = FakeFs::new(cx.executor());
7233        let project = Project::test(fs, [], cx).await;
7234        let (workspace, cx) =
7235            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7236        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7237
7238        let item = cx.new(|cx| {
7239            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7240        });
7241        let item_id = item.entity_id();
7242        workspace.update_in(cx, |workspace, window, cx| {
7243            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7244        });
7245
7246        // Autosave on window change.
7247        item.update(cx, |item, cx| {
7248            SettingsStore::update_global(cx, |settings, cx| {
7249                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7250                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
7251                })
7252            });
7253            item.is_dirty = true;
7254        });
7255
7256        // Deactivating the window saves the file.
7257        cx.deactivate_window();
7258        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7259
7260        // Re-activating the window doesn't save the file.
7261        cx.update(|window, _| window.activate_window());
7262        cx.executor().run_until_parked();
7263        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7264
7265        // Autosave on focus change.
7266        item.update_in(cx, |item, window, cx| {
7267            cx.focus_self(window);
7268            SettingsStore::update_global(cx, |settings, cx| {
7269                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7270                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
7271                })
7272            });
7273            item.is_dirty = true;
7274        });
7275
7276        // Blurring the item saves the file.
7277        item.update_in(cx, |_, window, _| window.blur());
7278        cx.executor().run_until_parked();
7279        item.update(cx, |item, _| assert_eq!(item.save_count, 2));
7280
7281        // Deactivating the window still saves the file.
7282        item.update_in(cx, |item, window, cx| {
7283            cx.focus_self(window);
7284            item.is_dirty = true;
7285        });
7286        cx.deactivate_window();
7287        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7288
7289        // Autosave after delay.
7290        item.update(cx, |item, cx| {
7291            SettingsStore::update_global(cx, |settings, cx| {
7292                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7293                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
7294                })
7295            });
7296            item.is_dirty = true;
7297            cx.emit(ItemEvent::Edit);
7298        });
7299
7300        // Delay hasn't fully expired, so the file is still dirty and unsaved.
7301        cx.executor().advance_clock(Duration::from_millis(250));
7302        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7303
7304        // After delay expires, the file is saved.
7305        cx.executor().advance_clock(Duration::from_millis(250));
7306        item.update(cx, |item, _| assert_eq!(item.save_count, 4));
7307
7308        // Autosave on focus change, ensuring closing the tab counts as such.
7309        item.update(cx, |item, cx| {
7310            SettingsStore::update_global(cx, |settings, cx| {
7311                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7312                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
7313                })
7314            });
7315            item.is_dirty = true;
7316            for project_item in &mut item.project_items {
7317                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7318            }
7319        });
7320
7321        pane.update_in(cx, |pane, window, cx| {
7322            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7323        })
7324        .await
7325        .unwrap();
7326        assert!(!cx.has_pending_prompt());
7327        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7328
7329        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
7330        workspace.update_in(cx, |workspace, window, cx| {
7331            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7332        });
7333        item.update_in(cx, |item, window, cx| {
7334            item.project_items[0].update(cx, |item, _| {
7335                item.entry_id = None;
7336            });
7337            item.is_dirty = true;
7338            window.blur();
7339        });
7340        cx.run_until_parked();
7341        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7342
7343        // Ensure autosave is prevented for deleted files also when closing the buffer.
7344        let _close_items = pane.update_in(cx, |pane, window, cx| {
7345            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7346        });
7347        cx.run_until_parked();
7348        assert!(cx.has_pending_prompt());
7349        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7350    }
7351
7352    #[gpui::test]
7353    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
7354        init_test(cx);
7355
7356        let fs = FakeFs::new(cx.executor());
7357
7358        let project = Project::test(fs, [], cx).await;
7359        let (workspace, cx) =
7360            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7361
7362        let item = cx.new(|cx| {
7363            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7364        });
7365        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7366        let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
7367        let toolbar_notify_count = Rc::new(RefCell::new(0));
7368
7369        workspace.update_in(cx, |workspace, window, cx| {
7370            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7371            let toolbar_notification_count = toolbar_notify_count.clone();
7372            cx.observe_in(&toolbar, window, move |_, _, _, _| {
7373                *toolbar_notification_count.borrow_mut() += 1
7374            })
7375            .detach();
7376        });
7377
7378        pane.update(cx, |pane, _| {
7379            assert!(!pane.can_navigate_backward());
7380            assert!(!pane.can_navigate_forward());
7381        });
7382
7383        item.update_in(cx, |item, _, cx| {
7384            item.set_state("one".to_string(), cx);
7385        });
7386
7387        // Toolbar must be notified to re-render the navigation buttons
7388        assert_eq!(*toolbar_notify_count.borrow(), 1);
7389
7390        pane.update(cx, |pane, _| {
7391            assert!(pane.can_navigate_backward());
7392            assert!(!pane.can_navigate_forward());
7393        });
7394
7395        workspace
7396            .update_in(cx, |workspace, window, cx| {
7397                workspace.go_back(pane.downgrade(), window, cx)
7398            })
7399            .await
7400            .unwrap();
7401
7402        assert_eq!(*toolbar_notify_count.borrow(), 2);
7403        pane.update(cx, |pane, _| {
7404            assert!(!pane.can_navigate_backward());
7405            assert!(pane.can_navigate_forward());
7406        });
7407    }
7408
7409    #[gpui::test]
7410    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7411        init_test(cx);
7412        let fs = FakeFs::new(cx.executor());
7413
7414        let project = Project::test(fs, [], cx).await;
7415        let (workspace, cx) =
7416            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7417
7418        let panel = workspace.update_in(cx, |workspace, window, cx| {
7419            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7420            workspace.add_panel(panel.clone(), window, cx);
7421
7422            workspace
7423                .right_dock()
7424                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
7425
7426            panel
7427        });
7428
7429        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7430        pane.update_in(cx, |pane, window, cx| {
7431            let item = cx.new(TestItem::new);
7432            pane.add_item(Box::new(item), true, true, None, window, cx);
7433        });
7434
7435        // Transfer focus from center to panel
7436        workspace.update_in(cx, |workspace, window, cx| {
7437            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7438        });
7439
7440        workspace.update_in(cx, |workspace, window, cx| {
7441            assert!(workspace.right_dock().read(cx).is_open());
7442            assert!(!panel.is_zoomed(window, cx));
7443            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7444        });
7445
7446        // Transfer focus from panel to center
7447        workspace.update_in(cx, |workspace, window, cx| {
7448            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7449        });
7450
7451        workspace.update_in(cx, |workspace, window, cx| {
7452            assert!(workspace.right_dock().read(cx).is_open());
7453            assert!(!panel.is_zoomed(window, cx));
7454            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7455        });
7456
7457        // Close the dock
7458        workspace.update_in(cx, |workspace, window, cx| {
7459            workspace.toggle_dock(DockPosition::Right, window, cx);
7460        });
7461
7462        workspace.update_in(cx, |workspace, window, cx| {
7463            assert!(!workspace.right_dock().read(cx).is_open());
7464            assert!(!panel.is_zoomed(window, cx));
7465            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7466        });
7467
7468        // Open the dock
7469        workspace.update_in(cx, |workspace, window, cx| {
7470            workspace.toggle_dock(DockPosition::Right, window, cx);
7471        });
7472
7473        workspace.update_in(cx, |workspace, window, cx| {
7474            assert!(workspace.right_dock().read(cx).is_open());
7475            assert!(!panel.is_zoomed(window, cx));
7476            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7477        });
7478
7479        // Focus and zoom panel
7480        panel.update_in(cx, |panel, window, cx| {
7481            cx.focus_self(window);
7482            panel.set_zoomed(true, window, cx)
7483        });
7484
7485        workspace.update_in(cx, |workspace, window, cx| {
7486            assert!(workspace.right_dock().read(cx).is_open());
7487            assert!(panel.is_zoomed(window, cx));
7488            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7489        });
7490
7491        // Transfer focus to the center closes the dock
7492        workspace.update_in(cx, |workspace, window, cx| {
7493            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7494        });
7495
7496        workspace.update_in(cx, |workspace, window, cx| {
7497            assert!(!workspace.right_dock().read(cx).is_open());
7498            assert!(panel.is_zoomed(window, cx));
7499            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7500        });
7501
7502        // Transferring focus back to the panel keeps it zoomed
7503        workspace.update_in(cx, |workspace, window, cx| {
7504            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7505        });
7506
7507        workspace.update_in(cx, |workspace, window, cx| {
7508            assert!(workspace.right_dock().read(cx).is_open());
7509            assert!(panel.is_zoomed(window, cx));
7510            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7511        });
7512
7513        // Close the dock while it is zoomed
7514        workspace.update_in(cx, |workspace, window, cx| {
7515            workspace.toggle_dock(DockPosition::Right, window, cx)
7516        });
7517
7518        workspace.update_in(cx, |workspace, window, cx| {
7519            assert!(!workspace.right_dock().read(cx).is_open());
7520            assert!(panel.is_zoomed(window, cx));
7521            assert!(workspace.zoomed.is_none());
7522            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7523        });
7524
7525        // Opening the dock, when it's zoomed, retains focus
7526        workspace.update_in(cx, |workspace, window, cx| {
7527            workspace.toggle_dock(DockPosition::Right, window, cx)
7528        });
7529
7530        workspace.update_in(cx, |workspace, window, cx| {
7531            assert!(workspace.right_dock().read(cx).is_open());
7532            assert!(panel.is_zoomed(window, cx));
7533            assert!(workspace.zoomed.is_some());
7534            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7535        });
7536
7537        // Unzoom and close the panel, zoom the active pane.
7538        panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
7539        workspace.update_in(cx, |workspace, window, cx| {
7540            workspace.toggle_dock(DockPosition::Right, window, cx)
7541        });
7542        pane.update_in(cx, |pane, window, cx| {
7543            pane.toggle_zoom(&Default::default(), window, cx)
7544        });
7545
7546        // Opening a dock unzooms the pane.
7547        workspace.update_in(cx, |workspace, window, cx| {
7548            workspace.toggle_dock(DockPosition::Right, window, cx)
7549        });
7550        workspace.update_in(cx, |workspace, window, cx| {
7551            let pane = pane.read(cx);
7552            assert!(!pane.is_zoomed());
7553            assert!(!pane.focus_handle(cx).is_focused(window));
7554            assert!(workspace.right_dock().read(cx).is_open());
7555            assert!(workspace.zoomed.is_none());
7556        });
7557    }
7558
7559    #[gpui::test]
7560    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7561        init_test(cx);
7562
7563        let fs = FakeFs::new(cx.executor());
7564
7565        let project = Project::test(fs, None, cx).await;
7566        let (workspace, cx) =
7567            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7568
7569        // Let's arrange the panes like this:
7570        //
7571        // +-----------------------+
7572        // |         top           |
7573        // +------+--------+-------+
7574        // | left | center | right |
7575        // +------+--------+-------+
7576        // |        bottom         |
7577        // +-----------------------+
7578
7579        let top_item = cx.new(|cx| {
7580            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7581        });
7582        let bottom_item = cx.new(|cx| {
7583            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7584        });
7585        let left_item = cx.new(|cx| {
7586            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7587        });
7588        let right_item = cx.new(|cx| {
7589            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7590        });
7591        let center_item = cx.new(|cx| {
7592            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7593        });
7594
7595        let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7596            let top_pane_id = workspace.active_pane().entity_id();
7597            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
7598            workspace.split_pane(
7599                workspace.active_pane().clone(),
7600                SplitDirection::Down,
7601                window,
7602                cx,
7603            );
7604            top_pane_id
7605        });
7606        let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7607            let bottom_pane_id = workspace.active_pane().entity_id();
7608            workspace.add_item_to_active_pane(
7609                Box::new(bottom_item.clone()),
7610                None,
7611                false,
7612                window,
7613                cx,
7614            );
7615            workspace.split_pane(
7616                workspace.active_pane().clone(),
7617                SplitDirection::Up,
7618                window,
7619                cx,
7620            );
7621            bottom_pane_id
7622        });
7623        let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7624            let left_pane_id = workspace.active_pane().entity_id();
7625            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
7626            workspace.split_pane(
7627                workspace.active_pane().clone(),
7628                SplitDirection::Right,
7629                window,
7630                cx,
7631            );
7632            left_pane_id
7633        });
7634        let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7635            let right_pane_id = workspace.active_pane().entity_id();
7636            workspace.add_item_to_active_pane(
7637                Box::new(right_item.clone()),
7638                None,
7639                false,
7640                window,
7641                cx,
7642            );
7643            workspace.split_pane(
7644                workspace.active_pane().clone(),
7645                SplitDirection::Left,
7646                window,
7647                cx,
7648            );
7649            right_pane_id
7650        });
7651        let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7652            let center_pane_id = workspace.active_pane().entity_id();
7653            workspace.add_item_to_active_pane(
7654                Box::new(center_item.clone()),
7655                None,
7656                false,
7657                window,
7658                cx,
7659            );
7660            center_pane_id
7661        });
7662        cx.executor().run_until_parked();
7663
7664        workspace.update_in(cx, |workspace, window, cx| {
7665            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7666
7667            // Join into next from center pane into right
7668            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7669        });
7670
7671        workspace.update_in(cx, |workspace, window, cx| {
7672            let active_pane = workspace.active_pane();
7673            assert_eq!(right_pane_id, active_pane.entity_id());
7674            assert_eq!(2, active_pane.read(cx).items_len());
7675            let item_ids_in_pane =
7676                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7677            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7678            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7679
7680            // Join into next from right pane into bottom
7681            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7682        });
7683
7684        workspace.update_in(cx, |workspace, window, cx| {
7685            let active_pane = workspace.active_pane();
7686            assert_eq!(bottom_pane_id, active_pane.entity_id());
7687            assert_eq!(3, active_pane.read(cx).items_len());
7688            let item_ids_in_pane =
7689                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7690            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7691            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7692            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7693
7694            // Join into next from bottom pane into left
7695            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7696        });
7697
7698        workspace.update_in(cx, |workspace, window, cx| {
7699            let active_pane = workspace.active_pane();
7700            assert_eq!(left_pane_id, active_pane.entity_id());
7701            assert_eq!(4, active_pane.read(cx).items_len());
7702            let item_ids_in_pane =
7703                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7704            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7705            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7706            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7707            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7708
7709            // Join into next from left pane into top
7710            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7711        });
7712
7713        workspace.update_in(cx, |workspace, window, cx| {
7714            let active_pane = workspace.active_pane();
7715            assert_eq!(top_pane_id, active_pane.entity_id());
7716            assert_eq!(5, active_pane.read(cx).items_len());
7717            let item_ids_in_pane =
7718                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7719            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7720            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7721            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7722            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7723            assert!(item_ids_in_pane.contains(&top_item.item_id()));
7724
7725            // Single pane left: no-op
7726            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
7727        });
7728
7729        workspace.update(cx, |workspace, _cx| {
7730            let active_pane = workspace.active_pane();
7731            assert_eq!(top_pane_id, active_pane.entity_id());
7732        });
7733    }
7734
7735    fn add_an_item_to_active_pane(
7736        cx: &mut VisualTestContext,
7737        workspace: &Entity<Workspace>,
7738        item_id: u64,
7739    ) -> Entity<TestItem> {
7740        let item = cx.new(|cx| {
7741            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7742                item_id,
7743                "item{item_id}.txt",
7744                cx,
7745            )])
7746        });
7747        workspace.update_in(cx, |workspace, window, cx| {
7748            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
7749        });
7750        return item;
7751    }
7752
7753    fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
7754        return workspace.update_in(cx, |workspace, window, cx| {
7755            let new_pane = workspace.split_pane(
7756                workspace.active_pane().clone(),
7757                SplitDirection::Right,
7758                window,
7759                cx,
7760            );
7761            new_pane
7762        });
7763    }
7764
7765    #[gpui::test]
7766    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7767        init_test(cx);
7768        let fs = FakeFs::new(cx.executor());
7769        let project = Project::test(fs, None, cx).await;
7770        let (workspace, cx) =
7771            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7772
7773        add_an_item_to_active_pane(cx, &workspace, 1);
7774        split_pane(cx, &workspace);
7775        add_an_item_to_active_pane(cx, &workspace, 2);
7776        split_pane(cx, &workspace); // empty pane
7777        split_pane(cx, &workspace);
7778        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7779
7780        cx.executor().run_until_parked();
7781
7782        workspace.update(cx, |workspace, cx| {
7783            let num_panes = workspace.panes().len();
7784            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7785            let active_item = workspace
7786                .active_pane()
7787                .read(cx)
7788                .active_item()
7789                .expect("item is in focus");
7790
7791            assert_eq!(num_panes, 4);
7792            assert_eq!(num_items_in_current_pane, 1);
7793            assert_eq!(active_item.item_id(), last_item.item_id());
7794        });
7795
7796        workspace.update_in(cx, |workspace, window, cx| {
7797            workspace.join_all_panes(window, cx);
7798        });
7799
7800        workspace.update(cx, |workspace, cx| {
7801            let num_panes = workspace.panes().len();
7802            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7803            let active_item = workspace
7804                .active_pane()
7805                .read(cx)
7806                .active_item()
7807                .expect("item is in focus");
7808
7809            assert_eq!(num_panes, 1);
7810            assert_eq!(num_items_in_current_pane, 3);
7811            assert_eq!(active_item.item_id(), last_item.item_id());
7812        });
7813    }
7814    struct TestModal(FocusHandle);
7815
7816    impl TestModal {
7817        fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
7818            Self(cx.focus_handle())
7819        }
7820    }
7821
7822    impl EventEmitter<DismissEvent> for TestModal {}
7823
7824    impl Focusable for TestModal {
7825        fn focus_handle(&self, _cx: &App) -> FocusHandle {
7826            self.0.clone()
7827        }
7828    }
7829
7830    impl ModalView for TestModal {}
7831
7832    impl Render for TestModal {
7833        fn render(
7834            &mut self,
7835            _window: &mut Window,
7836            _cx: &mut Context<TestModal>,
7837        ) -> impl IntoElement {
7838            div().track_focus(&self.0)
7839        }
7840    }
7841
7842    #[gpui::test]
7843    async fn test_panels(cx: &mut gpui::TestAppContext) {
7844        init_test(cx);
7845        let fs = FakeFs::new(cx.executor());
7846
7847        let project = Project::test(fs, [], cx).await;
7848        let (workspace, cx) =
7849            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7850
7851        let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
7852            let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
7853            workspace.add_panel(panel_1.clone(), window, cx);
7854            workspace.toggle_dock(DockPosition::Left, window, cx);
7855            let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7856            workspace.add_panel(panel_2.clone(), window, cx);
7857            workspace.toggle_dock(DockPosition::Right, window, cx);
7858
7859            let left_dock = workspace.left_dock();
7860            assert_eq!(
7861                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7862                panel_1.panel_id()
7863            );
7864            assert_eq!(
7865                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7866                panel_1.size(window, cx)
7867            );
7868
7869            left_dock.update(cx, |left_dock, cx| {
7870                left_dock.resize_active_panel(Some(px(1337.)), window, cx)
7871            });
7872            assert_eq!(
7873                workspace
7874                    .right_dock()
7875                    .read(cx)
7876                    .visible_panel()
7877                    .unwrap()
7878                    .panel_id(),
7879                panel_2.panel_id(),
7880            );
7881
7882            (panel_1, panel_2)
7883        });
7884
7885        // Move panel_1 to the right
7886        panel_1.update_in(cx, |panel_1, window, cx| {
7887            panel_1.set_position(DockPosition::Right, window, cx)
7888        });
7889
7890        workspace.update_in(cx, |workspace, window, cx| {
7891            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7892            // Since it was the only panel on the left, the left dock should now be closed.
7893            assert!(!workspace.left_dock().read(cx).is_open());
7894            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7895            let right_dock = workspace.right_dock();
7896            assert_eq!(
7897                right_dock.read(cx).visible_panel().unwrap().panel_id(),
7898                panel_1.panel_id()
7899            );
7900            assert_eq!(
7901                right_dock.read(cx).active_panel_size(window, cx).unwrap(),
7902                px(1337.)
7903            );
7904
7905            // Now we move panel_2 to the left
7906            panel_2.set_position(DockPosition::Left, window, cx);
7907        });
7908
7909        workspace.update(cx, |workspace, cx| {
7910            // Since panel_2 was not visible on the right, we don't open the left dock.
7911            assert!(!workspace.left_dock().read(cx).is_open());
7912            // And the right dock is unaffected in its displaying of panel_1
7913            assert!(workspace.right_dock().read(cx).is_open());
7914            assert_eq!(
7915                workspace
7916                    .right_dock()
7917                    .read(cx)
7918                    .visible_panel()
7919                    .unwrap()
7920                    .panel_id(),
7921                panel_1.panel_id(),
7922            );
7923        });
7924
7925        // Move panel_1 back to the left
7926        panel_1.update_in(cx, |panel_1, window, cx| {
7927            panel_1.set_position(DockPosition::Left, window, cx)
7928        });
7929
7930        workspace.update_in(cx, |workspace, window, cx| {
7931            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7932            let left_dock = workspace.left_dock();
7933            assert!(left_dock.read(cx).is_open());
7934            assert_eq!(
7935                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7936                panel_1.panel_id()
7937            );
7938            assert_eq!(
7939                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7940                px(1337.)
7941            );
7942            // And the right dock should be closed as it no longer has any panels.
7943            assert!(!workspace.right_dock().read(cx).is_open());
7944
7945            // Now we move panel_1 to the bottom
7946            panel_1.set_position(DockPosition::Bottom, window, cx);
7947        });
7948
7949        workspace.update_in(cx, |workspace, window, cx| {
7950            // Since panel_1 was visible on the left, we close the left dock.
7951            assert!(!workspace.left_dock().read(cx).is_open());
7952            // The bottom dock is sized based on the panel's default size,
7953            // since the panel orientation changed from vertical to horizontal.
7954            let bottom_dock = workspace.bottom_dock();
7955            assert_eq!(
7956                bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
7957                panel_1.size(window, cx),
7958            );
7959            // Close bottom dock and move panel_1 back to the left.
7960            bottom_dock.update(cx, |bottom_dock, cx| {
7961                bottom_dock.set_open(false, window, cx)
7962            });
7963            panel_1.set_position(DockPosition::Left, window, cx);
7964        });
7965
7966        // Emit activated event on panel 1
7967        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7968
7969        // Now the left dock is open and panel_1 is active and focused.
7970        workspace.update_in(cx, |workspace, window, cx| {
7971            let left_dock = workspace.left_dock();
7972            assert!(left_dock.read(cx).is_open());
7973            assert_eq!(
7974                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7975                panel_1.panel_id(),
7976            );
7977            assert!(panel_1.focus_handle(cx).is_focused(window));
7978        });
7979
7980        // Emit closed event on panel 2, which is not active
7981        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7982
7983        // Wo don't close the left dock, because panel_2 wasn't the active panel
7984        workspace.update(cx, |workspace, cx| {
7985            let left_dock = workspace.left_dock();
7986            assert!(left_dock.read(cx).is_open());
7987            assert_eq!(
7988                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7989                panel_1.panel_id(),
7990            );
7991        });
7992
7993        // Emitting a ZoomIn event shows the panel as zoomed.
7994        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7995        workspace.update(cx, |workspace, _| {
7996            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7997            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7998        });
7999
8000        // Move panel to another dock while it is zoomed
8001        panel_1.update_in(cx, |panel, window, cx| {
8002            panel.set_position(DockPosition::Right, window, cx)
8003        });
8004        workspace.update(cx, |workspace, _| {
8005            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8006
8007            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8008        });
8009
8010        // This is a helper for getting a:
8011        // - valid focus on an element,
8012        // - that isn't a part of the panes and panels system of the Workspace,
8013        // - and doesn't trigger the 'on_focus_lost' API.
8014        let focus_other_view = {
8015            let workspace = workspace.clone();
8016            move |cx: &mut VisualTestContext| {
8017                workspace.update_in(cx, |workspace, window, cx| {
8018                    if let Some(_) = workspace.active_modal::<TestModal>(cx) {
8019                        workspace.toggle_modal(window, cx, TestModal::new);
8020                        workspace.toggle_modal(window, cx, TestModal::new);
8021                    } else {
8022                        workspace.toggle_modal(window, cx, TestModal::new);
8023                    }
8024                })
8025            }
8026        };
8027
8028        // If focus is transferred to another view that's not a panel or another pane, we still show
8029        // the panel as zoomed.
8030        focus_other_view(cx);
8031        workspace.update(cx, |workspace, _| {
8032            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8033            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8034        });
8035
8036        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8037        workspace.update_in(cx, |_workspace, window, cx| {
8038            cx.focus_self(window);
8039        });
8040        workspace.update(cx, |workspace, _| {
8041            assert_eq!(workspace.zoomed, None);
8042            assert_eq!(workspace.zoomed_position, None);
8043        });
8044
8045        // If focus is transferred again to another view that's not a panel or a pane, we won't
8046        // show the panel as zoomed because it wasn't zoomed before.
8047        focus_other_view(cx);
8048        workspace.update(cx, |workspace, _| {
8049            assert_eq!(workspace.zoomed, None);
8050            assert_eq!(workspace.zoomed_position, None);
8051        });
8052
8053        // When the panel is activated, it is zoomed again.
8054        cx.dispatch_action(ToggleRightDock);
8055        workspace.update(cx, |workspace, _| {
8056            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8057            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8058        });
8059
8060        // Emitting a ZoomOut event unzooms the panel.
8061        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8062        workspace.update(cx, |workspace, _| {
8063            assert_eq!(workspace.zoomed, None);
8064            assert_eq!(workspace.zoomed_position, None);
8065        });
8066
8067        // Emit closed event on panel 1, which is active
8068        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8069
8070        // Now the left dock is closed, because panel_1 was the active panel
8071        workspace.update(cx, |workspace, cx| {
8072            let right_dock = workspace.right_dock();
8073            assert!(!right_dock.read(cx).is_open());
8074        });
8075    }
8076
8077    #[gpui::test]
8078    async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8079        init_test(cx);
8080
8081        let fs = FakeFs::new(cx.background_executor.clone());
8082        let project = Project::test(fs, [], cx).await;
8083        let (workspace, cx) =
8084            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8085        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8086
8087        let dirty_regular_buffer = cx.new(|cx| {
8088            TestItem::new(cx)
8089                .with_dirty(true)
8090                .with_label("1.txt")
8091                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8092        });
8093        let dirty_regular_buffer_2 = cx.new(|cx| {
8094            TestItem::new(cx)
8095                .with_dirty(true)
8096                .with_label("2.txt")
8097                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8098        });
8099        let dirty_multi_buffer_with_both = cx.new(|cx| {
8100            TestItem::new(cx)
8101                .with_dirty(true)
8102                .with_singleton(false)
8103                .with_label("Fake Project Search")
8104                .with_project_items(&[
8105                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8106                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8107                ])
8108        });
8109        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8110        workspace.update_in(cx, |workspace, window, cx| {
8111            workspace.add_item(
8112                pane.clone(),
8113                Box::new(dirty_regular_buffer.clone()),
8114                None,
8115                false,
8116                false,
8117                window,
8118                cx,
8119            );
8120            workspace.add_item(
8121                pane.clone(),
8122                Box::new(dirty_regular_buffer_2.clone()),
8123                None,
8124                false,
8125                false,
8126                window,
8127                cx,
8128            );
8129            workspace.add_item(
8130                pane.clone(),
8131                Box::new(dirty_multi_buffer_with_both.clone()),
8132                None,
8133                false,
8134                false,
8135                window,
8136                cx,
8137            );
8138        });
8139
8140        pane.update_in(cx, |pane, window, cx| {
8141            pane.activate_item(2, true, true, window, cx);
8142            assert_eq!(
8143                pane.active_item().unwrap().item_id(),
8144                multi_buffer_with_both_files_id,
8145                "Should select the multi buffer in the pane"
8146            );
8147        });
8148        let close_all_but_multi_buffer_task = pane
8149            .update_in(cx, |pane, window, cx| {
8150                pane.close_inactive_items(
8151                    &CloseInactiveItems {
8152                        save_intent: Some(SaveIntent::Save),
8153                        close_pinned: true,
8154                    },
8155                    window,
8156                    cx,
8157                )
8158            })
8159            .expect("should have inactive files to close");
8160        cx.background_executor.run_until_parked();
8161        assert!(
8162            !cx.has_pending_prompt(),
8163            "Multi buffer still has the unsaved buffer inside, so no save prompt should be shown"
8164        );
8165        close_all_but_multi_buffer_task
8166            .await
8167            .expect("Closing all buffers but the multi buffer failed");
8168        pane.update(cx, |pane, cx| {
8169            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8170            assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8171            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8172            assert_eq!(pane.items_len(), 1);
8173            assert_eq!(
8174                pane.active_item().unwrap().item_id(),
8175                multi_buffer_with_both_files_id,
8176                "Should have only the multi buffer left in the pane"
8177            );
8178            assert!(
8179                dirty_multi_buffer_with_both.read(cx).is_dirty,
8180                "The multi buffer containing the unsaved buffer should still be dirty"
8181            );
8182        });
8183
8184        let close_multi_buffer_task = pane
8185            .update_in(cx, |pane, window, cx| {
8186                pane.close_active_item(
8187                    &CloseActiveItem {
8188                        save_intent: Some(SaveIntent::Close),
8189                        close_pinned: false,
8190                    },
8191                    window,
8192                    cx,
8193                )
8194            })
8195            .expect("should have the multi buffer to close");
8196        cx.background_executor.run_until_parked();
8197        assert!(
8198            cx.has_pending_prompt(),
8199            "Dirty multi buffer should prompt a save dialog"
8200        );
8201        cx.simulate_prompt_answer(0);
8202        cx.background_executor.run_until_parked();
8203        close_multi_buffer_task
8204            .await
8205            .expect("Closing the multi buffer failed");
8206        pane.update(cx, |pane, cx| {
8207            assert_eq!(
8208                dirty_multi_buffer_with_both.read(cx).save_count,
8209                1,
8210                "Multi buffer item should get be saved"
8211            );
8212            // Test impl does not save inner items, so we do not assert them
8213            assert_eq!(
8214                pane.items_len(),
8215                0,
8216                "No more items should be left in the pane"
8217            );
8218            assert!(pane.active_item().is_none());
8219        });
8220    }
8221
8222    #[gpui::test]
8223    async fn test_no_save_prompt_when_dirty_singleton_buffer_closed_with_a_multi_buffer_containing_it_present_in_the_pane(
8224        cx: &mut TestAppContext,
8225    ) {
8226        init_test(cx);
8227
8228        let fs = FakeFs::new(cx.background_executor.clone());
8229        let project = Project::test(fs, [], cx).await;
8230        let (workspace, cx) =
8231            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8232        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8233
8234        let dirty_regular_buffer = cx.new(|cx| {
8235            TestItem::new(cx)
8236                .with_dirty(true)
8237                .with_label("1.txt")
8238                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8239        });
8240        let dirty_regular_buffer_2 = cx.new(|cx| {
8241            TestItem::new(cx)
8242                .with_dirty(true)
8243                .with_label("2.txt")
8244                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8245        });
8246        let clear_regular_buffer = cx.new(|cx| {
8247            TestItem::new(cx)
8248                .with_label("3.txt")
8249                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8250        });
8251
8252        let dirty_multi_buffer_with_both = cx.new(|cx| {
8253            TestItem::new(cx)
8254                .with_dirty(true)
8255                .with_singleton(false)
8256                .with_label("Fake Project Search")
8257                .with_project_items(&[
8258                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8259                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8260                    clear_regular_buffer.read(cx).project_items[0].clone(),
8261                ])
8262        });
8263        workspace.update_in(cx, |workspace, window, cx| {
8264            workspace.add_item(
8265                pane.clone(),
8266                Box::new(dirty_regular_buffer.clone()),
8267                None,
8268                false,
8269                false,
8270                window,
8271                cx,
8272            );
8273            workspace.add_item(
8274                pane.clone(),
8275                Box::new(dirty_multi_buffer_with_both.clone()),
8276                None,
8277                false,
8278                false,
8279                window,
8280                cx,
8281            );
8282        });
8283
8284        pane.update_in(cx, |pane, window, cx| {
8285            pane.activate_item(0, true, true, window, cx);
8286            assert_eq!(
8287                pane.active_item().unwrap().item_id(),
8288                dirty_regular_buffer.item_id(),
8289                "Should select the dirty singleton buffer in the pane"
8290            );
8291        });
8292        let close_singleton_buffer_task = pane
8293            .update_in(cx, |pane, window, cx| {
8294                pane.close_active_item(
8295                    &CloseActiveItem {
8296                        save_intent: None,
8297                        close_pinned: false,
8298                    },
8299                    window,
8300                    cx,
8301                )
8302            })
8303            .expect("should have active singleton buffer to close");
8304        cx.background_executor.run_until_parked();
8305        assert!(
8306            !cx.has_pending_prompt(),
8307            "Multi buffer is still in the pane and has the unsaved buffer inside, so no save prompt should be shown"
8308        );
8309
8310        close_singleton_buffer_task
8311            .await
8312            .expect("Should not fail closing the singleton buffer");
8313        pane.update(cx, |pane, cx| {
8314            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8315            assert_eq!(
8316                dirty_multi_buffer_with_both.read(cx).save_count,
8317                0,
8318                "Multi buffer itself should not be saved"
8319            );
8320            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8321            assert_eq!(
8322                pane.items_len(),
8323                1,
8324                "A dirty multi buffer should be present in the pane"
8325            );
8326            assert_eq!(
8327                pane.active_item().unwrap().item_id(),
8328                dirty_multi_buffer_with_both.item_id(),
8329                "Should activate the only remaining item in the pane"
8330            );
8331        });
8332    }
8333
8334    #[gpui::test]
8335    async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
8336        cx: &mut TestAppContext,
8337    ) {
8338        init_test(cx);
8339
8340        let fs = FakeFs::new(cx.background_executor.clone());
8341        let project = Project::test(fs, [], cx).await;
8342        let (workspace, cx) =
8343            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8344        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8345
8346        let dirty_regular_buffer = cx.new(|cx| {
8347            TestItem::new(cx)
8348                .with_dirty(true)
8349                .with_label("1.txt")
8350                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8351        });
8352        let dirty_regular_buffer_2 = cx.new(|cx| {
8353            TestItem::new(cx)
8354                .with_dirty(true)
8355                .with_label("2.txt")
8356                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8357        });
8358        let clear_regular_buffer = cx.new(|cx| {
8359            TestItem::new(cx)
8360                .with_label("3.txt")
8361                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8362        });
8363
8364        let dirty_multi_buffer_with_both = cx.new(|cx| {
8365            TestItem::new(cx)
8366                .with_dirty(true)
8367                .with_singleton(false)
8368                .with_label("Fake Project Search")
8369                .with_project_items(&[
8370                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8371                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8372                    clear_regular_buffer.read(cx).project_items[0].clone(),
8373                ])
8374        });
8375        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8376        workspace.update_in(cx, |workspace, window, cx| {
8377            workspace.add_item(
8378                pane.clone(),
8379                Box::new(dirty_regular_buffer.clone()),
8380                None,
8381                false,
8382                false,
8383                window,
8384                cx,
8385            );
8386            workspace.add_item(
8387                pane.clone(),
8388                Box::new(dirty_multi_buffer_with_both.clone()),
8389                None,
8390                false,
8391                false,
8392                window,
8393                cx,
8394            );
8395        });
8396
8397        pane.update_in(cx, |pane, window, cx| {
8398            pane.activate_item(1, true, true, window, cx);
8399            assert_eq!(
8400                pane.active_item().unwrap().item_id(),
8401                multi_buffer_with_both_files_id,
8402                "Should select the multi buffer in the pane"
8403            );
8404        });
8405        let _close_multi_buffer_task = pane
8406            .update_in(cx, |pane, window, cx| {
8407                pane.close_active_item(
8408                    &CloseActiveItem {
8409                        save_intent: None,
8410                        close_pinned: false,
8411                    },
8412                    window,
8413                    cx,
8414                )
8415            })
8416            .expect("should have active multi buffer to close");
8417        cx.background_executor.run_until_parked();
8418        assert!(
8419            cx.has_pending_prompt(),
8420            "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
8421        );
8422    }
8423
8424    #[gpui::test]
8425    async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
8426        cx: &mut TestAppContext,
8427    ) {
8428        init_test(cx);
8429
8430        let fs = FakeFs::new(cx.background_executor.clone());
8431        let project = Project::test(fs, [], cx).await;
8432        let (workspace, cx) =
8433            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8434        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8435
8436        let dirty_regular_buffer = cx.new(|cx| {
8437            TestItem::new(cx)
8438                .with_dirty(true)
8439                .with_label("1.txt")
8440                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8441        });
8442        let dirty_regular_buffer_2 = cx.new(|cx| {
8443            TestItem::new(cx)
8444                .with_dirty(true)
8445                .with_label("2.txt")
8446                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8447        });
8448        let clear_regular_buffer = cx.new(|cx| {
8449            TestItem::new(cx)
8450                .with_label("3.txt")
8451                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8452        });
8453
8454        let dirty_multi_buffer = cx.new(|cx| {
8455            TestItem::new(cx)
8456                .with_dirty(true)
8457                .with_singleton(false)
8458                .with_label("Fake Project Search")
8459                .with_project_items(&[
8460                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8461                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8462                    clear_regular_buffer.read(cx).project_items[0].clone(),
8463                ])
8464        });
8465        workspace.update_in(cx, |workspace, window, cx| {
8466            workspace.add_item(
8467                pane.clone(),
8468                Box::new(dirty_regular_buffer.clone()),
8469                None,
8470                false,
8471                false,
8472                window,
8473                cx,
8474            );
8475            workspace.add_item(
8476                pane.clone(),
8477                Box::new(dirty_regular_buffer_2.clone()),
8478                None,
8479                false,
8480                false,
8481                window,
8482                cx,
8483            );
8484            workspace.add_item(
8485                pane.clone(),
8486                Box::new(dirty_multi_buffer.clone()),
8487                None,
8488                false,
8489                false,
8490                window,
8491                cx,
8492            );
8493        });
8494
8495        pane.update_in(cx, |pane, window, cx| {
8496            pane.activate_item(2, true, true, window, cx);
8497            assert_eq!(
8498                pane.active_item().unwrap().item_id(),
8499                dirty_multi_buffer.item_id(),
8500                "Should select the multi buffer in the pane"
8501            );
8502        });
8503        let close_multi_buffer_task = pane
8504            .update_in(cx, |pane, window, cx| {
8505                pane.close_active_item(
8506                    &CloseActiveItem {
8507                        save_intent: None,
8508                        close_pinned: false,
8509                    },
8510                    window,
8511                    cx,
8512                )
8513            })
8514            .expect("should have active multi buffer to close");
8515        cx.background_executor.run_until_parked();
8516        assert!(
8517            !cx.has_pending_prompt(),
8518            "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8519        );
8520        close_multi_buffer_task
8521            .await
8522            .expect("Closing multi buffer failed");
8523        pane.update(cx, |pane, cx| {
8524            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8525            assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8526            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8527            assert_eq!(
8528                pane.items()
8529                    .map(|item| item.item_id())
8530                    .sorted()
8531                    .collect::<Vec<_>>(),
8532                vec![
8533                    dirty_regular_buffer.item_id(),
8534                    dirty_regular_buffer_2.item_id(),
8535                ],
8536                "Should have no multi buffer left in the pane"
8537            );
8538            assert!(dirty_regular_buffer.read(cx).is_dirty);
8539            assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8540        });
8541    }
8542
8543    #[gpui::test]
8544    async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8545        init_test(cx);
8546        let fs = FakeFs::new(cx.executor());
8547        let project = Project::test(fs, [], cx).await;
8548        let (workspace, cx) =
8549            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8550
8551        // Add a new panel to the right dock, opening the dock and setting the
8552        // focus to the new panel.
8553        let panel = workspace.update_in(cx, |workspace, window, cx| {
8554            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8555            workspace.add_panel(panel.clone(), window, cx);
8556
8557            workspace
8558                .right_dock()
8559                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8560
8561            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8562
8563            panel
8564        });
8565
8566        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8567        // panel to the next valid position which, in this case, is the left
8568        // dock.
8569        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8570        workspace.update(cx, |workspace, cx| {
8571            assert!(workspace.left_dock().read(cx).is_open());
8572            assert_eq!(panel.read(cx).position, DockPosition::Left);
8573        });
8574
8575        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8576        // panel to the next valid position which, in this case, is the bottom
8577        // dock.
8578        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8579        workspace.update(cx, |workspace, cx| {
8580            assert!(workspace.bottom_dock().read(cx).is_open());
8581            assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8582        });
8583
8584        // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8585        // around moving the panel to its initial position, the right dock.
8586        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8587        workspace.update(cx, |workspace, cx| {
8588            assert!(workspace.right_dock().read(cx).is_open());
8589            assert_eq!(panel.read(cx).position, DockPosition::Right);
8590        });
8591
8592        // Remove focus from the panel, ensuring that, if the panel is not
8593        // focused, the `MoveFocusedPanelToNextPosition` action does not update
8594        // the panel's position, so the panel is still in the right dock.
8595        workspace.update_in(cx, |workspace, window, cx| {
8596            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8597        });
8598
8599        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8600        workspace.update(cx, |workspace, cx| {
8601            assert!(workspace.right_dock().read(cx).is_open());
8602            assert_eq!(panel.read(cx).position, DockPosition::Right);
8603        });
8604    }
8605
8606    mod register_project_item_tests {
8607
8608        use super::*;
8609
8610        // View
8611        struct TestPngItemView {
8612            focus_handle: FocusHandle,
8613        }
8614        // Model
8615        struct TestPngItem {}
8616
8617        impl project::ProjectItem for TestPngItem {
8618            fn try_open(
8619                _project: &Entity<Project>,
8620                path: &ProjectPath,
8621                cx: &mut App,
8622            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8623                if path.path.extension().unwrap() == "png" {
8624                    Some(cx.spawn(|mut cx| async move { cx.new(|_| TestPngItem {}) }))
8625                } else {
8626                    None
8627                }
8628            }
8629
8630            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8631                None
8632            }
8633
8634            fn project_path(&self, _: &App) -> Option<ProjectPath> {
8635                None
8636            }
8637
8638            fn is_dirty(&self) -> bool {
8639                false
8640            }
8641        }
8642
8643        impl Item for TestPngItemView {
8644            type Event = ();
8645        }
8646        impl EventEmitter<()> for TestPngItemView {}
8647        impl Focusable for TestPngItemView {
8648            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8649                self.focus_handle.clone()
8650            }
8651        }
8652
8653        impl Render for TestPngItemView {
8654            fn render(
8655                &mut self,
8656                _window: &mut Window,
8657                _cx: &mut Context<Self>,
8658            ) -> impl IntoElement {
8659                Empty
8660            }
8661        }
8662
8663        impl ProjectItem for TestPngItemView {
8664            type Item = TestPngItem;
8665
8666            fn for_project_item(
8667                _project: Entity<Project>,
8668                _item: Entity<Self::Item>,
8669                _: &mut Window,
8670                cx: &mut Context<Self>,
8671            ) -> Self
8672            where
8673                Self: Sized,
8674            {
8675                Self {
8676                    focus_handle: cx.focus_handle(),
8677                }
8678            }
8679        }
8680
8681        // View
8682        struct TestIpynbItemView {
8683            focus_handle: FocusHandle,
8684        }
8685        // Model
8686        struct TestIpynbItem {}
8687
8688        impl project::ProjectItem for TestIpynbItem {
8689            fn try_open(
8690                _project: &Entity<Project>,
8691                path: &ProjectPath,
8692                cx: &mut App,
8693            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8694                if path.path.extension().unwrap() == "ipynb" {
8695                    Some(cx.spawn(|mut cx| async move { cx.new(|_| TestIpynbItem {}) }))
8696                } else {
8697                    None
8698                }
8699            }
8700
8701            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8702                None
8703            }
8704
8705            fn project_path(&self, _: &App) -> Option<ProjectPath> {
8706                None
8707            }
8708
8709            fn is_dirty(&self) -> bool {
8710                false
8711            }
8712        }
8713
8714        impl Item for TestIpynbItemView {
8715            type Event = ();
8716        }
8717        impl EventEmitter<()> for TestIpynbItemView {}
8718        impl Focusable for TestIpynbItemView {
8719            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8720                self.focus_handle.clone()
8721            }
8722        }
8723
8724        impl Render for TestIpynbItemView {
8725            fn render(
8726                &mut self,
8727                _window: &mut Window,
8728                _cx: &mut Context<Self>,
8729            ) -> impl IntoElement {
8730                Empty
8731            }
8732        }
8733
8734        impl ProjectItem for TestIpynbItemView {
8735            type Item = TestIpynbItem;
8736
8737            fn for_project_item(
8738                _project: Entity<Project>,
8739                _item: Entity<Self::Item>,
8740                _: &mut Window,
8741                cx: &mut Context<Self>,
8742            ) -> Self
8743            where
8744                Self: Sized,
8745            {
8746                Self {
8747                    focus_handle: cx.focus_handle(),
8748                }
8749            }
8750        }
8751
8752        struct TestAlternatePngItemView {
8753            focus_handle: FocusHandle,
8754        }
8755
8756        impl Item for TestAlternatePngItemView {
8757            type Event = ();
8758        }
8759
8760        impl EventEmitter<()> for TestAlternatePngItemView {}
8761        impl Focusable for TestAlternatePngItemView {
8762            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8763                self.focus_handle.clone()
8764            }
8765        }
8766
8767        impl Render for TestAlternatePngItemView {
8768            fn render(
8769                &mut self,
8770                _window: &mut Window,
8771                _cx: &mut Context<Self>,
8772            ) -> impl IntoElement {
8773                Empty
8774            }
8775        }
8776
8777        impl ProjectItem for TestAlternatePngItemView {
8778            type Item = TestPngItem;
8779
8780            fn for_project_item(
8781                _project: Entity<Project>,
8782                _item: Entity<Self::Item>,
8783                _: &mut Window,
8784                cx: &mut Context<Self>,
8785            ) -> Self
8786            where
8787                Self: Sized,
8788            {
8789                Self {
8790                    focus_handle: cx.focus_handle(),
8791                }
8792            }
8793        }
8794
8795        #[gpui::test]
8796        async fn test_register_project_item(cx: &mut TestAppContext) {
8797            init_test(cx);
8798
8799            cx.update(|cx| {
8800                register_project_item::<TestPngItemView>(cx);
8801                register_project_item::<TestIpynbItemView>(cx);
8802            });
8803
8804            let fs = FakeFs::new(cx.executor());
8805            fs.insert_tree(
8806                "/root1",
8807                json!({
8808                    "one.png": "BINARYDATAHERE",
8809                    "two.ipynb": "{ totally a notebook }",
8810                    "three.txt": "editing text, sure why not?"
8811                }),
8812            )
8813            .await;
8814
8815            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8816            let (workspace, cx) =
8817                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8818
8819            let worktree_id = project.update(cx, |project, cx| {
8820                project.worktrees(cx).next().unwrap().read(cx).id()
8821            });
8822
8823            let handle = workspace
8824                .update_in(cx, |workspace, window, cx| {
8825                    let project_path = (worktree_id, "one.png");
8826                    workspace.open_path(project_path, None, true, window, cx)
8827                })
8828                .await
8829                .unwrap();
8830
8831            // Now we can check if the handle we got back errored or not
8832            assert_eq!(
8833                handle.to_any().entity_type(),
8834                TypeId::of::<TestPngItemView>()
8835            );
8836
8837            let handle = workspace
8838                .update_in(cx, |workspace, window, cx| {
8839                    let project_path = (worktree_id, "two.ipynb");
8840                    workspace.open_path(project_path, None, true, window, cx)
8841                })
8842                .await
8843                .unwrap();
8844
8845            assert_eq!(
8846                handle.to_any().entity_type(),
8847                TypeId::of::<TestIpynbItemView>()
8848            );
8849
8850            let handle = workspace
8851                .update_in(cx, |workspace, window, cx| {
8852                    let project_path = (worktree_id, "three.txt");
8853                    workspace.open_path(project_path, None, true, window, cx)
8854                })
8855                .await;
8856            assert!(handle.is_err());
8857        }
8858
8859        #[gpui::test]
8860        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8861            init_test(cx);
8862
8863            cx.update(|cx| {
8864                register_project_item::<TestPngItemView>(cx);
8865                register_project_item::<TestAlternatePngItemView>(cx);
8866            });
8867
8868            let fs = FakeFs::new(cx.executor());
8869            fs.insert_tree(
8870                "/root1",
8871                json!({
8872                    "one.png": "BINARYDATAHERE",
8873                    "two.ipynb": "{ totally a notebook }",
8874                    "three.txt": "editing text, sure why not?"
8875                }),
8876            )
8877            .await;
8878            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8879            let (workspace, cx) =
8880                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8881            let worktree_id = project.update(cx, |project, cx| {
8882                project.worktrees(cx).next().unwrap().read(cx).id()
8883            });
8884
8885            let handle = workspace
8886                .update_in(cx, |workspace, window, cx| {
8887                    let project_path = (worktree_id, "one.png");
8888                    workspace.open_path(project_path, None, true, window, cx)
8889                })
8890                .await
8891                .unwrap();
8892
8893            // This _must_ be the second item registered
8894            assert_eq!(
8895                handle.to_any().entity_type(),
8896                TypeId::of::<TestAlternatePngItemView>()
8897            );
8898
8899            let handle = workspace
8900                .update_in(cx, |workspace, window, cx| {
8901                    let project_path = (worktree_id, "three.txt");
8902                    workspace.open_path(project_path, None, true, window, cx)
8903                })
8904                .await;
8905            assert!(handle.is_err());
8906        }
8907    }
8908
8909    pub fn init_test(cx: &mut TestAppContext) {
8910        cx.update(|cx| {
8911            let settings_store = SettingsStore::test(cx);
8912            cx.set_global(settings_store);
8913            theme::init(theme::LoadThemes::JustBase, cx);
8914            language::init(cx);
8915            crate::init_settings(cx);
8916            Project::init_settings(cx);
8917        });
8918    }
8919
8920    fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
8921        let item = TestProjectItem::new(id, path, cx);
8922        item.update(cx, |item, _| {
8923            item.is_dirty = true;
8924        });
8925        item
8926    }
8927}