workspace.rs

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