workspace.rs

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