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