workspace.rs

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