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