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