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