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(
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        let toolchains = DB.toolchains(workspace_id).await?;
6324        for (toolchain, worktree_id, path) in toolchains {
6325            project
6326                .update(cx, |this, cx| {
6327                    this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
6328                })?
6329                .await;
6330        }
6331        let mut project_paths_to_open = vec![];
6332        let mut project_path_errors = vec![];
6333
6334        for path in paths {
6335            let result = cx
6336                .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6337                .await;
6338            match result {
6339                Ok((_, project_path)) => {
6340                    project_paths_to_open.push((path.clone(), Some(project_path)));
6341                }
6342                Err(error) => {
6343                    project_path_errors.push(error);
6344                }
6345            };
6346        }
6347
6348        if project_paths_to_open.is_empty() {
6349            return Err(project_path_errors
6350                .pop()
6351                .unwrap_or_else(|| anyhow!("no paths given")));
6352        }
6353
6354        cx.update_window(window.into(), |_, window, cx| {
6355            window.replace_root(cx, |window, cx| {
6356                telemetry::event!("SSH Project Opened");
6357
6358                let mut workspace =
6359                    Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6360                workspace.set_serialized_ssh_project(serialized_ssh_project);
6361                workspace
6362            });
6363        })?;
6364
6365        window
6366            .update(cx, |_, window, cx| {
6367                window.activate_window();
6368
6369                open_items(serialized_workspace, project_paths_to_open, window, cx)
6370            })?
6371            .await?;
6372
6373        window.update(cx, |workspace, _, cx| {
6374            for error in project_path_errors {
6375                if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6376                    if let Some(path) = error.error_tag("path") {
6377                        workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6378                    }
6379                } else {
6380                    workspace.show_error(&error, cx)
6381                }
6382            }
6383        })
6384    })
6385}
6386
6387fn serialize_ssh_project(
6388    connection_options: SshConnectionOptions,
6389    paths: Vec<PathBuf>,
6390    cx: &AsyncApp,
6391) -> Task<
6392    Result<(
6393        SerializedSshProject,
6394        WorkspaceId,
6395        Option<SerializedWorkspace>,
6396    )>,
6397> {
6398    cx.background_spawn(async move {
6399        let serialized_ssh_project = persistence::DB
6400            .get_or_create_ssh_project(
6401                connection_options.host.clone(),
6402                connection_options.port,
6403                paths
6404                    .iter()
6405                    .map(|path| path.to_string_lossy().to_string())
6406                    .collect::<Vec<_>>(),
6407                connection_options.username.clone(),
6408            )
6409            .await?;
6410
6411        let serialized_workspace =
6412            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6413
6414        let workspace_id = if let Some(workspace_id) =
6415            serialized_workspace.as_ref().map(|workspace| workspace.id)
6416        {
6417            workspace_id
6418        } else {
6419            persistence::DB.next_id().await?
6420        };
6421
6422        Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6423    })
6424}
6425
6426pub fn join_in_room_project(
6427    project_id: u64,
6428    follow_user_id: u64,
6429    app_state: Arc<AppState>,
6430    cx: &mut App,
6431) -> Task<Result<()>> {
6432    let windows = cx.windows();
6433    cx.spawn(async move |cx| {
6434        let existing_workspace = windows.into_iter().find_map(|window_handle| {
6435            window_handle
6436                .downcast::<Workspace>()
6437                .and_then(|window_handle| {
6438                    window_handle
6439                        .update(cx, |workspace, _window, cx| {
6440                            if workspace.project().read(cx).remote_id() == Some(project_id) {
6441                                Some(window_handle)
6442                            } else {
6443                                None
6444                            }
6445                        })
6446                        .unwrap_or(None)
6447                })
6448        });
6449
6450        let workspace = if let Some(existing_workspace) = existing_workspace {
6451            existing_workspace
6452        } else {
6453            let active_call = cx.update(|cx| ActiveCall::global(cx))?;
6454            let room = active_call
6455                .read_with(cx, |call, _| call.room().cloned())?
6456                .ok_or_else(|| anyhow!("not in a call"))?;
6457            let project = room
6458                .update(cx, |room, cx| {
6459                    room.join_project(
6460                        project_id,
6461                        app_state.languages.clone(),
6462                        app_state.fs.clone(),
6463                        cx,
6464                    )
6465                })?
6466                .await?;
6467
6468            let window_bounds_override = window_bounds_env_override();
6469            cx.update(|cx| {
6470                let mut options = (app_state.build_window_options)(None, cx);
6471                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
6472                cx.open_window(options, |window, cx| {
6473                    cx.new(|cx| {
6474                        Workspace::new(Default::default(), project, app_state.clone(), window, cx)
6475                    })
6476                })
6477            })??
6478        };
6479
6480        workspace.update(cx, |workspace, window, cx| {
6481            cx.activate(true);
6482            window.activate_window();
6483
6484            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
6485                let follow_peer_id = room
6486                    .read(cx)
6487                    .remote_participants()
6488                    .iter()
6489                    .find(|(_, participant)| participant.user.id == follow_user_id)
6490                    .map(|(_, p)| p.peer_id)
6491                    .or_else(|| {
6492                        // If we couldn't follow the given user, follow the host instead.
6493                        let collaborator = workspace
6494                            .project()
6495                            .read(cx)
6496                            .collaborators()
6497                            .values()
6498                            .find(|collaborator| collaborator.is_host)?;
6499                        Some(collaborator.peer_id)
6500                    });
6501
6502                if let Some(follow_peer_id) = follow_peer_id {
6503                    workspace.follow(follow_peer_id, window, cx);
6504                }
6505            }
6506        })?;
6507
6508        anyhow::Ok(())
6509    })
6510}
6511
6512pub fn reload(reload: &Reload, cx: &mut App) {
6513    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
6514    let mut workspace_windows = cx
6515        .windows()
6516        .into_iter()
6517        .filter_map(|window| window.downcast::<Workspace>())
6518        .collect::<Vec<_>>();
6519
6520    // If multiple windows have unsaved changes, and need a save prompt,
6521    // prompt in the active window before switching to a different window.
6522    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
6523
6524    let mut prompt = None;
6525    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6526        prompt = window
6527            .update(cx, |_, window, cx| {
6528                window.prompt(
6529                    PromptLevel::Info,
6530                    "Are you sure you want to restart?",
6531                    None,
6532                    &["Restart", "Cancel"],
6533                    cx,
6534                )
6535            })
6536            .ok();
6537    }
6538
6539    let binary_path = reload.binary_path.clone();
6540    cx.spawn(async move |cx| {
6541        if let Some(prompt) = prompt {
6542            let answer = prompt.await?;
6543            if answer != 0 {
6544                return Ok(());
6545            }
6546        }
6547
6548        // If the user cancels any save prompt, then keep the app open.
6549        for window in workspace_windows {
6550            if let Ok(should_close) = window.update(cx, |workspace, window, cx| {
6551                workspace.prepare_to_close(CloseIntent::Quit, window, cx)
6552            }) {
6553                if !should_close.await? {
6554                    return Ok(());
6555                }
6556            }
6557        }
6558
6559        cx.update(|cx| cx.restart(binary_path))
6560    })
6561    .detach_and_log_err(cx);
6562}
6563
6564fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6565    let mut parts = value.split(',');
6566    let x: usize = parts.next()?.parse().ok()?;
6567    let y: usize = parts.next()?.parse().ok()?;
6568    Some(point(px(x as f32), px(y as f32)))
6569}
6570
6571fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6572    let mut parts = value.split(',');
6573    let width: usize = parts.next()?.parse().ok()?;
6574    let height: usize = parts.next()?.parse().ok()?;
6575    Some(size(px(width as f32), px(height as f32)))
6576}
6577
6578pub fn client_side_decorations(
6579    element: impl IntoElement,
6580    window: &mut Window,
6581    cx: &mut App,
6582) -> Stateful<Div> {
6583    const BORDER_SIZE: Pixels = px(1.0);
6584    let decorations = window.window_decorations();
6585
6586    if matches!(decorations, Decorations::Client { .. }) {
6587        window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6588    }
6589
6590    struct GlobalResizeEdge(ResizeEdge);
6591    impl Global for GlobalResizeEdge {}
6592
6593    div()
6594        .id("window-backdrop")
6595        .bg(transparent_black())
6596        .map(|div| match decorations {
6597            Decorations::Server => div,
6598            Decorations::Client { tiling, .. } => div
6599                .when(!(tiling.top || tiling.right), |div| {
6600                    div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6601                })
6602                .when(!(tiling.top || tiling.left), |div| {
6603                    div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6604                })
6605                .when(!(tiling.bottom || tiling.right), |div| {
6606                    div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6607                })
6608                .when(!(tiling.bottom || tiling.left), |div| {
6609                    div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6610                })
6611                .when(!tiling.top, |div| {
6612                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6613                })
6614                .when(!tiling.bottom, |div| {
6615                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6616                })
6617                .when(!tiling.left, |div| {
6618                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6619                })
6620                .when(!tiling.right, |div| {
6621                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6622                })
6623                .on_mouse_move(move |e, window, cx| {
6624                    let size = window.window_bounds().get_bounds().size;
6625                    let pos = e.position;
6626
6627                    let new_edge =
6628                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6629
6630                    let edge = cx.try_global::<GlobalResizeEdge>();
6631                    if new_edge != edge.map(|edge| edge.0) {
6632                        window
6633                            .window_handle()
6634                            .update(cx, |workspace, _, cx| {
6635                                cx.notify(workspace.entity_id());
6636                            })
6637                            .ok();
6638                    }
6639                })
6640                .on_mouse_down(MouseButton::Left, move |e, window, _| {
6641                    let size = window.window_bounds().get_bounds().size;
6642                    let pos = e.position;
6643
6644                    let edge = match resize_edge(
6645                        pos,
6646                        theme::CLIENT_SIDE_DECORATION_SHADOW,
6647                        size,
6648                        tiling,
6649                    ) {
6650                        Some(value) => value,
6651                        None => return,
6652                    };
6653
6654                    window.start_window_resize(edge);
6655                }),
6656        })
6657        .size_full()
6658        .child(
6659            div()
6660                .cursor(CursorStyle::Arrow)
6661                .map(|div| match decorations {
6662                    Decorations::Server => div,
6663                    Decorations::Client { tiling } => div
6664                        .border_color(cx.theme().colors().border)
6665                        .when(!(tiling.top || tiling.right), |div| {
6666                            div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6667                        })
6668                        .when(!(tiling.top || tiling.left), |div| {
6669                            div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6670                        })
6671                        .when(!(tiling.bottom || tiling.right), |div| {
6672                            div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6673                        })
6674                        .when(!(tiling.bottom || tiling.left), |div| {
6675                            div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6676                        })
6677                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6678                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6679                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6680                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6681                        .when(!tiling.is_tiled(), |div| {
6682                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
6683                                color: Hsla {
6684                                    h: 0.,
6685                                    s: 0.,
6686                                    l: 0.,
6687                                    a: 0.4,
6688                                },
6689                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6690                                spread_radius: px(0.),
6691                                offset: point(px(0.0), px(0.0)),
6692                            }])
6693                        }),
6694                })
6695                .on_mouse_move(|_e, _, cx| {
6696                    cx.stop_propagation();
6697                })
6698                .size_full()
6699                .child(element),
6700        )
6701        .map(|div| match decorations {
6702            Decorations::Server => div,
6703            Decorations::Client { tiling, .. } => div.child(
6704                canvas(
6705                    |_bounds, window, _| {
6706                        window.insert_hitbox(
6707                            Bounds::new(
6708                                point(px(0.0), px(0.0)),
6709                                window.window_bounds().get_bounds().size,
6710                            ),
6711                            false,
6712                        )
6713                    },
6714                    move |_bounds, hitbox, window, cx| {
6715                        let mouse = window.mouse_position();
6716                        let size = window.window_bounds().get_bounds().size;
6717                        let Some(edge) =
6718                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6719                        else {
6720                            return;
6721                        };
6722                        cx.set_global(GlobalResizeEdge(edge));
6723                        window.set_cursor_style(
6724                            match edge {
6725                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6726                                ResizeEdge::Left | ResizeEdge::Right => {
6727                                    CursorStyle::ResizeLeftRight
6728                                }
6729                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6730                                    CursorStyle::ResizeUpLeftDownRight
6731                                }
6732                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6733                                    CursorStyle::ResizeUpRightDownLeft
6734                                }
6735                            },
6736                            Some(&hitbox),
6737                        );
6738                    },
6739                )
6740                .size_full()
6741                .absolute(),
6742            ),
6743        })
6744}
6745
6746fn resize_edge(
6747    pos: Point<Pixels>,
6748    shadow_size: Pixels,
6749    window_size: Size<Pixels>,
6750    tiling: Tiling,
6751) -> Option<ResizeEdge> {
6752    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6753    if bounds.contains(&pos) {
6754        return None;
6755    }
6756
6757    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6758    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6759    if !tiling.top && top_left_bounds.contains(&pos) {
6760        return Some(ResizeEdge::TopLeft);
6761    }
6762
6763    let top_right_bounds = Bounds::new(
6764        Point::new(window_size.width - corner_size.width, px(0.)),
6765        corner_size,
6766    );
6767    if !tiling.top && top_right_bounds.contains(&pos) {
6768        return Some(ResizeEdge::TopRight);
6769    }
6770
6771    let bottom_left_bounds = Bounds::new(
6772        Point::new(px(0.), window_size.height - corner_size.height),
6773        corner_size,
6774    );
6775    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6776        return Some(ResizeEdge::BottomLeft);
6777    }
6778
6779    let bottom_right_bounds = Bounds::new(
6780        Point::new(
6781            window_size.width - corner_size.width,
6782            window_size.height - corner_size.height,
6783        ),
6784        corner_size,
6785    );
6786    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6787        return Some(ResizeEdge::BottomRight);
6788    }
6789
6790    if !tiling.top && pos.y < shadow_size {
6791        Some(ResizeEdge::Top)
6792    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6793        Some(ResizeEdge::Bottom)
6794    } else if !tiling.left && pos.x < shadow_size {
6795        Some(ResizeEdge::Left)
6796    } else if !tiling.right && pos.x > window_size.width - shadow_size {
6797        Some(ResizeEdge::Right)
6798    } else {
6799        None
6800    }
6801}
6802
6803fn join_pane_into_active(
6804    active_pane: &Entity<Pane>,
6805    pane: &Entity<Pane>,
6806    window: &mut Window,
6807    cx: &mut App,
6808) {
6809    if pane == active_pane {
6810        return;
6811    } else if pane.read(cx).items_len() == 0 {
6812        pane.update(cx, |_, cx| {
6813            cx.emit(pane::Event::Remove {
6814                focus_on_pane: None,
6815            });
6816        })
6817    } else {
6818        move_all_items(pane, active_pane, window, cx);
6819    }
6820}
6821
6822fn move_all_items(
6823    from_pane: &Entity<Pane>,
6824    to_pane: &Entity<Pane>,
6825    window: &mut Window,
6826    cx: &mut App,
6827) {
6828    let destination_is_different = from_pane != to_pane;
6829    let mut moved_items = 0;
6830    for (item_ix, item_handle) in from_pane
6831        .read(cx)
6832        .items()
6833        .enumerate()
6834        .map(|(ix, item)| (ix, item.clone()))
6835        .collect::<Vec<_>>()
6836    {
6837        let ix = item_ix - moved_items;
6838        if destination_is_different {
6839            // Close item from previous pane
6840            from_pane.update(cx, |source, cx| {
6841                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
6842            });
6843            moved_items += 1;
6844        }
6845
6846        // This automatically removes duplicate items in the pane
6847        to_pane.update(cx, |destination, cx| {
6848            destination.add_item(item_handle, true, true, None, window, cx);
6849            window.focus(&destination.focus_handle(cx))
6850        });
6851    }
6852}
6853
6854pub fn move_item(
6855    source: &Entity<Pane>,
6856    destination: &Entity<Pane>,
6857    item_id_to_move: EntityId,
6858    destination_index: usize,
6859    window: &mut Window,
6860    cx: &mut App,
6861) {
6862    let Some((item_ix, item_handle)) = source
6863        .read(cx)
6864        .items()
6865        .enumerate()
6866        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6867        .map(|(ix, item)| (ix, item.clone()))
6868    else {
6869        // Tab was closed during drag
6870        return;
6871    };
6872
6873    if source != destination {
6874        // Close item from previous pane
6875        source.update(cx, |source, cx| {
6876            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
6877        });
6878    }
6879
6880    // This automatically removes duplicate items in the pane
6881    destination.update(cx, |destination, cx| {
6882        destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
6883        window.focus(&destination.focus_handle(cx))
6884    });
6885}
6886
6887pub fn move_active_item(
6888    source: &Entity<Pane>,
6889    destination: &Entity<Pane>,
6890    focus_destination: bool,
6891    close_if_empty: bool,
6892    window: &mut Window,
6893    cx: &mut App,
6894) {
6895    if source == destination {
6896        return;
6897    }
6898    let Some(active_item) = source.read(cx).active_item() else {
6899        return;
6900    };
6901    source.update(cx, |source_pane, cx| {
6902        let item_id = active_item.item_id();
6903        source_pane.remove_item(item_id, false, close_if_empty, window, cx);
6904        destination.update(cx, |target_pane, cx| {
6905            target_pane.add_item(
6906                active_item,
6907                focus_destination,
6908                focus_destination,
6909                Some(target_pane.items_len()),
6910                window,
6911                cx,
6912            );
6913        });
6914    });
6915}
6916
6917#[cfg(test)]
6918mod tests {
6919    use std::{cell::RefCell, rc::Rc};
6920
6921    use super::*;
6922    use crate::{
6923        dock::{PanelEvent, test::TestPanel},
6924        item::{
6925            ItemEvent,
6926            test::{TestItem, TestProjectItem},
6927        },
6928    };
6929    use fs::FakeFs;
6930    use gpui::{
6931        DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
6932        UpdateGlobal, VisualTestContext, px,
6933    };
6934    use project::{Project, ProjectEntryId};
6935    use serde_json::json;
6936    use settings::SettingsStore;
6937
6938    #[gpui::test]
6939    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6940        init_test(cx);
6941
6942        let fs = FakeFs::new(cx.executor());
6943        let project = Project::test(fs, [], cx).await;
6944        let (workspace, cx) =
6945            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6946
6947        // Adding an item with no ambiguity renders the tab without detail.
6948        let item1 = cx.new(|cx| {
6949            let mut item = TestItem::new(cx);
6950            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6951            item
6952        });
6953        workspace.update_in(cx, |workspace, window, cx| {
6954            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6955        });
6956        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6957
6958        // Adding an item that creates ambiguity increases the level of detail on
6959        // both tabs.
6960        let item2 = cx.new_window_entity(|_window, cx| {
6961            let mut item = TestItem::new(cx);
6962            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6963            item
6964        });
6965        workspace.update_in(cx, |workspace, window, cx| {
6966            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6967        });
6968        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6969        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6970
6971        // Adding an item that creates ambiguity increases the level of detail only
6972        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6973        // we stop at the highest detail available.
6974        let item3 = cx.new(|cx| {
6975            let mut item = TestItem::new(cx);
6976            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6977            item
6978        });
6979        workspace.update_in(cx, |workspace, window, cx| {
6980            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6981        });
6982        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6983        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6984        item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6985    }
6986
6987    #[gpui::test]
6988    async fn test_tracking_active_path(cx: &mut TestAppContext) {
6989        init_test(cx);
6990
6991        let fs = FakeFs::new(cx.executor());
6992        fs.insert_tree(
6993            "/root1",
6994            json!({
6995                "one.txt": "",
6996                "two.txt": "",
6997            }),
6998        )
6999        .await;
7000        fs.insert_tree(
7001            "/root2",
7002            json!({
7003                "three.txt": "",
7004            }),
7005        )
7006        .await;
7007
7008        let project = Project::test(fs, ["root1".as_ref()], cx).await;
7009        let (workspace, cx) =
7010            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7011        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7012        let worktree_id = project.update(cx, |project, cx| {
7013            project.worktrees(cx).next().unwrap().read(cx).id()
7014        });
7015
7016        let item1 = cx.new(|cx| {
7017            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
7018        });
7019        let item2 = cx.new(|cx| {
7020            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
7021        });
7022
7023        // Add an item to an empty pane
7024        workspace.update_in(cx, |workspace, window, cx| {
7025            workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
7026        });
7027        project.update(cx, |project, cx| {
7028            assert_eq!(
7029                project.active_entry(),
7030                project
7031                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7032                    .map(|e| e.id)
7033            );
7034        });
7035        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7036
7037        // Add a second item to a non-empty pane
7038        workspace.update_in(cx, |workspace, window, cx| {
7039            workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
7040        });
7041        assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
7042        project.update(cx, |project, cx| {
7043            assert_eq!(
7044                project.active_entry(),
7045                project
7046                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
7047                    .map(|e| e.id)
7048            );
7049        });
7050
7051        // Close the active item
7052        pane.update_in(cx, |pane, window, cx| {
7053            pane.close_active_item(&Default::default(), window, cx)
7054                .unwrap()
7055        })
7056        .await
7057        .unwrap();
7058        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7059        project.update(cx, |project, cx| {
7060            assert_eq!(
7061                project.active_entry(),
7062                project
7063                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7064                    .map(|e| e.id)
7065            );
7066        });
7067
7068        // Add a project folder
7069        project
7070            .update(cx, |project, cx| {
7071                project.find_or_create_worktree("root2", true, cx)
7072            })
7073            .await
7074            .unwrap();
7075        assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
7076
7077        // Remove a project folder
7078        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
7079        assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
7080    }
7081
7082    #[gpui::test]
7083    async fn test_close_window(cx: &mut TestAppContext) {
7084        init_test(cx);
7085
7086        let fs = FakeFs::new(cx.executor());
7087        fs.insert_tree("/root", json!({ "one": "" })).await;
7088
7089        let project = Project::test(fs, ["root".as_ref()], cx).await;
7090        let (workspace, cx) =
7091            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7092
7093        // When there are no dirty items, there's nothing to do.
7094        let item1 = cx.new(TestItem::new);
7095        workspace.update_in(cx, |w, window, cx| {
7096            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
7097        });
7098        let task = workspace.update_in(cx, |w, window, cx| {
7099            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7100        });
7101        assert!(task.await.unwrap());
7102
7103        // When there are dirty untitled items, prompt to save each one. If the user
7104        // cancels any prompt, then abort.
7105        let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
7106        let item3 = cx.new(|cx| {
7107            TestItem::new(cx)
7108                .with_dirty(true)
7109                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7110        });
7111        workspace.update_in(cx, |w, window, cx| {
7112            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7113            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7114        });
7115        let task = workspace.update_in(cx, |w, window, cx| {
7116            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7117        });
7118        cx.executor().run_until_parked();
7119        cx.simulate_prompt_answer("Cancel"); // cancel save all
7120        cx.executor().run_until_parked();
7121        assert!(!cx.has_pending_prompt());
7122        assert!(!task.await.unwrap());
7123    }
7124
7125    #[gpui::test]
7126    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
7127        init_test(cx);
7128
7129        // Register TestItem as a serializable item
7130        cx.update(|cx| {
7131            register_serializable_item::<TestItem>(cx);
7132        });
7133
7134        let fs = FakeFs::new(cx.executor());
7135        fs.insert_tree("/root", json!({ "one": "" })).await;
7136
7137        let project = Project::test(fs, ["root".as_ref()], cx).await;
7138        let (workspace, cx) =
7139            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7140
7141        // When there are dirty untitled items, but they can serialize, then there is no prompt.
7142        let item1 = cx.new(|cx| {
7143            TestItem::new(cx)
7144                .with_dirty(true)
7145                .with_serialize(|| Some(Task::ready(Ok(()))))
7146        });
7147        let item2 = cx.new(|cx| {
7148            TestItem::new(cx)
7149                .with_dirty(true)
7150                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7151                .with_serialize(|| Some(Task::ready(Ok(()))))
7152        });
7153        workspace.update_in(cx, |w, window, cx| {
7154            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7155            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7156        });
7157        let task = workspace.update_in(cx, |w, window, cx| {
7158            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7159        });
7160        assert!(task.await.unwrap());
7161    }
7162
7163    #[gpui::test]
7164    async fn test_close_pane_items(cx: &mut TestAppContext) {
7165        init_test(cx);
7166
7167        let fs = FakeFs::new(cx.executor());
7168
7169        let project = Project::test(fs, None, cx).await;
7170        let (workspace, cx) =
7171            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7172
7173        let item1 = cx.new(|cx| {
7174            TestItem::new(cx)
7175                .with_dirty(true)
7176                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7177        });
7178        let item2 = cx.new(|cx| {
7179            TestItem::new(cx)
7180                .with_dirty(true)
7181                .with_conflict(true)
7182                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7183        });
7184        let item3 = cx.new(|cx| {
7185            TestItem::new(cx)
7186                .with_dirty(true)
7187                .with_conflict(true)
7188                .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7189        });
7190        let item4 = cx.new(|cx| {
7191            TestItem::new(cx).with_dirty(true).with_project_items(&[{
7192                let project_item = TestProjectItem::new_untitled(cx);
7193                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7194                project_item
7195            }])
7196        });
7197        let pane = workspace.update_in(cx, |workspace, window, cx| {
7198            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7199            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7200            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7201            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7202            workspace.active_pane().clone()
7203        });
7204
7205        let close_items = pane.update_in(cx, |pane, window, cx| {
7206            pane.activate_item(1, true, true, window, cx);
7207            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7208            let item1_id = item1.item_id();
7209            let item3_id = item3.item_id();
7210            let item4_id = item4.item_id();
7211            pane.close_items(window, cx, SaveIntent::Close, move |id| {
7212                [item1_id, item3_id, item4_id].contains(&id)
7213            })
7214        });
7215        cx.executor().run_until_parked();
7216
7217        assert!(cx.has_pending_prompt());
7218        cx.simulate_prompt_answer("Save all");
7219
7220        cx.executor().run_until_parked();
7221
7222        // Item 1 is saved. There's a prompt to save item 3.
7223        pane.update(cx, |pane, cx| {
7224            assert_eq!(item1.read(cx).save_count, 1);
7225            assert_eq!(item1.read(cx).save_as_count, 0);
7226            assert_eq!(item1.read(cx).reload_count, 0);
7227            assert_eq!(pane.items_len(), 3);
7228            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7229        });
7230        assert!(cx.has_pending_prompt());
7231
7232        // Cancel saving item 3.
7233        cx.simulate_prompt_answer("Discard");
7234        cx.executor().run_until_parked();
7235
7236        // Item 3 is reloaded. There's a prompt to save item 4.
7237        pane.update(cx, |pane, cx| {
7238            assert_eq!(item3.read(cx).save_count, 0);
7239            assert_eq!(item3.read(cx).save_as_count, 0);
7240            assert_eq!(item3.read(cx).reload_count, 1);
7241            assert_eq!(pane.items_len(), 2);
7242            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7243        });
7244
7245        // There's a prompt for a path for item 4.
7246        cx.simulate_new_path_selection(|_| Some(Default::default()));
7247        close_items.await.unwrap();
7248
7249        // The requested items are closed.
7250        pane.update(cx, |pane, cx| {
7251            assert_eq!(item4.read(cx).save_count, 0);
7252            assert_eq!(item4.read(cx).save_as_count, 1);
7253            assert_eq!(item4.read(cx).reload_count, 0);
7254            assert_eq!(pane.items_len(), 1);
7255            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7256        });
7257    }
7258
7259    #[gpui::test]
7260    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7261        init_test(cx);
7262
7263        let fs = FakeFs::new(cx.executor());
7264        let project = Project::test(fs, [], cx).await;
7265        let (workspace, cx) =
7266            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7267
7268        // Create several workspace items with single project entries, and two
7269        // workspace items with multiple project entries.
7270        let single_entry_items = (0..=4)
7271            .map(|project_entry_id| {
7272                cx.new(|cx| {
7273                    TestItem::new(cx)
7274                        .with_dirty(true)
7275                        .with_project_items(&[dirty_project_item(
7276                            project_entry_id,
7277                            &format!("{project_entry_id}.txt"),
7278                            cx,
7279                        )])
7280                })
7281            })
7282            .collect::<Vec<_>>();
7283        let item_2_3 = cx.new(|cx| {
7284            TestItem::new(cx)
7285                .with_dirty(true)
7286                .with_singleton(false)
7287                .with_project_items(&[
7288                    single_entry_items[2].read(cx).project_items[0].clone(),
7289                    single_entry_items[3].read(cx).project_items[0].clone(),
7290                ])
7291        });
7292        let item_3_4 = cx.new(|cx| {
7293            TestItem::new(cx)
7294                .with_dirty(true)
7295                .with_singleton(false)
7296                .with_project_items(&[
7297                    single_entry_items[3].read(cx).project_items[0].clone(),
7298                    single_entry_items[4].read(cx).project_items[0].clone(),
7299                ])
7300        });
7301
7302        // Create two panes that contain the following project entries:
7303        //   left pane:
7304        //     multi-entry items:   (2, 3)
7305        //     single-entry items:  0, 2, 3, 4
7306        //   right pane:
7307        //     single-entry items:  4, 1
7308        //     multi-entry items:   (3, 4)
7309        let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
7310            let left_pane = workspace.active_pane().clone();
7311            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7312            workspace.add_item_to_active_pane(
7313                single_entry_items[0].boxed_clone(),
7314                None,
7315                true,
7316                window,
7317                cx,
7318            );
7319            workspace.add_item_to_active_pane(
7320                single_entry_items[2].boxed_clone(),
7321                None,
7322                true,
7323                window,
7324                cx,
7325            );
7326            workspace.add_item_to_active_pane(
7327                single_entry_items[3].boxed_clone(),
7328                None,
7329                true,
7330                window,
7331                cx,
7332            );
7333            workspace.add_item_to_active_pane(
7334                single_entry_items[4].boxed_clone(),
7335                None,
7336                true,
7337                window,
7338                cx,
7339            );
7340
7341            let right_pane = workspace
7342                .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7343                .unwrap();
7344
7345            right_pane.update(cx, |pane, cx| {
7346                pane.add_item(
7347                    single_entry_items[1].boxed_clone(),
7348                    true,
7349                    true,
7350                    None,
7351                    window,
7352                    cx,
7353                );
7354                pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7355            });
7356
7357            (left_pane, right_pane)
7358        });
7359
7360        cx.focus(&right_pane);
7361
7362        let mut close = right_pane.update_in(cx, |pane, window, cx| {
7363            pane.close_all_items(&CloseAllItems::default(), window, cx)
7364                .unwrap()
7365        });
7366        cx.executor().run_until_parked();
7367
7368        let msg = cx.pending_prompt().unwrap().0;
7369        assert!(msg.contains("1.txt"));
7370        assert!(!msg.contains("2.txt"));
7371        assert!(!msg.contains("3.txt"));
7372        assert!(!msg.contains("4.txt"));
7373
7374        cx.simulate_prompt_answer("Cancel");
7375        close.await.unwrap();
7376
7377        left_pane
7378            .update_in(cx, |left_pane, window, cx| {
7379                left_pane.close_item_by_id(
7380                    single_entry_items[3].entity_id(),
7381                    SaveIntent::Skip,
7382                    window,
7383                    cx,
7384                )
7385            })
7386            .await
7387            .unwrap();
7388
7389        close = right_pane.update_in(cx, |pane, window, cx| {
7390            pane.close_all_items(&CloseAllItems::default(), window, cx)
7391                .unwrap()
7392        });
7393        cx.executor().run_until_parked();
7394
7395        let details = cx.pending_prompt().unwrap().1;
7396        assert!(details.contains("1.txt"));
7397        assert!(!details.contains("2.txt"));
7398        assert!(details.contains("3.txt"));
7399        // ideally this assertion could be made, but today we can only
7400        // save whole items not project items, so the orphaned item 3 causes
7401        // 4 to be saved too.
7402        // assert!(!details.contains("4.txt"));
7403
7404        cx.simulate_prompt_answer("Save all");
7405
7406        cx.executor().run_until_parked();
7407        close.await.unwrap();
7408        right_pane.update(cx, |pane, _| {
7409            assert_eq!(pane.items_len(), 0);
7410        });
7411    }
7412
7413    #[gpui::test]
7414    async fn test_autosave(cx: &mut gpui::TestAppContext) {
7415        init_test(cx);
7416
7417        let fs = FakeFs::new(cx.executor());
7418        let project = Project::test(fs, [], cx).await;
7419        let (workspace, cx) =
7420            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7421        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7422
7423        let item = cx.new(|cx| {
7424            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7425        });
7426        let item_id = item.entity_id();
7427        workspace.update_in(cx, |workspace, window, cx| {
7428            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7429        });
7430
7431        // Autosave on window change.
7432        item.update(cx, |item, cx| {
7433            SettingsStore::update_global(cx, |settings, cx| {
7434                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7435                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
7436                })
7437            });
7438            item.is_dirty = true;
7439        });
7440
7441        // Deactivating the window saves the file.
7442        cx.deactivate_window();
7443        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7444
7445        // Re-activating the window doesn't save the file.
7446        cx.update(|window, _| window.activate_window());
7447        cx.executor().run_until_parked();
7448        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7449
7450        // Autosave on focus change.
7451        item.update_in(cx, |item, window, cx| {
7452            cx.focus_self(window);
7453            SettingsStore::update_global(cx, |settings, cx| {
7454                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7455                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
7456                })
7457            });
7458            item.is_dirty = true;
7459        });
7460
7461        // Blurring the item saves the file.
7462        item.update_in(cx, |_, window, _| window.blur());
7463        cx.executor().run_until_parked();
7464        item.update(cx, |item, _| assert_eq!(item.save_count, 2));
7465
7466        // Deactivating the window still saves the file.
7467        item.update_in(cx, |item, window, cx| {
7468            cx.focus_self(window);
7469            item.is_dirty = true;
7470        });
7471        cx.deactivate_window();
7472        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7473
7474        // Autosave after delay.
7475        item.update(cx, |item, cx| {
7476            SettingsStore::update_global(cx, |settings, cx| {
7477                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7478                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
7479                })
7480            });
7481            item.is_dirty = true;
7482            cx.emit(ItemEvent::Edit);
7483        });
7484
7485        // Delay hasn't fully expired, so the file is still dirty and unsaved.
7486        cx.executor().advance_clock(Duration::from_millis(250));
7487        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7488
7489        // After delay expires, the file is saved.
7490        cx.executor().advance_clock(Duration::from_millis(250));
7491        item.update(cx, |item, _| assert_eq!(item.save_count, 4));
7492
7493        // Autosave on focus change, ensuring closing the tab counts as such.
7494        item.update(cx, |item, cx| {
7495            SettingsStore::update_global(cx, |settings, cx| {
7496                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7497                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
7498                })
7499            });
7500            item.is_dirty = true;
7501            for project_item in &mut item.project_items {
7502                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7503            }
7504        });
7505
7506        pane.update_in(cx, |pane, window, cx| {
7507            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7508        })
7509        .await
7510        .unwrap();
7511        assert!(!cx.has_pending_prompt());
7512        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7513
7514        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
7515        workspace.update_in(cx, |workspace, window, cx| {
7516            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7517        });
7518        item.update_in(cx, |item, window, cx| {
7519            item.project_items[0].update(cx, |item, _| {
7520                item.entry_id = None;
7521            });
7522            item.is_dirty = true;
7523            window.blur();
7524        });
7525        cx.run_until_parked();
7526        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7527
7528        // Ensure autosave is prevented for deleted files also when closing the buffer.
7529        let _close_items = pane.update_in(cx, |pane, window, cx| {
7530            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7531        });
7532        cx.run_until_parked();
7533        assert!(cx.has_pending_prompt());
7534        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7535    }
7536
7537    #[gpui::test]
7538    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
7539        init_test(cx);
7540
7541        let fs = FakeFs::new(cx.executor());
7542
7543        let project = Project::test(fs, [], cx).await;
7544        let (workspace, cx) =
7545            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7546
7547        let item = cx.new(|cx| {
7548            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7549        });
7550        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7551        let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
7552        let toolbar_notify_count = Rc::new(RefCell::new(0));
7553
7554        workspace.update_in(cx, |workspace, window, cx| {
7555            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7556            let toolbar_notification_count = toolbar_notify_count.clone();
7557            cx.observe_in(&toolbar, window, move |_, _, _, _| {
7558                *toolbar_notification_count.borrow_mut() += 1
7559            })
7560            .detach();
7561        });
7562
7563        pane.update(cx, |pane, _| {
7564            assert!(!pane.can_navigate_backward());
7565            assert!(!pane.can_navigate_forward());
7566        });
7567
7568        item.update_in(cx, |item, _, cx| {
7569            item.set_state("one".to_string(), cx);
7570        });
7571
7572        // Toolbar must be notified to re-render the navigation buttons
7573        assert_eq!(*toolbar_notify_count.borrow(), 1);
7574
7575        pane.update(cx, |pane, _| {
7576            assert!(pane.can_navigate_backward());
7577            assert!(!pane.can_navigate_forward());
7578        });
7579
7580        workspace
7581            .update_in(cx, |workspace, window, cx| {
7582                workspace.go_back(pane.downgrade(), window, cx)
7583            })
7584            .await
7585            .unwrap();
7586
7587        assert_eq!(*toolbar_notify_count.borrow(), 2);
7588        pane.update(cx, |pane, _| {
7589            assert!(!pane.can_navigate_backward());
7590            assert!(pane.can_navigate_forward());
7591        });
7592    }
7593
7594    #[gpui::test]
7595    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7596        init_test(cx);
7597        let fs = FakeFs::new(cx.executor());
7598
7599        let project = Project::test(fs, [], cx).await;
7600        let (workspace, cx) =
7601            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7602
7603        let panel = workspace.update_in(cx, |workspace, window, cx| {
7604            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7605            workspace.add_panel(panel.clone(), window, cx);
7606
7607            workspace
7608                .right_dock()
7609                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
7610
7611            panel
7612        });
7613
7614        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7615        pane.update_in(cx, |pane, window, cx| {
7616            let item = cx.new(TestItem::new);
7617            pane.add_item(Box::new(item), true, true, None, window, cx);
7618        });
7619
7620        // Transfer focus from center to panel
7621        workspace.update_in(cx, |workspace, window, cx| {
7622            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7623        });
7624
7625        workspace.update_in(cx, |workspace, window, cx| {
7626            assert!(workspace.right_dock().read(cx).is_open());
7627            assert!(!panel.is_zoomed(window, cx));
7628            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7629        });
7630
7631        // Transfer focus from panel to center
7632        workspace.update_in(cx, |workspace, window, cx| {
7633            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7634        });
7635
7636        workspace.update_in(cx, |workspace, window, cx| {
7637            assert!(workspace.right_dock().read(cx).is_open());
7638            assert!(!panel.is_zoomed(window, cx));
7639            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7640        });
7641
7642        // Close the dock
7643        workspace.update_in(cx, |workspace, window, cx| {
7644            workspace.toggle_dock(DockPosition::Right, window, cx);
7645        });
7646
7647        workspace.update_in(cx, |workspace, window, cx| {
7648            assert!(!workspace.right_dock().read(cx).is_open());
7649            assert!(!panel.is_zoomed(window, cx));
7650            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7651        });
7652
7653        // Open the dock
7654        workspace.update_in(cx, |workspace, window, cx| {
7655            workspace.toggle_dock(DockPosition::Right, window, cx);
7656        });
7657
7658        workspace.update_in(cx, |workspace, window, cx| {
7659            assert!(workspace.right_dock().read(cx).is_open());
7660            assert!(!panel.is_zoomed(window, cx));
7661            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7662        });
7663
7664        // Focus and zoom panel
7665        panel.update_in(cx, |panel, window, cx| {
7666            cx.focus_self(window);
7667            panel.set_zoomed(true, window, cx)
7668        });
7669
7670        workspace.update_in(cx, |workspace, window, cx| {
7671            assert!(workspace.right_dock().read(cx).is_open());
7672            assert!(panel.is_zoomed(window, cx));
7673            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7674        });
7675
7676        // Transfer focus to the center closes the dock
7677        workspace.update_in(cx, |workspace, window, cx| {
7678            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7679        });
7680
7681        workspace.update_in(cx, |workspace, window, cx| {
7682            assert!(!workspace.right_dock().read(cx).is_open());
7683            assert!(panel.is_zoomed(window, cx));
7684            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7685        });
7686
7687        // Transferring focus back to the panel keeps it zoomed
7688        workspace.update_in(cx, |workspace, window, cx| {
7689            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7690        });
7691
7692        workspace.update_in(cx, |workspace, window, cx| {
7693            assert!(workspace.right_dock().read(cx).is_open());
7694            assert!(panel.is_zoomed(window, cx));
7695            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7696        });
7697
7698        // Close the dock while it is zoomed
7699        workspace.update_in(cx, |workspace, window, cx| {
7700            workspace.toggle_dock(DockPosition::Right, window, cx)
7701        });
7702
7703        workspace.update_in(cx, |workspace, window, cx| {
7704            assert!(!workspace.right_dock().read(cx).is_open());
7705            assert!(panel.is_zoomed(window, cx));
7706            assert!(workspace.zoomed.is_none());
7707            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7708        });
7709
7710        // Opening the dock, when it's zoomed, retains focus
7711        workspace.update_in(cx, |workspace, window, cx| {
7712            workspace.toggle_dock(DockPosition::Right, window, cx)
7713        });
7714
7715        workspace.update_in(cx, |workspace, window, cx| {
7716            assert!(workspace.right_dock().read(cx).is_open());
7717            assert!(panel.is_zoomed(window, cx));
7718            assert!(workspace.zoomed.is_some());
7719            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7720        });
7721
7722        // Unzoom and close the panel, zoom the active pane.
7723        panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
7724        workspace.update_in(cx, |workspace, window, cx| {
7725            workspace.toggle_dock(DockPosition::Right, window, cx)
7726        });
7727        pane.update_in(cx, |pane, window, cx| {
7728            pane.toggle_zoom(&Default::default(), window, cx)
7729        });
7730
7731        // Opening a dock unzooms the pane.
7732        workspace.update_in(cx, |workspace, window, cx| {
7733            workspace.toggle_dock(DockPosition::Right, window, cx)
7734        });
7735        workspace.update_in(cx, |workspace, window, cx| {
7736            let pane = pane.read(cx);
7737            assert!(!pane.is_zoomed());
7738            assert!(!pane.focus_handle(cx).is_focused(window));
7739            assert!(workspace.right_dock().read(cx).is_open());
7740            assert!(workspace.zoomed.is_none());
7741        });
7742    }
7743
7744    #[gpui::test]
7745    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7746        init_test(cx);
7747
7748        let fs = FakeFs::new(cx.executor());
7749
7750        let project = Project::test(fs, None, cx).await;
7751        let (workspace, cx) =
7752            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7753
7754        // Let's arrange the panes like this:
7755        //
7756        // +-----------------------+
7757        // |         top           |
7758        // +------+--------+-------+
7759        // | left | center | right |
7760        // +------+--------+-------+
7761        // |        bottom         |
7762        // +-----------------------+
7763
7764        let top_item = cx.new(|cx| {
7765            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7766        });
7767        let bottom_item = cx.new(|cx| {
7768            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7769        });
7770        let left_item = cx.new(|cx| {
7771            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7772        });
7773        let right_item = cx.new(|cx| {
7774            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7775        });
7776        let center_item = cx.new(|cx| {
7777            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7778        });
7779
7780        let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7781            let top_pane_id = workspace.active_pane().entity_id();
7782            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
7783            workspace.split_pane(
7784                workspace.active_pane().clone(),
7785                SplitDirection::Down,
7786                window,
7787                cx,
7788            );
7789            top_pane_id
7790        });
7791        let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7792            let bottom_pane_id = workspace.active_pane().entity_id();
7793            workspace.add_item_to_active_pane(
7794                Box::new(bottom_item.clone()),
7795                None,
7796                false,
7797                window,
7798                cx,
7799            );
7800            workspace.split_pane(
7801                workspace.active_pane().clone(),
7802                SplitDirection::Up,
7803                window,
7804                cx,
7805            );
7806            bottom_pane_id
7807        });
7808        let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7809            let left_pane_id = workspace.active_pane().entity_id();
7810            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
7811            workspace.split_pane(
7812                workspace.active_pane().clone(),
7813                SplitDirection::Right,
7814                window,
7815                cx,
7816            );
7817            left_pane_id
7818        });
7819        let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7820            let right_pane_id = workspace.active_pane().entity_id();
7821            workspace.add_item_to_active_pane(
7822                Box::new(right_item.clone()),
7823                None,
7824                false,
7825                window,
7826                cx,
7827            );
7828            workspace.split_pane(
7829                workspace.active_pane().clone(),
7830                SplitDirection::Left,
7831                window,
7832                cx,
7833            );
7834            right_pane_id
7835        });
7836        let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7837            let center_pane_id = workspace.active_pane().entity_id();
7838            workspace.add_item_to_active_pane(
7839                Box::new(center_item.clone()),
7840                None,
7841                false,
7842                window,
7843                cx,
7844            );
7845            center_pane_id
7846        });
7847        cx.executor().run_until_parked();
7848
7849        workspace.update_in(cx, |workspace, window, cx| {
7850            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7851
7852            // Join into next from center pane into right
7853            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7854        });
7855
7856        workspace.update_in(cx, |workspace, window, cx| {
7857            let active_pane = workspace.active_pane();
7858            assert_eq!(right_pane_id, active_pane.entity_id());
7859            assert_eq!(2, active_pane.read(cx).items_len());
7860            let item_ids_in_pane =
7861                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7862            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7863            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7864
7865            // Join into next from right pane into bottom
7866            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7867        });
7868
7869        workspace.update_in(cx, |workspace, window, cx| {
7870            let active_pane = workspace.active_pane();
7871            assert_eq!(bottom_pane_id, active_pane.entity_id());
7872            assert_eq!(3, active_pane.read(cx).items_len());
7873            let item_ids_in_pane =
7874                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7875            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7876            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7877            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7878
7879            // Join into next from bottom pane into left
7880            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7881        });
7882
7883        workspace.update_in(cx, |workspace, window, cx| {
7884            let active_pane = workspace.active_pane();
7885            assert_eq!(left_pane_id, active_pane.entity_id());
7886            assert_eq!(4, active_pane.read(cx).items_len());
7887            let item_ids_in_pane =
7888                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7889            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7890            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7891            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7892            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7893
7894            // Join into next from left pane into top
7895            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7896        });
7897
7898        workspace.update_in(cx, |workspace, window, cx| {
7899            let active_pane = workspace.active_pane();
7900            assert_eq!(top_pane_id, active_pane.entity_id());
7901            assert_eq!(5, active_pane.read(cx).items_len());
7902            let item_ids_in_pane =
7903                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7904            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7905            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7906            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7907            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7908            assert!(item_ids_in_pane.contains(&top_item.item_id()));
7909
7910            // Single pane left: no-op
7911            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
7912        });
7913
7914        workspace.update(cx, |workspace, _cx| {
7915            let active_pane = workspace.active_pane();
7916            assert_eq!(top_pane_id, active_pane.entity_id());
7917        });
7918    }
7919
7920    fn add_an_item_to_active_pane(
7921        cx: &mut VisualTestContext,
7922        workspace: &Entity<Workspace>,
7923        item_id: u64,
7924    ) -> Entity<TestItem> {
7925        let item = cx.new(|cx| {
7926            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7927                item_id,
7928                "item{item_id}.txt",
7929                cx,
7930            )])
7931        });
7932        workspace.update_in(cx, |workspace, window, cx| {
7933            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
7934        });
7935        return item;
7936    }
7937
7938    fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
7939        return workspace.update_in(cx, |workspace, window, cx| {
7940            let new_pane = workspace.split_pane(
7941                workspace.active_pane().clone(),
7942                SplitDirection::Right,
7943                window,
7944                cx,
7945            );
7946            new_pane
7947        });
7948    }
7949
7950    #[gpui::test]
7951    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7952        init_test(cx);
7953        let fs = FakeFs::new(cx.executor());
7954        let project = Project::test(fs, None, cx).await;
7955        let (workspace, cx) =
7956            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7957
7958        add_an_item_to_active_pane(cx, &workspace, 1);
7959        split_pane(cx, &workspace);
7960        add_an_item_to_active_pane(cx, &workspace, 2);
7961        split_pane(cx, &workspace); // empty pane
7962        split_pane(cx, &workspace);
7963        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7964
7965        cx.executor().run_until_parked();
7966
7967        workspace.update(cx, |workspace, cx| {
7968            let num_panes = workspace.panes().len();
7969            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7970            let active_item = workspace
7971                .active_pane()
7972                .read(cx)
7973                .active_item()
7974                .expect("item is in focus");
7975
7976            assert_eq!(num_panes, 4);
7977            assert_eq!(num_items_in_current_pane, 1);
7978            assert_eq!(active_item.item_id(), last_item.item_id());
7979        });
7980
7981        workspace.update_in(cx, |workspace, window, cx| {
7982            workspace.join_all_panes(window, cx);
7983        });
7984
7985        workspace.update(cx, |workspace, cx| {
7986            let num_panes = workspace.panes().len();
7987            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7988            let active_item = workspace
7989                .active_pane()
7990                .read(cx)
7991                .active_item()
7992                .expect("item is in focus");
7993
7994            assert_eq!(num_panes, 1);
7995            assert_eq!(num_items_in_current_pane, 3);
7996            assert_eq!(active_item.item_id(), last_item.item_id());
7997        });
7998    }
7999    struct TestModal(FocusHandle);
8000
8001    impl TestModal {
8002        fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
8003            Self(cx.focus_handle())
8004        }
8005    }
8006
8007    impl EventEmitter<DismissEvent> for TestModal {}
8008
8009    impl Focusable for TestModal {
8010        fn focus_handle(&self, _cx: &App) -> FocusHandle {
8011            self.0.clone()
8012        }
8013    }
8014
8015    impl ModalView for TestModal {}
8016
8017    impl Render for TestModal {
8018        fn render(
8019            &mut self,
8020            _window: &mut Window,
8021            _cx: &mut Context<TestModal>,
8022        ) -> impl IntoElement {
8023            div().track_focus(&self.0)
8024        }
8025    }
8026
8027    #[gpui::test]
8028    async fn test_panels(cx: &mut gpui::TestAppContext) {
8029        init_test(cx);
8030        let fs = FakeFs::new(cx.executor());
8031
8032        let project = Project::test(fs, [], cx).await;
8033        let (workspace, cx) =
8034            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8035
8036        let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
8037            let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
8038            workspace.add_panel(panel_1.clone(), window, cx);
8039            workspace.toggle_dock(DockPosition::Left, window, cx);
8040            let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8041            workspace.add_panel(panel_2.clone(), window, cx);
8042            workspace.toggle_dock(DockPosition::Right, window, cx);
8043
8044            let left_dock = workspace.left_dock();
8045            assert_eq!(
8046                left_dock.read(cx).visible_panel().unwrap().panel_id(),
8047                panel_1.panel_id()
8048            );
8049            assert_eq!(
8050                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8051                panel_1.size(window, cx)
8052            );
8053
8054            left_dock.update(cx, |left_dock, cx| {
8055                left_dock.resize_active_panel(Some(px(1337.)), window, cx)
8056            });
8057            assert_eq!(
8058                workspace
8059                    .right_dock()
8060                    .read(cx)
8061                    .visible_panel()
8062                    .unwrap()
8063                    .panel_id(),
8064                panel_2.panel_id(),
8065            );
8066
8067            (panel_1, panel_2)
8068        });
8069
8070        // Move panel_1 to the right
8071        panel_1.update_in(cx, |panel_1, window, cx| {
8072            panel_1.set_position(DockPosition::Right, window, cx)
8073        });
8074
8075        workspace.update_in(cx, |workspace, window, cx| {
8076            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
8077            // Since it was the only panel on the left, the left dock should now be closed.
8078            assert!(!workspace.left_dock().read(cx).is_open());
8079            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
8080            let right_dock = workspace.right_dock();
8081            assert_eq!(
8082                right_dock.read(cx).visible_panel().unwrap().panel_id(),
8083                panel_1.panel_id()
8084            );
8085            assert_eq!(
8086                right_dock.read(cx).active_panel_size(window, cx).unwrap(),
8087                px(1337.)
8088            );
8089
8090            // Now we move panel_2 to the left
8091            panel_2.set_position(DockPosition::Left, window, cx);
8092        });
8093
8094        workspace.update(cx, |workspace, cx| {
8095            // Since panel_2 was not visible on the right, we don't open the left dock.
8096            assert!(!workspace.left_dock().read(cx).is_open());
8097            // And the right dock is unaffected in its displaying of panel_1
8098            assert!(workspace.right_dock().read(cx).is_open());
8099            assert_eq!(
8100                workspace
8101                    .right_dock()
8102                    .read(cx)
8103                    .visible_panel()
8104                    .unwrap()
8105                    .panel_id(),
8106                panel_1.panel_id(),
8107            );
8108        });
8109
8110        // Move panel_1 back to the left
8111        panel_1.update_in(cx, |panel_1, window, cx| {
8112            panel_1.set_position(DockPosition::Left, window, cx)
8113        });
8114
8115        workspace.update_in(cx, |workspace, window, cx| {
8116            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
8117            let left_dock = workspace.left_dock();
8118            assert!(left_dock.read(cx).is_open());
8119            assert_eq!(
8120                left_dock.read(cx).visible_panel().unwrap().panel_id(),
8121                panel_1.panel_id()
8122            );
8123            assert_eq!(
8124                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8125                px(1337.)
8126            );
8127            // And the right dock should be closed as it no longer has any panels.
8128            assert!(!workspace.right_dock().read(cx).is_open());
8129
8130            // Now we move panel_1 to the bottom
8131            panel_1.set_position(DockPosition::Bottom, window, cx);
8132        });
8133
8134        workspace.update_in(cx, |workspace, window, cx| {
8135            // Since panel_1 was visible on the left, we close the left dock.
8136            assert!(!workspace.left_dock().read(cx).is_open());
8137            // The bottom dock is sized based on the panel's default size,
8138            // since the panel orientation changed from vertical to horizontal.
8139            let bottom_dock = workspace.bottom_dock();
8140            assert_eq!(
8141                bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
8142                panel_1.size(window, cx),
8143            );
8144            // Close bottom dock and move panel_1 back to the left.
8145            bottom_dock.update(cx, |bottom_dock, cx| {
8146                bottom_dock.set_open(false, window, cx)
8147            });
8148            panel_1.set_position(DockPosition::Left, window, cx);
8149        });
8150
8151        // Emit activated event on panel 1
8152        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
8153
8154        // Now the left dock is open and panel_1 is active and focused.
8155        workspace.update_in(cx, |workspace, window, cx| {
8156            let left_dock = workspace.left_dock();
8157            assert!(left_dock.read(cx).is_open());
8158            assert_eq!(
8159                left_dock.read(cx).visible_panel().unwrap().panel_id(),
8160                panel_1.panel_id(),
8161            );
8162            assert!(panel_1.focus_handle(cx).is_focused(window));
8163        });
8164
8165        // Emit closed event on panel 2, which is not active
8166        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8167
8168        // Wo don't close the left dock, because panel_2 wasn't the active panel
8169        workspace.update(cx, |workspace, cx| {
8170            let left_dock = workspace.left_dock();
8171            assert!(left_dock.read(cx).is_open());
8172            assert_eq!(
8173                left_dock.read(cx).visible_panel().unwrap().panel_id(),
8174                panel_1.panel_id(),
8175            );
8176        });
8177
8178        // Emitting a ZoomIn event shows the panel as zoomed.
8179        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
8180        workspace.update(cx, |workspace, _| {
8181            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8182            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
8183        });
8184
8185        // Move panel to another dock while it is zoomed
8186        panel_1.update_in(cx, |panel, window, cx| {
8187            panel.set_position(DockPosition::Right, window, cx)
8188        });
8189        workspace.update(cx, |workspace, _| {
8190            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8191
8192            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8193        });
8194
8195        // This is a helper for getting a:
8196        // - valid focus on an element,
8197        // - that isn't a part of the panes and panels system of the Workspace,
8198        // - and doesn't trigger the 'on_focus_lost' API.
8199        let focus_other_view = {
8200            let workspace = workspace.clone();
8201            move |cx: &mut VisualTestContext| {
8202                workspace.update_in(cx, |workspace, window, cx| {
8203                    if let Some(_) = workspace.active_modal::<TestModal>(cx) {
8204                        workspace.toggle_modal(window, cx, TestModal::new);
8205                        workspace.toggle_modal(window, cx, TestModal::new);
8206                    } else {
8207                        workspace.toggle_modal(window, cx, TestModal::new);
8208                    }
8209                })
8210            }
8211        };
8212
8213        // If focus is transferred to another view that's not a panel or another pane, we still show
8214        // the panel as zoomed.
8215        focus_other_view(cx);
8216        workspace.update(cx, |workspace, _| {
8217            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8218            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8219        });
8220
8221        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8222        workspace.update_in(cx, |_workspace, window, cx| {
8223            cx.focus_self(window);
8224        });
8225        workspace.update(cx, |workspace, _| {
8226            assert_eq!(workspace.zoomed, None);
8227            assert_eq!(workspace.zoomed_position, None);
8228        });
8229
8230        // If focus is transferred again to another view that's not a panel or a pane, we won't
8231        // show the panel as zoomed because it wasn't zoomed before.
8232        focus_other_view(cx);
8233        workspace.update(cx, |workspace, _| {
8234            assert_eq!(workspace.zoomed, None);
8235            assert_eq!(workspace.zoomed_position, None);
8236        });
8237
8238        // When the panel is activated, it is zoomed again.
8239        cx.dispatch_action(ToggleRightDock);
8240        workspace.update(cx, |workspace, _| {
8241            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8242            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8243        });
8244
8245        // Emitting a ZoomOut event unzooms the panel.
8246        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8247        workspace.update(cx, |workspace, _| {
8248            assert_eq!(workspace.zoomed, None);
8249            assert_eq!(workspace.zoomed_position, None);
8250        });
8251
8252        // Emit closed event on panel 1, which is active
8253        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8254
8255        // Now the left dock is closed, because panel_1 was the active panel
8256        workspace.update(cx, |workspace, cx| {
8257            let right_dock = workspace.right_dock();
8258            assert!(!right_dock.read(cx).is_open());
8259        });
8260    }
8261
8262    #[gpui::test]
8263    async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8264        init_test(cx);
8265
8266        let fs = FakeFs::new(cx.background_executor.clone());
8267        let project = Project::test(fs, [], cx).await;
8268        let (workspace, cx) =
8269            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8270        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8271
8272        let dirty_regular_buffer = cx.new(|cx| {
8273            TestItem::new(cx)
8274                .with_dirty(true)
8275                .with_label("1.txt")
8276                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8277        });
8278        let dirty_regular_buffer_2 = cx.new(|cx| {
8279            TestItem::new(cx)
8280                .with_dirty(true)
8281                .with_label("2.txt")
8282                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8283        });
8284        let dirty_multi_buffer_with_both = cx.new(|cx| {
8285            TestItem::new(cx)
8286                .with_dirty(true)
8287                .with_singleton(false)
8288                .with_label("Fake Project Search")
8289                .with_project_items(&[
8290                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8291                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8292                ])
8293        });
8294        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8295        workspace.update_in(cx, |workspace, window, cx| {
8296            workspace.add_item(
8297                pane.clone(),
8298                Box::new(dirty_regular_buffer.clone()),
8299                None,
8300                false,
8301                false,
8302                window,
8303                cx,
8304            );
8305            workspace.add_item(
8306                pane.clone(),
8307                Box::new(dirty_regular_buffer_2.clone()),
8308                None,
8309                false,
8310                false,
8311                window,
8312                cx,
8313            );
8314            workspace.add_item(
8315                pane.clone(),
8316                Box::new(dirty_multi_buffer_with_both.clone()),
8317                None,
8318                false,
8319                false,
8320                window,
8321                cx,
8322            );
8323        });
8324
8325        pane.update_in(cx, |pane, window, cx| {
8326            pane.activate_item(2, true, true, window, cx);
8327            assert_eq!(
8328                pane.active_item().unwrap().item_id(),
8329                multi_buffer_with_both_files_id,
8330                "Should select the multi buffer in the pane"
8331            );
8332        });
8333        let close_all_but_multi_buffer_task = pane
8334            .update_in(cx, |pane, window, cx| {
8335                pane.close_inactive_items(
8336                    &CloseInactiveItems {
8337                        save_intent: Some(SaveIntent::Save),
8338                        close_pinned: true,
8339                    },
8340                    window,
8341                    cx,
8342                )
8343            })
8344            .expect("should have inactive files to close");
8345        cx.background_executor.run_until_parked();
8346        assert!(!cx.has_pending_prompt());
8347        close_all_but_multi_buffer_task
8348            .await
8349            .expect("Closing all buffers but the multi buffer failed");
8350        pane.update(cx, |pane, cx| {
8351            assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
8352            assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8353            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
8354            assert_eq!(pane.items_len(), 1);
8355            assert_eq!(
8356                pane.active_item().unwrap().item_id(),
8357                multi_buffer_with_both_files_id,
8358                "Should have only the multi buffer left in the pane"
8359            );
8360            assert!(
8361                dirty_multi_buffer_with_both.read(cx).is_dirty,
8362                "The multi buffer containing the unsaved buffer should still be dirty"
8363            );
8364        });
8365
8366        dirty_regular_buffer.update(cx, |buffer, cx| {
8367            buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
8368        });
8369
8370        let close_multi_buffer_task = pane
8371            .update_in(cx, |pane, window, cx| {
8372                pane.close_active_item(
8373                    &CloseActiveItem {
8374                        save_intent: Some(SaveIntent::Close),
8375                        close_pinned: false,
8376                    },
8377                    window,
8378                    cx,
8379                )
8380            })
8381            .expect("should have the multi buffer to close");
8382        cx.background_executor.run_until_parked();
8383        assert!(
8384            cx.has_pending_prompt(),
8385            "Dirty multi buffer should prompt a save dialog"
8386        );
8387        cx.simulate_prompt_answer("Save");
8388        cx.background_executor.run_until_parked();
8389        close_multi_buffer_task
8390            .await
8391            .expect("Closing the multi buffer failed");
8392        pane.update(cx, |pane, cx| {
8393            assert_eq!(
8394                dirty_multi_buffer_with_both.read(cx).save_count,
8395                1,
8396                "Multi buffer item should get be saved"
8397            );
8398            // Test impl does not save inner items, so we do not assert them
8399            assert_eq!(
8400                pane.items_len(),
8401                0,
8402                "No more items should be left in the pane"
8403            );
8404            assert!(pane.active_item().is_none());
8405        });
8406    }
8407
8408    #[gpui::test]
8409    async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
8410        cx: &mut TestAppContext,
8411    ) {
8412        init_test(cx);
8413
8414        let fs = FakeFs::new(cx.background_executor.clone());
8415        let project = Project::test(fs, [], cx).await;
8416        let (workspace, cx) =
8417            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8418        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8419
8420        let dirty_regular_buffer = cx.new(|cx| {
8421            TestItem::new(cx)
8422                .with_dirty(true)
8423                .with_label("1.txt")
8424                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8425        });
8426        let dirty_regular_buffer_2 = cx.new(|cx| {
8427            TestItem::new(cx)
8428                .with_dirty(true)
8429                .with_label("2.txt")
8430                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8431        });
8432        let clear_regular_buffer = cx.new(|cx| {
8433            TestItem::new(cx)
8434                .with_label("3.txt")
8435                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8436        });
8437
8438        let dirty_multi_buffer_with_both = cx.new(|cx| {
8439            TestItem::new(cx)
8440                .with_dirty(true)
8441                .with_singleton(false)
8442                .with_label("Fake Project Search")
8443                .with_project_items(&[
8444                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8445                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8446                    clear_regular_buffer.read(cx).project_items[0].clone(),
8447                ])
8448        });
8449        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8450        workspace.update_in(cx, |workspace, window, cx| {
8451            workspace.add_item(
8452                pane.clone(),
8453                Box::new(dirty_regular_buffer.clone()),
8454                None,
8455                false,
8456                false,
8457                window,
8458                cx,
8459            );
8460            workspace.add_item(
8461                pane.clone(),
8462                Box::new(dirty_multi_buffer_with_both.clone()),
8463                None,
8464                false,
8465                false,
8466                window,
8467                cx,
8468            );
8469        });
8470
8471        pane.update_in(cx, |pane, window, cx| {
8472            pane.activate_item(1, true, true, window, cx);
8473            assert_eq!(
8474                pane.active_item().unwrap().item_id(),
8475                multi_buffer_with_both_files_id,
8476                "Should select the multi buffer in the pane"
8477            );
8478        });
8479        let _close_multi_buffer_task = pane
8480            .update_in(cx, |pane, window, cx| {
8481                pane.close_active_item(
8482                    &CloseActiveItem {
8483                        save_intent: None,
8484                        close_pinned: false,
8485                    },
8486                    window,
8487                    cx,
8488                )
8489            })
8490            .expect("should have active multi buffer to close");
8491        cx.background_executor.run_until_parked();
8492        assert!(
8493            cx.has_pending_prompt(),
8494            "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
8495        );
8496    }
8497
8498    #[gpui::test]
8499    async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
8500        cx: &mut TestAppContext,
8501    ) {
8502        init_test(cx);
8503
8504        let fs = FakeFs::new(cx.background_executor.clone());
8505        let project = Project::test(fs, [], cx).await;
8506        let (workspace, cx) =
8507            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8508        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8509
8510        let dirty_regular_buffer = cx.new(|cx| {
8511            TestItem::new(cx)
8512                .with_dirty(true)
8513                .with_label("1.txt")
8514                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8515        });
8516        let dirty_regular_buffer_2 = cx.new(|cx| {
8517            TestItem::new(cx)
8518                .with_dirty(true)
8519                .with_label("2.txt")
8520                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8521        });
8522        let clear_regular_buffer = cx.new(|cx| {
8523            TestItem::new(cx)
8524                .with_label("3.txt")
8525                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8526        });
8527
8528        let dirty_multi_buffer = cx.new(|cx| {
8529            TestItem::new(cx)
8530                .with_dirty(true)
8531                .with_singleton(false)
8532                .with_label("Fake Project Search")
8533                .with_project_items(&[
8534                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8535                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8536                    clear_regular_buffer.read(cx).project_items[0].clone(),
8537                ])
8538        });
8539        workspace.update_in(cx, |workspace, window, cx| {
8540            workspace.add_item(
8541                pane.clone(),
8542                Box::new(dirty_regular_buffer.clone()),
8543                None,
8544                false,
8545                false,
8546                window,
8547                cx,
8548            );
8549            workspace.add_item(
8550                pane.clone(),
8551                Box::new(dirty_regular_buffer_2.clone()),
8552                None,
8553                false,
8554                false,
8555                window,
8556                cx,
8557            );
8558            workspace.add_item(
8559                pane.clone(),
8560                Box::new(dirty_multi_buffer.clone()),
8561                None,
8562                false,
8563                false,
8564                window,
8565                cx,
8566            );
8567        });
8568
8569        pane.update_in(cx, |pane, window, cx| {
8570            pane.activate_item(2, true, true, window, cx);
8571            assert_eq!(
8572                pane.active_item().unwrap().item_id(),
8573                dirty_multi_buffer.item_id(),
8574                "Should select the multi buffer in the pane"
8575            );
8576        });
8577        let close_multi_buffer_task = pane
8578            .update_in(cx, |pane, window, cx| {
8579                pane.close_active_item(
8580                    &CloseActiveItem {
8581                        save_intent: None,
8582                        close_pinned: false,
8583                    },
8584                    window,
8585                    cx,
8586                )
8587            })
8588            .expect("should have active multi buffer to close");
8589        cx.background_executor.run_until_parked();
8590        assert!(
8591            !cx.has_pending_prompt(),
8592            "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8593        );
8594        close_multi_buffer_task
8595            .await
8596            .expect("Closing multi buffer failed");
8597        pane.update(cx, |pane, cx| {
8598            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8599            assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8600            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8601            assert_eq!(
8602                pane.items()
8603                    .map(|item| item.item_id())
8604                    .sorted()
8605                    .collect::<Vec<_>>(),
8606                vec![
8607                    dirty_regular_buffer.item_id(),
8608                    dirty_regular_buffer_2.item_id(),
8609                ],
8610                "Should have no multi buffer left in the pane"
8611            );
8612            assert!(dirty_regular_buffer.read(cx).is_dirty);
8613            assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8614        });
8615    }
8616
8617    #[gpui::test]
8618    async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8619        init_test(cx);
8620        let fs = FakeFs::new(cx.executor());
8621        let project = Project::test(fs, [], cx).await;
8622        let (workspace, cx) =
8623            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8624
8625        // Add a new panel to the right dock, opening the dock and setting the
8626        // focus to the new panel.
8627        let panel = workspace.update_in(cx, |workspace, window, cx| {
8628            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8629            workspace.add_panel(panel.clone(), window, cx);
8630
8631            workspace
8632                .right_dock()
8633                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8634
8635            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8636
8637            panel
8638        });
8639
8640        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8641        // panel to the next valid position which, in this case, is the left
8642        // dock.
8643        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8644        workspace.update(cx, |workspace, cx| {
8645            assert!(workspace.left_dock().read(cx).is_open());
8646            assert_eq!(panel.read(cx).position, DockPosition::Left);
8647        });
8648
8649        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8650        // panel to the next valid position which, in this case, is the bottom
8651        // dock.
8652        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8653        workspace.update(cx, |workspace, cx| {
8654            assert!(workspace.bottom_dock().read(cx).is_open());
8655            assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8656        });
8657
8658        // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8659        // around moving the panel to its initial position, the right dock.
8660        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8661        workspace.update(cx, |workspace, cx| {
8662            assert!(workspace.right_dock().read(cx).is_open());
8663            assert_eq!(panel.read(cx).position, DockPosition::Right);
8664        });
8665
8666        // Remove focus from the panel, ensuring that, if the panel is not
8667        // focused, the `MoveFocusedPanelToNextPosition` action does not update
8668        // the panel's position, so the panel is still in the right dock.
8669        workspace.update_in(cx, |workspace, window, cx| {
8670            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8671        });
8672
8673        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8674        workspace.update(cx, |workspace, cx| {
8675            assert!(workspace.right_dock().read(cx).is_open());
8676            assert_eq!(panel.read(cx).position, DockPosition::Right);
8677        });
8678    }
8679
8680    mod register_project_item_tests {
8681
8682        use super::*;
8683
8684        // View
8685        struct TestPngItemView {
8686            focus_handle: FocusHandle,
8687        }
8688        // Model
8689        struct TestPngItem {}
8690
8691        impl project::ProjectItem for TestPngItem {
8692            fn try_open(
8693                _project: &Entity<Project>,
8694                path: &ProjectPath,
8695                cx: &mut App,
8696            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8697                if path.path.extension().unwrap() == "png" {
8698                    Some(cx.spawn(async move |cx| cx.new(|_| TestPngItem {})))
8699                } else {
8700                    None
8701                }
8702            }
8703
8704            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8705                None
8706            }
8707
8708            fn project_path(&self, _: &App) -> Option<ProjectPath> {
8709                None
8710            }
8711
8712            fn is_dirty(&self) -> bool {
8713                false
8714            }
8715        }
8716
8717        impl Item for TestPngItemView {
8718            type Event = ();
8719        }
8720        impl EventEmitter<()> for TestPngItemView {}
8721        impl Focusable for TestPngItemView {
8722            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8723                self.focus_handle.clone()
8724            }
8725        }
8726
8727        impl Render for TestPngItemView {
8728            fn render(
8729                &mut self,
8730                _window: &mut Window,
8731                _cx: &mut Context<Self>,
8732            ) -> impl IntoElement {
8733                Empty
8734            }
8735        }
8736
8737        impl ProjectItem for TestPngItemView {
8738            type Item = TestPngItem;
8739
8740            fn for_project_item(
8741                _project: Entity<Project>,
8742                _pane: &Pane,
8743                _item: Entity<Self::Item>,
8744                _: &mut Window,
8745                cx: &mut Context<Self>,
8746            ) -> Self
8747            where
8748                Self: Sized,
8749            {
8750                Self {
8751                    focus_handle: cx.focus_handle(),
8752                }
8753            }
8754        }
8755
8756        // View
8757        struct TestIpynbItemView {
8758            focus_handle: FocusHandle,
8759        }
8760        // Model
8761        struct TestIpynbItem {}
8762
8763        impl project::ProjectItem for TestIpynbItem {
8764            fn try_open(
8765                _project: &Entity<Project>,
8766                path: &ProjectPath,
8767                cx: &mut App,
8768            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8769                if path.path.extension().unwrap() == "ipynb" {
8770                    Some(cx.spawn(async move |cx| cx.new(|_| TestIpynbItem {})))
8771                } else {
8772                    None
8773                }
8774            }
8775
8776            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8777                None
8778            }
8779
8780            fn project_path(&self, _: &App) -> Option<ProjectPath> {
8781                None
8782            }
8783
8784            fn is_dirty(&self) -> bool {
8785                false
8786            }
8787        }
8788
8789        impl Item for TestIpynbItemView {
8790            type Event = ();
8791        }
8792        impl EventEmitter<()> for TestIpynbItemView {}
8793        impl Focusable for TestIpynbItemView {
8794            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8795                self.focus_handle.clone()
8796            }
8797        }
8798
8799        impl Render for TestIpynbItemView {
8800            fn render(
8801                &mut self,
8802                _window: &mut Window,
8803                _cx: &mut Context<Self>,
8804            ) -> impl IntoElement {
8805                Empty
8806            }
8807        }
8808
8809        impl ProjectItem for TestIpynbItemView {
8810            type Item = TestIpynbItem;
8811
8812            fn for_project_item(
8813                _project: Entity<Project>,
8814                _pane: &Pane,
8815                _item: Entity<Self::Item>,
8816                _: &mut Window,
8817                cx: &mut Context<Self>,
8818            ) -> Self
8819            where
8820                Self: Sized,
8821            {
8822                Self {
8823                    focus_handle: cx.focus_handle(),
8824                }
8825            }
8826        }
8827
8828        struct TestAlternatePngItemView {
8829            focus_handle: FocusHandle,
8830        }
8831
8832        impl Item for TestAlternatePngItemView {
8833            type Event = ();
8834        }
8835
8836        impl EventEmitter<()> for TestAlternatePngItemView {}
8837        impl Focusable for TestAlternatePngItemView {
8838            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8839                self.focus_handle.clone()
8840            }
8841        }
8842
8843        impl Render for TestAlternatePngItemView {
8844            fn render(
8845                &mut self,
8846                _window: &mut Window,
8847                _cx: &mut Context<Self>,
8848            ) -> impl IntoElement {
8849                Empty
8850            }
8851        }
8852
8853        impl ProjectItem for TestAlternatePngItemView {
8854            type Item = TestPngItem;
8855
8856            fn for_project_item(
8857                _project: Entity<Project>,
8858                _pane: &Pane,
8859                _item: Entity<Self::Item>,
8860                _: &mut Window,
8861                cx: &mut Context<Self>,
8862            ) -> Self
8863            where
8864                Self: Sized,
8865            {
8866                Self {
8867                    focus_handle: cx.focus_handle(),
8868                }
8869            }
8870        }
8871
8872        #[gpui::test]
8873        async fn test_register_project_item(cx: &mut TestAppContext) {
8874            init_test(cx);
8875
8876            cx.update(|cx| {
8877                register_project_item::<TestPngItemView>(cx);
8878                register_project_item::<TestIpynbItemView>(cx);
8879            });
8880
8881            let fs = FakeFs::new(cx.executor());
8882            fs.insert_tree(
8883                "/root1",
8884                json!({
8885                    "one.png": "BINARYDATAHERE",
8886                    "two.ipynb": "{ totally a notebook }",
8887                    "three.txt": "editing text, sure why not?"
8888                }),
8889            )
8890            .await;
8891
8892            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8893            let (workspace, cx) =
8894                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8895
8896            let worktree_id = project.update(cx, |project, cx| {
8897                project.worktrees(cx).next().unwrap().read(cx).id()
8898            });
8899
8900            let handle = workspace
8901                .update_in(cx, |workspace, window, cx| {
8902                    let project_path = (worktree_id, "one.png");
8903                    workspace.open_path(project_path, None, true, window, cx)
8904                })
8905                .await
8906                .unwrap();
8907
8908            // Now we can check if the handle we got back errored or not
8909            assert_eq!(
8910                handle.to_any().entity_type(),
8911                TypeId::of::<TestPngItemView>()
8912            );
8913
8914            let handle = workspace
8915                .update_in(cx, |workspace, window, cx| {
8916                    let project_path = (worktree_id, "two.ipynb");
8917                    workspace.open_path(project_path, None, true, window, cx)
8918                })
8919                .await
8920                .unwrap();
8921
8922            assert_eq!(
8923                handle.to_any().entity_type(),
8924                TypeId::of::<TestIpynbItemView>()
8925            );
8926
8927            let handle = workspace
8928                .update_in(cx, |workspace, window, cx| {
8929                    let project_path = (worktree_id, "three.txt");
8930                    workspace.open_path(project_path, None, true, window, cx)
8931                })
8932                .await;
8933            assert!(handle.is_err());
8934        }
8935
8936        #[gpui::test]
8937        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8938            init_test(cx);
8939
8940            cx.update(|cx| {
8941                register_project_item::<TestPngItemView>(cx);
8942                register_project_item::<TestAlternatePngItemView>(cx);
8943            });
8944
8945            let fs = FakeFs::new(cx.executor());
8946            fs.insert_tree(
8947                "/root1",
8948                json!({
8949                    "one.png": "BINARYDATAHERE",
8950                    "two.ipynb": "{ totally a notebook }",
8951                    "three.txt": "editing text, sure why not?"
8952                }),
8953            )
8954            .await;
8955            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8956            let (workspace, cx) =
8957                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8958            let worktree_id = project.update(cx, |project, cx| {
8959                project.worktrees(cx).next().unwrap().read(cx).id()
8960            });
8961
8962            let handle = workspace
8963                .update_in(cx, |workspace, window, cx| {
8964                    let project_path = (worktree_id, "one.png");
8965                    workspace.open_path(project_path, None, true, window, cx)
8966                })
8967                .await
8968                .unwrap();
8969
8970            // This _must_ be the second item registered
8971            assert_eq!(
8972                handle.to_any().entity_type(),
8973                TypeId::of::<TestAlternatePngItemView>()
8974            );
8975
8976            let handle = workspace
8977                .update_in(cx, |workspace, window, cx| {
8978                    let project_path = (worktree_id, "three.txt");
8979                    workspace.open_path(project_path, None, true, window, cx)
8980                })
8981                .await;
8982            assert!(handle.is_err());
8983        }
8984    }
8985
8986    pub fn init_test(cx: &mut TestAppContext) {
8987        cx.update(|cx| {
8988            let settings_store = SettingsStore::test(cx);
8989            cx.set_global(settings_store);
8990            theme::init(theme::LoadThemes::JustBase, cx);
8991            language::init(cx);
8992            crate::init_settings(cx);
8993            Project::init_settings(cx);
8994        });
8995    }
8996
8997    fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
8998        let item = TestProjectItem::new(id, path, cx);
8999        item.update(cx, |item, _| {
9000            item.is_dirty = true;
9001        });
9002        item
9003    }
9004}