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