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