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