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