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