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