workspace.rs

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