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