workspace.rs

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