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