workspace.rs

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