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