workspace.rs

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