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            if self.active_pane() == pane {
4446                self.panes
4447                    .last()
4448                    .unwrap()
4449                    .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4450            }
4451        }
4452        if self.last_active_center_pane == Some(pane.downgrade()) {
4453            self.last_active_center_pane = None;
4454        }
4455        cx.notify();
4456    }
4457
4458    fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4459        if self._schedule_serialize.is_none() {
4460            self._schedule_serialize = Some(cx.spawn_in(window, |this, mut cx| async move {
4461                cx.background_executor()
4462                    .timer(Duration::from_millis(100))
4463                    .await;
4464                this.update_in(&mut cx, |this, window, cx| {
4465                    this.serialize_workspace_internal(window, cx).detach();
4466                    this._schedule_serialize.take();
4467                })
4468                .log_err();
4469            }));
4470        }
4471    }
4472
4473    fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
4474        let Some(database_id) = self.database_id() else {
4475            return Task::ready(());
4476        };
4477
4478        fn serialize_pane_handle(
4479            pane_handle: &Entity<Pane>,
4480            window: &mut Window,
4481            cx: &mut App,
4482        ) -> SerializedPane {
4483            let (items, active, pinned_count) = {
4484                let pane = pane_handle.read(cx);
4485                let active_item_id = pane.active_item().map(|item| item.item_id());
4486                (
4487                    pane.items()
4488                        .filter_map(|handle| {
4489                            let handle = handle.to_serializable_item_handle(cx)?;
4490
4491                            Some(SerializedItem {
4492                                kind: Arc::from(handle.serialized_item_kind()),
4493                                item_id: handle.item_id().as_u64(),
4494                                active: Some(handle.item_id()) == active_item_id,
4495                                preview: pane.is_active_preview_item(handle.item_id()),
4496                            })
4497                        })
4498                        .collect::<Vec<_>>(),
4499                    pane.has_focus(window, cx),
4500                    pane.pinned_count(),
4501                )
4502            };
4503
4504            SerializedPane::new(items, active, pinned_count)
4505        }
4506
4507        fn build_serialized_pane_group(
4508            pane_group: &Member,
4509            window: &mut Window,
4510            cx: &mut App,
4511        ) -> SerializedPaneGroup {
4512            match pane_group {
4513                Member::Axis(PaneAxis {
4514                    axis,
4515                    members,
4516                    flexes,
4517                    bounding_boxes: _,
4518                }) => SerializedPaneGroup::Group {
4519                    axis: SerializedAxis(*axis),
4520                    children: members
4521                        .iter()
4522                        .map(|member| build_serialized_pane_group(member, window, cx))
4523                        .collect::<Vec<_>>(),
4524                    flexes: Some(flexes.lock().clone()),
4525                },
4526                Member::Pane(pane_handle) => {
4527                    SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
4528                }
4529            }
4530        }
4531
4532        fn build_serialized_docks(
4533            this: &Workspace,
4534            window: &mut Window,
4535            cx: &mut App,
4536        ) -> DockStructure {
4537            let left_dock = this.left_dock.read(cx);
4538            let left_visible = left_dock.is_open();
4539            let left_active_panel = left_dock
4540                .active_panel()
4541                .map(|panel| panel.persistent_name().to_string());
4542            let left_dock_zoom = left_dock
4543                .active_panel()
4544                .map(|panel| panel.is_zoomed(window, cx))
4545                .unwrap_or(false);
4546
4547            let right_dock = this.right_dock.read(cx);
4548            let right_visible = right_dock.is_open();
4549            let right_active_panel = right_dock
4550                .active_panel()
4551                .map(|panel| panel.persistent_name().to_string());
4552            let right_dock_zoom = right_dock
4553                .active_panel()
4554                .map(|panel| panel.is_zoomed(window, cx))
4555                .unwrap_or(false);
4556
4557            let bottom_dock = this.bottom_dock.read(cx);
4558            let bottom_visible = bottom_dock.is_open();
4559            let bottom_active_panel = bottom_dock
4560                .active_panel()
4561                .map(|panel| panel.persistent_name().to_string());
4562            let bottom_dock_zoom = bottom_dock
4563                .active_panel()
4564                .map(|panel| panel.is_zoomed(window, cx))
4565                .unwrap_or(false);
4566
4567            DockStructure {
4568                left: DockData {
4569                    visible: left_visible,
4570                    active_panel: left_active_panel,
4571                    zoom: left_dock_zoom,
4572                },
4573                right: DockData {
4574                    visible: right_visible,
4575                    active_panel: right_active_panel,
4576                    zoom: right_dock_zoom,
4577                },
4578                bottom: DockData {
4579                    visible: bottom_visible,
4580                    active_panel: bottom_active_panel,
4581                    zoom: bottom_dock_zoom,
4582                },
4583            }
4584        }
4585
4586        let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4587            Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4588        } else if let Some(local_paths) = self.local_paths(cx) {
4589            if !local_paths.is_empty() {
4590                Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4591            } else {
4592                None
4593            }
4594        } else {
4595            None
4596        };
4597
4598        if let Some(location) = location {
4599            let center_group = build_serialized_pane_group(&self.center.root, window, cx);
4600            let docks = build_serialized_docks(self, window, cx);
4601            let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
4602            let serialized_workspace = SerializedWorkspace {
4603                id: database_id,
4604                location,
4605                center_group,
4606                window_bounds,
4607                display: Default::default(),
4608                docks,
4609                centered_layout: self.centered_layout,
4610                session_id: self.session_id.clone(),
4611                window_id: Some(window.window_handle().window_id().as_u64()),
4612            };
4613            return window.spawn(cx, |_| persistence::DB.save_workspace(serialized_workspace));
4614        }
4615        Task::ready(())
4616    }
4617
4618    async fn serialize_items(
4619        this: &WeakEntity<Self>,
4620        items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4621        cx: &mut AsyncWindowContext,
4622    ) -> Result<()> {
4623        const CHUNK_SIZE: usize = 200;
4624
4625        let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4626
4627        while let Some(items_received) = serializable_items.next().await {
4628            let unique_items =
4629                items_received
4630                    .into_iter()
4631                    .fold(HashMap::default(), |mut acc, item| {
4632                        acc.entry(item.item_id()).or_insert(item);
4633                        acc
4634                    });
4635
4636            // We use into_iter() here so that the references to the items are moved into
4637            // the tasks and not kept alive while we're sleeping.
4638            for (_, item) in unique_items.into_iter() {
4639                if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
4640                    item.serialize(workspace, false, window, cx)
4641                }) {
4642                    cx.background_executor()
4643                        .spawn(async move { task.await.log_err() })
4644                        .detach();
4645                }
4646            }
4647
4648            cx.background_executor()
4649                .timer(SERIALIZATION_THROTTLE_TIME)
4650                .await;
4651        }
4652
4653        Ok(())
4654    }
4655
4656    pub(crate) fn enqueue_item_serialization(
4657        &mut self,
4658        item: Box<dyn SerializableItemHandle>,
4659    ) -> Result<()> {
4660        self.serializable_items_tx
4661            .unbounded_send(item)
4662            .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4663    }
4664
4665    pub(crate) fn load_workspace(
4666        serialized_workspace: SerializedWorkspace,
4667        paths_to_open: Vec<Option<ProjectPath>>,
4668        window: &mut Window,
4669        cx: &mut Context<Workspace>,
4670    ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4671        cx.spawn_in(window, |workspace, mut cx| async move {
4672            let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4673
4674            let mut center_group = None;
4675            let mut center_items = None;
4676
4677            // Traverse the splits tree and add to things
4678            if let Some((group, active_pane, items)) = serialized_workspace
4679                .center_group
4680                .deserialize(
4681                    &project,
4682                    serialized_workspace.id,
4683                    workspace.clone(),
4684                    &mut cx,
4685                )
4686                .await
4687            {
4688                center_items = Some(items);
4689                center_group = Some((group, active_pane))
4690            }
4691
4692            let mut items_by_project_path = HashMap::default();
4693            let mut item_ids_by_kind = HashMap::default();
4694            let mut all_deserialized_items = Vec::default();
4695            cx.update(|_, cx| {
4696                for item in center_items.unwrap_or_default().into_iter().flatten() {
4697                    if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4698                        item_ids_by_kind
4699                            .entry(serializable_item_handle.serialized_item_kind())
4700                            .or_insert(Vec::new())
4701                            .push(item.item_id().as_u64() as ItemId);
4702                    }
4703
4704                    if let Some(project_path) = item.project_path(cx) {
4705                        items_by_project_path.insert(project_path, item.clone());
4706                    }
4707                    all_deserialized_items.push(item);
4708                }
4709            })?;
4710
4711            let opened_items = paths_to_open
4712                .into_iter()
4713                .map(|path_to_open| {
4714                    path_to_open
4715                        .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4716                })
4717                .collect::<Vec<_>>();
4718
4719            // Remove old panes from workspace panes list
4720            workspace.update_in(&mut cx, |workspace, window, cx| {
4721                if let Some((center_group, active_pane)) = center_group {
4722                    workspace.remove_panes(workspace.center.root.clone(), window, cx);
4723
4724                    // Swap workspace center group
4725                    workspace.center = PaneGroup::with_root(center_group);
4726                    if let Some(active_pane) = active_pane {
4727                        workspace.set_active_pane(&active_pane, window, cx);
4728                        cx.focus_self(window);
4729                    } else {
4730                        workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
4731                    }
4732                }
4733
4734                let docks = serialized_workspace.docks;
4735
4736                for (dock, serialized_dock) in [
4737                    (&mut workspace.right_dock, docks.right),
4738                    (&mut workspace.left_dock, docks.left),
4739                    (&mut workspace.bottom_dock, docks.bottom),
4740                ]
4741                .iter_mut()
4742                {
4743                    dock.update(cx, |dock, cx| {
4744                        dock.serialized_dock = Some(serialized_dock.clone());
4745                        dock.restore_state(window, cx);
4746                    });
4747                }
4748
4749                cx.notify();
4750            })?;
4751
4752            // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4753            // after loading the items, we might have different items and in order to avoid
4754            // the database filling up, we delete items that haven't been loaded now.
4755            //
4756            // The items that have been loaded, have been saved after they've been added to the workspace.
4757            let clean_up_tasks = workspace.update_in(&mut cx, |_, window, cx| {
4758                item_ids_by_kind
4759                    .into_iter()
4760                    .map(|(item_kind, loaded_items)| {
4761                        SerializableItemRegistry::cleanup(
4762                            item_kind,
4763                            serialized_workspace.id,
4764                            loaded_items,
4765                            window,
4766                            cx,
4767                        )
4768                        .log_err()
4769                    })
4770                    .collect::<Vec<_>>()
4771            })?;
4772
4773            futures::future::join_all(clean_up_tasks).await;
4774
4775            workspace
4776                .update_in(&mut cx, |workspace, window, cx| {
4777                    // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4778                    workspace.serialize_workspace_internal(window, cx).detach();
4779
4780                    // Ensure that we mark the window as edited if we did load dirty items
4781                    workspace.update_window_edited(window, cx);
4782                })
4783                .ok();
4784
4785            Ok(opened_items)
4786        })
4787    }
4788
4789    fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
4790        self.add_workspace_actions_listeners(div, window, cx)
4791            .on_action(cx.listener(Self::close_inactive_items_and_panes))
4792            .on_action(cx.listener(Self::close_all_items_and_panes))
4793            .on_action(cx.listener(Self::save_all))
4794            .on_action(cx.listener(Self::send_keystrokes))
4795            .on_action(cx.listener(Self::add_folder_to_project))
4796            .on_action(cx.listener(Self::follow_next_collaborator))
4797            .on_action(cx.listener(Self::close_window))
4798            .on_action(cx.listener(Self::activate_pane_at_index))
4799            .on_action(cx.listener(Self::move_item_to_pane_at_index))
4800            .on_action(cx.listener(Self::move_focused_panel_to_next_position))
4801            .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
4802                let pane = workspace.active_pane().clone();
4803                workspace.unfollow_in_pane(&pane, window, cx);
4804            }))
4805            .on_action(cx.listener(|workspace, action: &Save, window, cx| {
4806                workspace
4807                    .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
4808                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4809            }))
4810            .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
4811                workspace
4812                    .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
4813                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4814            }))
4815            .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
4816                workspace
4817                    .save_active_item(SaveIntent::SaveAs, window, cx)
4818                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4819            }))
4820            .on_action(
4821                cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
4822                    workspace.activate_previous_pane(window, cx)
4823                }),
4824            )
4825            .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4826                workspace.activate_next_pane(window, cx)
4827            }))
4828            .on_action(
4829                cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
4830                    workspace.activate_next_window(cx)
4831                }),
4832            )
4833            .on_action(
4834                cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
4835                    workspace.activate_previous_window(cx)
4836                }),
4837            )
4838            .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
4839                workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
4840            }))
4841            .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
4842                workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
4843            }))
4844            .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
4845                workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
4846            }))
4847            .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
4848                workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
4849            }))
4850            .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4851                workspace.activate_next_pane(window, cx)
4852            }))
4853            .on_action(cx.listener(
4854                |workspace, action: &MoveItemToPaneInDirection, window, cx| {
4855                    workspace.move_item_to_pane_in_direction(action, window, cx)
4856                },
4857            ))
4858            .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
4859                workspace.swap_pane_in_direction(SplitDirection::Left, cx)
4860            }))
4861            .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
4862                workspace.swap_pane_in_direction(SplitDirection::Right, cx)
4863            }))
4864            .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
4865                workspace.swap_pane_in_direction(SplitDirection::Up, cx)
4866            }))
4867            .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
4868                workspace.swap_pane_in_direction(SplitDirection::Down, cx)
4869            }))
4870            .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
4871                this.toggle_dock(DockPosition::Left, window, cx);
4872            }))
4873            .on_action(cx.listener(
4874                |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
4875                    workspace.toggle_dock(DockPosition::Right, window, cx);
4876                },
4877            ))
4878            .on_action(cx.listener(
4879                |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
4880                    workspace.toggle_dock(DockPosition::Bottom, window, cx);
4881                },
4882            ))
4883            .on_action(
4884                cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
4885                    workspace.close_all_docks(window, cx);
4886                }),
4887            )
4888            .on_action(cx.listener(
4889                |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
4890                    workspace.clear_all_notifications(cx);
4891                },
4892            ))
4893            .on_action(cx.listener(
4894                |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
4895                    workspace.reopen_closed_item(window, cx).detach();
4896                },
4897            ))
4898            .on_action(cx.listener(Workspace::toggle_centered_layout))
4899    }
4900
4901    #[cfg(any(test, feature = "test-support"))]
4902    pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
4903        use node_runtime::NodeRuntime;
4904        use session::Session;
4905
4906        let client = project.read(cx).client();
4907        let user_store = project.read(cx).user_store();
4908
4909        let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
4910        let session = cx.new(|cx| AppSession::new(Session::test(), cx));
4911        window.activate_window();
4912        let app_state = Arc::new(AppState {
4913            languages: project.read(cx).languages().clone(),
4914            workspace_store,
4915            client,
4916            user_store,
4917            fs: project.read(cx).fs().clone(),
4918            build_window_options: |_, _| Default::default(),
4919            node_runtime: NodeRuntime::unavailable(),
4920            session,
4921        });
4922        let workspace = Self::new(Default::default(), project, app_state, window, cx);
4923        workspace
4924            .active_pane
4925            .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4926        workspace
4927    }
4928
4929    pub fn register_action<A: Action>(
4930        &mut self,
4931        callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
4932    ) -> &mut Self {
4933        let callback = Arc::new(callback);
4934
4935        self.workspace_actions.push(Box::new(move |div, _, cx| {
4936            let callback = callback.clone();
4937            div.on_action(cx.listener(move |workspace, event, window, cx| {
4938                (callback.clone())(workspace, event, window, cx)
4939            }))
4940        }));
4941        self
4942    }
4943
4944    fn add_workspace_actions_listeners(
4945        &self,
4946        mut div: Div,
4947        window: &mut Window,
4948        cx: &mut Context<Self>,
4949    ) -> Div {
4950        for action in self.workspace_actions.iter() {
4951            div = (action)(div, window, cx)
4952        }
4953        div
4954    }
4955
4956    pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
4957        self.modal_layer.read(cx).has_active_modal()
4958    }
4959
4960    pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
4961        self.modal_layer.read(cx).active_modal()
4962    }
4963
4964    pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
4965    where
4966        B: FnOnce(&mut Window, &mut Context<V>) -> V,
4967    {
4968        self.modal_layer.update(cx, |modal_layer, cx| {
4969            modal_layer.toggle_modal(window, cx, build)
4970        })
4971    }
4972
4973    pub fn toggle_centered_layout(
4974        &mut self,
4975        _: &ToggleCenteredLayout,
4976        _: &mut Window,
4977        cx: &mut Context<Self>,
4978    ) {
4979        self.centered_layout = !self.centered_layout;
4980        if let Some(database_id) = self.database_id() {
4981            cx.background_executor()
4982                .spawn(DB.set_centered_layout(database_id, self.centered_layout))
4983                .detach_and_log_err(cx);
4984        }
4985        cx.notify();
4986    }
4987
4988    fn adjust_padding(padding: Option<f32>) -> f32 {
4989        padding
4990            .unwrap_or(Self::DEFAULT_PADDING)
4991            .clamp(0.0, Self::MAX_PADDING)
4992    }
4993
4994    fn render_dock(
4995        &self,
4996        position: DockPosition,
4997        dock: &Entity<Dock>,
4998        window: &mut Window,
4999        cx: &mut App,
5000    ) -> Option<Div> {
5001        if self.zoomed_position == Some(position) {
5002            return None;
5003        }
5004
5005        let leader_border = dock.read(cx).active_panel().and_then(|panel| {
5006            let pane = panel.pane(cx)?;
5007            let follower_states = &self.follower_states;
5008            leader_border_for_pane(follower_states, &pane, window, cx)
5009        });
5010
5011        Some(
5012            div()
5013                .flex()
5014                .flex_none()
5015                .overflow_hidden()
5016                .child(dock.clone())
5017                .children(leader_border),
5018        )
5019    }
5020
5021    pub fn for_window(window: &mut Window, _: &mut App) -> Option<Entity<Workspace>> {
5022        window.root().flatten()
5023    }
5024
5025    pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
5026        self.zoomed.as_ref()
5027    }
5028
5029    pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
5030        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5031            return;
5032        };
5033        let windows = cx.windows();
5034        let Some(next_window) = windows
5035            .iter()
5036            .cycle()
5037            .skip_while(|window| window.window_id() != current_window_id)
5038            .nth(1)
5039        else {
5040            return;
5041        };
5042        next_window
5043            .update(cx, |_, window, _| window.activate_window())
5044            .ok();
5045    }
5046
5047    pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
5048        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5049            return;
5050        };
5051        let windows = cx.windows();
5052        let Some(prev_window) = windows
5053            .iter()
5054            .rev()
5055            .cycle()
5056            .skip_while(|window| window.window_id() != current_window_id)
5057            .nth(1)
5058        else {
5059            return;
5060        };
5061        prev_window
5062            .update(cx, |_, window, _| window.activate_window())
5063            .ok();
5064    }
5065}
5066
5067fn leader_border_for_pane(
5068    follower_states: &HashMap<PeerId, FollowerState>,
5069    pane: &Entity<Pane>,
5070    _: &Window,
5071    cx: &App,
5072) -> Option<Div> {
5073    let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
5074        if state.pane() == pane {
5075            Some((*leader_id, state))
5076        } else {
5077            None
5078        }
5079    })?;
5080
5081    let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
5082    let leader = room.remote_participant_for_peer_id(leader_id)?;
5083
5084    let mut leader_color = cx
5085        .theme()
5086        .players()
5087        .color_for_participant(leader.participant_index.0)
5088        .cursor;
5089    leader_color.fade_out(0.3);
5090    Some(
5091        div()
5092            .absolute()
5093            .size_full()
5094            .left_0()
5095            .top_0()
5096            .border_2()
5097            .border_color(leader_color),
5098    )
5099}
5100
5101fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
5102    ZED_WINDOW_POSITION
5103        .zip(*ZED_WINDOW_SIZE)
5104        .map(|(position, size)| Bounds {
5105            origin: position,
5106            size,
5107        })
5108}
5109
5110fn open_items(
5111    serialized_workspace: Option<SerializedWorkspace>,
5112    mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
5113    window: &mut Window,
5114    cx: &mut Context<Workspace>,
5115) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
5116    let restored_items = serialized_workspace.map(|serialized_workspace| {
5117        Workspace::load_workspace(
5118            serialized_workspace,
5119            project_paths_to_open
5120                .iter()
5121                .map(|(_, project_path)| project_path)
5122                .cloned()
5123                .collect(),
5124            window,
5125            cx,
5126        )
5127    });
5128
5129    cx.spawn_in(window, |workspace, mut cx| async move {
5130        let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
5131
5132        if let Some(restored_items) = restored_items {
5133            let restored_items = restored_items.await?;
5134
5135            let restored_project_paths = restored_items
5136                .iter()
5137                .filter_map(|item| {
5138                    cx.update(|_, cx| item.as_ref()?.project_path(cx))
5139                        .ok()
5140                        .flatten()
5141                })
5142                .collect::<HashSet<_>>();
5143
5144            for restored_item in restored_items {
5145                opened_items.push(restored_item.map(Ok));
5146            }
5147
5148            project_paths_to_open
5149                .iter_mut()
5150                .for_each(|(_, project_path)| {
5151                    if let Some(project_path_to_open) = project_path {
5152                        if restored_project_paths.contains(project_path_to_open) {
5153                            *project_path = None;
5154                        }
5155                    }
5156                });
5157        } else {
5158            for _ in 0..project_paths_to_open.len() {
5159                opened_items.push(None);
5160            }
5161        }
5162        assert!(opened_items.len() == project_paths_to_open.len());
5163
5164        let tasks =
5165            project_paths_to_open
5166                .into_iter()
5167                .enumerate()
5168                .map(|(ix, (abs_path, project_path))| {
5169                    let workspace = workspace.clone();
5170                    cx.spawn(|mut cx| async move {
5171                        let file_project_path = project_path?;
5172                        let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
5173                            workspace.project().update(cx, |project, cx| {
5174                                project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
5175                            })
5176                        });
5177
5178                        // We only want to open file paths here. If one of the items
5179                        // here is a directory, it was already opened further above
5180                        // with a `find_or_create_worktree`.
5181                        if let Ok(task) = abs_path_task {
5182                            if task.await.map_or(true, |p| p.is_file()) {
5183                                return Some((
5184                                    ix,
5185                                    workspace
5186                                        .update_in(&mut cx, |workspace, window, cx| {
5187                                            workspace.open_path(
5188                                                file_project_path,
5189                                                None,
5190                                                true,
5191                                                window,
5192                                                cx,
5193                                            )
5194                                        })
5195                                        .log_err()?
5196                                        .await,
5197                                ));
5198                            }
5199                        }
5200                        None
5201                    })
5202                });
5203
5204        let tasks = tasks.collect::<Vec<_>>();
5205
5206        let tasks = futures::future::join_all(tasks);
5207        for (ix, path_open_result) in tasks.await.into_iter().flatten() {
5208            opened_items[ix] = Some(path_open_result);
5209        }
5210
5211        Ok(opened_items)
5212    })
5213}
5214
5215enum ActivateInDirectionTarget {
5216    Pane(Entity<Pane>),
5217    Dock(Entity<Dock>),
5218}
5219
5220fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
5221    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";
5222
5223    workspace
5224        .update(cx, |workspace, _, cx| {
5225            if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
5226                struct DatabaseFailedNotification;
5227
5228                workspace.show_notification(
5229                    NotificationId::unique::<DatabaseFailedNotification>(),
5230                    cx,
5231                    |cx| {
5232                        cx.new(|_| {
5233                            MessageNotification::new("Failed to load the database file.")
5234                                .primary_message("File an Issue")
5235                                .primary_icon(IconName::Plus)
5236                                .primary_on_click(|_window, cx| cx.open_url(REPORT_ISSUE_URL))
5237                        })
5238                    },
5239                );
5240            }
5241        })
5242        .log_err();
5243}
5244
5245impl Focusable for Workspace {
5246    fn focus_handle(&self, cx: &App) -> FocusHandle {
5247        self.active_pane.focus_handle(cx)
5248    }
5249}
5250
5251#[derive(Clone)]
5252struct DraggedDock(DockPosition);
5253
5254impl Render for DraggedDock {
5255    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5256        gpui::Empty
5257    }
5258}
5259
5260impl Render for Workspace {
5261    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5262        let mut context = KeyContext::new_with_defaults();
5263        context.add("Workspace");
5264        context.set("keyboard_layout", cx.keyboard_layout().clone());
5265        let centered_layout = self.centered_layout
5266            && self.center.panes().len() == 1
5267            && self.active_item(cx).is_some();
5268        let render_padding = |size| {
5269            (size > 0.0).then(|| {
5270                div()
5271                    .h_full()
5272                    .w(relative(size))
5273                    .bg(cx.theme().colors().editor_background)
5274                    .border_color(cx.theme().colors().pane_group_border)
5275            })
5276        };
5277        let paddings = if centered_layout {
5278            let settings = WorkspaceSettings::get_global(cx).centered_layout;
5279            (
5280                render_padding(Self::adjust_padding(settings.left_padding)),
5281                render_padding(Self::adjust_padding(settings.right_padding)),
5282            )
5283        } else {
5284            (None, None)
5285        };
5286        let ui_font = theme::setup_ui_font(window, cx);
5287
5288        let theme = cx.theme().clone();
5289        let colors = theme.colors();
5290
5291        client_side_decorations(
5292            self.actions(div(), window, cx)
5293                .key_context(context)
5294                .relative()
5295                .size_full()
5296                .flex()
5297                .flex_col()
5298                .font(ui_font)
5299                .gap_0()
5300                .justify_start()
5301                .items_start()
5302                .text_color(colors.text)
5303                .overflow_hidden()
5304                .children(self.titlebar_item.clone())
5305                .child(
5306                    div()
5307                        .size_full()
5308                        .relative()
5309                        .flex_1()
5310                        .flex()
5311                        .flex_col()
5312                        .child(
5313                            div()
5314                                .id("workspace")
5315                                .bg(colors.background)
5316                                .relative()
5317                                .flex_1()
5318                                .w_full()
5319                                .flex()
5320                                .flex_col()
5321                                .overflow_hidden()
5322                                .border_t_1()
5323                                .border_b_1()
5324                                .border_color(colors.border)
5325                                .child({
5326                                    let this = cx.entity().clone();
5327                                    canvas(
5328                                        move |bounds, window, cx| {
5329                                            this.update(cx, |this, cx| {
5330                                                let bounds_changed = this.bounds != bounds;
5331                                                this.bounds = bounds;
5332
5333                                                if bounds_changed {
5334                                                    this.left_dock.update(cx, |dock, cx| {
5335                                                        dock.clamp_panel_size(
5336                                                            bounds.size.width,
5337                                                            window,
5338                                                            cx,
5339                                                        )
5340                                                    });
5341
5342                                                    this.right_dock.update(cx, |dock, cx| {
5343                                                        dock.clamp_panel_size(
5344                                                            bounds.size.width,
5345                                                            window,
5346                                                            cx,
5347                                                        )
5348                                                    });
5349
5350                                                    this.bottom_dock.update(cx, |dock, cx| {
5351                                                        dock.clamp_panel_size(
5352                                                            bounds.size.height,
5353                                                            window,
5354                                                            cx,
5355                                                        )
5356                                                    });
5357                                                }
5358                                            })
5359                                        },
5360                                        |_, _, _, _| {},
5361                                    )
5362                                    .absolute()
5363                                    .size_full()
5364                                })
5365                                .when(self.zoomed.is_none(), |this| {
5366                                    this.on_drag_move(cx.listener(
5367                                        move |workspace,
5368                                              e: &DragMoveEvent<DraggedDock>,
5369                                              window,
5370                                              cx| {
5371                                            if workspace.previous_dock_drag_coordinates
5372                                                != Some(e.event.position)
5373                                            {
5374                                                workspace.previous_dock_drag_coordinates =
5375                                                    Some(e.event.position);
5376                                                match e.drag(cx).0 {
5377                                                    DockPosition::Left => {
5378                                                        resize_left_dock(
5379                                                            e.event.position.x
5380                                                                - workspace.bounds.left(),
5381                                                            workspace,
5382                                                            window,
5383                                                            cx,
5384                                                        );
5385                                                    }
5386                                                    DockPosition::Right => {
5387                                                        resize_right_dock(
5388                                                            workspace.bounds.right()
5389                                                                - e.event.position.x,
5390                                                            workspace,
5391                                                            window,
5392                                                            cx,
5393                                                        );
5394                                                    }
5395                                                    DockPosition::Bottom => {
5396                                                        resize_bottom_dock(
5397                                                            workspace.bounds.bottom()
5398                                                                - e.event.position.y,
5399                                                            workspace,
5400                                                            window,
5401                                                            cx,
5402                                                        );
5403                                                    }
5404                                                };
5405                                                workspace.serialize_workspace(window, cx);
5406                                            }
5407                                        },
5408                                    ))
5409                                })
5410                                .child(
5411                                    div()
5412                                        .flex()
5413                                        .flex_row()
5414                                        .h_full()
5415                                        // Left Dock
5416                                        .children(self.render_dock(
5417                                            DockPosition::Left,
5418                                            &self.left_dock,
5419                                            window,
5420                                            cx,
5421                                        ))
5422                                        // Panes
5423                                        .child(
5424                                            div()
5425                                                .flex()
5426                                                .flex_col()
5427                                                .flex_1()
5428                                                .overflow_hidden()
5429                                                .child(
5430                                                    h_flex()
5431                                                        .flex_1()
5432                                                        .when_some(paddings.0, |this, p| {
5433                                                            this.child(p.border_r_1())
5434                                                        })
5435                                                        .child(self.center.render(
5436                                                            &self.project,
5437                                                            &self.follower_states,
5438                                                            self.active_call(),
5439                                                            &self.active_pane,
5440                                                            self.zoomed.as_ref(),
5441                                                            &self.app_state,
5442                                                            window,
5443                                                            cx,
5444                                                        ))
5445                                                        .when_some(paddings.1, |this, p| {
5446                                                            this.child(p.border_l_1())
5447                                                        }),
5448                                                )
5449                                                .children(self.render_dock(
5450                                                    DockPosition::Bottom,
5451                                                    &self.bottom_dock,
5452                                                    window,
5453                                                    cx,
5454                                                )),
5455                                        )
5456                                        // Right Dock
5457                                        .children(self.render_dock(
5458                                            DockPosition::Right,
5459                                            &self.right_dock,
5460                                            window,
5461                                            cx,
5462                                        )),
5463                                )
5464                                .children(self.zoomed.as_ref().and_then(|view| {
5465                                    let zoomed_view = view.upgrade()?;
5466                                    let div = div()
5467                                        .occlude()
5468                                        .absolute()
5469                                        .overflow_hidden()
5470                                        .border_color(colors.border)
5471                                        .bg(colors.background)
5472                                        .child(zoomed_view)
5473                                        .inset_0()
5474                                        .shadow_lg();
5475
5476                                    Some(match self.zoomed_position {
5477                                        Some(DockPosition::Left) => div.right_2().border_r_1(),
5478                                        Some(DockPosition::Right) => div.left_2().border_l_1(),
5479                                        Some(DockPosition::Bottom) => div.top_2().border_t_1(),
5480                                        None => {
5481                                            div.top_2().bottom_2().left_2().right_2().border_1()
5482                                        }
5483                                    })
5484                                }))
5485                                .children(self.render_notifications(window, cx)),
5486                        )
5487                        .child(self.status_bar.clone())
5488                        .child(self.modal_layer.clone()),
5489                ),
5490            window,
5491            cx,
5492        )
5493    }
5494}
5495
5496fn resize_bottom_dock(
5497    new_size: Pixels,
5498    workspace: &mut Workspace,
5499    window: &mut Window,
5500    cx: &mut App,
5501) {
5502    let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
5503    workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5504        bottom_dock.resize_active_panel(Some(size), window, cx);
5505    });
5506}
5507
5508fn resize_right_dock(
5509    new_size: Pixels,
5510    workspace: &mut Workspace,
5511    window: &mut Window,
5512    cx: &mut App,
5513) {
5514    let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
5515    workspace.right_dock.update(cx, |right_dock, cx| {
5516        right_dock.resize_active_panel(Some(size), window, cx);
5517    });
5518}
5519
5520fn resize_left_dock(
5521    new_size: Pixels,
5522    workspace: &mut Workspace,
5523    window: &mut Window,
5524    cx: &mut App,
5525) {
5526    let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5527
5528    workspace.left_dock.update(cx, |left_dock, cx| {
5529        left_dock.resize_active_panel(Some(size), window, cx);
5530    });
5531}
5532
5533impl WorkspaceStore {
5534    pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
5535        Self {
5536            workspaces: Default::default(),
5537            _subscriptions: vec![
5538                client.add_request_handler(cx.weak_entity(), Self::handle_follow),
5539                client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
5540            ],
5541            client,
5542        }
5543    }
5544
5545    pub fn update_followers(
5546        &self,
5547        project_id: Option<u64>,
5548        update: proto::update_followers::Variant,
5549        cx: &App,
5550    ) -> Option<()> {
5551        let active_call = ActiveCall::try_global(cx)?;
5552        let room_id = active_call.read(cx).room()?.read(cx).id();
5553        self.client
5554            .send(proto::UpdateFollowers {
5555                room_id,
5556                project_id,
5557                variant: Some(update),
5558            })
5559            .log_err()
5560    }
5561
5562    pub async fn handle_follow(
5563        this: Entity<Self>,
5564        envelope: TypedEnvelope<proto::Follow>,
5565        mut cx: AsyncApp,
5566    ) -> Result<proto::FollowResponse> {
5567        this.update(&mut cx, |this, cx| {
5568            let follower = Follower {
5569                project_id: envelope.payload.project_id,
5570                peer_id: envelope.original_sender_id()?,
5571            };
5572
5573            let mut response = proto::FollowResponse::default();
5574            this.workspaces.retain(|workspace| {
5575                workspace
5576                    .update(cx, |workspace, window, cx| {
5577                        let handler_response =
5578                            workspace.handle_follow(follower.project_id, window, cx);
5579                        if let Some(active_view) = handler_response.active_view.clone() {
5580                            if workspace.project.read(cx).remote_id() == follower.project_id {
5581                                response.active_view = Some(active_view)
5582                            }
5583                        }
5584                    })
5585                    .is_ok()
5586            });
5587
5588            Ok(response)
5589        })?
5590    }
5591
5592    async fn handle_update_followers(
5593        this: Entity<Self>,
5594        envelope: TypedEnvelope<proto::UpdateFollowers>,
5595        mut cx: AsyncApp,
5596    ) -> Result<()> {
5597        let leader_id = envelope.original_sender_id()?;
5598        let update = envelope.payload;
5599
5600        this.update(&mut cx, |this, cx| {
5601            this.workspaces.retain(|workspace| {
5602                workspace
5603                    .update(cx, |workspace, window, cx| {
5604                        let project_id = workspace.project.read(cx).remote_id();
5605                        if update.project_id != project_id && update.project_id.is_some() {
5606                            return;
5607                        }
5608                        workspace.handle_update_followers(leader_id, update.clone(), window, cx);
5609                    })
5610                    .is_ok()
5611            });
5612            Ok(())
5613        })?
5614    }
5615}
5616
5617impl ViewId {
5618    pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5619        Ok(Self {
5620            creator: message
5621                .creator
5622                .ok_or_else(|| anyhow!("creator is missing"))?,
5623            id: message.id,
5624        })
5625    }
5626
5627    pub(crate) fn to_proto(self) -> proto::ViewId {
5628        proto::ViewId {
5629            creator: Some(self.creator),
5630            id: self.id,
5631        }
5632    }
5633}
5634
5635impl FollowerState {
5636    fn pane(&self) -> &Entity<Pane> {
5637        self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5638    }
5639}
5640
5641pub trait WorkspaceHandle {
5642    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
5643}
5644
5645impl WorkspaceHandle for Entity<Workspace> {
5646    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
5647        self.read(cx)
5648            .worktrees(cx)
5649            .flat_map(|worktree| {
5650                let worktree_id = worktree.read(cx).id();
5651                worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5652                    worktree_id,
5653                    path: f.path.clone(),
5654                })
5655            })
5656            .collect::<Vec<_>>()
5657    }
5658}
5659
5660impl std::fmt::Debug for OpenPaths {
5661    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5662        f.debug_struct("OpenPaths")
5663            .field("paths", &self.paths)
5664            .finish()
5665    }
5666}
5667
5668pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5669    DB.last_workspace().await.log_err().flatten()
5670}
5671
5672pub fn last_session_workspace_locations(
5673    last_session_id: &str,
5674    last_session_window_stack: Option<Vec<WindowId>>,
5675) -> Option<Vec<SerializedWorkspaceLocation>> {
5676    DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5677        .log_err()
5678}
5679
5680actions!(collab, [OpenChannelNotes]);
5681actions!(zed, [OpenLog]);
5682
5683async fn join_channel_internal(
5684    channel_id: ChannelId,
5685    app_state: &Arc<AppState>,
5686    requesting_window: Option<WindowHandle<Workspace>>,
5687    active_call: &Entity<ActiveCall>,
5688    cx: &mut AsyncApp,
5689) -> Result<bool> {
5690    let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5691        let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5692            return (false, None);
5693        };
5694
5695        let already_in_channel = room.channel_id() == Some(channel_id);
5696        let should_prompt = room.is_sharing_project()
5697            && !room.remote_participants().is_empty()
5698            && !already_in_channel;
5699        let open_room = if already_in_channel {
5700            active_call.room().cloned()
5701        } else {
5702            None
5703        };
5704        (should_prompt, open_room)
5705    })?;
5706
5707    if let Some(room) = open_room {
5708        let task = room.update(cx, |room, cx| {
5709            if let Some((project, host)) = room.most_active_project(cx) {
5710                return Some(join_in_room_project(project, host, app_state.clone(), cx));
5711            }
5712
5713            None
5714        })?;
5715        if let Some(task) = task {
5716            task.await?;
5717        }
5718        return anyhow::Ok(true);
5719    }
5720
5721    if should_prompt {
5722        if let Some(workspace) = requesting_window {
5723            let answer = workspace
5724                .update(cx, |_, window, cx| {
5725                    window.prompt(
5726                        PromptLevel::Warning,
5727                        "Do you want to switch channels?",
5728                        Some("Leaving this call will unshare your current project."),
5729                        &["Yes, Join Channel", "Cancel"],
5730                        cx,
5731                    )
5732                })?
5733                .await;
5734
5735            if answer == Ok(1) {
5736                return Ok(false);
5737            }
5738        } else {
5739            return Ok(false); // unreachable!() hopefully
5740        }
5741    }
5742
5743    let client = cx.update(|cx| active_call.read(cx).client())?;
5744
5745    let mut client_status = client.status();
5746
5747    // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5748    'outer: loop {
5749        let Some(status) = client_status.recv().await else {
5750            return Err(anyhow!("error connecting"));
5751        };
5752
5753        match status {
5754            Status::Connecting
5755            | Status::Authenticating
5756            | Status::Reconnecting
5757            | Status::Reauthenticating => continue,
5758            Status::Connected { .. } => break 'outer,
5759            Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5760            Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5761            Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5762                return Err(ErrorCode::Disconnected.into());
5763            }
5764        }
5765    }
5766
5767    let room = active_call
5768        .update(cx, |active_call, cx| {
5769            active_call.join_channel(channel_id, cx)
5770        })?
5771        .await?;
5772
5773    let Some(room) = room else {
5774        return anyhow::Ok(true);
5775    };
5776
5777    room.update(cx, |room, _| room.room_update_completed())?
5778        .await;
5779
5780    let task = room.update(cx, |room, cx| {
5781        if let Some((project, host)) = room.most_active_project(cx) {
5782            return Some(join_in_room_project(project, host, app_state.clone(), cx));
5783        }
5784
5785        // If you are the first to join a channel, see if you should share your project.
5786        if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5787            if let Some(workspace) = requesting_window {
5788                let project = workspace.update(cx, |workspace, _, cx| {
5789                    let project = workspace.project.read(cx);
5790
5791                    if !CallSettings::get_global(cx).share_on_join {
5792                        return None;
5793                    }
5794
5795                    if (project.is_local() || project.is_via_ssh())
5796                        && project.visible_worktrees(cx).any(|tree| {
5797                            tree.read(cx)
5798                                .root_entry()
5799                                .map_or(false, |entry| entry.is_dir())
5800                        })
5801                    {
5802                        Some(workspace.project.clone())
5803                    } else {
5804                        None
5805                    }
5806                });
5807                if let Ok(Some(project)) = project {
5808                    return Some(cx.spawn(|room, mut cx| async move {
5809                        room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5810                            .await?;
5811                        Ok(())
5812                    }));
5813                }
5814            }
5815        }
5816
5817        None
5818    })?;
5819    if let Some(task) = task {
5820        task.await?;
5821        return anyhow::Ok(true);
5822    }
5823    anyhow::Ok(false)
5824}
5825
5826pub fn join_channel(
5827    channel_id: ChannelId,
5828    app_state: Arc<AppState>,
5829    requesting_window: Option<WindowHandle<Workspace>>,
5830    cx: &mut App,
5831) -> Task<Result<()>> {
5832    let active_call = ActiveCall::global(cx);
5833    cx.spawn(|mut cx| async move {
5834        let result = join_channel_internal(
5835            channel_id,
5836            &app_state,
5837            requesting_window,
5838            &active_call,
5839            &mut cx,
5840        )
5841            .await;
5842
5843        // join channel succeeded, and opened a window
5844        if matches!(result, Ok(true)) {
5845            return anyhow::Ok(());
5846        }
5847
5848        // find an existing workspace to focus and show call controls
5849        let mut active_window =
5850            requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5851        if active_window.is_none() {
5852            // no open workspaces, make one to show the error in (blergh)
5853            let (window_handle, _) = cx
5854                .update(|cx| {
5855                    Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5856                })?
5857                .await?;
5858
5859            if result.is_ok() {
5860                cx.update(|cx| {
5861                    cx.dispatch_action(&OpenChannelNotes);
5862                }).log_err();
5863            }
5864
5865            active_window = Some(window_handle);
5866        }
5867
5868        if let Err(err) = result {
5869            log::error!("failed to join channel: {}", err);
5870            if let Some(active_window) = active_window {
5871                active_window
5872                    .update(&mut cx, |_, window, cx| {
5873                        let detail: SharedString = match err.error_code() {
5874                            ErrorCode::SignedOut => {
5875                                "Please sign in to continue.".into()
5876                            }
5877                            ErrorCode::UpgradeRequired => {
5878                                "Your are running an unsupported version of Zed. Please update to continue.".into()
5879                            }
5880                            ErrorCode::NoSuchChannel => {
5881                                "No matching channel was found. Please check the link and try again.".into()
5882                            }
5883                            ErrorCode::Forbidden => {
5884                                "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5885                            }
5886                            ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5887                            _ => format!("{}\n\nPlease try again.", err).into(),
5888                        };
5889                        window.prompt(
5890                            PromptLevel::Critical,
5891                            "Failed to join channel",
5892                            Some(&detail),
5893                            &["Ok"],
5894                        cx)
5895                    })?
5896                    .await
5897                    .ok();
5898            }
5899        }
5900
5901        // return ok, we showed the error to the user.
5902        anyhow::Ok(())
5903    })
5904}
5905
5906pub async fn get_any_active_workspace(
5907    app_state: Arc<AppState>,
5908    mut cx: AsyncApp,
5909) -> anyhow::Result<WindowHandle<Workspace>> {
5910    // find an existing workspace to focus and show call controls
5911    let active_window = activate_any_workspace_window(&mut cx);
5912    if active_window.is_none() {
5913        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5914            .await?;
5915    }
5916    activate_any_workspace_window(&mut cx).context("could not open zed")
5917}
5918
5919fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
5920    cx.update(|cx| {
5921        if let Some(workspace_window) = cx
5922            .active_window()
5923            .and_then(|window| window.downcast::<Workspace>())
5924        {
5925            return Some(workspace_window);
5926        }
5927
5928        for window in cx.windows() {
5929            if let Some(workspace_window) = window.downcast::<Workspace>() {
5930                workspace_window
5931                    .update(cx, |_, window, _| window.activate_window())
5932                    .ok();
5933                return Some(workspace_window);
5934            }
5935        }
5936        None
5937    })
5938    .ok()
5939    .flatten()
5940}
5941
5942pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
5943    cx.windows()
5944        .into_iter()
5945        .filter_map(|window| window.downcast::<Workspace>())
5946        .filter(|workspace| {
5947            workspace
5948                .read(cx)
5949                .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5950        })
5951        .collect()
5952}
5953
5954#[derive(Default)]
5955pub struct OpenOptions {
5956    pub open_new_workspace: Option<bool>,
5957    pub replace_window: Option<WindowHandle<Workspace>>,
5958    pub env: Option<HashMap<String, String>>,
5959}
5960
5961#[allow(clippy::type_complexity)]
5962pub fn open_paths(
5963    abs_paths: &[PathBuf],
5964    app_state: Arc<AppState>,
5965    open_options: OpenOptions,
5966    cx: &mut App,
5967) -> Task<
5968    anyhow::Result<(
5969        WindowHandle<Workspace>,
5970        Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5971    )>,
5972> {
5973    let abs_paths = abs_paths.to_vec();
5974    let mut existing = None;
5975    let mut best_match = None;
5976    let mut open_visible = OpenVisible::All;
5977
5978    if open_options.open_new_workspace != Some(true) {
5979        for window in local_workspace_windows(cx) {
5980            if let Ok(workspace) = window.read(cx) {
5981                let m = workspace
5982                    .project
5983                    .read(cx)
5984                    .visibility_for_paths(&abs_paths, cx);
5985                if m > best_match {
5986                    existing = Some(window);
5987                    best_match = m;
5988                } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5989                    existing = Some(window)
5990                }
5991            }
5992        }
5993    }
5994
5995    cx.spawn(move |mut cx| async move {
5996        if open_options.open_new_workspace.is_none() && existing.is_none() {
5997            let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5998            if futures::future::join_all(all_files)
5999                .await
6000                .into_iter()
6001                .filter_map(|result| result.ok().flatten())
6002                .all(|file| !file.is_dir)
6003            {
6004                cx.update(|cx| {
6005                    if let Some(window) = cx
6006                        .active_window()
6007                        .and_then(|window| window.downcast::<Workspace>())
6008                    {
6009                        if let Ok(workspace) = window.read(cx) {
6010                            let project = workspace.project().read(cx);
6011                            if project.is_local() && !project.is_via_collab() {
6012                                existing = Some(window);
6013                                open_visible = OpenVisible::None;
6014                                return;
6015                            }
6016                        }
6017                    }
6018                    for window in local_workspace_windows(cx) {
6019                        if let Ok(workspace) = window.read(cx) {
6020                            let project = workspace.project().read(cx);
6021                            if project.is_via_collab() {
6022                                continue;
6023                            }
6024                            existing = Some(window);
6025                            open_visible = OpenVisible::None;
6026                            break;
6027                        }
6028                    }
6029                })?;
6030            }
6031        }
6032
6033        if let Some(existing) = existing {
6034            let open_task = existing
6035                .update(&mut cx, |workspace, window, cx| {
6036                    window.activate_window();
6037                    workspace.open_paths(abs_paths, open_visible, None, window, cx)
6038                })?
6039                .await;
6040
6041            _ = existing.update(&mut cx, |workspace, _, cx| {
6042                for item in open_task.iter().flatten() {
6043                    if let Err(e) = item {
6044                        workspace.show_error(&e, cx);
6045                    }
6046                }
6047            });
6048
6049            Ok((existing, open_task))
6050        } else {
6051            cx.update(move |cx| {
6052                Workspace::new_local(
6053                    abs_paths,
6054                    app_state.clone(),
6055                    open_options.replace_window,
6056                    open_options.env,
6057                    cx,
6058                )
6059            })?
6060            .await
6061        }
6062    })
6063}
6064
6065pub fn open_new(
6066    open_options: OpenOptions,
6067    app_state: Arc<AppState>,
6068    cx: &mut App,
6069    init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6070) -> Task<anyhow::Result<()>> {
6071    let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6072    cx.spawn(|mut cx| async move {
6073        let (workspace, opened_paths) = task.await?;
6074        workspace.update(&mut cx, |workspace, window, cx| {
6075            if opened_paths.is_empty() {
6076                init(workspace, window, cx)
6077            }
6078        })?;
6079        Ok(())
6080    })
6081}
6082
6083pub fn create_and_open_local_file(
6084    path: &'static Path,
6085    window: &mut Window,
6086    cx: &mut Context<Workspace>,
6087    default_content: impl 'static + Send + FnOnce() -> Rope,
6088) -> Task<Result<Box<dyn ItemHandle>>> {
6089    cx.spawn_in(window, |workspace, mut cx| async move {
6090        let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
6091        if !fs.is_file(path).await {
6092            fs.create_file(path, Default::default()).await?;
6093            fs.save(path, &default_content(), Default::default())
6094                .await?;
6095        }
6096
6097        let mut items = workspace
6098            .update_in(&mut cx, |workspace, window, cx| {
6099                workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6100                    workspace.open_paths(
6101                        vec![path.to_path_buf()],
6102                        OpenVisible::None,
6103                        None,
6104                        window,
6105                        cx,
6106                    )
6107                })
6108            })?
6109            .await?
6110            .await;
6111
6112        let item = items.pop().flatten();
6113        item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
6114    })
6115}
6116
6117pub fn open_ssh_project(
6118    window: WindowHandle<Workspace>,
6119    connection_options: SshConnectionOptions,
6120    cancel_rx: oneshot::Receiver<()>,
6121    delegate: Arc<dyn SshClientDelegate>,
6122    app_state: Arc<AppState>,
6123    paths: Vec<PathBuf>,
6124    cx: &mut App,
6125) -> Task<Result<()>> {
6126    cx.spawn(|mut cx| async move {
6127        let (serialized_ssh_project, workspace_id, serialized_workspace) =
6128            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6129
6130        let session = match cx
6131            .update(|cx| {
6132                remote::SshRemoteClient::new(
6133                    ConnectionIdentifier::Workspace(workspace_id.0),
6134                    connection_options,
6135                    cancel_rx,
6136                    delegate,
6137                    cx,
6138                )
6139            })?
6140            .await?
6141        {
6142            Some(result) => result,
6143            None => return Ok(()),
6144        };
6145
6146        let project = cx.update(|cx| {
6147            project::Project::ssh(
6148                session,
6149                app_state.client.clone(),
6150                app_state.node_runtime.clone(),
6151                app_state.user_store.clone(),
6152                app_state.languages.clone(),
6153                app_state.fs.clone(),
6154                cx,
6155            )
6156        })?;
6157
6158        let toolchains = DB.toolchains(workspace_id).await?;
6159        for (toolchain, worktree_id) in toolchains {
6160            project
6161                .update(&mut cx, |this, cx| {
6162                    this.activate_toolchain(worktree_id, toolchain, cx)
6163                })?
6164                .await;
6165        }
6166        let mut project_paths_to_open = vec![];
6167        let mut project_path_errors = vec![];
6168
6169        for path in paths {
6170            let result = cx
6171                .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6172                .await;
6173            match result {
6174                Ok((_, project_path)) => {
6175                    project_paths_to_open.push((path.clone(), Some(project_path)));
6176                }
6177                Err(error) => {
6178                    project_path_errors.push(error);
6179                }
6180            };
6181        }
6182
6183        if project_paths_to_open.is_empty() {
6184            return Err(project_path_errors
6185                .pop()
6186                .unwrap_or_else(|| anyhow!("no paths given")));
6187        }
6188
6189        cx.update_window(window.into(), |_, window, cx| {
6190            window.replace_root(cx, |window, cx| {
6191                telemetry::event!("SSH Project Opened");
6192
6193                let mut workspace =
6194                    Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6195                workspace.set_serialized_ssh_project(serialized_ssh_project);
6196                workspace
6197            });
6198        })?;
6199
6200        window
6201            .update(&mut cx, |_, window, cx| {
6202                window.activate_window();
6203
6204                open_items(serialized_workspace, project_paths_to_open, window, cx)
6205            })?
6206            .await?;
6207
6208        window.update(&mut cx, |workspace, _, cx| {
6209            for error in project_path_errors {
6210                if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6211                    if let Some(path) = error.error_tag("path") {
6212                        workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6213                    }
6214                } else {
6215                    workspace.show_error(&error, cx)
6216                }
6217            }
6218        })
6219    })
6220}
6221
6222fn serialize_ssh_project(
6223    connection_options: SshConnectionOptions,
6224    paths: Vec<PathBuf>,
6225    cx: &AsyncApp,
6226) -> Task<
6227    Result<(
6228        SerializedSshProject,
6229        WorkspaceId,
6230        Option<SerializedWorkspace>,
6231    )>,
6232> {
6233    cx.background_executor().spawn(async move {
6234        let serialized_ssh_project = persistence::DB
6235            .get_or_create_ssh_project(
6236                connection_options.host.clone(),
6237                connection_options.port,
6238                paths
6239                    .iter()
6240                    .map(|path| path.to_string_lossy().to_string())
6241                    .collect::<Vec<_>>(),
6242                connection_options.username.clone(),
6243            )
6244            .await?;
6245
6246        let serialized_workspace =
6247            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6248
6249        let workspace_id = if let Some(workspace_id) =
6250            serialized_workspace.as_ref().map(|workspace| workspace.id)
6251        {
6252            workspace_id
6253        } else {
6254            persistence::DB.next_id().await?
6255        };
6256
6257        Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6258    })
6259}
6260
6261pub fn join_in_room_project(
6262    project_id: u64,
6263    follow_user_id: u64,
6264    app_state: Arc<AppState>,
6265    cx: &mut App,
6266) -> Task<Result<()>> {
6267    let windows = cx.windows();
6268    cx.spawn(|mut cx| async move {
6269        let existing_workspace = windows.into_iter().find_map(|window_handle| {
6270            window_handle
6271                .downcast::<Workspace>()
6272                .and_then(|window_handle| {
6273                    window_handle
6274                        .update(&mut cx, |workspace, _window, cx| {
6275                            if workspace.project().read(cx).remote_id() == Some(project_id) {
6276                                Some(window_handle)
6277                            } else {
6278                                None
6279                            }
6280                        })
6281                        .unwrap_or(None)
6282                })
6283        });
6284
6285        let workspace = if let Some(existing_workspace) = existing_workspace {
6286            existing_workspace
6287        } else {
6288            let active_call = cx.update(|cx| ActiveCall::global(cx))?;
6289            let room = active_call
6290                .read_with(&cx, |call, _| call.room().cloned())?
6291                .ok_or_else(|| anyhow!("not in a call"))?;
6292            let project = room
6293                .update(&mut cx, |room, cx| {
6294                    room.join_project(
6295                        project_id,
6296                        app_state.languages.clone(),
6297                        app_state.fs.clone(),
6298                        cx,
6299                    )
6300                })?
6301                .await?;
6302
6303            let window_bounds_override = window_bounds_env_override();
6304            cx.update(|cx| {
6305                let mut options = (app_state.build_window_options)(None, cx);
6306                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
6307                cx.open_window(options, |window, cx| {
6308                    cx.new(|cx| {
6309                        Workspace::new(Default::default(), project, app_state.clone(), window, cx)
6310                    })
6311                })
6312            })??
6313        };
6314
6315        workspace.update(&mut cx, |workspace, window, cx| {
6316            cx.activate(true);
6317            window.activate_window();
6318
6319            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
6320                let follow_peer_id = room
6321                    .read(cx)
6322                    .remote_participants()
6323                    .iter()
6324                    .find(|(_, participant)| participant.user.id == follow_user_id)
6325                    .map(|(_, p)| p.peer_id)
6326                    .or_else(|| {
6327                        // If we couldn't follow the given user, follow the host instead.
6328                        let collaborator = workspace
6329                            .project()
6330                            .read(cx)
6331                            .collaborators()
6332                            .values()
6333                            .find(|collaborator| collaborator.is_host)?;
6334                        Some(collaborator.peer_id)
6335                    });
6336
6337                if let Some(follow_peer_id) = follow_peer_id {
6338                    workspace.follow(follow_peer_id, window, cx);
6339                }
6340            }
6341        })?;
6342
6343        anyhow::Ok(())
6344    })
6345}
6346
6347pub fn reload(reload: &Reload, cx: &mut App) {
6348    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
6349    let mut workspace_windows = cx
6350        .windows()
6351        .into_iter()
6352        .filter_map(|window| window.downcast::<Workspace>())
6353        .collect::<Vec<_>>();
6354
6355    // If multiple windows have unsaved changes, and need a save prompt,
6356    // prompt in the active window before switching to a different window.
6357    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
6358
6359    let mut prompt = None;
6360    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6361        prompt = window
6362            .update(cx, |_, window, cx| {
6363                window.prompt(
6364                    PromptLevel::Info,
6365                    "Are you sure you want to restart?",
6366                    None,
6367                    &["Restart", "Cancel"],
6368                    cx,
6369                )
6370            })
6371            .ok();
6372    }
6373
6374    let binary_path = reload.binary_path.clone();
6375    cx.spawn(|mut cx| async move {
6376        if let Some(prompt) = prompt {
6377            let answer = prompt.await?;
6378            if answer != 0 {
6379                return Ok(());
6380            }
6381        }
6382
6383        // If the user cancels any save prompt, then keep the app open.
6384        for window in workspace_windows {
6385            if let Ok(should_close) = window.update(&mut cx, |workspace, window, cx| {
6386                workspace.prepare_to_close(CloseIntent::Quit, window, cx)
6387            }) {
6388                if !should_close.await? {
6389                    return Ok(());
6390                }
6391            }
6392        }
6393
6394        cx.update(|cx| cx.restart(binary_path))
6395    })
6396    .detach_and_log_err(cx);
6397}
6398
6399fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6400    let mut parts = value.split(',');
6401    let x: usize = parts.next()?.parse().ok()?;
6402    let y: usize = parts.next()?.parse().ok()?;
6403    Some(point(px(x as f32), px(y as f32)))
6404}
6405
6406fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6407    let mut parts = value.split(',');
6408    let width: usize = parts.next()?.parse().ok()?;
6409    let height: usize = parts.next()?.parse().ok()?;
6410    Some(size(px(width as f32), px(height as f32)))
6411}
6412
6413pub fn client_side_decorations(
6414    element: impl IntoElement,
6415    window: &mut Window,
6416    cx: &mut App,
6417) -> Stateful<Div> {
6418    const BORDER_SIZE: Pixels = px(1.0);
6419    let decorations = window.window_decorations();
6420
6421    if matches!(decorations, Decorations::Client { .. }) {
6422        window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6423    }
6424
6425    struct GlobalResizeEdge(ResizeEdge);
6426    impl Global for GlobalResizeEdge {}
6427
6428    div()
6429        .id("window-backdrop")
6430        .bg(transparent_black())
6431        .map(|div| match decorations {
6432            Decorations::Server => div,
6433            Decorations::Client { tiling, .. } => div
6434                .when(!(tiling.top || tiling.right), |div| {
6435                    div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6436                })
6437                .when(!(tiling.top || tiling.left), |div| {
6438                    div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6439                })
6440                .when(!(tiling.bottom || tiling.right), |div| {
6441                    div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6442                })
6443                .when(!(tiling.bottom || tiling.left), |div| {
6444                    div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6445                })
6446                .when(!tiling.top, |div| {
6447                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6448                })
6449                .when(!tiling.bottom, |div| {
6450                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6451                })
6452                .when(!tiling.left, |div| {
6453                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6454                })
6455                .when(!tiling.right, |div| {
6456                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6457                })
6458                .on_mouse_move(move |e, window, cx| {
6459                    let size = window.window_bounds().get_bounds().size;
6460                    let pos = e.position;
6461
6462                    let new_edge =
6463                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6464
6465                    let edge = cx.try_global::<GlobalResizeEdge>();
6466                    if new_edge != edge.map(|edge| edge.0) {
6467                        window
6468                            .window_handle()
6469                            .update(cx, |workspace, _, cx| {
6470                                cx.notify(workspace.entity_id());
6471                            })
6472                            .ok();
6473                    }
6474                })
6475                .on_mouse_down(MouseButton::Left, move |e, window, _| {
6476                    let size = window.window_bounds().get_bounds().size;
6477                    let pos = e.position;
6478
6479                    let edge = match resize_edge(
6480                        pos,
6481                        theme::CLIENT_SIDE_DECORATION_SHADOW,
6482                        size,
6483                        tiling,
6484                    ) {
6485                        Some(value) => value,
6486                        None => return,
6487                    };
6488
6489                    window.start_window_resize(edge);
6490                }),
6491        })
6492        .size_full()
6493        .child(
6494            div()
6495                .cursor(CursorStyle::Arrow)
6496                .map(|div| match decorations {
6497                    Decorations::Server => div,
6498                    Decorations::Client { tiling } => div
6499                        .border_color(cx.theme().colors().border)
6500                        .when(!(tiling.top || tiling.right), |div| {
6501                            div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6502                        })
6503                        .when(!(tiling.top || tiling.left), |div| {
6504                            div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6505                        })
6506                        .when(!(tiling.bottom || tiling.right), |div| {
6507                            div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6508                        })
6509                        .when(!(tiling.bottom || tiling.left), |div| {
6510                            div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6511                        })
6512                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6513                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6514                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6515                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6516                        .when(!tiling.is_tiled(), |div| {
6517                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
6518                                color: Hsla {
6519                                    h: 0.,
6520                                    s: 0.,
6521                                    l: 0.,
6522                                    a: 0.4,
6523                                },
6524                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6525                                spread_radius: px(0.),
6526                                offset: point(px(0.0), px(0.0)),
6527                            }])
6528                        }),
6529                })
6530                .on_mouse_move(|_e, _, cx| {
6531                    cx.stop_propagation();
6532                })
6533                .size_full()
6534                .child(element),
6535        )
6536        .map(|div| match decorations {
6537            Decorations::Server => div,
6538            Decorations::Client { tiling, .. } => div.child(
6539                canvas(
6540                    |_bounds, window, _| {
6541                        window.insert_hitbox(
6542                            Bounds::new(
6543                                point(px(0.0), px(0.0)),
6544                                window.window_bounds().get_bounds().size,
6545                            ),
6546                            false,
6547                        )
6548                    },
6549                    move |_bounds, hitbox, window, cx| {
6550                        let mouse = window.mouse_position();
6551                        let size = window.window_bounds().get_bounds().size;
6552                        let Some(edge) =
6553                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6554                        else {
6555                            return;
6556                        };
6557                        cx.set_global(GlobalResizeEdge(edge));
6558                        window.set_cursor_style(
6559                            match edge {
6560                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6561                                ResizeEdge::Left | ResizeEdge::Right => {
6562                                    CursorStyle::ResizeLeftRight
6563                                }
6564                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6565                                    CursorStyle::ResizeUpLeftDownRight
6566                                }
6567                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6568                                    CursorStyle::ResizeUpRightDownLeft
6569                                }
6570                            },
6571                            &hitbox,
6572                        );
6573                    },
6574                )
6575                .size_full()
6576                .absolute(),
6577            ),
6578        })
6579}
6580
6581fn resize_edge(
6582    pos: Point<Pixels>,
6583    shadow_size: Pixels,
6584    window_size: Size<Pixels>,
6585    tiling: Tiling,
6586) -> Option<ResizeEdge> {
6587    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6588    if bounds.contains(&pos) {
6589        return None;
6590    }
6591
6592    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6593    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6594    if !tiling.top && top_left_bounds.contains(&pos) {
6595        return Some(ResizeEdge::TopLeft);
6596    }
6597
6598    let top_right_bounds = Bounds::new(
6599        Point::new(window_size.width - corner_size.width, px(0.)),
6600        corner_size,
6601    );
6602    if !tiling.top && top_right_bounds.contains(&pos) {
6603        return Some(ResizeEdge::TopRight);
6604    }
6605
6606    let bottom_left_bounds = Bounds::new(
6607        Point::new(px(0.), window_size.height - corner_size.height),
6608        corner_size,
6609    );
6610    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6611        return Some(ResizeEdge::BottomLeft);
6612    }
6613
6614    let bottom_right_bounds = Bounds::new(
6615        Point::new(
6616            window_size.width - corner_size.width,
6617            window_size.height - corner_size.height,
6618        ),
6619        corner_size,
6620    );
6621    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6622        return Some(ResizeEdge::BottomRight);
6623    }
6624
6625    if !tiling.top && pos.y < shadow_size {
6626        Some(ResizeEdge::Top)
6627    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6628        Some(ResizeEdge::Bottom)
6629    } else if !tiling.left && pos.x < shadow_size {
6630        Some(ResizeEdge::Left)
6631    } else if !tiling.right && pos.x > window_size.width - shadow_size {
6632        Some(ResizeEdge::Right)
6633    } else {
6634        None
6635    }
6636}
6637
6638fn join_pane_into_active(
6639    active_pane: &Entity<Pane>,
6640    pane: &Entity<Pane>,
6641    window: &mut Window,
6642    cx: &mut App,
6643) {
6644    if pane == active_pane {
6645        return;
6646    } else if pane.read(cx).items_len() == 0 {
6647        pane.update(cx, |_, cx| {
6648            cx.emit(pane::Event::Remove {
6649                focus_on_pane: None,
6650            });
6651        })
6652    } else {
6653        move_all_items(pane, active_pane, window, cx);
6654    }
6655}
6656
6657fn move_all_items(
6658    from_pane: &Entity<Pane>,
6659    to_pane: &Entity<Pane>,
6660    window: &mut Window,
6661    cx: &mut App,
6662) {
6663    let destination_is_different = from_pane != to_pane;
6664    let mut moved_items = 0;
6665    for (item_ix, item_handle) in from_pane
6666        .read(cx)
6667        .items()
6668        .enumerate()
6669        .map(|(ix, item)| (ix, item.clone()))
6670        .collect::<Vec<_>>()
6671    {
6672        let ix = item_ix - moved_items;
6673        if destination_is_different {
6674            // Close item from previous pane
6675            from_pane.update(cx, |source, cx| {
6676                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
6677            });
6678            moved_items += 1;
6679        }
6680
6681        // This automatically removes duplicate items in the pane
6682        to_pane.update(cx, |destination, cx| {
6683            destination.add_item(item_handle, true, true, None, window, cx);
6684            window.focus(&destination.focus_handle(cx))
6685        });
6686    }
6687}
6688
6689pub fn move_item(
6690    source: &Entity<Pane>,
6691    destination: &Entity<Pane>,
6692    item_id_to_move: EntityId,
6693    destination_index: usize,
6694    window: &mut Window,
6695    cx: &mut App,
6696) {
6697    let Some((item_ix, item_handle)) = source
6698        .read(cx)
6699        .items()
6700        .enumerate()
6701        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6702        .map(|(ix, item)| (ix, item.clone()))
6703    else {
6704        // Tab was closed during drag
6705        return;
6706    };
6707
6708    if source != destination {
6709        // Close item from previous pane
6710        source.update(cx, |source, cx| {
6711            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
6712        });
6713    }
6714
6715    // This automatically removes duplicate items in the pane
6716    destination.update(cx, |destination, cx| {
6717        destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
6718        window.focus(&destination.focus_handle(cx))
6719    });
6720}
6721
6722pub fn move_active_item(
6723    source: &Entity<Pane>,
6724    destination: &Entity<Pane>,
6725    focus_destination: bool,
6726    close_if_empty: bool,
6727    window: &mut Window,
6728    cx: &mut App,
6729) {
6730    if source == destination {
6731        return;
6732    }
6733    let Some(active_item) = source.read(cx).active_item() else {
6734        return;
6735    };
6736    source.update(cx, |source_pane, cx| {
6737        let item_id = active_item.item_id();
6738        source_pane.remove_item(item_id, false, close_if_empty, window, cx);
6739        destination.update(cx, |target_pane, cx| {
6740            target_pane.add_item(
6741                active_item,
6742                focus_destination,
6743                focus_destination,
6744                Some(target_pane.items_len()),
6745                window,
6746                cx,
6747            );
6748        });
6749    });
6750}
6751
6752#[cfg(test)]
6753mod tests {
6754    use std::{cell::RefCell, rc::Rc};
6755
6756    use super::*;
6757    use crate::{
6758        dock::{test::TestPanel, PanelEvent},
6759        item::{
6760            test::{TestItem, TestProjectItem},
6761            ItemEvent,
6762        },
6763    };
6764    use fs::FakeFs;
6765    use gpui::{
6766        px, DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
6767        UpdateGlobal, VisualTestContext,
6768    };
6769    use project::{Project, ProjectEntryId};
6770    use serde_json::json;
6771    use settings::SettingsStore;
6772
6773    #[gpui::test]
6774    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6775        init_test(cx);
6776
6777        let fs = FakeFs::new(cx.executor());
6778        let project = Project::test(fs, [], cx).await;
6779        let (workspace, cx) =
6780            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6781
6782        // Adding an item with no ambiguity renders the tab without detail.
6783        let item1 = cx.new(|cx| {
6784            let mut item = TestItem::new(cx);
6785            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6786            item
6787        });
6788        workspace.update_in(cx, |workspace, window, cx| {
6789            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6790        });
6791        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6792
6793        // Adding an item that creates ambiguity increases the level of detail on
6794        // both tabs.
6795        let item2 = cx.new_window_entity(|_window, cx| {
6796            let mut item = TestItem::new(cx);
6797            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6798            item
6799        });
6800        workspace.update_in(cx, |workspace, window, cx| {
6801            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6802        });
6803        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6804        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6805
6806        // Adding an item that creates ambiguity increases the level of detail only
6807        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6808        // we stop at the highest detail available.
6809        let item3 = cx.new(|cx| {
6810            let mut item = TestItem::new(cx);
6811            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6812            item
6813        });
6814        workspace.update_in(cx, |workspace, window, cx| {
6815            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6816        });
6817        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6818        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6819        item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6820    }
6821
6822    #[gpui::test]
6823    async fn test_tracking_active_path(cx: &mut TestAppContext) {
6824        init_test(cx);
6825
6826        let fs = FakeFs::new(cx.executor());
6827        fs.insert_tree(
6828            "/root1",
6829            json!({
6830                "one.txt": "",
6831                "two.txt": "",
6832            }),
6833        )
6834        .await;
6835        fs.insert_tree(
6836            "/root2",
6837            json!({
6838                "three.txt": "",
6839            }),
6840        )
6841        .await;
6842
6843        let project = Project::test(fs, ["root1".as_ref()], cx).await;
6844        let (workspace, cx) =
6845            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6846        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6847        let worktree_id = project.update(cx, |project, cx| {
6848            project.worktrees(cx).next().unwrap().read(cx).id()
6849        });
6850
6851        let item1 = cx.new(|cx| {
6852            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6853        });
6854        let item2 = cx.new(|cx| {
6855            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6856        });
6857
6858        // Add an item to an empty pane
6859        workspace.update_in(cx, |workspace, window, cx| {
6860            workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
6861        });
6862        project.update(cx, |project, cx| {
6863            assert_eq!(
6864                project.active_entry(),
6865                project
6866                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6867                    .map(|e| e.id)
6868            );
6869        });
6870        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6871
6872        // Add a second item to a non-empty pane
6873        workspace.update_in(cx, |workspace, window, cx| {
6874            workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
6875        });
6876        assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6877        project.update(cx, |project, cx| {
6878            assert_eq!(
6879                project.active_entry(),
6880                project
6881                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6882                    .map(|e| e.id)
6883            );
6884        });
6885
6886        // Close the active item
6887        pane.update_in(cx, |pane, window, cx| {
6888            pane.close_active_item(&Default::default(), window, cx)
6889                .unwrap()
6890        })
6891        .await
6892        .unwrap();
6893        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6894        project.update(cx, |project, cx| {
6895            assert_eq!(
6896                project.active_entry(),
6897                project
6898                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6899                    .map(|e| e.id)
6900            );
6901        });
6902
6903        // Add a project folder
6904        project
6905            .update(cx, |project, cx| {
6906                project.find_or_create_worktree("root2", true, cx)
6907            })
6908            .await
6909            .unwrap();
6910        assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6911
6912        // Remove a project folder
6913        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6914        assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6915    }
6916
6917    #[gpui::test]
6918    async fn test_close_window(cx: &mut TestAppContext) {
6919        init_test(cx);
6920
6921        let fs = FakeFs::new(cx.executor());
6922        fs.insert_tree("/root", json!({ "one": "" })).await;
6923
6924        let project = Project::test(fs, ["root".as_ref()], cx).await;
6925        let (workspace, cx) =
6926            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6927
6928        // When there are no dirty items, there's nothing to do.
6929        let item1 = cx.new(TestItem::new);
6930        workspace.update_in(cx, |w, window, cx| {
6931            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
6932        });
6933        let task = workspace.update_in(cx, |w, window, cx| {
6934            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6935        });
6936        assert!(task.await.unwrap());
6937
6938        // When there are dirty untitled items, prompt to save each one. If the user
6939        // cancels any prompt, then abort.
6940        let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
6941        let item3 = cx.new(|cx| {
6942            TestItem::new(cx)
6943                .with_dirty(true)
6944                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6945        });
6946        workspace.update_in(cx, |w, window, cx| {
6947            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6948            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6949        });
6950        let task = workspace.update_in(cx, |w, window, cx| {
6951            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6952        });
6953        cx.executor().run_until_parked();
6954        cx.simulate_prompt_answer(2); // cancel save all
6955        cx.executor().run_until_parked();
6956        cx.simulate_prompt_answer(2); // cancel save all
6957        cx.executor().run_until_parked();
6958        assert!(!cx.has_pending_prompt());
6959        assert!(!task.await.unwrap());
6960    }
6961
6962    #[gpui::test]
6963    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6964        init_test(cx);
6965
6966        // Register TestItem as a serializable item
6967        cx.update(|cx| {
6968            register_serializable_item::<TestItem>(cx);
6969        });
6970
6971        let fs = FakeFs::new(cx.executor());
6972        fs.insert_tree("/root", json!({ "one": "" })).await;
6973
6974        let project = Project::test(fs, ["root".as_ref()], cx).await;
6975        let (workspace, cx) =
6976            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6977
6978        // When there are dirty untitled items, but they can serialize, then there is no prompt.
6979        let item1 = cx.new(|cx| {
6980            TestItem::new(cx)
6981                .with_dirty(true)
6982                .with_serialize(|| Some(Task::ready(Ok(()))))
6983        });
6984        let item2 = cx.new(|cx| {
6985            TestItem::new(cx)
6986                .with_dirty(true)
6987                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6988                .with_serialize(|| Some(Task::ready(Ok(()))))
6989        });
6990        workspace.update_in(cx, |w, window, cx| {
6991            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6992            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6993        });
6994        let task = workspace.update_in(cx, |w, window, cx| {
6995            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6996        });
6997        assert!(task.await.unwrap());
6998    }
6999
7000    #[gpui::test]
7001    async fn test_close_pane_items(cx: &mut TestAppContext) {
7002        init_test(cx);
7003
7004        let fs = FakeFs::new(cx.executor());
7005
7006        let project = Project::test(fs, None, cx).await;
7007        let (workspace, cx) =
7008            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7009
7010        let item1 = cx.new(|cx| {
7011            TestItem::new(cx)
7012                .with_dirty(true)
7013                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7014        });
7015        let item2 = cx.new(|cx| {
7016            TestItem::new(cx)
7017                .with_dirty(true)
7018                .with_conflict(true)
7019                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7020        });
7021        let item3 = cx.new(|cx| {
7022            TestItem::new(cx)
7023                .with_dirty(true)
7024                .with_conflict(true)
7025                .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7026        });
7027        let item4 = cx.new(|cx| {
7028            TestItem::new(cx).with_dirty(true).with_project_items(&[{
7029                let project_item = TestProjectItem::new_untitled(cx);
7030                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7031                project_item
7032            }])
7033        });
7034        let pane = workspace.update_in(cx, |workspace, window, cx| {
7035            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7036            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7037            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7038            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7039            workspace.active_pane().clone()
7040        });
7041
7042        let close_items = pane.update_in(cx, |pane, window, cx| {
7043            pane.activate_item(1, true, true, window, cx);
7044            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7045            let item1_id = item1.item_id();
7046            let item3_id = item3.item_id();
7047            let item4_id = item4.item_id();
7048            pane.close_items(window, cx, SaveIntent::Close, move |id| {
7049                [item1_id, item3_id, item4_id].contains(&id)
7050            })
7051        });
7052        cx.executor().run_until_parked();
7053
7054        assert!(cx.has_pending_prompt());
7055        // Ignore "Save all" prompt
7056        cx.simulate_prompt_answer(2);
7057        cx.executor().run_until_parked();
7058        // There's a prompt to save item 1.
7059        pane.update(cx, |pane, _| {
7060            assert_eq!(pane.items_len(), 4);
7061            assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
7062        });
7063        // Confirm saving item 1.
7064        cx.simulate_prompt_answer(0);
7065        cx.executor().run_until_parked();
7066
7067        // Item 1 is saved. There's a prompt to save item 3.
7068        pane.update(cx, |pane, cx| {
7069            assert_eq!(item1.read(cx).save_count, 1);
7070            assert_eq!(item1.read(cx).save_as_count, 0);
7071            assert_eq!(item1.read(cx).reload_count, 0);
7072            assert_eq!(pane.items_len(), 3);
7073            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7074        });
7075        assert!(cx.has_pending_prompt());
7076
7077        // Cancel saving item 3.
7078        cx.simulate_prompt_answer(1);
7079        cx.executor().run_until_parked();
7080
7081        // Item 3 is reloaded. There's a prompt to save item 4.
7082        pane.update(cx, |pane, cx| {
7083            assert_eq!(item3.read(cx).save_count, 0);
7084            assert_eq!(item3.read(cx).save_as_count, 0);
7085            assert_eq!(item3.read(cx).reload_count, 1);
7086            assert_eq!(pane.items_len(), 2);
7087            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7088        });
7089        assert!(cx.has_pending_prompt());
7090
7091        // Confirm saving item 4.
7092        cx.simulate_prompt_answer(0);
7093        cx.executor().run_until_parked();
7094
7095        // There's a prompt for a path for item 4.
7096        cx.simulate_new_path_selection(|_| Some(Default::default()));
7097        close_items.await.unwrap();
7098
7099        // The requested items are closed.
7100        pane.update(cx, |pane, cx| {
7101            assert_eq!(item4.read(cx).save_count, 0);
7102            assert_eq!(item4.read(cx).save_as_count, 1);
7103            assert_eq!(item4.read(cx).reload_count, 0);
7104            assert_eq!(pane.items_len(), 1);
7105            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7106        });
7107    }
7108
7109    #[gpui::test]
7110    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7111        init_test(cx);
7112
7113        let fs = FakeFs::new(cx.executor());
7114        let project = Project::test(fs, [], cx).await;
7115        let (workspace, cx) =
7116            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7117
7118        // Create several workspace items with single project entries, and two
7119        // workspace items with multiple project entries.
7120        let single_entry_items = (0..=4)
7121            .map(|project_entry_id| {
7122                cx.new(|cx| {
7123                    TestItem::new(cx)
7124                        .with_dirty(true)
7125                        .with_project_items(&[dirty_project_item(
7126                            project_entry_id,
7127                            &format!("{project_entry_id}.txt"),
7128                            cx,
7129                        )])
7130                })
7131            })
7132            .collect::<Vec<_>>();
7133        let item_2_3 = cx.new(|cx| {
7134            TestItem::new(cx)
7135                .with_dirty(true)
7136                .with_singleton(false)
7137                .with_project_items(&[
7138                    single_entry_items[2].read(cx).project_items[0].clone(),
7139                    single_entry_items[3].read(cx).project_items[0].clone(),
7140                ])
7141        });
7142        let item_3_4 = cx.new(|cx| {
7143            TestItem::new(cx)
7144                .with_dirty(true)
7145                .with_singleton(false)
7146                .with_project_items(&[
7147                    single_entry_items[3].read(cx).project_items[0].clone(),
7148                    single_entry_items[4].read(cx).project_items[0].clone(),
7149                ])
7150        });
7151
7152        // Create two panes that contain the following project entries:
7153        //   left pane:
7154        //     multi-entry items:   (2, 3)
7155        //     single-entry items:  0, 1, 2, 3, 4
7156        //   right pane:
7157        //     single-entry items:  1
7158        //     multi-entry items:   (3, 4)
7159        let left_pane = workspace.update_in(cx, |workspace, window, cx| {
7160            let left_pane = workspace.active_pane().clone();
7161            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7162            for item in single_entry_items {
7163                workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
7164            }
7165            left_pane.update(cx, |pane, cx| {
7166                pane.activate_item(2, true, true, window, cx);
7167            });
7168
7169            let right_pane = workspace
7170                .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7171                .unwrap();
7172
7173            right_pane.update(cx, |pane, cx| {
7174                pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7175            });
7176
7177            left_pane
7178        });
7179
7180        cx.focus(&left_pane);
7181
7182        // When closing all of the items in the left pane, we should be prompted twice:
7183        // once for project entry 0, and once for project entry 2. Project entries 1,
7184        // 3, and 4 are all still open in the other paten. After those two
7185        // prompts, the task should complete.
7186
7187        let close = left_pane.update_in(cx, |pane, window, cx| {
7188            pane.close_all_items(&CloseAllItems::default(), window, cx)
7189                .unwrap()
7190        });
7191        cx.executor().run_until_parked();
7192
7193        // Discard "Save all" prompt
7194        cx.simulate_prompt_answer(2);
7195
7196        cx.executor().run_until_parked();
7197        left_pane.update(cx, |pane, cx| {
7198            assert_eq!(
7199                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
7200                &[ProjectEntryId::from_proto(0)]
7201            );
7202        });
7203        cx.simulate_prompt_answer(0);
7204
7205        cx.executor().run_until_parked();
7206        left_pane.update(cx, |pane, cx| {
7207            assert_eq!(
7208                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
7209                &[ProjectEntryId::from_proto(2)]
7210            );
7211        });
7212        cx.simulate_prompt_answer(0);
7213
7214        cx.executor().run_until_parked();
7215        close.await.unwrap();
7216        left_pane.update(cx, |pane, _| {
7217            assert_eq!(pane.items_len(), 0);
7218        });
7219    }
7220
7221    #[gpui::test]
7222    async fn test_autosave(cx: &mut gpui::TestAppContext) {
7223        init_test(cx);
7224
7225        let fs = FakeFs::new(cx.executor());
7226        let project = Project::test(fs, [], cx).await;
7227        let (workspace, cx) =
7228            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7229        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7230
7231        let item = cx.new(|cx| {
7232            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7233        });
7234        let item_id = item.entity_id();
7235        workspace.update_in(cx, |workspace, window, cx| {
7236            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7237        });
7238
7239        // Autosave on window change.
7240        item.update(cx, |item, cx| {
7241            SettingsStore::update_global(cx, |settings, cx| {
7242                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7243                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
7244                })
7245            });
7246            item.is_dirty = true;
7247        });
7248
7249        // Deactivating the window saves the file.
7250        cx.deactivate_window();
7251        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7252
7253        // Re-activating the window doesn't save the file.
7254        cx.update(|window, _| window.activate_window());
7255        cx.executor().run_until_parked();
7256        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7257
7258        // Autosave on focus change.
7259        item.update_in(cx, |item, window, cx| {
7260            cx.focus_self(window);
7261            SettingsStore::update_global(cx, |settings, cx| {
7262                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7263                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
7264                })
7265            });
7266            item.is_dirty = true;
7267        });
7268
7269        // Blurring the item saves the file.
7270        item.update_in(cx, |_, window, _| window.blur());
7271        cx.executor().run_until_parked();
7272        item.update(cx, |item, _| assert_eq!(item.save_count, 2));
7273
7274        // Deactivating the window still saves the file.
7275        item.update_in(cx, |item, window, cx| {
7276            cx.focus_self(window);
7277            item.is_dirty = true;
7278        });
7279        cx.deactivate_window();
7280        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7281
7282        // Autosave after delay.
7283        item.update(cx, |item, cx| {
7284            SettingsStore::update_global(cx, |settings, cx| {
7285                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7286                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
7287                })
7288            });
7289            item.is_dirty = true;
7290            cx.emit(ItemEvent::Edit);
7291        });
7292
7293        // Delay hasn't fully expired, so the file is still dirty and unsaved.
7294        cx.executor().advance_clock(Duration::from_millis(250));
7295        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7296
7297        // After delay expires, the file is saved.
7298        cx.executor().advance_clock(Duration::from_millis(250));
7299        item.update(cx, |item, _| assert_eq!(item.save_count, 4));
7300
7301        // Autosave on focus change, ensuring closing the tab counts as such.
7302        item.update(cx, |item, cx| {
7303            SettingsStore::update_global(cx, |settings, cx| {
7304                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7305                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
7306                })
7307            });
7308            item.is_dirty = true;
7309            for project_item in &mut item.project_items {
7310                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7311            }
7312        });
7313
7314        pane.update_in(cx, |pane, window, cx| {
7315            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7316        })
7317        .await
7318        .unwrap();
7319        assert!(!cx.has_pending_prompt());
7320        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7321
7322        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
7323        workspace.update_in(cx, |workspace, window, cx| {
7324            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7325        });
7326        item.update_in(cx, |item, window, cx| {
7327            item.project_items[0].update(cx, |item, _| {
7328                item.entry_id = None;
7329            });
7330            item.is_dirty = true;
7331            window.blur();
7332        });
7333        cx.run_until_parked();
7334        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7335
7336        // Ensure autosave is prevented for deleted files also when closing the buffer.
7337        let _close_items = pane.update_in(cx, |pane, window, cx| {
7338            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7339        });
7340        cx.run_until_parked();
7341        assert!(cx.has_pending_prompt());
7342        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7343    }
7344
7345    #[gpui::test]
7346    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
7347        init_test(cx);
7348
7349        let fs = FakeFs::new(cx.executor());
7350
7351        let project = Project::test(fs, [], cx).await;
7352        let (workspace, cx) =
7353            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7354
7355        let item = cx.new(|cx| {
7356            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7357        });
7358        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7359        let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
7360        let toolbar_notify_count = Rc::new(RefCell::new(0));
7361
7362        workspace.update_in(cx, |workspace, window, cx| {
7363            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7364            let toolbar_notification_count = toolbar_notify_count.clone();
7365            cx.observe_in(&toolbar, window, move |_, _, _, _| {
7366                *toolbar_notification_count.borrow_mut() += 1
7367            })
7368            .detach();
7369        });
7370
7371        pane.update(cx, |pane, _| {
7372            assert!(!pane.can_navigate_backward());
7373            assert!(!pane.can_navigate_forward());
7374        });
7375
7376        item.update_in(cx, |item, _, cx| {
7377            item.set_state("one".to_string(), cx);
7378        });
7379
7380        // Toolbar must be notified to re-render the navigation buttons
7381        assert_eq!(*toolbar_notify_count.borrow(), 1);
7382
7383        pane.update(cx, |pane, _| {
7384            assert!(pane.can_navigate_backward());
7385            assert!(!pane.can_navigate_forward());
7386        });
7387
7388        workspace
7389            .update_in(cx, |workspace, window, cx| {
7390                workspace.go_back(pane.downgrade(), window, cx)
7391            })
7392            .await
7393            .unwrap();
7394
7395        assert_eq!(*toolbar_notify_count.borrow(), 2);
7396        pane.update(cx, |pane, _| {
7397            assert!(!pane.can_navigate_backward());
7398            assert!(pane.can_navigate_forward());
7399        });
7400    }
7401
7402    #[gpui::test]
7403    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7404        init_test(cx);
7405        let fs = FakeFs::new(cx.executor());
7406
7407        let project = Project::test(fs, [], cx).await;
7408        let (workspace, cx) =
7409            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7410
7411        let panel = workspace.update_in(cx, |workspace, window, cx| {
7412            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7413            workspace.add_panel(panel.clone(), window, cx);
7414
7415            workspace
7416                .right_dock()
7417                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
7418
7419            panel
7420        });
7421
7422        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7423        pane.update_in(cx, |pane, window, cx| {
7424            let item = cx.new(TestItem::new);
7425            pane.add_item(Box::new(item), true, true, None, window, cx);
7426        });
7427
7428        // Transfer focus from center to panel
7429        workspace.update_in(cx, |workspace, window, cx| {
7430            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7431        });
7432
7433        workspace.update_in(cx, |workspace, window, cx| {
7434            assert!(workspace.right_dock().read(cx).is_open());
7435            assert!(!panel.is_zoomed(window, cx));
7436            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7437        });
7438
7439        // Transfer focus from panel to center
7440        workspace.update_in(cx, |workspace, window, cx| {
7441            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7442        });
7443
7444        workspace.update_in(cx, |workspace, window, cx| {
7445            assert!(workspace.right_dock().read(cx).is_open());
7446            assert!(!panel.is_zoomed(window, cx));
7447            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7448        });
7449
7450        // Close the dock
7451        workspace.update_in(cx, |workspace, window, cx| {
7452            workspace.toggle_dock(DockPosition::Right, window, cx);
7453        });
7454
7455        workspace.update_in(cx, |workspace, window, cx| {
7456            assert!(!workspace.right_dock().read(cx).is_open());
7457            assert!(!panel.is_zoomed(window, cx));
7458            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7459        });
7460
7461        // Open the dock
7462        workspace.update_in(cx, |workspace, window, cx| {
7463            workspace.toggle_dock(DockPosition::Right, window, cx);
7464        });
7465
7466        workspace.update_in(cx, |workspace, window, cx| {
7467            assert!(workspace.right_dock().read(cx).is_open());
7468            assert!(!panel.is_zoomed(window, cx));
7469            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7470        });
7471
7472        // Focus and zoom panel
7473        panel.update_in(cx, |panel, window, cx| {
7474            cx.focus_self(window);
7475            panel.set_zoomed(true, window, cx)
7476        });
7477
7478        workspace.update_in(cx, |workspace, window, cx| {
7479            assert!(workspace.right_dock().read(cx).is_open());
7480            assert!(panel.is_zoomed(window, cx));
7481            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7482        });
7483
7484        // Transfer focus to the center closes the dock
7485        workspace.update_in(cx, |workspace, window, cx| {
7486            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7487        });
7488
7489        workspace.update_in(cx, |workspace, window, cx| {
7490            assert!(!workspace.right_dock().read(cx).is_open());
7491            assert!(panel.is_zoomed(window, cx));
7492            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7493        });
7494
7495        // Transferring focus back to the panel keeps it zoomed
7496        workspace.update_in(cx, |workspace, window, cx| {
7497            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7498        });
7499
7500        workspace.update_in(cx, |workspace, window, cx| {
7501            assert!(workspace.right_dock().read(cx).is_open());
7502            assert!(panel.is_zoomed(window, cx));
7503            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7504        });
7505
7506        // Close the dock while it is zoomed
7507        workspace.update_in(cx, |workspace, window, cx| {
7508            workspace.toggle_dock(DockPosition::Right, window, cx)
7509        });
7510
7511        workspace.update_in(cx, |workspace, window, cx| {
7512            assert!(!workspace.right_dock().read(cx).is_open());
7513            assert!(panel.is_zoomed(window, cx));
7514            assert!(workspace.zoomed.is_none());
7515            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7516        });
7517
7518        // Opening the dock, when it's zoomed, retains focus
7519        workspace.update_in(cx, |workspace, window, cx| {
7520            workspace.toggle_dock(DockPosition::Right, window, cx)
7521        });
7522
7523        workspace.update_in(cx, |workspace, window, cx| {
7524            assert!(workspace.right_dock().read(cx).is_open());
7525            assert!(panel.is_zoomed(window, cx));
7526            assert!(workspace.zoomed.is_some());
7527            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7528        });
7529
7530        // Unzoom and close the panel, zoom the active pane.
7531        panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
7532        workspace.update_in(cx, |workspace, window, cx| {
7533            workspace.toggle_dock(DockPosition::Right, window, cx)
7534        });
7535        pane.update_in(cx, |pane, window, cx| {
7536            pane.toggle_zoom(&Default::default(), window, cx)
7537        });
7538
7539        // Opening a dock unzooms the pane.
7540        workspace.update_in(cx, |workspace, window, cx| {
7541            workspace.toggle_dock(DockPosition::Right, window, cx)
7542        });
7543        workspace.update_in(cx, |workspace, window, cx| {
7544            let pane = pane.read(cx);
7545            assert!(!pane.is_zoomed());
7546            assert!(!pane.focus_handle(cx).is_focused(window));
7547            assert!(workspace.right_dock().read(cx).is_open());
7548            assert!(workspace.zoomed.is_none());
7549        });
7550    }
7551
7552    #[gpui::test]
7553    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7554        init_test(cx);
7555
7556        let fs = FakeFs::new(cx.executor());
7557
7558        let project = Project::test(fs, None, cx).await;
7559        let (workspace, cx) =
7560            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7561
7562        // Let's arrange the panes like this:
7563        //
7564        // +-----------------------+
7565        // |         top           |
7566        // +------+--------+-------+
7567        // | left | center | right |
7568        // +------+--------+-------+
7569        // |        bottom         |
7570        // +-----------------------+
7571
7572        let top_item = cx.new(|cx| {
7573            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7574        });
7575        let bottom_item = cx.new(|cx| {
7576            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7577        });
7578        let left_item = cx.new(|cx| {
7579            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7580        });
7581        let right_item = cx.new(|cx| {
7582            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7583        });
7584        let center_item = cx.new(|cx| {
7585            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7586        });
7587
7588        let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7589            let top_pane_id = workspace.active_pane().entity_id();
7590            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
7591            workspace.split_pane(
7592                workspace.active_pane().clone(),
7593                SplitDirection::Down,
7594                window,
7595                cx,
7596            );
7597            top_pane_id
7598        });
7599        let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7600            let bottom_pane_id = workspace.active_pane().entity_id();
7601            workspace.add_item_to_active_pane(
7602                Box::new(bottom_item.clone()),
7603                None,
7604                false,
7605                window,
7606                cx,
7607            );
7608            workspace.split_pane(
7609                workspace.active_pane().clone(),
7610                SplitDirection::Up,
7611                window,
7612                cx,
7613            );
7614            bottom_pane_id
7615        });
7616        let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7617            let left_pane_id = workspace.active_pane().entity_id();
7618            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
7619            workspace.split_pane(
7620                workspace.active_pane().clone(),
7621                SplitDirection::Right,
7622                window,
7623                cx,
7624            );
7625            left_pane_id
7626        });
7627        let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7628            let right_pane_id = workspace.active_pane().entity_id();
7629            workspace.add_item_to_active_pane(
7630                Box::new(right_item.clone()),
7631                None,
7632                false,
7633                window,
7634                cx,
7635            );
7636            workspace.split_pane(
7637                workspace.active_pane().clone(),
7638                SplitDirection::Left,
7639                window,
7640                cx,
7641            );
7642            right_pane_id
7643        });
7644        let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7645            let center_pane_id = workspace.active_pane().entity_id();
7646            workspace.add_item_to_active_pane(
7647                Box::new(center_item.clone()),
7648                None,
7649                false,
7650                window,
7651                cx,
7652            );
7653            center_pane_id
7654        });
7655        cx.executor().run_until_parked();
7656
7657        workspace.update_in(cx, |workspace, window, cx| {
7658            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7659
7660            // Join into next from center pane into right
7661            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7662        });
7663
7664        workspace.update_in(cx, |workspace, window, cx| {
7665            let active_pane = workspace.active_pane();
7666            assert_eq!(right_pane_id, active_pane.entity_id());
7667            assert_eq!(2, active_pane.read(cx).items_len());
7668            let item_ids_in_pane =
7669                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7670            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7671            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7672
7673            // Join into next from right pane into bottom
7674            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7675        });
7676
7677        workspace.update_in(cx, |workspace, window, cx| {
7678            let active_pane = workspace.active_pane();
7679            assert_eq!(bottom_pane_id, active_pane.entity_id());
7680            assert_eq!(3, active_pane.read(cx).items_len());
7681            let item_ids_in_pane =
7682                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7683            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7684            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7685            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7686
7687            // Join into next from bottom pane into left
7688            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7689        });
7690
7691        workspace.update_in(cx, |workspace, window, cx| {
7692            let active_pane = workspace.active_pane();
7693            assert_eq!(left_pane_id, active_pane.entity_id());
7694            assert_eq!(4, active_pane.read(cx).items_len());
7695            let item_ids_in_pane =
7696                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7697            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7698            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7699            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7700            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7701
7702            // Join into next from left pane into top
7703            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7704        });
7705
7706        workspace.update_in(cx, |workspace, window, cx| {
7707            let active_pane = workspace.active_pane();
7708            assert_eq!(top_pane_id, active_pane.entity_id());
7709            assert_eq!(5, active_pane.read(cx).items_len());
7710            let item_ids_in_pane =
7711                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7712            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7713            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7714            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7715            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7716            assert!(item_ids_in_pane.contains(&top_item.item_id()));
7717
7718            // Single pane left: no-op
7719            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
7720        });
7721
7722        workspace.update(cx, |workspace, _cx| {
7723            let active_pane = workspace.active_pane();
7724            assert_eq!(top_pane_id, active_pane.entity_id());
7725        });
7726    }
7727
7728    fn add_an_item_to_active_pane(
7729        cx: &mut VisualTestContext,
7730        workspace: &Entity<Workspace>,
7731        item_id: u64,
7732    ) -> Entity<TestItem> {
7733        let item = cx.new(|cx| {
7734            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7735                item_id,
7736                "item{item_id}.txt",
7737                cx,
7738            )])
7739        });
7740        workspace.update_in(cx, |workspace, window, cx| {
7741            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
7742        });
7743        return item;
7744    }
7745
7746    fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
7747        return workspace.update_in(cx, |workspace, window, cx| {
7748            let new_pane = workspace.split_pane(
7749                workspace.active_pane().clone(),
7750                SplitDirection::Right,
7751                window,
7752                cx,
7753            );
7754            new_pane
7755        });
7756    }
7757
7758    #[gpui::test]
7759    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7760        init_test(cx);
7761        let fs = FakeFs::new(cx.executor());
7762        let project = Project::test(fs, None, cx).await;
7763        let (workspace, cx) =
7764            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7765
7766        add_an_item_to_active_pane(cx, &workspace, 1);
7767        split_pane(cx, &workspace);
7768        add_an_item_to_active_pane(cx, &workspace, 2);
7769        split_pane(cx, &workspace); // empty pane
7770        split_pane(cx, &workspace);
7771        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7772
7773        cx.executor().run_until_parked();
7774
7775        workspace.update(cx, |workspace, cx| {
7776            let num_panes = workspace.panes().len();
7777            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7778            let active_item = workspace
7779                .active_pane()
7780                .read(cx)
7781                .active_item()
7782                .expect("item is in focus");
7783
7784            assert_eq!(num_panes, 4);
7785            assert_eq!(num_items_in_current_pane, 1);
7786            assert_eq!(active_item.item_id(), last_item.item_id());
7787        });
7788
7789        workspace.update_in(cx, |workspace, window, cx| {
7790            workspace.join_all_panes(window, cx);
7791        });
7792
7793        workspace.update(cx, |workspace, cx| {
7794            let num_panes = workspace.panes().len();
7795            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7796            let active_item = workspace
7797                .active_pane()
7798                .read(cx)
7799                .active_item()
7800                .expect("item is in focus");
7801
7802            assert_eq!(num_panes, 1);
7803            assert_eq!(num_items_in_current_pane, 3);
7804            assert_eq!(active_item.item_id(), last_item.item_id());
7805        });
7806    }
7807    struct TestModal(FocusHandle);
7808
7809    impl TestModal {
7810        fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
7811            Self(cx.focus_handle())
7812        }
7813    }
7814
7815    impl EventEmitter<DismissEvent> for TestModal {}
7816
7817    impl Focusable for TestModal {
7818        fn focus_handle(&self, _cx: &App) -> FocusHandle {
7819            self.0.clone()
7820        }
7821    }
7822
7823    impl ModalView for TestModal {}
7824
7825    impl Render for TestModal {
7826        fn render(
7827            &mut self,
7828            _window: &mut Window,
7829            _cx: &mut Context<TestModal>,
7830        ) -> impl IntoElement {
7831            div().track_focus(&self.0)
7832        }
7833    }
7834
7835    #[gpui::test]
7836    async fn test_panels(cx: &mut gpui::TestAppContext) {
7837        init_test(cx);
7838        let fs = FakeFs::new(cx.executor());
7839
7840        let project = Project::test(fs, [], cx).await;
7841        let (workspace, cx) =
7842            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7843
7844        let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
7845            let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
7846            workspace.add_panel(panel_1.clone(), window, cx);
7847            workspace.toggle_dock(DockPosition::Left, window, cx);
7848            let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7849            workspace.add_panel(panel_2.clone(), window, cx);
7850            workspace.toggle_dock(DockPosition::Right, window, cx);
7851
7852            let left_dock = workspace.left_dock();
7853            assert_eq!(
7854                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7855                panel_1.panel_id()
7856            );
7857            assert_eq!(
7858                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7859                panel_1.size(window, cx)
7860            );
7861
7862            left_dock.update(cx, |left_dock, cx| {
7863                left_dock.resize_active_panel(Some(px(1337.)), window, cx)
7864            });
7865            assert_eq!(
7866                workspace
7867                    .right_dock()
7868                    .read(cx)
7869                    .visible_panel()
7870                    .unwrap()
7871                    .panel_id(),
7872                panel_2.panel_id(),
7873            );
7874
7875            (panel_1, panel_2)
7876        });
7877
7878        // Move panel_1 to the right
7879        panel_1.update_in(cx, |panel_1, window, cx| {
7880            panel_1.set_position(DockPosition::Right, window, cx)
7881        });
7882
7883        workspace.update_in(cx, |workspace, window, cx| {
7884            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7885            // Since it was the only panel on the left, the left dock should now be closed.
7886            assert!(!workspace.left_dock().read(cx).is_open());
7887            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7888            let right_dock = workspace.right_dock();
7889            assert_eq!(
7890                right_dock.read(cx).visible_panel().unwrap().panel_id(),
7891                panel_1.panel_id()
7892            );
7893            assert_eq!(
7894                right_dock.read(cx).active_panel_size(window, cx).unwrap(),
7895                px(1337.)
7896            );
7897
7898            // Now we move panel_2 to the left
7899            panel_2.set_position(DockPosition::Left, window, cx);
7900        });
7901
7902        workspace.update(cx, |workspace, cx| {
7903            // Since panel_2 was not visible on the right, we don't open the left dock.
7904            assert!(!workspace.left_dock().read(cx).is_open());
7905            // And the right dock is unaffected in its displaying of panel_1
7906            assert!(workspace.right_dock().read(cx).is_open());
7907            assert_eq!(
7908                workspace
7909                    .right_dock()
7910                    .read(cx)
7911                    .visible_panel()
7912                    .unwrap()
7913                    .panel_id(),
7914                panel_1.panel_id(),
7915            );
7916        });
7917
7918        // Move panel_1 back to the left
7919        panel_1.update_in(cx, |panel_1, window, cx| {
7920            panel_1.set_position(DockPosition::Left, window, cx)
7921        });
7922
7923        workspace.update_in(cx, |workspace, window, cx| {
7924            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7925            let left_dock = workspace.left_dock();
7926            assert!(left_dock.read(cx).is_open());
7927            assert_eq!(
7928                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7929                panel_1.panel_id()
7930            );
7931            assert_eq!(
7932                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7933                px(1337.)
7934            );
7935            // And the right dock should be closed as it no longer has any panels.
7936            assert!(!workspace.right_dock().read(cx).is_open());
7937
7938            // Now we move panel_1 to the bottom
7939            panel_1.set_position(DockPosition::Bottom, window, cx);
7940        });
7941
7942        workspace.update_in(cx, |workspace, window, cx| {
7943            // Since panel_1 was visible on the left, we close the left dock.
7944            assert!(!workspace.left_dock().read(cx).is_open());
7945            // The bottom dock is sized based on the panel's default size,
7946            // since the panel orientation changed from vertical to horizontal.
7947            let bottom_dock = workspace.bottom_dock();
7948            assert_eq!(
7949                bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
7950                panel_1.size(window, cx),
7951            );
7952            // Close bottom dock and move panel_1 back to the left.
7953            bottom_dock.update(cx, |bottom_dock, cx| {
7954                bottom_dock.set_open(false, window, cx)
7955            });
7956            panel_1.set_position(DockPosition::Left, window, cx);
7957        });
7958
7959        // Emit activated event on panel 1
7960        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7961
7962        // Now the left dock is open and panel_1 is active and focused.
7963        workspace.update_in(cx, |workspace, window, cx| {
7964            let left_dock = workspace.left_dock();
7965            assert!(left_dock.read(cx).is_open());
7966            assert_eq!(
7967                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7968                panel_1.panel_id(),
7969            );
7970            assert!(panel_1.focus_handle(cx).is_focused(window));
7971        });
7972
7973        // Emit closed event on panel 2, which is not active
7974        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7975
7976        // Wo don't close the left dock, because panel_2 wasn't the active panel
7977        workspace.update(cx, |workspace, cx| {
7978            let left_dock = workspace.left_dock();
7979            assert!(left_dock.read(cx).is_open());
7980            assert_eq!(
7981                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7982                panel_1.panel_id(),
7983            );
7984        });
7985
7986        // Emitting a ZoomIn event shows the panel as zoomed.
7987        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7988        workspace.update(cx, |workspace, _| {
7989            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7990            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7991        });
7992
7993        // Move panel to another dock while it is zoomed
7994        panel_1.update_in(cx, |panel, window, cx| {
7995            panel.set_position(DockPosition::Right, window, cx)
7996        });
7997        workspace.update(cx, |workspace, _| {
7998            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7999
8000            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8001        });
8002
8003        // This is a helper for getting a:
8004        // - valid focus on an element,
8005        // - that isn't a part of the panes and panels system of the Workspace,
8006        // - and doesn't trigger the 'on_focus_lost' API.
8007        let focus_other_view = {
8008            let workspace = workspace.clone();
8009            move |cx: &mut VisualTestContext| {
8010                workspace.update_in(cx, |workspace, window, cx| {
8011                    if let Some(_) = workspace.active_modal::<TestModal>(cx) {
8012                        workspace.toggle_modal(window, cx, TestModal::new);
8013                        workspace.toggle_modal(window, cx, TestModal::new);
8014                    } else {
8015                        workspace.toggle_modal(window, cx, TestModal::new);
8016                    }
8017                })
8018            }
8019        };
8020
8021        // If focus is transferred to another view that's not a panel or another pane, we still show
8022        // the panel as zoomed.
8023        focus_other_view(cx);
8024        workspace.update(cx, |workspace, _| {
8025            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8026            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8027        });
8028
8029        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8030        workspace.update_in(cx, |_workspace, window, cx| {
8031            cx.focus_self(window);
8032        });
8033        workspace.update(cx, |workspace, _| {
8034            assert_eq!(workspace.zoomed, None);
8035            assert_eq!(workspace.zoomed_position, None);
8036        });
8037
8038        // If focus is transferred again to another view that's not a panel or a pane, we won't
8039        // show the panel as zoomed because it wasn't zoomed before.
8040        focus_other_view(cx);
8041        workspace.update(cx, |workspace, _| {
8042            assert_eq!(workspace.zoomed, None);
8043            assert_eq!(workspace.zoomed_position, None);
8044        });
8045
8046        // When the panel is activated, it is zoomed again.
8047        cx.dispatch_action(ToggleRightDock);
8048        workspace.update(cx, |workspace, _| {
8049            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8050            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8051        });
8052
8053        // Emitting a ZoomOut event unzooms the panel.
8054        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8055        workspace.update(cx, |workspace, _| {
8056            assert_eq!(workspace.zoomed, None);
8057            assert_eq!(workspace.zoomed_position, None);
8058        });
8059
8060        // Emit closed event on panel 1, which is active
8061        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8062
8063        // Now the left dock is closed, because panel_1 was the active panel
8064        workspace.update(cx, |workspace, cx| {
8065            let right_dock = workspace.right_dock();
8066            assert!(!right_dock.read(cx).is_open());
8067        });
8068    }
8069
8070    #[gpui::test]
8071    async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8072        init_test(cx);
8073
8074        let fs = FakeFs::new(cx.background_executor.clone());
8075        let project = Project::test(fs, [], cx).await;
8076        let (workspace, cx) =
8077            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8078        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8079
8080        let dirty_regular_buffer = cx.new(|cx| {
8081            TestItem::new(cx)
8082                .with_dirty(true)
8083                .with_label("1.txt")
8084                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8085        });
8086        let dirty_regular_buffer_2 = cx.new(|cx| {
8087            TestItem::new(cx)
8088                .with_dirty(true)
8089                .with_label("2.txt")
8090                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8091        });
8092        let dirty_multi_buffer_with_both = cx.new(|cx| {
8093            TestItem::new(cx)
8094                .with_dirty(true)
8095                .with_singleton(false)
8096                .with_label("Fake Project Search")
8097                .with_project_items(&[
8098                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8099                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8100                ])
8101        });
8102        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8103        workspace.update_in(cx, |workspace, window, cx| {
8104            workspace.add_item(
8105                pane.clone(),
8106                Box::new(dirty_regular_buffer.clone()),
8107                None,
8108                false,
8109                false,
8110                window,
8111                cx,
8112            );
8113            workspace.add_item(
8114                pane.clone(),
8115                Box::new(dirty_regular_buffer_2.clone()),
8116                None,
8117                false,
8118                false,
8119                window,
8120                cx,
8121            );
8122            workspace.add_item(
8123                pane.clone(),
8124                Box::new(dirty_multi_buffer_with_both.clone()),
8125                None,
8126                false,
8127                false,
8128                window,
8129                cx,
8130            );
8131        });
8132
8133        pane.update_in(cx, |pane, window, cx| {
8134            pane.activate_item(2, true, true, window, cx);
8135            assert_eq!(
8136                pane.active_item().unwrap().item_id(),
8137                multi_buffer_with_both_files_id,
8138                "Should select the multi buffer in the pane"
8139            );
8140        });
8141        let close_all_but_multi_buffer_task = pane
8142            .update_in(cx, |pane, window, cx| {
8143                pane.close_inactive_items(
8144                    &CloseInactiveItems {
8145                        save_intent: Some(SaveIntent::Save),
8146                        close_pinned: true,
8147                    },
8148                    window,
8149                    cx,
8150                )
8151            })
8152            .expect("should have inactive files to close");
8153        cx.background_executor.run_until_parked();
8154        assert!(
8155            !cx.has_pending_prompt(),
8156            "Multi buffer still has the unsaved buffer inside, so no save prompt should be shown"
8157        );
8158        close_all_but_multi_buffer_task
8159            .await
8160            .expect("Closing all buffers but the multi buffer failed");
8161        pane.update(cx, |pane, cx| {
8162            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8163            assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8164            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8165            assert_eq!(pane.items_len(), 1);
8166            assert_eq!(
8167                pane.active_item().unwrap().item_id(),
8168                multi_buffer_with_both_files_id,
8169                "Should have only the multi buffer left in the pane"
8170            );
8171            assert!(
8172                dirty_multi_buffer_with_both.read(cx).is_dirty,
8173                "The multi buffer containing the unsaved buffer should still be dirty"
8174            );
8175        });
8176
8177        let close_multi_buffer_task = pane
8178            .update_in(cx, |pane, window, cx| {
8179                pane.close_active_item(
8180                    &CloseActiveItem {
8181                        save_intent: Some(SaveIntent::Close),
8182                        close_pinned: false,
8183                    },
8184                    window,
8185                    cx,
8186                )
8187            })
8188            .expect("should have the multi buffer to close");
8189        cx.background_executor.run_until_parked();
8190        assert!(
8191            cx.has_pending_prompt(),
8192            "Dirty multi buffer should prompt a save dialog"
8193        );
8194        cx.simulate_prompt_answer(0);
8195        cx.background_executor.run_until_parked();
8196        close_multi_buffer_task
8197            .await
8198            .expect("Closing the multi buffer failed");
8199        pane.update(cx, |pane, cx| {
8200            assert_eq!(
8201                dirty_multi_buffer_with_both.read(cx).save_count,
8202                1,
8203                "Multi buffer item should get be saved"
8204            );
8205            // Test impl does not save inner items, so we do not assert them
8206            assert_eq!(
8207                pane.items_len(),
8208                0,
8209                "No more items should be left in the pane"
8210            );
8211            assert!(pane.active_item().is_none());
8212        });
8213    }
8214
8215    #[gpui::test]
8216    async fn test_no_save_prompt_when_dirty_singleton_buffer_closed_with_a_multi_buffer_containing_it_present_in_the_pane(
8217        cx: &mut TestAppContext,
8218    ) {
8219        init_test(cx);
8220
8221        let fs = FakeFs::new(cx.background_executor.clone());
8222        let project = Project::test(fs, [], cx).await;
8223        let (workspace, cx) =
8224            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8225        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8226
8227        let dirty_regular_buffer = cx.new(|cx| {
8228            TestItem::new(cx)
8229                .with_dirty(true)
8230                .with_label("1.txt")
8231                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8232        });
8233        let dirty_regular_buffer_2 = cx.new(|cx| {
8234            TestItem::new(cx)
8235                .with_dirty(true)
8236                .with_label("2.txt")
8237                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8238        });
8239        let clear_regular_buffer = cx.new(|cx| {
8240            TestItem::new(cx)
8241                .with_label("3.txt")
8242                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8243        });
8244
8245        let dirty_multi_buffer_with_both = cx.new(|cx| {
8246            TestItem::new(cx)
8247                .with_dirty(true)
8248                .with_singleton(false)
8249                .with_label("Fake Project Search")
8250                .with_project_items(&[
8251                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8252                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8253                    clear_regular_buffer.read(cx).project_items[0].clone(),
8254                ])
8255        });
8256        workspace.update_in(cx, |workspace, window, cx| {
8257            workspace.add_item(
8258                pane.clone(),
8259                Box::new(dirty_regular_buffer.clone()),
8260                None,
8261                false,
8262                false,
8263                window,
8264                cx,
8265            );
8266            workspace.add_item(
8267                pane.clone(),
8268                Box::new(dirty_multi_buffer_with_both.clone()),
8269                None,
8270                false,
8271                false,
8272                window,
8273                cx,
8274            );
8275        });
8276
8277        pane.update_in(cx, |pane, window, cx| {
8278            pane.activate_item(0, true, true, window, cx);
8279            assert_eq!(
8280                pane.active_item().unwrap().item_id(),
8281                dirty_regular_buffer.item_id(),
8282                "Should select the dirty singleton buffer in the pane"
8283            );
8284        });
8285        let close_singleton_buffer_task = pane
8286            .update_in(cx, |pane, window, cx| {
8287                pane.close_active_item(
8288                    &CloseActiveItem {
8289                        save_intent: None,
8290                        close_pinned: false,
8291                    },
8292                    window,
8293                    cx,
8294                )
8295            })
8296            .expect("should have active singleton buffer to close");
8297        cx.background_executor.run_until_parked();
8298        assert!(
8299            !cx.has_pending_prompt(),
8300            "Multi buffer is still in the pane and has the unsaved buffer inside, so no save prompt should be shown"
8301        );
8302
8303        close_singleton_buffer_task
8304            .await
8305            .expect("Should not fail closing the singleton buffer");
8306        pane.update(cx, |pane, cx| {
8307            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8308            assert_eq!(
8309                dirty_multi_buffer_with_both.read(cx).save_count,
8310                0,
8311                "Multi buffer itself should not be saved"
8312            );
8313            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8314            assert_eq!(
8315                pane.items_len(),
8316                1,
8317                "A dirty multi buffer should be present in the pane"
8318            );
8319            assert_eq!(
8320                pane.active_item().unwrap().item_id(),
8321                dirty_multi_buffer_with_both.item_id(),
8322                "Should activate the only remaining item in the pane"
8323            );
8324        });
8325    }
8326
8327    #[gpui::test]
8328    async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
8329        cx: &mut TestAppContext,
8330    ) {
8331        init_test(cx);
8332
8333        let fs = FakeFs::new(cx.background_executor.clone());
8334        let project = Project::test(fs, [], cx).await;
8335        let (workspace, cx) =
8336            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8337        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8338
8339        let dirty_regular_buffer = cx.new(|cx| {
8340            TestItem::new(cx)
8341                .with_dirty(true)
8342                .with_label("1.txt")
8343                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8344        });
8345        let dirty_regular_buffer_2 = cx.new(|cx| {
8346            TestItem::new(cx)
8347                .with_dirty(true)
8348                .with_label("2.txt")
8349                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8350        });
8351        let clear_regular_buffer = cx.new(|cx| {
8352            TestItem::new(cx)
8353                .with_label("3.txt")
8354                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8355        });
8356
8357        let dirty_multi_buffer_with_both = cx.new(|cx| {
8358            TestItem::new(cx)
8359                .with_dirty(true)
8360                .with_singleton(false)
8361                .with_label("Fake Project Search")
8362                .with_project_items(&[
8363                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8364                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8365                    clear_regular_buffer.read(cx).project_items[0].clone(),
8366                ])
8367        });
8368        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8369        workspace.update_in(cx, |workspace, window, cx| {
8370            workspace.add_item(
8371                pane.clone(),
8372                Box::new(dirty_regular_buffer.clone()),
8373                None,
8374                false,
8375                false,
8376                window,
8377                cx,
8378            );
8379            workspace.add_item(
8380                pane.clone(),
8381                Box::new(dirty_multi_buffer_with_both.clone()),
8382                None,
8383                false,
8384                false,
8385                window,
8386                cx,
8387            );
8388        });
8389
8390        pane.update_in(cx, |pane, window, cx| {
8391            pane.activate_item(1, true, true, window, cx);
8392            assert_eq!(
8393                pane.active_item().unwrap().item_id(),
8394                multi_buffer_with_both_files_id,
8395                "Should select the multi buffer in the pane"
8396            );
8397        });
8398        let _close_multi_buffer_task = pane
8399            .update_in(cx, |pane, window, cx| {
8400                pane.close_active_item(
8401                    &CloseActiveItem {
8402                        save_intent: None,
8403                        close_pinned: false,
8404                    },
8405                    window,
8406                    cx,
8407                )
8408            })
8409            .expect("should have active multi buffer to close");
8410        cx.background_executor.run_until_parked();
8411        assert!(
8412            cx.has_pending_prompt(),
8413            "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
8414        );
8415    }
8416
8417    #[gpui::test]
8418    async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
8419        cx: &mut TestAppContext,
8420    ) {
8421        init_test(cx);
8422
8423        let fs = FakeFs::new(cx.background_executor.clone());
8424        let project = Project::test(fs, [], cx).await;
8425        let (workspace, cx) =
8426            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8427        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8428
8429        let dirty_regular_buffer = cx.new(|cx| {
8430            TestItem::new(cx)
8431                .with_dirty(true)
8432                .with_label("1.txt")
8433                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8434        });
8435        let dirty_regular_buffer_2 = cx.new(|cx| {
8436            TestItem::new(cx)
8437                .with_dirty(true)
8438                .with_label("2.txt")
8439                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8440        });
8441        let clear_regular_buffer = cx.new(|cx| {
8442            TestItem::new(cx)
8443                .with_label("3.txt")
8444                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8445        });
8446
8447        let dirty_multi_buffer = cx.new(|cx| {
8448            TestItem::new(cx)
8449                .with_dirty(true)
8450                .with_singleton(false)
8451                .with_label("Fake Project Search")
8452                .with_project_items(&[
8453                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8454                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8455                    clear_regular_buffer.read(cx).project_items[0].clone(),
8456                ])
8457        });
8458        workspace.update_in(cx, |workspace, window, cx| {
8459            workspace.add_item(
8460                pane.clone(),
8461                Box::new(dirty_regular_buffer.clone()),
8462                None,
8463                false,
8464                false,
8465                window,
8466                cx,
8467            );
8468            workspace.add_item(
8469                pane.clone(),
8470                Box::new(dirty_regular_buffer_2.clone()),
8471                None,
8472                false,
8473                false,
8474                window,
8475                cx,
8476            );
8477            workspace.add_item(
8478                pane.clone(),
8479                Box::new(dirty_multi_buffer.clone()),
8480                None,
8481                false,
8482                false,
8483                window,
8484                cx,
8485            );
8486        });
8487
8488        pane.update_in(cx, |pane, window, cx| {
8489            pane.activate_item(2, true, true, window, cx);
8490            assert_eq!(
8491                pane.active_item().unwrap().item_id(),
8492                dirty_multi_buffer.item_id(),
8493                "Should select the multi buffer in the pane"
8494            );
8495        });
8496        let close_multi_buffer_task = pane
8497            .update_in(cx, |pane, window, cx| {
8498                pane.close_active_item(
8499                    &CloseActiveItem {
8500                        save_intent: None,
8501                        close_pinned: false,
8502                    },
8503                    window,
8504                    cx,
8505                )
8506            })
8507            .expect("should have active multi buffer to close");
8508        cx.background_executor.run_until_parked();
8509        assert!(
8510            !cx.has_pending_prompt(),
8511            "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8512        );
8513        close_multi_buffer_task
8514            .await
8515            .expect("Closing multi buffer failed");
8516        pane.update(cx, |pane, cx| {
8517            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8518            assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8519            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8520            assert_eq!(
8521                pane.items()
8522                    .map(|item| item.item_id())
8523                    .sorted()
8524                    .collect::<Vec<_>>(),
8525                vec![
8526                    dirty_regular_buffer.item_id(),
8527                    dirty_regular_buffer_2.item_id(),
8528                ],
8529                "Should have no multi buffer left in the pane"
8530            );
8531            assert!(dirty_regular_buffer.read(cx).is_dirty);
8532            assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8533        });
8534    }
8535
8536    #[gpui::test]
8537    async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8538        init_test(cx);
8539        let fs = FakeFs::new(cx.executor());
8540        let project = Project::test(fs, [], cx).await;
8541        let (workspace, cx) =
8542            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8543
8544        // Add a new panel to the right dock, opening the dock and setting the
8545        // focus to the new panel.
8546        let panel = workspace.update_in(cx, |workspace, window, cx| {
8547            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8548            workspace.add_panel(panel.clone(), window, cx);
8549
8550            workspace
8551                .right_dock()
8552                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8553
8554            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8555
8556            panel
8557        });
8558
8559        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8560        // panel to the next valid position which, in this case, is the left
8561        // dock.
8562        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8563        workspace.update(cx, |workspace, cx| {
8564            assert!(workspace.left_dock().read(cx).is_open());
8565            assert_eq!(panel.read(cx).position, DockPosition::Left);
8566        });
8567
8568        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8569        // panel to the next valid position which, in this case, is the bottom
8570        // dock.
8571        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8572        workspace.update(cx, |workspace, cx| {
8573            assert!(workspace.bottom_dock().read(cx).is_open());
8574            assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8575        });
8576
8577        // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8578        // around moving the panel to its initial position, the right dock.
8579        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8580        workspace.update(cx, |workspace, cx| {
8581            assert!(workspace.right_dock().read(cx).is_open());
8582            assert_eq!(panel.read(cx).position, DockPosition::Right);
8583        });
8584
8585        // Remove focus from the panel, ensuring that, if the panel is not
8586        // focused, the `MoveFocusedPanelToNextPosition` action does not update
8587        // the panel's position, so the panel is still in the right dock.
8588        workspace.update_in(cx, |workspace, window, cx| {
8589            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8590        });
8591
8592        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8593        workspace.update(cx, |workspace, cx| {
8594            assert!(workspace.right_dock().read(cx).is_open());
8595            assert_eq!(panel.read(cx).position, DockPosition::Right);
8596        });
8597    }
8598
8599    mod register_project_item_tests {
8600
8601        use super::*;
8602
8603        // View
8604        struct TestPngItemView {
8605            focus_handle: FocusHandle,
8606        }
8607        // Model
8608        struct TestPngItem {}
8609
8610        impl project::ProjectItem for TestPngItem {
8611            fn try_open(
8612                _project: &Entity<Project>,
8613                path: &ProjectPath,
8614                cx: &mut App,
8615            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8616                if path.path.extension().unwrap() == "png" {
8617                    Some(cx.spawn(|mut cx| async move { cx.new(|_| TestPngItem {}) }))
8618                } else {
8619                    None
8620                }
8621            }
8622
8623            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8624                None
8625            }
8626
8627            fn project_path(&self, _: &App) -> Option<ProjectPath> {
8628                None
8629            }
8630
8631            fn is_dirty(&self) -> bool {
8632                false
8633            }
8634        }
8635
8636        impl Item for TestPngItemView {
8637            type Event = ();
8638        }
8639        impl EventEmitter<()> for TestPngItemView {}
8640        impl Focusable for TestPngItemView {
8641            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8642                self.focus_handle.clone()
8643            }
8644        }
8645
8646        impl Render for TestPngItemView {
8647            fn render(
8648                &mut self,
8649                _window: &mut Window,
8650                _cx: &mut Context<Self>,
8651            ) -> impl IntoElement {
8652                Empty
8653            }
8654        }
8655
8656        impl ProjectItem for TestPngItemView {
8657            type Item = TestPngItem;
8658
8659            fn for_project_item(
8660                _project: Entity<Project>,
8661                _item: Entity<Self::Item>,
8662                _: &mut Window,
8663                cx: &mut Context<Self>,
8664            ) -> Self
8665            where
8666                Self: Sized,
8667            {
8668                Self {
8669                    focus_handle: cx.focus_handle(),
8670                }
8671            }
8672        }
8673
8674        // View
8675        struct TestIpynbItemView {
8676            focus_handle: FocusHandle,
8677        }
8678        // Model
8679        struct TestIpynbItem {}
8680
8681        impl project::ProjectItem for TestIpynbItem {
8682            fn try_open(
8683                _project: &Entity<Project>,
8684                path: &ProjectPath,
8685                cx: &mut App,
8686            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8687                if path.path.extension().unwrap() == "ipynb" {
8688                    Some(cx.spawn(|mut cx| async move { cx.new(|_| TestIpynbItem {}) }))
8689                } else {
8690                    None
8691                }
8692            }
8693
8694            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8695                None
8696            }
8697
8698            fn project_path(&self, _: &App) -> Option<ProjectPath> {
8699                None
8700            }
8701
8702            fn is_dirty(&self) -> bool {
8703                false
8704            }
8705        }
8706
8707        impl Item for TestIpynbItemView {
8708            type Event = ();
8709        }
8710        impl EventEmitter<()> for TestIpynbItemView {}
8711        impl Focusable for TestIpynbItemView {
8712            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8713                self.focus_handle.clone()
8714            }
8715        }
8716
8717        impl Render for TestIpynbItemView {
8718            fn render(
8719                &mut self,
8720                _window: &mut Window,
8721                _cx: &mut Context<Self>,
8722            ) -> impl IntoElement {
8723                Empty
8724            }
8725        }
8726
8727        impl ProjectItem for TestIpynbItemView {
8728            type Item = TestIpynbItem;
8729
8730            fn for_project_item(
8731                _project: Entity<Project>,
8732                _item: Entity<Self::Item>,
8733                _: &mut Window,
8734                cx: &mut Context<Self>,
8735            ) -> Self
8736            where
8737                Self: Sized,
8738            {
8739                Self {
8740                    focus_handle: cx.focus_handle(),
8741                }
8742            }
8743        }
8744
8745        struct TestAlternatePngItemView {
8746            focus_handle: FocusHandle,
8747        }
8748
8749        impl Item for TestAlternatePngItemView {
8750            type Event = ();
8751        }
8752
8753        impl EventEmitter<()> for TestAlternatePngItemView {}
8754        impl Focusable for TestAlternatePngItemView {
8755            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8756                self.focus_handle.clone()
8757            }
8758        }
8759
8760        impl Render for TestAlternatePngItemView {
8761            fn render(
8762                &mut self,
8763                _window: &mut Window,
8764                _cx: &mut Context<Self>,
8765            ) -> impl IntoElement {
8766                Empty
8767            }
8768        }
8769
8770        impl ProjectItem for TestAlternatePngItemView {
8771            type Item = TestPngItem;
8772
8773            fn for_project_item(
8774                _project: Entity<Project>,
8775                _item: Entity<Self::Item>,
8776                _: &mut Window,
8777                cx: &mut Context<Self>,
8778            ) -> Self
8779            where
8780                Self: Sized,
8781            {
8782                Self {
8783                    focus_handle: cx.focus_handle(),
8784                }
8785            }
8786        }
8787
8788        #[gpui::test]
8789        async fn test_register_project_item(cx: &mut TestAppContext) {
8790            init_test(cx);
8791
8792            cx.update(|cx| {
8793                register_project_item::<TestPngItemView>(cx);
8794                register_project_item::<TestIpynbItemView>(cx);
8795            });
8796
8797            let fs = FakeFs::new(cx.executor());
8798            fs.insert_tree(
8799                "/root1",
8800                json!({
8801                    "one.png": "BINARYDATAHERE",
8802                    "two.ipynb": "{ totally a notebook }",
8803                    "three.txt": "editing text, sure why not?"
8804                }),
8805            )
8806            .await;
8807
8808            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8809            let (workspace, cx) =
8810                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8811
8812            let worktree_id = project.update(cx, |project, cx| {
8813                project.worktrees(cx).next().unwrap().read(cx).id()
8814            });
8815
8816            let handle = workspace
8817                .update_in(cx, |workspace, window, cx| {
8818                    let project_path = (worktree_id, "one.png");
8819                    workspace.open_path(project_path, None, true, window, cx)
8820                })
8821                .await
8822                .unwrap();
8823
8824            // Now we can check if the handle we got back errored or not
8825            assert_eq!(
8826                handle.to_any().entity_type(),
8827                TypeId::of::<TestPngItemView>()
8828            );
8829
8830            let handle = workspace
8831                .update_in(cx, |workspace, window, cx| {
8832                    let project_path = (worktree_id, "two.ipynb");
8833                    workspace.open_path(project_path, None, true, window, cx)
8834                })
8835                .await
8836                .unwrap();
8837
8838            assert_eq!(
8839                handle.to_any().entity_type(),
8840                TypeId::of::<TestIpynbItemView>()
8841            );
8842
8843            let handle = workspace
8844                .update_in(cx, |workspace, window, cx| {
8845                    let project_path = (worktree_id, "three.txt");
8846                    workspace.open_path(project_path, None, true, window, cx)
8847                })
8848                .await;
8849            assert!(handle.is_err());
8850        }
8851
8852        #[gpui::test]
8853        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8854            init_test(cx);
8855
8856            cx.update(|cx| {
8857                register_project_item::<TestPngItemView>(cx);
8858                register_project_item::<TestAlternatePngItemView>(cx);
8859            });
8860
8861            let fs = FakeFs::new(cx.executor());
8862            fs.insert_tree(
8863                "/root1",
8864                json!({
8865                    "one.png": "BINARYDATAHERE",
8866                    "two.ipynb": "{ totally a notebook }",
8867                    "three.txt": "editing text, sure why not?"
8868                }),
8869            )
8870            .await;
8871            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8872            let (workspace, cx) =
8873                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8874            let worktree_id = project.update(cx, |project, cx| {
8875                project.worktrees(cx).next().unwrap().read(cx).id()
8876            });
8877
8878            let handle = workspace
8879                .update_in(cx, |workspace, window, cx| {
8880                    let project_path = (worktree_id, "one.png");
8881                    workspace.open_path(project_path, None, true, window, cx)
8882                })
8883                .await
8884                .unwrap();
8885
8886            // This _must_ be the second item registered
8887            assert_eq!(
8888                handle.to_any().entity_type(),
8889                TypeId::of::<TestAlternatePngItemView>()
8890            );
8891
8892            let handle = workspace
8893                .update_in(cx, |workspace, window, cx| {
8894                    let project_path = (worktree_id, "three.txt");
8895                    workspace.open_path(project_path, None, true, window, cx)
8896                })
8897                .await;
8898            assert!(handle.is_err());
8899        }
8900    }
8901
8902    pub fn init_test(cx: &mut TestAppContext) {
8903        cx.update(|cx| {
8904            let settings_store = SettingsStore::test(cx);
8905            cx.set_global(settings_store);
8906            theme::init(theme::LoadThemes::JustBase, cx);
8907            language::init(cx);
8908            crate::init_settings(cx);
8909            Project::init_settings(cx);
8910        });
8911    }
8912
8913    fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
8914        let item = TestProjectItem::new(id, path, cx);
8915        item.update(cx, |item, _| {
8916            item.is_dirty = true;
8917        });
8918        item
8919    }
8920}