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_spawn(persistence::DB.update_timestamp(database_id))
4341                    .detach();
4342            }
4343        } else {
4344            for pane in &self.panes {
4345                pane.update(cx, |pane, cx| {
4346                    if let Some(item) = pane.active_item() {
4347                        item.workspace_deactivated(window, cx);
4348                    }
4349                    for item in pane.items() {
4350                        if matches!(
4351                            item.workspace_settings(cx).autosave,
4352                            AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
4353                        ) {
4354                            Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
4355                                .detach_and_log_err(cx);
4356                        }
4357                    }
4358                });
4359            }
4360        }
4361    }
4362
4363    pub fn active_call(&self) -> Option<&Entity<ActiveCall>> {
4364        self.active_call.as_ref().map(|(call, _)| call)
4365    }
4366
4367    fn on_active_call_event(
4368        &mut self,
4369        _: &Entity<ActiveCall>,
4370        event: &call::room::Event,
4371        window: &mut Window,
4372        cx: &mut Context<Self>,
4373    ) {
4374        match event {
4375            call::room::Event::ParticipantLocationChanged { participant_id }
4376            | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4377                self.leader_updated(*participant_id, window, cx);
4378            }
4379            _ => {}
4380        }
4381    }
4382
4383    pub fn database_id(&self) -> Option<WorkspaceId> {
4384        self.database_id
4385    }
4386
4387    fn local_paths(&self, cx: &App) -> Option<Vec<Arc<Path>>> {
4388        let project = self.project().read(cx);
4389
4390        if project.is_local() {
4391            Some(
4392                project
4393                    .visible_worktrees(cx)
4394                    .map(|worktree| worktree.read(cx).abs_path())
4395                    .collect::<Vec<_>>(),
4396            )
4397        } else {
4398            None
4399        }
4400    }
4401
4402    fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
4403        match member {
4404            Member::Axis(PaneAxis { members, .. }) => {
4405                for child in members.iter() {
4406                    self.remove_panes(child.clone(), window, cx)
4407                }
4408            }
4409            Member::Pane(pane) => {
4410                self.force_remove_pane(&pane, &None, window, cx);
4411            }
4412        }
4413    }
4414
4415    fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
4416        self.session_id.take();
4417        self.serialize_workspace_internal(window, cx)
4418    }
4419
4420    fn force_remove_pane(
4421        &mut self,
4422        pane: &Entity<Pane>,
4423        focus_on: &Option<Entity<Pane>>,
4424        window: &mut Window,
4425        cx: &mut Context<Workspace>,
4426    ) {
4427        self.panes.retain(|p| p != pane);
4428        if let Some(focus_on) = focus_on {
4429            focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4430        } else {
4431            if self.active_pane() == pane {
4432                self.panes
4433                    .last()
4434                    .unwrap()
4435                    .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4436            }
4437        }
4438        if self.last_active_center_pane == Some(pane.downgrade()) {
4439            self.last_active_center_pane = None;
4440        }
4441        cx.notify();
4442    }
4443
4444    fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4445        if self._schedule_serialize.is_none() {
4446            self._schedule_serialize = Some(cx.spawn_in(window, |this, mut cx| async move {
4447                cx.background_executor()
4448                    .timer(Duration::from_millis(100))
4449                    .await;
4450                this.update_in(&mut cx, |this, window, cx| {
4451                    this.serialize_workspace_internal(window, cx).detach();
4452                    this._schedule_serialize.take();
4453                })
4454                .log_err();
4455            }));
4456        }
4457    }
4458
4459    fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
4460        let Some(database_id) = self.database_id() else {
4461            return Task::ready(());
4462        };
4463
4464        fn serialize_pane_handle(
4465            pane_handle: &Entity<Pane>,
4466            window: &mut Window,
4467            cx: &mut App,
4468        ) -> SerializedPane {
4469            let (items, active, pinned_count) = {
4470                let pane = pane_handle.read(cx);
4471                let active_item_id = pane.active_item().map(|item| item.item_id());
4472                (
4473                    pane.items()
4474                        .filter_map(|handle| {
4475                            let handle = handle.to_serializable_item_handle(cx)?;
4476
4477                            Some(SerializedItem {
4478                                kind: Arc::from(handle.serialized_item_kind()),
4479                                item_id: handle.item_id().as_u64(),
4480                                active: Some(handle.item_id()) == active_item_id,
4481                                preview: pane.is_active_preview_item(handle.item_id()),
4482                            })
4483                        })
4484                        .collect::<Vec<_>>(),
4485                    pane.has_focus(window, cx),
4486                    pane.pinned_count(),
4487                )
4488            };
4489
4490            SerializedPane::new(items, active, pinned_count)
4491        }
4492
4493        fn build_serialized_pane_group(
4494            pane_group: &Member,
4495            window: &mut Window,
4496            cx: &mut App,
4497        ) -> SerializedPaneGroup {
4498            match pane_group {
4499                Member::Axis(PaneAxis {
4500                    axis,
4501                    members,
4502                    flexes,
4503                    bounding_boxes: _,
4504                }) => SerializedPaneGroup::Group {
4505                    axis: SerializedAxis(*axis),
4506                    children: members
4507                        .iter()
4508                        .map(|member| build_serialized_pane_group(member, window, cx))
4509                        .collect::<Vec<_>>(),
4510                    flexes: Some(flexes.lock().clone()),
4511                },
4512                Member::Pane(pane_handle) => {
4513                    SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
4514                }
4515            }
4516        }
4517
4518        fn build_serialized_docks(
4519            this: &Workspace,
4520            window: &mut Window,
4521            cx: &mut App,
4522        ) -> DockStructure {
4523            let left_dock = this.left_dock.read(cx);
4524            let left_visible = left_dock.is_open();
4525            let left_active_panel = left_dock
4526                .active_panel()
4527                .map(|panel| panel.persistent_name().to_string());
4528            let left_dock_zoom = left_dock
4529                .active_panel()
4530                .map(|panel| panel.is_zoomed(window, cx))
4531                .unwrap_or(false);
4532
4533            let right_dock = this.right_dock.read(cx);
4534            let right_visible = right_dock.is_open();
4535            let right_active_panel = right_dock
4536                .active_panel()
4537                .map(|panel| panel.persistent_name().to_string());
4538            let right_dock_zoom = right_dock
4539                .active_panel()
4540                .map(|panel| panel.is_zoomed(window, cx))
4541                .unwrap_or(false);
4542
4543            let bottom_dock = this.bottom_dock.read(cx);
4544            let bottom_visible = bottom_dock.is_open();
4545            let bottom_active_panel = bottom_dock
4546                .active_panel()
4547                .map(|panel| panel.persistent_name().to_string());
4548            let bottom_dock_zoom = bottom_dock
4549                .active_panel()
4550                .map(|panel| panel.is_zoomed(window, cx))
4551                .unwrap_or(false);
4552
4553            DockStructure {
4554                left: DockData {
4555                    visible: left_visible,
4556                    active_panel: left_active_panel,
4557                    zoom: left_dock_zoom,
4558                },
4559                right: DockData {
4560                    visible: right_visible,
4561                    active_panel: right_active_panel,
4562                    zoom: right_dock_zoom,
4563                },
4564                bottom: DockData {
4565                    visible: bottom_visible,
4566                    active_panel: bottom_active_panel,
4567                    zoom: bottom_dock_zoom,
4568                },
4569            }
4570        }
4571
4572        let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4573            Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4574        } else if let Some(local_paths) = self.local_paths(cx) {
4575            if !local_paths.is_empty() {
4576                Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4577            } else {
4578                None
4579            }
4580        } else {
4581            None
4582        };
4583
4584        if let Some(location) = location {
4585            let center_group = build_serialized_pane_group(&self.center.root, window, cx);
4586            let docks = build_serialized_docks(self, window, cx);
4587            let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
4588            let serialized_workspace = SerializedWorkspace {
4589                id: database_id,
4590                location,
4591                center_group,
4592                window_bounds,
4593                display: Default::default(),
4594                docks,
4595                centered_layout: self.centered_layout,
4596                session_id: self.session_id.clone(),
4597                window_id: Some(window.window_handle().window_id().as_u64()),
4598            };
4599            return window.spawn(cx, |_| persistence::DB.save_workspace(serialized_workspace));
4600        }
4601        Task::ready(())
4602    }
4603
4604    async fn serialize_items(
4605        this: &WeakEntity<Self>,
4606        items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4607        cx: &mut AsyncWindowContext,
4608    ) -> Result<()> {
4609        const CHUNK_SIZE: usize = 200;
4610
4611        let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4612
4613        while let Some(items_received) = serializable_items.next().await {
4614            let unique_items =
4615                items_received
4616                    .into_iter()
4617                    .fold(HashMap::default(), |mut acc, item| {
4618                        acc.entry(item.item_id()).or_insert(item);
4619                        acc
4620                    });
4621
4622            // We use into_iter() here so that the references to the items are moved into
4623            // the tasks and not kept alive while we're sleeping.
4624            for (_, item) in unique_items.into_iter() {
4625                if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
4626                    item.serialize(workspace, false, window, cx)
4627                }) {
4628                    cx.background_spawn(async move { task.await.log_err() })
4629                        .detach();
4630                }
4631            }
4632
4633            cx.background_executor()
4634                .timer(SERIALIZATION_THROTTLE_TIME)
4635                .await;
4636        }
4637
4638        Ok(())
4639    }
4640
4641    pub(crate) fn enqueue_item_serialization(
4642        &mut self,
4643        item: Box<dyn SerializableItemHandle>,
4644    ) -> Result<()> {
4645        self.serializable_items_tx
4646            .unbounded_send(item)
4647            .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4648    }
4649
4650    pub(crate) fn load_workspace(
4651        serialized_workspace: SerializedWorkspace,
4652        paths_to_open: Vec<Option<ProjectPath>>,
4653        window: &mut Window,
4654        cx: &mut Context<Workspace>,
4655    ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4656        cx.spawn_in(window, |workspace, mut cx| async move {
4657            let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4658
4659            let mut center_group = None;
4660            let mut center_items = None;
4661
4662            // Traverse the splits tree and add to things
4663            if let Some((group, active_pane, items)) = serialized_workspace
4664                .center_group
4665                .deserialize(
4666                    &project,
4667                    serialized_workspace.id,
4668                    workspace.clone(),
4669                    &mut cx,
4670                )
4671                .await
4672            {
4673                center_items = Some(items);
4674                center_group = Some((group, active_pane))
4675            }
4676
4677            let mut items_by_project_path = HashMap::default();
4678            let mut item_ids_by_kind = HashMap::default();
4679            let mut all_deserialized_items = Vec::default();
4680            cx.update(|_, cx| {
4681                for item in center_items.unwrap_or_default().into_iter().flatten() {
4682                    if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4683                        item_ids_by_kind
4684                            .entry(serializable_item_handle.serialized_item_kind())
4685                            .or_insert(Vec::new())
4686                            .push(item.item_id().as_u64() as ItemId);
4687                    }
4688
4689                    if let Some(project_path) = item.project_path(cx) {
4690                        items_by_project_path.insert(project_path, item.clone());
4691                    }
4692                    all_deserialized_items.push(item);
4693                }
4694            })?;
4695
4696            let opened_items = paths_to_open
4697                .into_iter()
4698                .map(|path_to_open| {
4699                    path_to_open
4700                        .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4701                })
4702                .collect::<Vec<_>>();
4703
4704            // Remove old panes from workspace panes list
4705            workspace.update_in(&mut cx, |workspace, window, cx| {
4706                if let Some((center_group, active_pane)) = center_group {
4707                    workspace.remove_panes(workspace.center.root.clone(), window, cx);
4708
4709                    // Swap workspace center group
4710                    workspace.center = PaneGroup::with_root(center_group);
4711                    if let Some(active_pane) = active_pane {
4712                        workspace.set_active_pane(&active_pane, window, cx);
4713                        cx.focus_self(window);
4714                    } else {
4715                        workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
4716                    }
4717                }
4718
4719                let docks = serialized_workspace.docks;
4720
4721                for (dock, serialized_dock) in [
4722                    (&mut workspace.right_dock, docks.right),
4723                    (&mut workspace.left_dock, docks.left),
4724                    (&mut workspace.bottom_dock, docks.bottom),
4725                ]
4726                .iter_mut()
4727                {
4728                    dock.update(cx, |dock, cx| {
4729                        dock.serialized_dock = Some(serialized_dock.clone());
4730                        dock.restore_state(window, cx);
4731                    });
4732                }
4733
4734                cx.notify();
4735            })?;
4736
4737            // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4738            // after loading the items, we might have different items and in order to avoid
4739            // the database filling up, we delete items that haven't been loaded now.
4740            //
4741            // The items that have been loaded, have been saved after they've been added to the workspace.
4742            let clean_up_tasks = workspace.update_in(&mut cx, |_, window, cx| {
4743                item_ids_by_kind
4744                    .into_iter()
4745                    .map(|(item_kind, loaded_items)| {
4746                        SerializableItemRegistry::cleanup(
4747                            item_kind,
4748                            serialized_workspace.id,
4749                            loaded_items,
4750                            window,
4751                            cx,
4752                        )
4753                        .log_err()
4754                    })
4755                    .collect::<Vec<_>>()
4756            })?;
4757
4758            futures::future::join_all(clean_up_tasks).await;
4759
4760            workspace
4761                .update_in(&mut cx, |workspace, window, cx| {
4762                    // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4763                    workspace.serialize_workspace_internal(window, cx).detach();
4764
4765                    // Ensure that we mark the window as edited if we did load dirty items
4766                    workspace.update_window_edited(window, cx);
4767                })
4768                .ok();
4769
4770            Ok(opened_items)
4771        })
4772    }
4773
4774    fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
4775        self.add_workspace_actions_listeners(div, window, cx)
4776            .on_action(cx.listener(Self::close_inactive_items_and_panes))
4777            .on_action(cx.listener(Self::close_all_items_and_panes))
4778            .on_action(cx.listener(Self::save_all))
4779            .on_action(cx.listener(Self::send_keystrokes))
4780            .on_action(cx.listener(Self::add_folder_to_project))
4781            .on_action(cx.listener(Self::follow_next_collaborator))
4782            .on_action(cx.listener(Self::close_window))
4783            .on_action(cx.listener(Self::activate_pane_at_index))
4784            .on_action(cx.listener(Self::move_item_to_pane_at_index))
4785            .on_action(cx.listener(Self::move_focused_panel_to_next_position))
4786            .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
4787                let pane = workspace.active_pane().clone();
4788                workspace.unfollow_in_pane(&pane, window, cx);
4789            }))
4790            .on_action(cx.listener(|workspace, action: &Save, window, cx| {
4791                workspace
4792                    .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
4793                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4794            }))
4795            .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
4796                workspace
4797                    .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
4798                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4799            }))
4800            .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
4801                workspace
4802                    .save_active_item(SaveIntent::SaveAs, window, cx)
4803                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4804            }))
4805            .on_action(
4806                cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
4807                    workspace.activate_previous_pane(window, cx)
4808                }),
4809            )
4810            .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4811                workspace.activate_next_pane(window, cx)
4812            }))
4813            .on_action(
4814                cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
4815                    workspace.activate_next_window(cx)
4816                }),
4817            )
4818            .on_action(
4819                cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
4820                    workspace.activate_previous_window(cx)
4821                }),
4822            )
4823            .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
4824                workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
4825            }))
4826            .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
4827                workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
4828            }))
4829            .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
4830                workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
4831            }))
4832            .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
4833                workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
4834            }))
4835            .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4836                workspace.activate_next_pane(window, cx)
4837            }))
4838            .on_action(cx.listener(
4839                |workspace, action: &MoveItemToPaneInDirection, window, cx| {
4840                    workspace.move_item_to_pane_in_direction(action, window, cx)
4841                },
4842            ))
4843            .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
4844                workspace.swap_pane_in_direction(SplitDirection::Left, cx)
4845            }))
4846            .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
4847                workspace.swap_pane_in_direction(SplitDirection::Right, cx)
4848            }))
4849            .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
4850                workspace.swap_pane_in_direction(SplitDirection::Up, cx)
4851            }))
4852            .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
4853                workspace.swap_pane_in_direction(SplitDirection::Down, cx)
4854            }))
4855            .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
4856                this.toggle_dock(DockPosition::Left, window, cx);
4857            }))
4858            .on_action(cx.listener(
4859                |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
4860                    workspace.toggle_dock(DockPosition::Right, window, cx);
4861                },
4862            ))
4863            .on_action(cx.listener(
4864                |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
4865                    workspace.toggle_dock(DockPosition::Bottom, window, cx);
4866                },
4867            ))
4868            .on_action(
4869                cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
4870                    workspace.close_all_docks(window, cx);
4871                }),
4872            )
4873            .on_action(cx.listener(
4874                |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
4875                    workspace.clear_all_notifications(cx);
4876                },
4877            ))
4878            .on_action(cx.listener(
4879                |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
4880                    workspace.reopen_closed_item(window, cx).detach();
4881                },
4882            ))
4883            .on_action(cx.listener(Workspace::toggle_centered_layout))
4884    }
4885
4886    #[cfg(any(test, feature = "test-support"))]
4887    pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
4888        use node_runtime::NodeRuntime;
4889        use session::Session;
4890
4891        let client = project.read(cx).client();
4892        let user_store = project.read(cx).user_store();
4893
4894        let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
4895        let session = cx.new(|cx| AppSession::new(Session::test(), cx));
4896        window.activate_window();
4897        let app_state = Arc::new(AppState {
4898            languages: project.read(cx).languages().clone(),
4899            workspace_store,
4900            client,
4901            user_store,
4902            fs: project.read(cx).fs().clone(),
4903            build_window_options: |_, _| Default::default(),
4904            node_runtime: NodeRuntime::unavailable(),
4905            session,
4906        });
4907        let workspace = Self::new(Default::default(), project, app_state, window, cx);
4908        workspace
4909            .active_pane
4910            .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4911        workspace
4912    }
4913
4914    pub fn register_action<A: Action>(
4915        &mut self,
4916        callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
4917    ) -> &mut Self {
4918        let callback = Arc::new(callback);
4919
4920        self.workspace_actions.push(Box::new(move |div, _, cx| {
4921            let callback = callback.clone();
4922            div.on_action(cx.listener(move |workspace, event, window, cx| {
4923                (callback.clone())(workspace, event, window, cx)
4924            }))
4925        }));
4926        self
4927    }
4928
4929    fn add_workspace_actions_listeners(
4930        &self,
4931        mut div: Div,
4932        window: &mut Window,
4933        cx: &mut Context<Self>,
4934    ) -> Div {
4935        for action in self.workspace_actions.iter() {
4936            div = (action)(div, window, cx)
4937        }
4938        div
4939    }
4940
4941    pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
4942        self.modal_layer.read(cx).has_active_modal()
4943    }
4944
4945    pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
4946        self.modal_layer.read(cx).active_modal()
4947    }
4948
4949    pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
4950    where
4951        B: FnOnce(&mut Window, &mut Context<V>) -> V,
4952    {
4953        self.modal_layer.update(cx, |modal_layer, cx| {
4954            modal_layer.toggle_modal(window, cx, build)
4955        })
4956    }
4957
4958    pub fn toggle_centered_layout(
4959        &mut self,
4960        _: &ToggleCenteredLayout,
4961        _: &mut Window,
4962        cx: &mut Context<Self>,
4963    ) {
4964        self.centered_layout = !self.centered_layout;
4965        if let Some(database_id) = self.database_id() {
4966            cx.background_spawn(DB.set_centered_layout(database_id, self.centered_layout))
4967                .detach_and_log_err(cx);
4968        }
4969        cx.notify();
4970    }
4971
4972    fn adjust_padding(padding: Option<f32>) -> f32 {
4973        padding
4974            .unwrap_or(Self::DEFAULT_PADDING)
4975            .clamp(0.0, Self::MAX_PADDING)
4976    }
4977
4978    fn render_dock(
4979        &self,
4980        position: DockPosition,
4981        dock: &Entity<Dock>,
4982        window: &mut Window,
4983        cx: &mut App,
4984    ) -> Option<Div> {
4985        if self.zoomed_position == Some(position) {
4986            return None;
4987        }
4988
4989        let leader_border = dock.read(cx).active_panel().and_then(|panel| {
4990            let pane = panel.pane(cx)?;
4991            let follower_states = &self.follower_states;
4992            leader_border_for_pane(follower_states, &pane, window, cx)
4993        });
4994
4995        Some(
4996            div()
4997                .flex()
4998                .flex_none()
4999                .overflow_hidden()
5000                .child(dock.clone())
5001                .children(leader_border),
5002        )
5003    }
5004
5005    pub fn for_window(window: &mut Window, _: &mut App) -> Option<Entity<Workspace>> {
5006        window.root().flatten()
5007    }
5008
5009    pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
5010        self.zoomed.as_ref()
5011    }
5012
5013    pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
5014        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5015            return;
5016        };
5017        let windows = cx.windows();
5018        let Some(next_window) = windows
5019            .iter()
5020            .cycle()
5021            .skip_while(|window| window.window_id() != current_window_id)
5022            .nth(1)
5023        else {
5024            return;
5025        };
5026        next_window
5027            .update(cx, |_, window, _| window.activate_window())
5028            .ok();
5029    }
5030
5031    pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
5032        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5033            return;
5034        };
5035        let windows = cx.windows();
5036        let Some(prev_window) = windows
5037            .iter()
5038            .rev()
5039            .cycle()
5040            .skip_while(|window| window.window_id() != current_window_id)
5041            .nth(1)
5042        else {
5043            return;
5044        };
5045        prev_window
5046            .update(cx, |_, window, _| window.activate_window())
5047            .ok();
5048    }
5049}
5050
5051fn leader_border_for_pane(
5052    follower_states: &HashMap<PeerId, FollowerState>,
5053    pane: &Entity<Pane>,
5054    _: &Window,
5055    cx: &App,
5056) -> Option<Div> {
5057    let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
5058        if state.pane() == pane {
5059            Some((*leader_id, state))
5060        } else {
5061            None
5062        }
5063    })?;
5064
5065    let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
5066    let leader = room.remote_participant_for_peer_id(leader_id)?;
5067
5068    let mut leader_color = cx
5069        .theme()
5070        .players()
5071        .color_for_participant(leader.participant_index.0)
5072        .cursor;
5073    leader_color.fade_out(0.3);
5074    Some(
5075        div()
5076            .absolute()
5077            .size_full()
5078            .left_0()
5079            .top_0()
5080            .border_2()
5081            .border_color(leader_color),
5082    )
5083}
5084
5085fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
5086    ZED_WINDOW_POSITION
5087        .zip(*ZED_WINDOW_SIZE)
5088        .map(|(position, size)| Bounds {
5089            origin: position,
5090            size,
5091        })
5092}
5093
5094fn open_items(
5095    serialized_workspace: Option<SerializedWorkspace>,
5096    mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
5097    window: &mut Window,
5098    cx: &mut Context<Workspace>,
5099) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
5100    let restored_items = serialized_workspace.map(|serialized_workspace| {
5101        Workspace::load_workspace(
5102            serialized_workspace,
5103            project_paths_to_open
5104                .iter()
5105                .map(|(_, project_path)| project_path)
5106                .cloned()
5107                .collect(),
5108            window,
5109            cx,
5110        )
5111    });
5112
5113    cx.spawn_in(window, |workspace, mut cx| async move {
5114        let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
5115
5116        if let Some(restored_items) = restored_items {
5117            let restored_items = restored_items.await?;
5118
5119            let restored_project_paths = restored_items
5120                .iter()
5121                .filter_map(|item| {
5122                    cx.update(|_, cx| item.as_ref()?.project_path(cx))
5123                        .ok()
5124                        .flatten()
5125                })
5126                .collect::<HashSet<_>>();
5127
5128            for restored_item in restored_items {
5129                opened_items.push(restored_item.map(Ok));
5130            }
5131
5132            project_paths_to_open
5133                .iter_mut()
5134                .for_each(|(_, project_path)| {
5135                    if let Some(project_path_to_open) = project_path {
5136                        if restored_project_paths.contains(project_path_to_open) {
5137                            *project_path = None;
5138                        }
5139                    }
5140                });
5141        } else {
5142            for _ in 0..project_paths_to_open.len() {
5143                opened_items.push(None);
5144            }
5145        }
5146        assert!(opened_items.len() == project_paths_to_open.len());
5147
5148        let tasks =
5149            project_paths_to_open
5150                .into_iter()
5151                .enumerate()
5152                .map(|(ix, (abs_path, project_path))| {
5153                    let workspace = workspace.clone();
5154                    cx.spawn(|mut cx| async move {
5155                        let file_project_path = project_path?;
5156                        let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
5157                            workspace.project().update(cx, |project, cx| {
5158                                project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
5159                            })
5160                        });
5161
5162                        // We only want to open file paths here. If one of the items
5163                        // here is a directory, it was already opened further above
5164                        // with a `find_or_create_worktree`.
5165                        if let Ok(task) = abs_path_task {
5166                            if task.await.map_or(true, |p| p.is_file()) {
5167                                return Some((
5168                                    ix,
5169                                    workspace
5170                                        .update_in(&mut cx, |workspace, window, cx| {
5171                                            workspace.open_path(
5172                                                file_project_path,
5173                                                None,
5174                                                true,
5175                                                window,
5176                                                cx,
5177                                            )
5178                                        })
5179                                        .log_err()?
5180                                        .await,
5181                                ));
5182                            }
5183                        }
5184                        None
5185                    })
5186                });
5187
5188        let tasks = tasks.collect::<Vec<_>>();
5189
5190        let tasks = futures::future::join_all(tasks);
5191        for (ix, path_open_result) in tasks.await.into_iter().flatten() {
5192            opened_items[ix] = Some(path_open_result);
5193        }
5194
5195        Ok(opened_items)
5196    })
5197}
5198
5199enum ActivateInDirectionTarget {
5200    Pane(Entity<Pane>),
5201    Dock(Entity<Dock>),
5202}
5203
5204fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
5205    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";
5206
5207    workspace
5208        .update(cx, |workspace, _, cx| {
5209            if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
5210                struct DatabaseFailedNotification;
5211
5212                workspace.show_notification(
5213                    NotificationId::unique::<DatabaseFailedNotification>(),
5214                    cx,
5215                    |cx| {
5216                        cx.new(|_| {
5217                            MessageNotification::new("Failed to load the database file.")
5218                                .primary_message("File an Issue")
5219                                .primary_icon(IconName::Plus)
5220                                .primary_on_click(|_window, cx| cx.open_url(REPORT_ISSUE_URL))
5221                        })
5222                    },
5223                );
5224            }
5225        })
5226        .log_err();
5227}
5228
5229impl Focusable for Workspace {
5230    fn focus_handle(&self, cx: &App) -> FocusHandle {
5231        self.active_pane.focus_handle(cx)
5232    }
5233}
5234
5235#[derive(Clone)]
5236struct DraggedDock(DockPosition);
5237
5238impl Render for DraggedDock {
5239    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5240        gpui::Empty
5241    }
5242}
5243
5244impl Render for Workspace {
5245    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5246        let mut context = KeyContext::new_with_defaults();
5247        context.add("Workspace");
5248        context.set("keyboard_layout", cx.keyboard_layout().clone());
5249        let centered_layout = self.centered_layout
5250            && self.center.panes().len() == 1
5251            && self.active_item(cx).is_some();
5252        let render_padding = |size| {
5253            (size > 0.0).then(|| {
5254                div()
5255                    .h_full()
5256                    .w(relative(size))
5257                    .bg(cx.theme().colors().editor_background)
5258                    .border_color(cx.theme().colors().pane_group_border)
5259            })
5260        };
5261        let paddings = if centered_layout {
5262            let settings = WorkspaceSettings::get_global(cx).centered_layout;
5263            (
5264                render_padding(Self::adjust_padding(settings.left_padding)),
5265                render_padding(Self::adjust_padding(settings.right_padding)),
5266            )
5267        } else {
5268            (None, None)
5269        };
5270        let ui_font = theme::setup_ui_font(window, cx);
5271
5272        let theme = cx.theme().clone();
5273        let colors = theme.colors();
5274
5275        client_side_decorations(
5276            self.actions(div(), window, cx)
5277                .key_context(context)
5278                .relative()
5279                .size_full()
5280                .flex()
5281                .flex_col()
5282                .font(ui_font)
5283                .gap_0()
5284                .justify_start()
5285                .items_start()
5286                .text_color(colors.text)
5287                .overflow_hidden()
5288                .children(self.titlebar_item.clone())
5289                .child(
5290                    div()
5291                        .size_full()
5292                        .relative()
5293                        .flex_1()
5294                        .flex()
5295                        .flex_col()
5296                        .child(
5297                            div()
5298                                .id("workspace")
5299                                .bg(colors.background)
5300                                .relative()
5301                                .flex_1()
5302                                .w_full()
5303                                .flex()
5304                                .flex_col()
5305                                .overflow_hidden()
5306                                .border_t_1()
5307                                .border_b_1()
5308                                .border_color(colors.border)
5309                                .child({
5310                                    let this = cx.entity().clone();
5311                                    canvas(
5312                                        move |bounds, window, cx| {
5313                                            this.update(cx, |this, cx| {
5314                                                let bounds_changed = this.bounds != bounds;
5315                                                this.bounds = bounds;
5316
5317                                                if bounds_changed {
5318                                                    this.left_dock.update(cx, |dock, cx| {
5319                                                        dock.clamp_panel_size(
5320                                                            bounds.size.width,
5321                                                            window,
5322                                                            cx,
5323                                                        )
5324                                                    });
5325
5326                                                    this.right_dock.update(cx, |dock, cx| {
5327                                                        dock.clamp_panel_size(
5328                                                            bounds.size.width,
5329                                                            window,
5330                                                            cx,
5331                                                        )
5332                                                    });
5333
5334                                                    this.bottom_dock.update(cx, |dock, cx| {
5335                                                        dock.clamp_panel_size(
5336                                                            bounds.size.height,
5337                                                            window,
5338                                                            cx,
5339                                                        )
5340                                                    });
5341                                                }
5342                                            })
5343                                        },
5344                                        |_, _, _, _| {},
5345                                    )
5346                                    .absolute()
5347                                    .size_full()
5348                                })
5349                                .when(self.zoomed.is_none(), |this| {
5350                                    this.on_drag_move(cx.listener(
5351                                        move |workspace,
5352                                              e: &DragMoveEvent<DraggedDock>,
5353                                              window,
5354                                              cx| {
5355                                            if workspace.previous_dock_drag_coordinates
5356                                                != Some(e.event.position)
5357                                            {
5358                                                workspace.previous_dock_drag_coordinates =
5359                                                    Some(e.event.position);
5360                                                match e.drag(cx).0 {
5361                                                    DockPosition::Left => {
5362                                                        resize_left_dock(
5363                                                            e.event.position.x
5364                                                                - workspace.bounds.left(),
5365                                                            workspace,
5366                                                            window,
5367                                                            cx,
5368                                                        );
5369                                                    }
5370                                                    DockPosition::Right => {
5371                                                        resize_right_dock(
5372                                                            workspace.bounds.right()
5373                                                                - e.event.position.x,
5374                                                            workspace,
5375                                                            window,
5376                                                            cx,
5377                                                        );
5378                                                    }
5379                                                    DockPosition::Bottom => {
5380                                                        resize_bottom_dock(
5381                                                            workspace.bounds.bottom()
5382                                                                - e.event.position.y,
5383                                                            workspace,
5384                                                            window,
5385                                                            cx,
5386                                                        );
5387                                                    }
5388                                                };
5389                                                workspace.serialize_workspace(window, cx);
5390                                            }
5391                                        },
5392                                    ))
5393                                })
5394                                .child(
5395                                    div()
5396                                        .flex()
5397                                        .flex_row()
5398                                        .h_full()
5399                                        // Left Dock
5400                                        .children(self.render_dock(
5401                                            DockPosition::Left,
5402                                            &self.left_dock,
5403                                            window,
5404                                            cx,
5405                                        ))
5406                                        // Panes
5407                                        .child(
5408                                            div()
5409                                                .flex()
5410                                                .flex_col()
5411                                                .flex_1()
5412                                                .overflow_hidden()
5413                                                .child(
5414                                                    h_flex()
5415                                                        .flex_1()
5416                                                        .when_some(paddings.0, |this, p| {
5417                                                            this.child(p.border_r_1())
5418                                                        })
5419                                                        .child(self.center.render(
5420                                                            &self.project,
5421                                                            &self.follower_states,
5422                                                            self.active_call(),
5423                                                            &self.active_pane,
5424                                                            self.zoomed.as_ref(),
5425                                                            &self.app_state,
5426                                                            window,
5427                                                            cx,
5428                                                        ))
5429                                                        .when_some(paddings.1, |this, p| {
5430                                                            this.child(p.border_l_1())
5431                                                        }),
5432                                                )
5433                                                .children(self.render_dock(
5434                                                    DockPosition::Bottom,
5435                                                    &self.bottom_dock,
5436                                                    window,
5437                                                    cx,
5438                                                )),
5439                                        )
5440                                        // Right Dock
5441                                        .children(self.render_dock(
5442                                            DockPosition::Right,
5443                                            &self.right_dock,
5444                                            window,
5445                                            cx,
5446                                        )),
5447                                )
5448                                .children(self.zoomed.as_ref().and_then(|view| {
5449                                    let zoomed_view = view.upgrade()?;
5450                                    let div = div()
5451                                        .occlude()
5452                                        .absolute()
5453                                        .overflow_hidden()
5454                                        .border_color(colors.border)
5455                                        .bg(colors.background)
5456                                        .child(zoomed_view)
5457                                        .inset_0()
5458                                        .shadow_lg();
5459
5460                                    Some(match self.zoomed_position {
5461                                        Some(DockPosition::Left) => div.right_2().border_r_1(),
5462                                        Some(DockPosition::Right) => div.left_2().border_l_1(),
5463                                        Some(DockPosition::Bottom) => div.top_2().border_t_1(),
5464                                        None => {
5465                                            div.top_2().bottom_2().left_2().right_2().border_1()
5466                                        }
5467                                    })
5468                                }))
5469                                .children(self.render_notifications(window, cx)),
5470                        )
5471                        .child(self.status_bar.clone())
5472                        .child(self.modal_layer.clone()),
5473                ),
5474            window,
5475            cx,
5476        )
5477    }
5478}
5479
5480fn resize_bottom_dock(
5481    new_size: Pixels,
5482    workspace: &mut Workspace,
5483    window: &mut Window,
5484    cx: &mut App,
5485) {
5486    let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
5487    workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5488        bottom_dock.resize_active_panel(Some(size), window, cx);
5489    });
5490}
5491
5492fn resize_right_dock(
5493    new_size: Pixels,
5494    workspace: &mut Workspace,
5495    window: &mut Window,
5496    cx: &mut App,
5497) {
5498    let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
5499    workspace.right_dock.update(cx, |right_dock, cx| {
5500        right_dock.resize_active_panel(Some(size), window, cx);
5501    });
5502}
5503
5504fn resize_left_dock(
5505    new_size: Pixels,
5506    workspace: &mut Workspace,
5507    window: &mut Window,
5508    cx: &mut App,
5509) {
5510    let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5511
5512    workspace.left_dock.update(cx, |left_dock, cx| {
5513        left_dock.resize_active_panel(Some(size), window, cx);
5514    });
5515}
5516
5517impl WorkspaceStore {
5518    pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
5519        Self {
5520            workspaces: Default::default(),
5521            _subscriptions: vec![
5522                client.add_request_handler(cx.weak_entity(), Self::handle_follow),
5523                client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
5524            ],
5525            client,
5526        }
5527    }
5528
5529    pub fn update_followers(
5530        &self,
5531        project_id: Option<u64>,
5532        update: proto::update_followers::Variant,
5533        cx: &App,
5534    ) -> Option<()> {
5535        let active_call = ActiveCall::try_global(cx)?;
5536        let room_id = active_call.read(cx).room()?.read(cx).id();
5537        self.client
5538            .send(proto::UpdateFollowers {
5539                room_id,
5540                project_id,
5541                variant: Some(update),
5542            })
5543            .log_err()
5544    }
5545
5546    pub async fn handle_follow(
5547        this: Entity<Self>,
5548        envelope: TypedEnvelope<proto::Follow>,
5549        mut cx: AsyncApp,
5550    ) -> Result<proto::FollowResponse> {
5551        this.update(&mut cx, |this, cx| {
5552            let follower = Follower {
5553                project_id: envelope.payload.project_id,
5554                peer_id: envelope.original_sender_id()?,
5555            };
5556
5557            let mut response = proto::FollowResponse::default();
5558            this.workspaces.retain(|workspace| {
5559                workspace
5560                    .update(cx, |workspace, window, cx| {
5561                        let handler_response =
5562                            workspace.handle_follow(follower.project_id, window, cx);
5563                        if let Some(active_view) = handler_response.active_view.clone() {
5564                            if workspace.project.read(cx).remote_id() == follower.project_id {
5565                                response.active_view = Some(active_view)
5566                            }
5567                        }
5568                    })
5569                    .is_ok()
5570            });
5571
5572            Ok(response)
5573        })?
5574    }
5575
5576    async fn handle_update_followers(
5577        this: Entity<Self>,
5578        envelope: TypedEnvelope<proto::UpdateFollowers>,
5579        mut cx: AsyncApp,
5580    ) -> Result<()> {
5581        let leader_id = envelope.original_sender_id()?;
5582        let update = envelope.payload;
5583
5584        this.update(&mut cx, |this, cx| {
5585            this.workspaces.retain(|workspace| {
5586                workspace
5587                    .update(cx, |workspace, window, cx| {
5588                        let project_id = workspace.project.read(cx).remote_id();
5589                        if update.project_id != project_id && update.project_id.is_some() {
5590                            return;
5591                        }
5592                        workspace.handle_update_followers(leader_id, update.clone(), window, cx);
5593                    })
5594                    .is_ok()
5595            });
5596            Ok(())
5597        })?
5598    }
5599}
5600
5601impl ViewId {
5602    pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5603        Ok(Self {
5604            creator: message
5605                .creator
5606                .ok_or_else(|| anyhow!("creator is missing"))?,
5607            id: message.id,
5608        })
5609    }
5610
5611    pub(crate) fn to_proto(self) -> proto::ViewId {
5612        proto::ViewId {
5613            creator: Some(self.creator),
5614            id: self.id,
5615        }
5616    }
5617}
5618
5619impl FollowerState {
5620    fn pane(&self) -> &Entity<Pane> {
5621        self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5622    }
5623}
5624
5625pub trait WorkspaceHandle {
5626    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
5627}
5628
5629impl WorkspaceHandle for Entity<Workspace> {
5630    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
5631        self.read(cx)
5632            .worktrees(cx)
5633            .flat_map(|worktree| {
5634                let worktree_id = worktree.read(cx).id();
5635                worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5636                    worktree_id,
5637                    path: f.path.clone(),
5638                })
5639            })
5640            .collect::<Vec<_>>()
5641    }
5642}
5643
5644impl std::fmt::Debug for OpenPaths {
5645    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5646        f.debug_struct("OpenPaths")
5647            .field("paths", &self.paths)
5648            .finish()
5649    }
5650}
5651
5652pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5653    DB.last_workspace().await.log_err().flatten()
5654}
5655
5656pub fn last_session_workspace_locations(
5657    last_session_id: &str,
5658    last_session_window_stack: Option<Vec<WindowId>>,
5659) -> Option<Vec<SerializedWorkspaceLocation>> {
5660    DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5661        .log_err()
5662}
5663
5664actions!(collab, [OpenChannelNotes]);
5665actions!(zed, [OpenLog]);
5666
5667async fn join_channel_internal(
5668    channel_id: ChannelId,
5669    app_state: &Arc<AppState>,
5670    requesting_window: Option<WindowHandle<Workspace>>,
5671    active_call: &Entity<ActiveCall>,
5672    cx: &mut AsyncApp,
5673) -> Result<bool> {
5674    let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5675        let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5676            return (false, None);
5677        };
5678
5679        let already_in_channel = room.channel_id() == Some(channel_id);
5680        let should_prompt = room.is_sharing_project()
5681            && !room.remote_participants().is_empty()
5682            && !already_in_channel;
5683        let open_room = if already_in_channel {
5684            active_call.room().cloned()
5685        } else {
5686            None
5687        };
5688        (should_prompt, open_room)
5689    })?;
5690
5691    if let Some(room) = open_room {
5692        let task = room.update(cx, |room, cx| {
5693            if let Some((project, host)) = room.most_active_project(cx) {
5694                return Some(join_in_room_project(project, host, app_state.clone(), cx));
5695            }
5696
5697            None
5698        })?;
5699        if let Some(task) = task {
5700            task.await?;
5701        }
5702        return anyhow::Ok(true);
5703    }
5704
5705    if should_prompt {
5706        if let Some(workspace) = requesting_window {
5707            let answer = workspace
5708                .update(cx, |_, window, cx| {
5709                    window.prompt(
5710                        PromptLevel::Warning,
5711                        "Do you want to switch channels?",
5712                        Some("Leaving this call will unshare your current project."),
5713                        &["Yes, Join Channel", "Cancel"],
5714                        cx,
5715                    )
5716                })?
5717                .await;
5718
5719            if answer == Ok(1) {
5720                return Ok(false);
5721            }
5722        } else {
5723            return Ok(false); // unreachable!() hopefully
5724        }
5725    }
5726
5727    let client = cx.update(|cx| active_call.read(cx).client())?;
5728
5729    let mut client_status = client.status();
5730
5731    // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5732    'outer: loop {
5733        let Some(status) = client_status.recv().await else {
5734            return Err(anyhow!("error connecting"));
5735        };
5736
5737        match status {
5738            Status::Connecting
5739            | Status::Authenticating
5740            | Status::Reconnecting
5741            | Status::Reauthenticating => continue,
5742            Status::Connected { .. } => break 'outer,
5743            Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5744            Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5745            Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5746                return Err(ErrorCode::Disconnected.into());
5747            }
5748        }
5749    }
5750
5751    let room = active_call
5752        .update(cx, |active_call, cx| {
5753            active_call.join_channel(channel_id, cx)
5754        })?
5755        .await?;
5756
5757    let Some(room) = room else {
5758        return anyhow::Ok(true);
5759    };
5760
5761    room.update(cx, |room, _| room.room_update_completed())?
5762        .await;
5763
5764    let task = room.update(cx, |room, cx| {
5765        if let Some((project, host)) = room.most_active_project(cx) {
5766            return Some(join_in_room_project(project, host, app_state.clone(), cx));
5767        }
5768
5769        // If you are the first to join a channel, see if you should share your project.
5770        if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5771            if let Some(workspace) = requesting_window {
5772                let project = workspace.update(cx, |workspace, _, cx| {
5773                    let project = workspace.project.read(cx);
5774
5775                    if !CallSettings::get_global(cx).share_on_join {
5776                        return None;
5777                    }
5778
5779                    if (project.is_local() || project.is_via_ssh())
5780                        && project.visible_worktrees(cx).any(|tree| {
5781                            tree.read(cx)
5782                                .root_entry()
5783                                .map_or(false, |entry| entry.is_dir())
5784                        })
5785                    {
5786                        Some(workspace.project.clone())
5787                    } else {
5788                        None
5789                    }
5790                });
5791                if let Ok(Some(project)) = project {
5792                    return Some(cx.spawn(|room, mut cx| async move {
5793                        room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5794                            .await?;
5795                        Ok(())
5796                    }));
5797                }
5798            }
5799        }
5800
5801        None
5802    })?;
5803    if let Some(task) = task {
5804        task.await?;
5805        return anyhow::Ok(true);
5806    }
5807    anyhow::Ok(false)
5808}
5809
5810pub fn join_channel(
5811    channel_id: ChannelId,
5812    app_state: Arc<AppState>,
5813    requesting_window: Option<WindowHandle<Workspace>>,
5814    cx: &mut App,
5815) -> Task<Result<()>> {
5816    let active_call = ActiveCall::global(cx);
5817    cx.spawn(|mut cx| async move {
5818        let result = join_channel_internal(
5819            channel_id,
5820            &app_state,
5821            requesting_window,
5822            &active_call,
5823            &mut cx,
5824        )
5825            .await;
5826
5827        // join channel succeeded, and opened a window
5828        if matches!(result, Ok(true)) {
5829            return anyhow::Ok(());
5830        }
5831
5832        // find an existing workspace to focus and show call controls
5833        let mut active_window =
5834            requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5835        if active_window.is_none() {
5836            // no open workspaces, make one to show the error in (blergh)
5837            let (window_handle, _) = cx
5838                .update(|cx| {
5839                    Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5840                })?
5841                .await?;
5842
5843            if result.is_ok() {
5844                cx.update(|cx| {
5845                    cx.dispatch_action(&OpenChannelNotes);
5846                }).log_err();
5847            }
5848
5849            active_window = Some(window_handle);
5850        }
5851
5852        if let Err(err) = result {
5853            log::error!("failed to join channel: {}", err);
5854            if let Some(active_window) = active_window {
5855                active_window
5856                    .update(&mut cx, |_, window, cx| {
5857                        let detail: SharedString = match err.error_code() {
5858                            ErrorCode::SignedOut => {
5859                                "Please sign in to continue.".into()
5860                            }
5861                            ErrorCode::UpgradeRequired => {
5862                                "Your are running an unsupported version of Zed. Please update to continue.".into()
5863                            }
5864                            ErrorCode::NoSuchChannel => {
5865                                "No matching channel was found. Please check the link and try again.".into()
5866                            }
5867                            ErrorCode::Forbidden => {
5868                                "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5869                            }
5870                            ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5871                            _ => format!("{}\n\nPlease try again.", err).into(),
5872                        };
5873                        window.prompt(
5874                            PromptLevel::Critical,
5875                            "Failed to join channel",
5876                            Some(&detail),
5877                            &["Ok"],
5878                        cx)
5879                    })?
5880                    .await
5881                    .ok();
5882            }
5883        }
5884
5885        // return ok, we showed the error to the user.
5886        anyhow::Ok(())
5887    })
5888}
5889
5890pub async fn get_any_active_workspace(
5891    app_state: Arc<AppState>,
5892    mut cx: AsyncApp,
5893) -> anyhow::Result<WindowHandle<Workspace>> {
5894    // find an existing workspace to focus and show call controls
5895    let active_window = activate_any_workspace_window(&mut cx);
5896    if active_window.is_none() {
5897        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5898            .await?;
5899    }
5900    activate_any_workspace_window(&mut cx).context("could not open zed")
5901}
5902
5903fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
5904    cx.update(|cx| {
5905        if let Some(workspace_window) = cx
5906            .active_window()
5907            .and_then(|window| window.downcast::<Workspace>())
5908        {
5909            return Some(workspace_window);
5910        }
5911
5912        for window in cx.windows() {
5913            if let Some(workspace_window) = window.downcast::<Workspace>() {
5914                workspace_window
5915                    .update(cx, |_, window, _| window.activate_window())
5916                    .ok();
5917                return Some(workspace_window);
5918            }
5919        }
5920        None
5921    })
5922    .ok()
5923    .flatten()
5924}
5925
5926pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
5927    cx.windows()
5928        .into_iter()
5929        .filter_map(|window| window.downcast::<Workspace>())
5930        .filter(|workspace| {
5931            workspace
5932                .read(cx)
5933                .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5934        })
5935        .collect()
5936}
5937
5938#[derive(Default)]
5939pub struct OpenOptions {
5940    pub open_new_workspace: Option<bool>,
5941    pub replace_window: Option<WindowHandle<Workspace>>,
5942    pub env: Option<HashMap<String, String>>,
5943}
5944#[allow(clippy::type_complexity)]
5945pub fn open_paths(
5946    abs_paths: &[PathBuf],
5947    app_state: Arc<AppState>,
5948    open_options: OpenOptions,
5949    cx: &mut App,
5950) -> Task<
5951    anyhow::Result<(
5952        WindowHandle<Workspace>,
5953        Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5954    )>,
5955> {
5956    let abs_paths = abs_paths.to_vec();
5957    let mut existing = None;
5958    let mut best_match = None;
5959    let mut open_visible = OpenVisible::All;
5960
5961    cx.spawn(move |mut cx| async move {
5962        if open_options.open_new_workspace != Some(true) {
5963            let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5964            let all_metadatas = futures::future::join_all(all_paths)
5965                .await
5966                .into_iter()
5967                .filter_map(|result| result.ok().flatten())
5968                .collect::<Vec<_>>();
5969
5970            cx.update(|cx| {
5971                for window in local_workspace_windows(&cx) {
5972                    if let Ok(workspace) = window.read(&cx) {
5973                        let m = workspace.project.read(&cx).visibility_for_paths(
5974                            &abs_paths,
5975                            &all_metadatas,
5976                            open_options.open_new_workspace == None,
5977                            cx,
5978                        );
5979                        if m > best_match {
5980                            existing = Some(window);
5981                            best_match = m;
5982                        } else if best_match.is_none()
5983                            && open_options.open_new_workspace == Some(false)
5984                        {
5985                            existing = Some(window)
5986                        }
5987                    }
5988                }
5989            })?;
5990
5991            if open_options.open_new_workspace.is_none() && existing.is_none() {
5992                if all_metadatas.iter().all(|file| !file.is_dir) {
5993                    cx.update(|cx| {
5994                        if let Some(window) = cx
5995                            .active_window()
5996                            .and_then(|window| window.downcast::<Workspace>())
5997                        {
5998                            if let Ok(workspace) = window.read(cx) {
5999                                let project = workspace.project().read(cx);
6000                                if project.is_local() && !project.is_via_collab() {
6001                                    existing = Some(window);
6002                                    open_visible = OpenVisible::None;
6003                                    return;
6004                                }
6005                            }
6006                        }
6007                        for window in local_workspace_windows(cx) {
6008                            if let Ok(workspace) = window.read(cx) {
6009                                let project = workspace.project().read(cx);
6010                                if project.is_via_collab() {
6011                                    continue;
6012                                }
6013                                existing = Some(window);
6014                                open_visible = OpenVisible::None;
6015                                break;
6016                            }
6017                        }
6018                    })?;
6019                }
6020            }
6021        }
6022
6023        if let Some(existing) = existing {
6024            let open_task = existing
6025                .update(&mut cx, |workspace, window, cx| {
6026                    window.activate_window();
6027                    workspace.open_paths(abs_paths, open_visible, None, window, cx)
6028                })?
6029                .await;
6030
6031            _ = existing.update(&mut cx, |workspace, _, cx| {
6032                for item in open_task.iter().flatten() {
6033                    if let Err(e) = item {
6034                        workspace.show_error(&e, cx);
6035                    }
6036                }
6037            });
6038
6039            Ok((existing, open_task))
6040        } else {
6041            cx.update(move |cx| {
6042                Workspace::new_local(
6043                    abs_paths,
6044                    app_state.clone(),
6045                    open_options.replace_window,
6046                    open_options.env,
6047                    cx,
6048                )
6049            })?
6050            .await
6051        }
6052    })
6053}
6054
6055pub fn open_new(
6056    open_options: OpenOptions,
6057    app_state: Arc<AppState>,
6058    cx: &mut App,
6059    init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6060) -> Task<anyhow::Result<()>> {
6061    let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6062    cx.spawn(|mut cx| async move {
6063        let (workspace, opened_paths) = task.await?;
6064        workspace.update(&mut cx, |workspace, window, cx| {
6065            if opened_paths.is_empty() {
6066                init(workspace, window, cx)
6067            }
6068        })?;
6069        Ok(())
6070    })
6071}
6072
6073pub fn create_and_open_local_file(
6074    path: &'static Path,
6075    window: &mut Window,
6076    cx: &mut Context<Workspace>,
6077    default_content: impl 'static + Send + FnOnce() -> Rope,
6078) -> Task<Result<Box<dyn ItemHandle>>> {
6079    cx.spawn_in(window, |workspace, mut cx| async move {
6080        let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
6081        if !fs.is_file(path).await {
6082            fs.create_file(path, Default::default()).await?;
6083            fs.save(path, &default_content(), Default::default())
6084                .await?;
6085        }
6086
6087        let mut items = workspace
6088            .update_in(&mut cx, |workspace, window, cx| {
6089                workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6090                    workspace.open_paths(
6091                        vec![path.to_path_buf()],
6092                        OpenVisible::None,
6093                        None,
6094                        window,
6095                        cx,
6096                    )
6097                })
6098            })?
6099            .await?
6100            .await;
6101
6102        let item = items.pop().flatten();
6103        item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
6104    })
6105}
6106
6107pub fn open_ssh_project(
6108    window: WindowHandle<Workspace>,
6109    connection_options: SshConnectionOptions,
6110    cancel_rx: oneshot::Receiver<()>,
6111    delegate: Arc<dyn SshClientDelegate>,
6112    app_state: Arc<AppState>,
6113    paths: Vec<PathBuf>,
6114    cx: &mut App,
6115) -> Task<Result<()>> {
6116    cx.spawn(|mut cx| async move {
6117        let (serialized_ssh_project, workspace_id, serialized_workspace) =
6118            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6119
6120        let session = match cx
6121            .update(|cx| {
6122                remote::SshRemoteClient::new(
6123                    ConnectionIdentifier::Workspace(workspace_id.0),
6124                    connection_options,
6125                    cancel_rx,
6126                    delegate,
6127                    cx,
6128                )
6129            })?
6130            .await?
6131        {
6132            Some(result) => result,
6133            None => return Ok(()),
6134        };
6135
6136        let project = cx.update(|cx| {
6137            project::Project::ssh(
6138                session,
6139                app_state.client.clone(),
6140                app_state.node_runtime.clone(),
6141                app_state.user_store.clone(),
6142                app_state.languages.clone(),
6143                app_state.fs.clone(),
6144                cx,
6145            )
6146        })?;
6147
6148        let toolchains = DB.toolchains(workspace_id).await?;
6149        for (toolchain, worktree_id) in toolchains {
6150            project
6151                .update(&mut cx, |this, cx| {
6152                    this.activate_toolchain(worktree_id, toolchain, cx)
6153                })?
6154                .await;
6155        }
6156        let mut project_paths_to_open = vec![];
6157        let mut project_path_errors = vec![];
6158
6159        for path in paths {
6160            let result = cx
6161                .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6162                .await;
6163            match result {
6164                Ok((_, project_path)) => {
6165                    project_paths_to_open.push((path.clone(), Some(project_path)));
6166                }
6167                Err(error) => {
6168                    project_path_errors.push(error);
6169                }
6170            };
6171        }
6172
6173        if project_paths_to_open.is_empty() {
6174            return Err(project_path_errors
6175                .pop()
6176                .unwrap_or_else(|| anyhow!("no paths given")));
6177        }
6178
6179        cx.update_window(window.into(), |_, window, cx| {
6180            window.replace_root(cx, |window, cx| {
6181                telemetry::event!("SSH Project Opened");
6182
6183                let mut workspace =
6184                    Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6185                workspace.set_serialized_ssh_project(serialized_ssh_project);
6186                workspace
6187            });
6188        })?;
6189
6190        window
6191            .update(&mut cx, |_, window, cx| {
6192                window.activate_window();
6193
6194                open_items(serialized_workspace, project_paths_to_open, window, cx)
6195            })?
6196            .await?;
6197
6198        window.update(&mut cx, |workspace, _, cx| {
6199            for error in project_path_errors {
6200                if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6201                    if let Some(path) = error.error_tag("path") {
6202                        workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6203                    }
6204                } else {
6205                    workspace.show_error(&error, cx)
6206                }
6207            }
6208        })
6209    })
6210}
6211
6212fn serialize_ssh_project(
6213    connection_options: SshConnectionOptions,
6214    paths: Vec<PathBuf>,
6215    cx: &AsyncApp,
6216) -> Task<
6217    Result<(
6218        SerializedSshProject,
6219        WorkspaceId,
6220        Option<SerializedWorkspace>,
6221    )>,
6222> {
6223    cx.background_spawn(async move {
6224        let serialized_ssh_project = persistence::DB
6225            .get_or_create_ssh_project(
6226                connection_options.host.clone(),
6227                connection_options.port,
6228                paths
6229                    .iter()
6230                    .map(|path| path.to_string_lossy().to_string())
6231                    .collect::<Vec<_>>(),
6232                connection_options.username.clone(),
6233            )
6234            .await?;
6235
6236        let serialized_workspace =
6237            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6238
6239        let workspace_id = if let Some(workspace_id) =
6240            serialized_workspace.as_ref().map(|workspace| workspace.id)
6241        {
6242            workspace_id
6243        } else {
6244            persistence::DB.next_id().await?
6245        };
6246
6247        Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6248    })
6249}
6250
6251pub fn join_in_room_project(
6252    project_id: u64,
6253    follow_user_id: u64,
6254    app_state: Arc<AppState>,
6255    cx: &mut App,
6256) -> Task<Result<()>> {
6257    let windows = cx.windows();
6258    cx.spawn(|mut cx| async move {
6259        let existing_workspace = windows.into_iter().find_map(|window_handle| {
6260            window_handle
6261                .downcast::<Workspace>()
6262                .and_then(|window_handle| {
6263                    window_handle
6264                        .update(&mut cx, |workspace, _window, cx| {
6265                            if workspace.project().read(cx).remote_id() == Some(project_id) {
6266                                Some(window_handle)
6267                            } else {
6268                                None
6269                            }
6270                        })
6271                        .unwrap_or(None)
6272                })
6273        });
6274
6275        let workspace = if let Some(existing_workspace) = existing_workspace {
6276            existing_workspace
6277        } else {
6278            let active_call = cx.update(|cx| ActiveCall::global(cx))?;
6279            let room = active_call
6280                .read_with(&cx, |call, _| call.room().cloned())?
6281                .ok_or_else(|| anyhow!("not in a call"))?;
6282            let project = room
6283                .update(&mut cx, |room, cx| {
6284                    room.join_project(
6285                        project_id,
6286                        app_state.languages.clone(),
6287                        app_state.fs.clone(),
6288                        cx,
6289                    )
6290                })?
6291                .await?;
6292
6293            let window_bounds_override = window_bounds_env_override();
6294            cx.update(|cx| {
6295                let mut options = (app_state.build_window_options)(None, cx);
6296                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
6297                cx.open_window(options, |window, cx| {
6298                    cx.new(|cx| {
6299                        Workspace::new(Default::default(), project, app_state.clone(), window, cx)
6300                    })
6301                })
6302            })??
6303        };
6304
6305        workspace.update(&mut cx, |workspace, window, cx| {
6306            cx.activate(true);
6307            window.activate_window();
6308
6309            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
6310                let follow_peer_id = room
6311                    .read(cx)
6312                    .remote_participants()
6313                    .iter()
6314                    .find(|(_, participant)| participant.user.id == follow_user_id)
6315                    .map(|(_, p)| p.peer_id)
6316                    .or_else(|| {
6317                        // If we couldn't follow the given user, follow the host instead.
6318                        let collaborator = workspace
6319                            .project()
6320                            .read(cx)
6321                            .collaborators()
6322                            .values()
6323                            .find(|collaborator| collaborator.is_host)?;
6324                        Some(collaborator.peer_id)
6325                    });
6326
6327                if let Some(follow_peer_id) = follow_peer_id {
6328                    workspace.follow(follow_peer_id, window, cx);
6329                }
6330            }
6331        })?;
6332
6333        anyhow::Ok(())
6334    })
6335}
6336
6337pub fn reload(reload: &Reload, cx: &mut App) {
6338    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
6339    let mut workspace_windows = cx
6340        .windows()
6341        .into_iter()
6342        .filter_map(|window| window.downcast::<Workspace>())
6343        .collect::<Vec<_>>();
6344
6345    // If multiple windows have unsaved changes, and need a save prompt,
6346    // prompt in the active window before switching to a different window.
6347    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
6348
6349    let mut prompt = None;
6350    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6351        prompt = window
6352            .update(cx, |_, window, cx| {
6353                window.prompt(
6354                    PromptLevel::Info,
6355                    "Are you sure you want to restart?",
6356                    None,
6357                    &["Restart", "Cancel"],
6358                    cx,
6359                )
6360            })
6361            .ok();
6362    }
6363
6364    let binary_path = reload.binary_path.clone();
6365    cx.spawn(|mut cx| async move {
6366        if let Some(prompt) = prompt {
6367            let answer = prompt.await?;
6368            if answer != 0 {
6369                return Ok(());
6370            }
6371        }
6372
6373        // If the user cancels any save prompt, then keep the app open.
6374        for window in workspace_windows {
6375            if let Ok(should_close) = window.update(&mut cx, |workspace, window, cx| {
6376                workspace.prepare_to_close(CloseIntent::Quit, window, cx)
6377            }) {
6378                if !should_close.await? {
6379                    return Ok(());
6380                }
6381            }
6382        }
6383
6384        cx.update(|cx| cx.restart(binary_path))
6385    })
6386    .detach_and_log_err(cx);
6387}
6388
6389fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6390    let mut parts = value.split(',');
6391    let x: usize = parts.next()?.parse().ok()?;
6392    let y: usize = parts.next()?.parse().ok()?;
6393    Some(point(px(x as f32), px(y as f32)))
6394}
6395
6396fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6397    let mut parts = value.split(',');
6398    let width: usize = parts.next()?.parse().ok()?;
6399    let height: usize = parts.next()?.parse().ok()?;
6400    Some(size(px(width as f32), px(height as f32)))
6401}
6402
6403pub fn client_side_decorations(
6404    element: impl IntoElement,
6405    window: &mut Window,
6406    cx: &mut App,
6407) -> Stateful<Div> {
6408    const BORDER_SIZE: Pixels = px(1.0);
6409    let decorations = window.window_decorations();
6410
6411    if matches!(decorations, Decorations::Client { .. }) {
6412        window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6413    }
6414
6415    struct GlobalResizeEdge(ResizeEdge);
6416    impl Global for GlobalResizeEdge {}
6417
6418    div()
6419        .id("window-backdrop")
6420        .bg(transparent_black())
6421        .map(|div| match decorations {
6422            Decorations::Server => div,
6423            Decorations::Client { tiling, .. } => div
6424                .when(!(tiling.top || tiling.right), |div| {
6425                    div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6426                })
6427                .when(!(tiling.top || tiling.left), |div| {
6428                    div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6429                })
6430                .when(!(tiling.bottom || tiling.right), |div| {
6431                    div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6432                })
6433                .when(!(tiling.bottom || tiling.left), |div| {
6434                    div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6435                })
6436                .when(!tiling.top, |div| {
6437                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6438                })
6439                .when(!tiling.bottom, |div| {
6440                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6441                })
6442                .when(!tiling.left, |div| {
6443                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6444                })
6445                .when(!tiling.right, |div| {
6446                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6447                })
6448                .on_mouse_move(move |e, window, cx| {
6449                    let size = window.window_bounds().get_bounds().size;
6450                    let pos = e.position;
6451
6452                    let new_edge =
6453                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6454
6455                    let edge = cx.try_global::<GlobalResizeEdge>();
6456                    if new_edge != edge.map(|edge| edge.0) {
6457                        window
6458                            .window_handle()
6459                            .update(cx, |workspace, _, cx| {
6460                                cx.notify(workspace.entity_id());
6461                            })
6462                            .ok();
6463                    }
6464                })
6465                .on_mouse_down(MouseButton::Left, move |e, window, _| {
6466                    let size = window.window_bounds().get_bounds().size;
6467                    let pos = e.position;
6468
6469                    let edge = match resize_edge(
6470                        pos,
6471                        theme::CLIENT_SIDE_DECORATION_SHADOW,
6472                        size,
6473                        tiling,
6474                    ) {
6475                        Some(value) => value,
6476                        None => return,
6477                    };
6478
6479                    window.start_window_resize(edge);
6480                }),
6481        })
6482        .size_full()
6483        .child(
6484            div()
6485                .cursor(CursorStyle::Arrow)
6486                .map(|div| match decorations {
6487                    Decorations::Server => div,
6488                    Decorations::Client { tiling } => div
6489                        .border_color(cx.theme().colors().border)
6490                        .when(!(tiling.top || tiling.right), |div| {
6491                            div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6492                        })
6493                        .when(!(tiling.top || tiling.left), |div| {
6494                            div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6495                        })
6496                        .when(!(tiling.bottom || tiling.right), |div| {
6497                            div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6498                        })
6499                        .when(!(tiling.bottom || tiling.left), |div| {
6500                            div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6501                        })
6502                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6503                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6504                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6505                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6506                        .when(!tiling.is_tiled(), |div| {
6507                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
6508                                color: Hsla {
6509                                    h: 0.,
6510                                    s: 0.,
6511                                    l: 0.,
6512                                    a: 0.4,
6513                                },
6514                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6515                                spread_radius: px(0.),
6516                                offset: point(px(0.0), px(0.0)),
6517                            }])
6518                        }),
6519                })
6520                .on_mouse_move(|_e, _, cx| {
6521                    cx.stop_propagation();
6522                })
6523                .size_full()
6524                .child(element),
6525        )
6526        .map(|div| match decorations {
6527            Decorations::Server => div,
6528            Decorations::Client { tiling, .. } => div.child(
6529                canvas(
6530                    |_bounds, window, _| {
6531                        window.insert_hitbox(
6532                            Bounds::new(
6533                                point(px(0.0), px(0.0)),
6534                                window.window_bounds().get_bounds().size,
6535                            ),
6536                            false,
6537                        )
6538                    },
6539                    move |_bounds, hitbox, window, cx| {
6540                        let mouse = window.mouse_position();
6541                        let size = window.window_bounds().get_bounds().size;
6542                        let Some(edge) =
6543                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6544                        else {
6545                            return;
6546                        };
6547                        cx.set_global(GlobalResizeEdge(edge));
6548                        window.set_cursor_style(
6549                            match edge {
6550                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6551                                ResizeEdge::Left | ResizeEdge::Right => {
6552                                    CursorStyle::ResizeLeftRight
6553                                }
6554                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6555                                    CursorStyle::ResizeUpLeftDownRight
6556                                }
6557                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6558                                    CursorStyle::ResizeUpRightDownLeft
6559                                }
6560                            },
6561                            &hitbox,
6562                        );
6563                    },
6564                )
6565                .size_full()
6566                .absolute(),
6567            ),
6568        })
6569}
6570
6571fn resize_edge(
6572    pos: Point<Pixels>,
6573    shadow_size: Pixels,
6574    window_size: Size<Pixels>,
6575    tiling: Tiling,
6576) -> Option<ResizeEdge> {
6577    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6578    if bounds.contains(&pos) {
6579        return None;
6580    }
6581
6582    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6583    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6584    if !tiling.top && top_left_bounds.contains(&pos) {
6585        return Some(ResizeEdge::TopLeft);
6586    }
6587
6588    let top_right_bounds = Bounds::new(
6589        Point::new(window_size.width - corner_size.width, px(0.)),
6590        corner_size,
6591    );
6592    if !tiling.top && top_right_bounds.contains(&pos) {
6593        return Some(ResizeEdge::TopRight);
6594    }
6595
6596    let bottom_left_bounds = Bounds::new(
6597        Point::new(px(0.), window_size.height - corner_size.height),
6598        corner_size,
6599    );
6600    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6601        return Some(ResizeEdge::BottomLeft);
6602    }
6603
6604    let bottom_right_bounds = Bounds::new(
6605        Point::new(
6606            window_size.width - corner_size.width,
6607            window_size.height - corner_size.height,
6608        ),
6609        corner_size,
6610    );
6611    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6612        return Some(ResizeEdge::BottomRight);
6613    }
6614
6615    if !tiling.top && pos.y < shadow_size {
6616        Some(ResizeEdge::Top)
6617    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6618        Some(ResizeEdge::Bottom)
6619    } else if !tiling.left && pos.x < shadow_size {
6620        Some(ResizeEdge::Left)
6621    } else if !tiling.right && pos.x > window_size.width - shadow_size {
6622        Some(ResizeEdge::Right)
6623    } else {
6624        None
6625    }
6626}
6627
6628fn join_pane_into_active(
6629    active_pane: &Entity<Pane>,
6630    pane: &Entity<Pane>,
6631    window: &mut Window,
6632    cx: &mut App,
6633) {
6634    if pane == active_pane {
6635        return;
6636    } else if pane.read(cx).items_len() == 0 {
6637        pane.update(cx, |_, cx| {
6638            cx.emit(pane::Event::Remove {
6639                focus_on_pane: None,
6640            });
6641        })
6642    } else {
6643        move_all_items(pane, active_pane, window, cx);
6644    }
6645}
6646
6647fn move_all_items(
6648    from_pane: &Entity<Pane>,
6649    to_pane: &Entity<Pane>,
6650    window: &mut Window,
6651    cx: &mut App,
6652) {
6653    let destination_is_different = from_pane != to_pane;
6654    let mut moved_items = 0;
6655    for (item_ix, item_handle) in from_pane
6656        .read(cx)
6657        .items()
6658        .enumerate()
6659        .map(|(ix, item)| (ix, item.clone()))
6660        .collect::<Vec<_>>()
6661    {
6662        let ix = item_ix - moved_items;
6663        if destination_is_different {
6664            // Close item from previous pane
6665            from_pane.update(cx, |source, cx| {
6666                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
6667            });
6668            moved_items += 1;
6669        }
6670
6671        // This automatically removes duplicate items in the pane
6672        to_pane.update(cx, |destination, cx| {
6673            destination.add_item(item_handle, true, true, None, window, cx);
6674            window.focus(&destination.focus_handle(cx))
6675        });
6676    }
6677}
6678
6679pub fn move_item(
6680    source: &Entity<Pane>,
6681    destination: &Entity<Pane>,
6682    item_id_to_move: EntityId,
6683    destination_index: usize,
6684    window: &mut Window,
6685    cx: &mut App,
6686) {
6687    let Some((item_ix, item_handle)) = source
6688        .read(cx)
6689        .items()
6690        .enumerate()
6691        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6692        .map(|(ix, item)| (ix, item.clone()))
6693    else {
6694        // Tab was closed during drag
6695        return;
6696    };
6697
6698    if source != destination {
6699        // Close item from previous pane
6700        source.update(cx, |source, cx| {
6701            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
6702        });
6703    }
6704
6705    // This automatically removes duplicate items in the pane
6706    destination.update(cx, |destination, cx| {
6707        destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
6708        window.focus(&destination.focus_handle(cx))
6709    });
6710}
6711
6712pub fn move_active_item(
6713    source: &Entity<Pane>,
6714    destination: &Entity<Pane>,
6715    focus_destination: bool,
6716    close_if_empty: bool,
6717    window: &mut Window,
6718    cx: &mut App,
6719) {
6720    if source == destination {
6721        return;
6722    }
6723    let Some(active_item) = source.read(cx).active_item() else {
6724        return;
6725    };
6726    source.update(cx, |source_pane, cx| {
6727        let item_id = active_item.item_id();
6728        source_pane.remove_item(item_id, false, close_if_empty, window, cx);
6729        destination.update(cx, |target_pane, cx| {
6730            target_pane.add_item(
6731                active_item,
6732                focus_destination,
6733                focus_destination,
6734                Some(target_pane.items_len()),
6735                window,
6736                cx,
6737            );
6738        });
6739    });
6740}
6741
6742#[cfg(test)]
6743mod tests {
6744    use std::{cell::RefCell, rc::Rc};
6745
6746    use super::*;
6747    use crate::{
6748        dock::{test::TestPanel, PanelEvent},
6749        item::{
6750            test::{TestItem, TestProjectItem},
6751            ItemEvent,
6752        },
6753    };
6754    use fs::FakeFs;
6755    use gpui::{
6756        px, DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
6757        UpdateGlobal, VisualTestContext,
6758    };
6759    use project::{Project, ProjectEntryId};
6760    use serde_json::json;
6761    use settings::SettingsStore;
6762
6763    #[gpui::test]
6764    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6765        init_test(cx);
6766
6767        let fs = FakeFs::new(cx.executor());
6768        let project = Project::test(fs, [], cx).await;
6769        let (workspace, cx) =
6770            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6771
6772        // Adding an item with no ambiguity renders the tab without detail.
6773        let item1 = cx.new(|cx| {
6774            let mut item = TestItem::new(cx);
6775            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6776            item
6777        });
6778        workspace.update_in(cx, |workspace, window, cx| {
6779            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6780        });
6781        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6782
6783        // Adding an item that creates ambiguity increases the level of detail on
6784        // both tabs.
6785        let item2 = cx.new_window_entity(|_window, cx| {
6786            let mut item = TestItem::new(cx);
6787            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6788            item
6789        });
6790        workspace.update_in(cx, |workspace, window, cx| {
6791            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6792        });
6793        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6794        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6795
6796        // Adding an item that creates ambiguity increases the level of detail only
6797        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6798        // we stop at the highest detail available.
6799        let item3 = cx.new(|cx| {
6800            let mut item = TestItem::new(cx);
6801            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6802            item
6803        });
6804        workspace.update_in(cx, |workspace, window, cx| {
6805            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6806        });
6807        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6808        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6809        item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6810    }
6811
6812    #[gpui::test]
6813    async fn test_tracking_active_path(cx: &mut TestAppContext) {
6814        init_test(cx);
6815
6816        let fs = FakeFs::new(cx.executor());
6817        fs.insert_tree(
6818            "/root1",
6819            json!({
6820                "one.txt": "",
6821                "two.txt": "",
6822            }),
6823        )
6824        .await;
6825        fs.insert_tree(
6826            "/root2",
6827            json!({
6828                "three.txt": "",
6829            }),
6830        )
6831        .await;
6832
6833        let project = Project::test(fs, ["root1".as_ref()], cx).await;
6834        let (workspace, cx) =
6835            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6836        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6837        let worktree_id = project.update(cx, |project, cx| {
6838            project.worktrees(cx).next().unwrap().read(cx).id()
6839        });
6840
6841        let item1 = cx.new(|cx| {
6842            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6843        });
6844        let item2 = cx.new(|cx| {
6845            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6846        });
6847
6848        // Add an item to an empty pane
6849        workspace.update_in(cx, |workspace, window, cx| {
6850            workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
6851        });
6852        project.update(cx, |project, cx| {
6853            assert_eq!(
6854                project.active_entry(),
6855                project
6856                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6857                    .map(|e| e.id)
6858            );
6859        });
6860        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6861
6862        // Add a second item to a non-empty pane
6863        workspace.update_in(cx, |workspace, window, cx| {
6864            workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
6865        });
6866        assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6867        project.update(cx, |project, cx| {
6868            assert_eq!(
6869                project.active_entry(),
6870                project
6871                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6872                    .map(|e| e.id)
6873            );
6874        });
6875
6876        // Close the active item
6877        pane.update_in(cx, |pane, window, cx| {
6878            pane.close_active_item(&Default::default(), window, cx)
6879                .unwrap()
6880        })
6881        .await
6882        .unwrap();
6883        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6884        project.update(cx, |project, cx| {
6885            assert_eq!(
6886                project.active_entry(),
6887                project
6888                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6889                    .map(|e| e.id)
6890            );
6891        });
6892
6893        // Add a project folder
6894        project
6895            .update(cx, |project, cx| {
6896                project.find_or_create_worktree("root2", true, cx)
6897            })
6898            .await
6899            .unwrap();
6900        assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6901
6902        // Remove a project folder
6903        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6904        assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6905    }
6906
6907    #[gpui::test]
6908    async fn test_close_window(cx: &mut TestAppContext) {
6909        init_test(cx);
6910
6911        let fs = FakeFs::new(cx.executor());
6912        fs.insert_tree("/root", json!({ "one": "" })).await;
6913
6914        let project = Project::test(fs, ["root".as_ref()], cx).await;
6915        let (workspace, cx) =
6916            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6917
6918        // When there are no dirty items, there's nothing to do.
6919        let item1 = cx.new(TestItem::new);
6920        workspace.update_in(cx, |w, window, cx| {
6921            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
6922        });
6923        let task = workspace.update_in(cx, |w, window, cx| {
6924            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6925        });
6926        assert!(task.await.unwrap());
6927
6928        // When there are dirty untitled items, prompt to save each one. If the user
6929        // cancels any prompt, then abort.
6930        let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
6931        let item3 = cx.new(|cx| {
6932            TestItem::new(cx)
6933                .with_dirty(true)
6934                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6935        });
6936        workspace.update_in(cx, |w, window, cx| {
6937            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6938            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6939        });
6940        let task = workspace.update_in(cx, |w, window, cx| {
6941            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6942        });
6943        cx.executor().run_until_parked();
6944        cx.simulate_prompt_answer("Cancel"); // cancel save all
6945        cx.executor().run_until_parked();
6946        assert!(!cx.has_pending_prompt());
6947        assert!(!task.await.unwrap());
6948    }
6949
6950    #[gpui::test]
6951    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6952        init_test(cx);
6953
6954        // Register TestItem as a serializable item
6955        cx.update(|cx| {
6956            register_serializable_item::<TestItem>(cx);
6957        });
6958
6959        let fs = FakeFs::new(cx.executor());
6960        fs.insert_tree("/root", json!({ "one": "" })).await;
6961
6962        let project = Project::test(fs, ["root".as_ref()], cx).await;
6963        let (workspace, cx) =
6964            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6965
6966        // When there are dirty untitled items, but they can serialize, then there is no prompt.
6967        let item1 = cx.new(|cx| {
6968            TestItem::new(cx)
6969                .with_dirty(true)
6970                .with_serialize(|| Some(Task::ready(Ok(()))))
6971        });
6972        let item2 = cx.new(|cx| {
6973            TestItem::new(cx)
6974                .with_dirty(true)
6975                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6976                .with_serialize(|| Some(Task::ready(Ok(()))))
6977        });
6978        workspace.update_in(cx, |w, window, cx| {
6979            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6980            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6981        });
6982        let task = workspace.update_in(cx, |w, window, cx| {
6983            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6984        });
6985        assert!(task.await.unwrap());
6986    }
6987
6988    #[gpui::test]
6989    async fn test_close_pane_items(cx: &mut TestAppContext) {
6990        init_test(cx);
6991
6992        let fs = FakeFs::new(cx.executor());
6993
6994        let project = Project::test(fs, None, cx).await;
6995        let (workspace, cx) =
6996            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6997
6998        let item1 = cx.new(|cx| {
6999            TestItem::new(cx)
7000                .with_dirty(true)
7001                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7002        });
7003        let item2 = cx.new(|cx| {
7004            TestItem::new(cx)
7005                .with_dirty(true)
7006                .with_conflict(true)
7007                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7008        });
7009        let item3 = cx.new(|cx| {
7010            TestItem::new(cx)
7011                .with_dirty(true)
7012                .with_conflict(true)
7013                .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7014        });
7015        let item4 = cx.new(|cx| {
7016            TestItem::new(cx).with_dirty(true).with_project_items(&[{
7017                let project_item = TestProjectItem::new_untitled(cx);
7018                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7019                project_item
7020            }])
7021        });
7022        let pane = workspace.update_in(cx, |workspace, window, cx| {
7023            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7024            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7025            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7026            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7027            workspace.active_pane().clone()
7028        });
7029
7030        let close_items = pane.update_in(cx, |pane, window, cx| {
7031            pane.activate_item(1, true, true, window, cx);
7032            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7033            let item1_id = item1.item_id();
7034            let item3_id = item3.item_id();
7035            let item4_id = item4.item_id();
7036            pane.close_items(window, cx, SaveIntent::Close, move |id| {
7037                [item1_id, item3_id, item4_id].contains(&id)
7038            })
7039        });
7040        cx.executor().run_until_parked();
7041
7042        assert!(cx.has_pending_prompt());
7043        cx.simulate_prompt_answer("Save all");
7044
7045        cx.executor().run_until_parked();
7046
7047        // Item 1 is saved. There's a prompt to save item 3.
7048        pane.update(cx, |pane, cx| {
7049            assert_eq!(item1.read(cx).save_count, 1);
7050            assert_eq!(item1.read(cx).save_as_count, 0);
7051            assert_eq!(item1.read(cx).reload_count, 0);
7052            assert_eq!(pane.items_len(), 3);
7053            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7054        });
7055        assert!(cx.has_pending_prompt());
7056
7057        // Cancel saving item 3.
7058        cx.simulate_prompt_answer("Discard");
7059        cx.executor().run_until_parked();
7060
7061        // Item 3 is reloaded. There's a prompt to save item 4.
7062        pane.update(cx, |pane, cx| {
7063            assert_eq!(item3.read(cx).save_count, 0);
7064            assert_eq!(item3.read(cx).save_as_count, 0);
7065            assert_eq!(item3.read(cx).reload_count, 1);
7066            assert_eq!(pane.items_len(), 2);
7067            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7068        });
7069
7070        // There's a prompt for a path for item 4.
7071        cx.simulate_new_path_selection(|_| Some(Default::default()));
7072        close_items.await.unwrap();
7073
7074        // The requested items are closed.
7075        pane.update(cx, |pane, cx| {
7076            assert_eq!(item4.read(cx).save_count, 0);
7077            assert_eq!(item4.read(cx).save_as_count, 1);
7078            assert_eq!(item4.read(cx).reload_count, 0);
7079            assert_eq!(pane.items_len(), 1);
7080            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7081        });
7082    }
7083
7084    #[gpui::test]
7085    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7086        init_test(cx);
7087
7088        let fs = FakeFs::new(cx.executor());
7089        let project = Project::test(fs, [], cx).await;
7090        let (workspace, cx) =
7091            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7092
7093        // Create several workspace items with single project entries, and two
7094        // workspace items with multiple project entries.
7095        let single_entry_items = (0..=4)
7096            .map(|project_entry_id| {
7097                cx.new(|cx| {
7098                    TestItem::new(cx)
7099                        .with_dirty(true)
7100                        .with_project_items(&[dirty_project_item(
7101                            project_entry_id,
7102                            &format!("{project_entry_id}.txt"),
7103                            cx,
7104                        )])
7105                })
7106            })
7107            .collect::<Vec<_>>();
7108        let item_2_3 = cx.new(|cx| {
7109            TestItem::new(cx)
7110                .with_dirty(true)
7111                .with_singleton(false)
7112                .with_project_items(&[
7113                    single_entry_items[2].read(cx).project_items[0].clone(),
7114                    single_entry_items[3].read(cx).project_items[0].clone(),
7115                ])
7116        });
7117        let item_3_4 = cx.new(|cx| {
7118            TestItem::new(cx)
7119                .with_dirty(true)
7120                .with_singleton(false)
7121                .with_project_items(&[
7122                    single_entry_items[3].read(cx).project_items[0].clone(),
7123                    single_entry_items[4].read(cx).project_items[0].clone(),
7124                ])
7125        });
7126
7127        // Create two panes that contain the following project entries:
7128        //   left pane:
7129        //     multi-entry items:   (2, 3)
7130        //     single-entry items:  0, 2, 3, 4
7131        //   right pane:
7132        //     single-entry items:  4, 1
7133        //     multi-entry items:   (3, 4)
7134        let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
7135            let left_pane = workspace.active_pane().clone();
7136            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7137            workspace.add_item_to_active_pane(
7138                single_entry_items[0].boxed_clone(),
7139                None,
7140                true,
7141                window,
7142                cx,
7143            );
7144            workspace.add_item_to_active_pane(
7145                single_entry_items[2].boxed_clone(),
7146                None,
7147                true,
7148                window,
7149                cx,
7150            );
7151            workspace.add_item_to_active_pane(
7152                single_entry_items[3].boxed_clone(),
7153                None,
7154                true,
7155                window,
7156                cx,
7157            );
7158            workspace.add_item_to_active_pane(
7159                single_entry_items[4].boxed_clone(),
7160                None,
7161                true,
7162                window,
7163                cx,
7164            );
7165
7166            let right_pane = workspace
7167                .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7168                .unwrap();
7169
7170            right_pane.update(cx, |pane, cx| {
7171                pane.add_item(
7172                    single_entry_items[1].boxed_clone(),
7173                    true,
7174                    true,
7175                    None,
7176                    window,
7177                    cx,
7178                );
7179                pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7180            });
7181
7182            (left_pane, right_pane)
7183        });
7184
7185        cx.focus(&right_pane);
7186
7187        let mut close = right_pane.update_in(cx, |pane, window, cx| {
7188            pane.close_all_items(&CloseAllItems::default(), window, cx)
7189                .unwrap()
7190        });
7191        cx.executor().run_until_parked();
7192
7193        let msg = cx.pending_prompt().unwrap().0;
7194        assert!(msg.contains("1.txt"));
7195        assert!(!msg.contains("2.txt"));
7196        assert!(!msg.contains("3.txt"));
7197        assert!(!msg.contains("4.txt"));
7198
7199        cx.simulate_prompt_answer("Cancel");
7200        close.await.unwrap();
7201
7202        left_pane
7203            .update_in(cx, |left_pane, window, cx| {
7204                left_pane.close_item_by_id(
7205                    single_entry_items[3].entity_id(),
7206                    SaveIntent::Skip,
7207                    window,
7208                    cx,
7209                )
7210            })
7211            .await
7212            .unwrap();
7213
7214        close = right_pane.update_in(cx, |pane, window, cx| {
7215            pane.close_all_items(&CloseAllItems::default(), window, cx)
7216                .unwrap()
7217        });
7218        cx.executor().run_until_parked();
7219
7220        let details = cx.pending_prompt().unwrap().1;
7221        assert!(details.contains("1.txt"));
7222        assert!(!details.contains("2.txt"));
7223        assert!(details.contains("3.txt"));
7224        // ideally this assertion could be made, but today we can only
7225        // save whole items not project items, so the orphaned item 3 causes
7226        // 4 to be saved too.
7227        // assert!(!details.contains("4.txt"));
7228
7229        cx.simulate_prompt_answer("Save all");
7230
7231        cx.executor().run_until_parked();
7232        close.await.unwrap();
7233        right_pane.update(cx, |pane, _| {
7234            assert_eq!(pane.items_len(), 0);
7235        });
7236    }
7237
7238    #[gpui::test]
7239    async fn test_autosave(cx: &mut gpui::TestAppContext) {
7240        init_test(cx);
7241
7242        let fs = FakeFs::new(cx.executor());
7243        let project = Project::test(fs, [], cx).await;
7244        let (workspace, cx) =
7245            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7246        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7247
7248        let item = cx.new(|cx| {
7249            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7250        });
7251        let item_id = item.entity_id();
7252        workspace.update_in(cx, |workspace, window, cx| {
7253            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7254        });
7255
7256        // Autosave on window change.
7257        item.update(cx, |item, cx| {
7258            SettingsStore::update_global(cx, |settings, cx| {
7259                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7260                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
7261                })
7262            });
7263            item.is_dirty = true;
7264        });
7265
7266        // Deactivating the window saves the file.
7267        cx.deactivate_window();
7268        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7269
7270        // Re-activating the window doesn't save the file.
7271        cx.update(|window, _| window.activate_window());
7272        cx.executor().run_until_parked();
7273        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7274
7275        // Autosave on focus change.
7276        item.update_in(cx, |item, window, cx| {
7277            cx.focus_self(window);
7278            SettingsStore::update_global(cx, |settings, cx| {
7279                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7280                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
7281                })
7282            });
7283            item.is_dirty = true;
7284        });
7285
7286        // Blurring the item saves the file.
7287        item.update_in(cx, |_, window, _| window.blur());
7288        cx.executor().run_until_parked();
7289        item.update(cx, |item, _| assert_eq!(item.save_count, 2));
7290
7291        // Deactivating the window still saves the file.
7292        item.update_in(cx, |item, window, cx| {
7293            cx.focus_self(window);
7294            item.is_dirty = true;
7295        });
7296        cx.deactivate_window();
7297        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7298
7299        // Autosave after delay.
7300        item.update(cx, |item, cx| {
7301            SettingsStore::update_global(cx, |settings, cx| {
7302                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7303                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
7304                })
7305            });
7306            item.is_dirty = true;
7307            cx.emit(ItemEvent::Edit);
7308        });
7309
7310        // Delay hasn't fully expired, so the file is still dirty and unsaved.
7311        cx.executor().advance_clock(Duration::from_millis(250));
7312        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7313
7314        // After delay expires, the file is saved.
7315        cx.executor().advance_clock(Duration::from_millis(250));
7316        item.update(cx, |item, _| assert_eq!(item.save_count, 4));
7317
7318        // Autosave on focus change, ensuring closing the tab counts as such.
7319        item.update(cx, |item, cx| {
7320            SettingsStore::update_global(cx, |settings, cx| {
7321                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7322                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
7323                })
7324            });
7325            item.is_dirty = true;
7326            for project_item in &mut item.project_items {
7327                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7328            }
7329        });
7330
7331        pane.update_in(cx, |pane, window, cx| {
7332            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7333        })
7334        .await
7335        .unwrap();
7336        assert!(!cx.has_pending_prompt());
7337        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7338
7339        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
7340        workspace.update_in(cx, |workspace, window, cx| {
7341            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7342        });
7343        item.update_in(cx, |item, window, cx| {
7344            item.project_items[0].update(cx, |item, _| {
7345                item.entry_id = None;
7346            });
7347            item.is_dirty = true;
7348            window.blur();
7349        });
7350        cx.run_until_parked();
7351        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7352
7353        // Ensure autosave is prevented for deleted files also when closing the buffer.
7354        let _close_items = pane.update_in(cx, |pane, window, cx| {
7355            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7356        });
7357        cx.run_until_parked();
7358        assert!(cx.has_pending_prompt());
7359        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7360    }
7361
7362    #[gpui::test]
7363    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
7364        init_test(cx);
7365
7366        let fs = FakeFs::new(cx.executor());
7367
7368        let project = Project::test(fs, [], cx).await;
7369        let (workspace, cx) =
7370            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7371
7372        let item = cx.new(|cx| {
7373            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7374        });
7375        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7376        let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
7377        let toolbar_notify_count = Rc::new(RefCell::new(0));
7378
7379        workspace.update_in(cx, |workspace, window, cx| {
7380            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7381            let toolbar_notification_count = toolbar_notify_count.clone();
7382            cx.observe_in(&toolbar, window, move |_, _, _, _| {
7383                *toolbar_notification_count.borrow_mut() += 1
7384            })
7385            .detach();
7386        });
7387
7388        pane.update(cx, |pane, _| {
7389            assert!(!pane.can_navigate_backward());
7390            assert!(!pane.can_navigate_forward());
7391        });
7392
7393        item.update_in(cx, |item, _, cx| {
7394            item.set_state("one".to_string(), cx);
7395        });
7396
7397        // Toolbar must be notified to re-render the navigation buttons
7398        assert_eq!(*toolbar_notify_count.borrow(), 1);
7399
7400        pane.update(cx, |pane, _| {
7401            assert!(pane.can_navigate_backward());
7402            assert!(!pane.can_navigate_forward());
7403        });
7404
7405        workspace
7406            .update_in(cx, |workspace, window, cx| {
7407                workspace.go_back(pane.downgrade(), window, cx)
7408            })
7409            .await
7410            .unwrap();
7411
7412        assert_eq!(*toolbar_notify_count.borrow(), 2);
7413        pane.update(cx, |pane, _| {
7414            assert!(!pane.can_navigate_backward());
7415            assert!(pane.can_navigate_forward());
7416        });
7417    }
7418
7419    #[gpui::test]
7420    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7421        init_test(cx);
7422        let fs = FakeFs::new(cx.executor());
7423
7424        let project = Project::test(fs, [], cx).await;
7425        let (workspace, cx) =
7426            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7427
7428        let panel = workspace.update_in(cx, |workspace, window, cx| {
7429            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7430            workspace.add_panel(panel.clone(), window, cx);
7431
7432            workspace
7433                .right_dock()
7434                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
7435
7436            panel
7437        });
7438
7439        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7440        pane.update_in(cx, |pane, window, cx| {
7441            let item = cx.new(TestItem::new);
7442            pane.add_item(Box::new(item), true, true, None, window, cx);
7443        });
7444
7445        // Transfer focus from center to panel
7446        workspace.update_in(cx, |workspace, window, cx| {
7447            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7448        });
7449
7450        workspace.update_in(cx, |workspace, window, cx| {
7451            assert!(workspace.right_dock().read(cx).is_open());
7452            assert!(!panel.is_zoomed(window, cx));
7453            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7454        });
7455
7456        // Transfer focus from panel to center
7457        workspace.update_in(cx, |workspace, window, cx| {
7458            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7459        });
7460
7461        workspace.update_in(cx, |workspace, window, cx| {
7462            assert!(workspace.right_dock().read(cx).is_open());
7463            assert!(!panel.is_zoomed(window, cx));
7464            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7465        });
7466
7467        // Close the dock
7468        workspace.update_in(cx, |workspace, window, cx| {
7469            workspace.toggle_dock(DockPosition::Right, window, cx);
7470        });
7471
7472        workspace.update_in(cx, |workspace, window, cx| {
7473            assert!(!workspace.right_dock().read(cx).is_open());
7474            assert!(!panel.is_zoomed(window, cx));
7475            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7476        });
7477
7478        // Open the dock
7479        workspace.update_in(cx, |workspace, window, cx| {
7480            workspace.toggle_dock(DockPosition::Right, window, cx);
7481        });
7482
7483        workspace.update_in(cx, |workspace, window, cx| {
7484            assert!(workspace.right_dock().read(cx).is_open());
7485            assert!(!panel.is_zoomed(window, cx));
7486            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7487        });
7488
7489        // Focus and zoom panel
7490        panel.update_in(cx, |panel, window, cx| {
7491            cx.focus_self(window);
7492            panel.set_zoomed(true, window, cx)
7493        });
7494
7495        workspace.update_in(cx, |workspace, window, cx| {
7496            assert!(workspace.right_dock().read(cx).is_open());
7497            assert!(panel.is_zoomed(window, cx));
7498            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7499        });
7500
7501        // Transfer focus to the center closes the dock
7502        workspace.update_in(cx, |workspace, window, cx| {
7503            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7504        });
7505
7506        workspace.update_in(cx, |workspace, window, cx| {
7507            assert!(!workspace.right_dock().read(cx).is_open());
7508            assert!(panel.is_zoomed(window, cx));
7509            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7510        });
7511
7512        // Transferring focus back to the panel keeps it zoomed
7513        workspace.update_in(cx, |workspace, window, cx| {
7514            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7515        });
7516
7517        workspace.update_in(cx, |workspace, window, cx| {
7518            assert!(workspace.right_dock().read(cx).is_open());
7519            assert!(panel.is_zoomed(window, cx));
7520            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7521        });
7522
7523        // Close the dock while it is zoomed
7524        workspace.update_in(cx, |workspace, window, cx| {
7525            workspace.toggle_dock(DockPosition::Right, window, cx)
7526        });
7527
7528        workspace.update_in(cx, |workspace, window, cx| {
7529            assert!(!workspace.right_dock().read(cx).is_open());
7530            assert!(panel.is_zoomed(window, cx));
7531            assert!(workspace.zoomed.is_none());
7532            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7533        });
7534
7535        // Opening the dock, when it's zoomed, retains focus
7536        workspace.update_in(cx, |workspace, window, cx| {
7537            workspace.toggle_dock(DockPosition::Right, window, cx)
7538        });
7539
7540        workspace.update_in(cx, |workspace, window, cx| {
7541            assert!(workspace.right_dock().read(cx).is_open());
7542            assert!(panel.is_zoomed(window, cx));
7543            assert!(workspace.zoomed.is_some());
7544            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7545        });
7546
7547        // Unzoom and close the panel, zoom the active pane.
7548        panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
7549        workspace.update_in(cx, |workspace, window, cx| {
7550            workspace.toggle_dock(DockPosition::Right, window, cx)
7551        });
7552        pane.update_in(cx, |pane, window, cx| {
7553            pane.toggle_zoom(&Default::default(), window, cx)
7554        });
7555
7556        // Opening a dock unzooms the pane.
7557        workspace.update_in(cx, |workspace, window, cx| {
7558            workspace.toggle_dock(DockPosition::Right, window, cx)
7559        });
7560        workspace.update_in(cx, |workspace, window, cx| {
7561            let pane = pane.read(cx);
7562            assert!(!pane.is_zoomed());
7563            assert!(!pane.focus_handle(cx).is_focused(window));
7564            assert!(workspace.right_dock().read(cx).is_open());
7565            assert!(workspace.zoomed.is_none());
7566        });
7567    }
7568
7569    #[gpui::test]
7570    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7571        init_test(cx);
7572
7573        let fs = FakeFs::new(cx.executor());
7574
7575        let project = Project::test(fs, None, cx).await;
7576        let (workspace, cx) =
7577            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7578
7579        // Let's arrange the panes like this:
7580        //
7581        // +-----------------------+
7582        // |         top           |
7583        // +------+--------+-------+
7584        // | left | center | right |
7585        // +------+--------+-------+
7586        // |        bottom         |
7587        // +-----------------------+
7588
7589        let top_item = cx.new(|cx| {
7590            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7591        });
7592        let bottom_item = cx.new(|cx| {
7593            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7594        });
7595        let left_item = cx.new(|cx| {
7596            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7597        });
7598        let right_item = cx.new(|cx| {
7599            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7600        });
7601        let center_item = cx.new(|cx| {
7602            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7603        });
7604
7605        let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7606            let top_pane_id = workspace.active_pane().entity_id();
7607            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
7608            workspace.split_pane(
7609                workspace.active_pane().clone(),
7610                SplitDirection::Down,
7611                window,
7612                cx,
7613            );
7614            top_pane_id
7615        });
7616        let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7617            let bottom_pane_id = workspace.active_pane().entity_id();
7618            workspace.add_item_to_active_pane(
7619                Box::new(bottom_item.clone()),
7620                None,
7621                false,
7622                window,
7623                cx,
7624            );
7625            workspace.split_pane(
7626                workspace.active_pane().clone(),
7627                SplitDirection::Up,
7628                window,
7629                cx,
7630            );
7631            bottom_pane_id
7632        });
7633        let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7634            let left_pane_id = workspace.active_pane().entity_id();
7635            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
7636            workspace.split_pane(
7637                workspace.active_pane().clone(),
7638                SplitDirection::Right,
7639                window,
7640                cx,
7641            );
7642            left_pane_id
7643        });
7644        let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7645            let right_pane_id = workspace.active_pane().entity_id();
7646            workspace.add_item_to_active_pane(
7647                Box::new(right_item.clone()),
7648                None,
7649                false,
7650                window,
7651                cx,
7652            );
7653            workspace.split_pane(
7654                workspace.active_pane().clone(),
7655                SplitDirection::Left,
7656                window,
7657                cx,
7658            );
7659            right_pane_id
7660        });
7661        let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7662            let center_pane_id = workspace.active_pane().entity_id();
7663            workspace.add_item_to_active_pane(
7664                Box::new(center_item.clone()),
7665                None,
7666                false,
7667                window,
7668                cx,
7669            );
7670            center_pane_id
7671        });
7672        cx.executor().run_until_parked();
7673
7674        workspace.update_in(cx, |workspace, window, cx| {
7675            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7676
7677            // Join into next from center pane into right
7678            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7679        });
7680
7681        workspace.update_in(cx, |workspace, window, cx| {
7682            let active_pane = workspace.active_pane();
7683            assert_eq!(right_pane_id, active_pane.entity_id());
7684            assert_eq!(2, active_pane.read(cx).items_len());
7685            let item_ids_in_pane =
7686                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7687            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7688            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7689
7690            // Join into next from right pane into bottom
7691            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7692        });
7693
7694        workspace.update_in(cx, |workspace, window, cx| {
7695            let active_pane = workspace.active_pane();
7696            assert_eq!(bottom_pane_id, active_pane.entity_id());
7697            assert_eq!(3, active_pane.read(cx).items_len());
7698            let item_ids_in_pane =
7699                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7700            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7701            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7702            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7703
7704            // Join into next from bottom pane into left
7705            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7706        });
7707
7708        workspace.update_in(cx, |workspace, window, cx| {
7709            let active_pane = workspace.active_pane();
7710            assert_eq!(left_pane_id, active_pane.entity_id());
7711            assert_eq!(4, active_pane.read(cx).items_len());
7712            let item_ids_in_pane =
7713                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7714            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7715            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7716            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7717            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7718
7719            // Join into next from left pane into top
7720            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7721        });
7722
7723        workspace.update_in(cx, |workspace, window, cx| {
7724            let active_pane = workspace.active_pane();
7725            assert_eq!(top_pane_id, active_pane.entity_id());
7726            assert_eq!(5, active_pane.read(cx).items_len());
7727            let item_ids_in_pane =
7728                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7729            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7730            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7731            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7732            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7733            assert!(item_ids_in_pane.contains(&top_item.item_id()));
7734
7735            // Single pane left: no-op
7736            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
7737        });
7738
7739        workspace.update(cx, |workspace, _cx| {
7740            let active_pane = workspace.active_pane();
7741            assert_eq!(top_pane_id, active_pane.entity_id());
7742        });
7743    }
7744
7745    fn add_an_item_to_active_pane(
7746        cx: &mut VisualTestContext,
7747        workspace: &Entity<Workspace>,
7748        item_id: u64,
7749    ) -> Entity<TestItem> {
7750        let item = cx.new(|cx| {
7751            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7752                item_id,
7753                "item{item_id}.txt",
7754                cx,
7755            )])
7756        });
7757        workspace.update_in(cx, |workspace, window, cx| {
7758            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
7759        });
7760        return item;
7761    }
7762
7763    fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
7764        return workspace.update_in(cx, |workspace, window, cx| {
7765            let new_pane = workspace.split_pane(
7766                workspace.active_pane().clone(),
7767                SplitDirection::Right,
7768                window,
7769                cx,
7770            );
7771            new_pane
7772        });
7773    }
7774
7775    #[gpui::test]
7776    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7777        init_test(cx);
7778        let fs = FakeFs::new(cx.executor());
7779        let project = Project::test(fs, None, cx).await;
7780        let (workspace, cx) =
7781            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7782
7783        add_an_item_to_active_pane(cx, &workspace, 1);
7784        split_pane(cx, &workspace);
7785        add_an_item_to_active_pane(cx, &workspace, 2);
7786        split_pane(cx, &workspace); // empty pane
7787        split_pane(cx, &workspace);
7788        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7789
7790        cx.executor().run_until_parked();
7791
7792        workspace.update(cx, |workspace, cx| {
7793            let num_panes = workspace.panes().len();
7794            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7795            let active_item = workspace
7796                .active_pane()
7797                .read(cx)
7798                .active_item()
7799                .expect("item is in focus");
7800
7801            assert_eq!(num_panes, 4);
7802            assert_eq!(num_items_in_current_pane, 1);
7803            assert_eq!(active_item.item_id(), last_item.item_id());
7804        });
7805
7806        workspace.update_in(cx, |workspace, window, cx| {
7807            workspace.join_all_panes(window, cx);
7808        });
7809
7810        workspace.update(cx, |workspace, cx| {
7811            let num_panes = workspace.panes().len();
7812            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7813            let active_item = workspace
7814                .active_pane()
7815                .read(cx)
7816                .active_item()
7817                .expect("item is in focus");
7818
7819            assert_eq!(num_panes, 1);
7820            assert_eq!(num_items_in_current_pane, 3);
7821            assert_eq!(active_item.item_id(), last_item.item_id());
7822        });
7823    }
7824    struct TestModal(FocusHandle);
7825
7826    impl TestModal {
7827        fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
7828            Self(cx.focus_handle())
7829        }
7830    }
7831
7832    impl EventEmitter<DismissEvent> for TestModal {}
7833
7834    impl Focusable for TestModal {
7835        fn focus_handle(&self, _cx: &App) -> FocusHandle {
7836            self.0.clone()
7837        }
7838    }
7839
7840    impl ModalView for TestModal {}
7841
7842    impl Render for TestModal {
7843        fn render(
7844            &mut self,
7845            _window: &mut Window,
7846            _cx: &mut Context<TestModal>,
7847        ) -> impl IntoElement {
7848            div().track_focus(&self.0)
7849        }
7850    }
7851
7852    #[gpui::test]
7853    async fn test_panels(cx: &mut gpui::TestAppContext) {
7854        init_test(cx);
7855        let fs = FakeFs::new(cx.executor());
7856
7857        let project = Project::test(fs, [], cx).await;
7858        let (workspace, cx) =
7859            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7860
7861        let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
7862            let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
7863            workspace.add_panel(panel_1.clone(), window, cx);
7864            workspace.toggle_dock(DockPosition::Left, window, cx);
7865            let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7866            workspace.add_panel(panel_2.clone(), window, cx);
7867            workspace.toggle_dock(DockPosition::Right, window, cx);
7868
7869            let left_dock = workspace.left_dock();
7870            assert_eq!(
7871                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7872                panel_1.panel_id()
7873            );
7874            assert_eq!(
7875                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7876                panel_1.size(window, cx)
7877            );
7878
7879            left_dock.update(cx, |left_dock, cx| {
7880                left_dock.resize_active_panel(Some(px(1337.)), window, cx)
7881            });
7882            assert_eq!(
7883                workspace
7884                    .right_dock()
7885                    .read(cx)
7886                    .visible_panel()
7887                    .unwrap()
7888                    .panel_id(),
7889                panel_2.panel_id(),
7890            );
7891
7892            (panel_1, panel_2)
7893        });
7894
7895        // Move panel_1 to the right
7896        panel_1.update_in(cx, |panel_1, window, cx| {
7897            panel_1.set_position(DockPosition::Right, window, cx)
7898        });
7899
7900        workspace.update_in(cx, |workspace, window, cx| {
7901            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7902            // Since it was the only panel on the left, the left dock should now be closed.
7903            assert!(!workspace.left_dock().read(cx).is_open());
7904            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7905            let right_dock = workspace.right_dock();
7906            assert_eq!(
7907                right_dock.read(cx).visible_panel().unwrap().panel_id(),
7908                panel_1.panel_id()
7909            );
7910            assert_eq!(
7911                right_dock.read(cx).active_panel_size(window, cx).unwrap(),
7912                px(1337.)
7913            );
7914
7915            // Now we move panel_2 to the left
7916            panel_2.set_position(DockPosition::Left, window, cx);
7917        });
7918
7919        workspace.update(cx, |workspace, cx| {
7920            // Since panel_2 was not visible on the right, we don't open the left dock.
7921            assert!(!workspace.left_dock().read(cx).is_open());
7922            // And the right dock is unaffected in its displaying of panel_1
7923            assert!(workspace.right_dock().read(cx).is_open());
7924            assert_eq!(
7925                workspace
7926                    .right_dock()
7927                    .read(cx)
7928                    .visible_panel()
7929                    .unwrap()
7930                    .panel_id(),
7931                panel_1.panel_id(),
7932            );
7933        });
7934
7935        // Move panel_1 back to the left
7936        panel_1.update_in(cx, |panel_1, window, cx| {
7937            panel_1.set_position(DockPosition::Left, window, cx)
7938        });
7939
7940        workspace.update_in(cx, |workspace, window, cx| {
7941            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7942            let left_dock = workspace.left_dock();
7943            assert!(left_dock.read(cx).is_open());
7944            assert_eq!(
7945                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7946                panel_1.panel_id()
7947            );
7948            assert_eq!(
7949                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7950                px(1337.)
7951            );
7952            // And the right dock should be closed as it no longer has any panels.
7953            assert!(!workspace.right_dock().read(cx).is_open());
7954
7955            // Now we move panel_1 to the bottom
7956            panel_1.set_position(DockPosition::Bottom, window, cx);
7957        });
7958
7959        workspace.update_in(cx, |workspace, window, cx| {
7960            // Since panel_1 was visible on the left, we close the left dock.
7961            assert!(!workspace.left_dock().read(cx).is_open());
7962            // The bottom dock is sized based on the panel's default size,
7963            // since the panel orientation changed from vertical to horizontal.
7964            let bottom_dock = workspace.bottom_dock();
7965            assert_eq!(
7966                bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
7967                panel_1.size(window, cx),
7968            );
7969            // Close bottom dock and move panel_1 back to the left.
7970            bottom_dock.update(cx, |bottom_dock, cx| {
7971                bottom_dock.set_open(false, window, cx)
7972            });
7973            panel_1.set_position(DockPosition::Left, window, cx);
7974        });
7975
7976        // Emit activated event on panel 1
7977        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7978
7979        // Now the left dock is open and panel_1 is active and focused.
7980        workspace.update_in(cx, |workspace, window, cx| {
7981            let left_dock = workspace.left_dock();
7982            assert!(left_dock.read(cx).is_open());
7983            assert_eq!(
7984                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7985                panel_1.panel_id(),
7986            );
7987            assert!(panel_1.focus_handle(cx).is_focused(window));
7988        });
7989
7990        // Emit closed event on panel 2, which is not active
7991        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7992
7993        // Wo don't close the left dock, because panel_2 wasn't the active panel
7994        workspace.update(cx, |workspace, cx| {
7995            let left_dock = workspace.left_dock();
7996            assert!(left_dock.read(cx).is_open());
7997            assert_eq!(
7998                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7999                panel_1.panel_id(),
8000            );
8001        });
8002
8003        // Emitting a ZoomIn event shows the panel as zoomed.
8004        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
8005        workspace.update(cx, |workspace, _| {
8006            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8007            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
8008        });
8009
8010        // Move panel to another dock while it is zoomed
8011        panel_1.update_in(cx, |panel, window, cx| {
8012            panel.set_position(DockPosition::Right, window, cx)
8013        });
8014        workspace.update(cx, |workspace, _| {
8015            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8016
8017            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8018        });
8019
8020        // This is a helper for getting a:
8021        // - valid focus on an element,
8022        // - that isn't a part of the panes and panels system of the Workspace,
8023        // - and doesn't trigger the 'on_focus_lost' API.
8024        let focus_other_view = {
8025            let workspace = workspace.clone();
8026            move |cx: &mut VisualTestContext| {
8027                workspace.update_in(cx, |workspace, window, cx| {
8028                    if let Some(_) = workspace.active_modal::<TestModal>(cx) {
8029                        workspace.toggle_modal(window, cx, TestModal::new);
8030                        workspace.toggle_modal(window, cx, TestModal::new);
8031                    } else {
8032                        workspace.toggle_modal(window, cx, TestModal::new);
8033                    }
8034                })
8035            }
8036        };
8037
8038        // If focus is transferred to another view that's not a panel or another pane, we still show
8039        // the panel as zoomed.
8040        focus_other_view(cx);
8041        workspace.update(cx, |workspace, _| {
8042            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8043            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8044        });
8045
8046        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8047        workspace.update_in(cx, |_workspace, window, cx| {
8048            cx.focus_self(window);
8049        });
8050        workspace.update(cx, |workspace, _| {
8051            assert_eq!(workspace.zoomed, None);
8052            assert_eq!(workspace.zoomed_position, None);
8053        });
8054
8055        // If focus is transferred again to another view that's not a panel or a pane, we won't
8056        // show the panel as zoomed because it wasn't zoomed before.
8057        focus_other_view(cx);
8058        workspace.update(cx, |workspace, _| {
8059            assert_eq!(workspace.zoomed, None);
8060            assert_eq!(workspace.zoomed_position, None);
8061        });
8062
8063        // When the panel is activated, it is zoomed again.
8064        cx.dispatch_action(ToggleRightDock);
8065        workspace.update(cx, |workspace, _| {
8066            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8067            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8068        });
8069
8070        // Emitting a ZoomOut event unzooms the panel.
8071        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8072        workspace.update(cx, |workspace, _| {
8073            assert_eq!(workspace.zoomed, None);
8074            assert_eq!(workspace.zoomed_position, None);
8075        });
8076
8077        // Emit closed event on panel 1, which is active
8078        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8079
8080        // Now the left dock is closed, because panel_1 was the active panel
8081        workspace.update(cx, |workspace, cx| {
8082            let right_dock = workspace.right_dock();
8083            assert!(!right_dock.read(cx).is_open());
8084        });
8085    }
8086
8087    #[gpui::test]
8088    async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8089        init_test(cx);
8090
8091        let fs = FakeFs::new(cx.background_executor.clone());
8092        let project = Project::test(fs, [], cx).await;
8093        let (workspace, cx) =
8094            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8095        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8096
8097        let dirty_regular_buffer = cx.new(|cx| {
8098            TestItem::new(cx)
8099                .with_dirty(true)
8100                .with_label("1.txt")
8101                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8102        });
8103        let dirty_regular_buffer_2 = cx.new(|cx| {
8104            TestItem::new(cx)
8105                .with_dirty(true)
8106                .with_label("2.txt")
8107                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8108        });
8109        let dirty_multi_buffer_with_both = cx.new(|cx| {
8110            TestItem::new(cx)
8111                .with_dirty(true)
8112                .with_singleton(false)
8113                .with_label("Fake Project Search")
8114                .with_project_items(&[
8115                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8116                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8117                ])
8118        });
8119        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8120        workspace.update_in(cx, |workspace, window, cx| {
8121            workspace.add_item(
8122                pane.clone(),
8123                Box::new(dirty_regular_buffer.clone()),
8124                None,
8125                false,
8126                false,
8127                window,
8128                cx,
8129            );
8130            workspace.add_item(
8131                pane.clone(),
8132                Box::new(dirty_regular_buffer_2.clone()),
8133                None,
8134                false,
8135                false,
8136                window,
8137                cx,
8138            );
8139            workspace.add_item(
8140                pane.clone(),
8141                Box::new(dirty_multi_buffer_with_both.clone()),
8142                None,
8143                false,
8144                false,
8145                window,
8146                cx,
8147            );
8148        });
8149
8150        pane.update_in(cx, |pane, window, cx| {
8151            pane.activate_item(2, true, true, window, cx);
8152            assert_eq!(
8153                pane.active_item().unwrap().item_id(),
8154                multi_buffer_with_both_files_id,
8155                "Should select the multi buffer in the pane"
8156            );
8157        });
8158        let close_all_but_multi_buffer_task = pane
8159            .update_in(cx, |pane, window, cx| {
8160                pane.close_inactive_items(
8161                    &CloseInactiveItems {
8162                        save_intent: Some(SaveIntent::Save),
8163                        close_pinned: true,
8164                    },
8165                    window,
8166                    cx,
8167                )
8168            })
8169            .expect("should have inactive files to close");
8170        cx.background_executor.run_until_parked();
8171        assert!(!cx.has_pending_prompt());
8172        close_all_but_multi_buffer_task
8173            .await
8174            .expect("Closing all buffers but the multi buffer failed");
8175        pane.update(cx, |pane, cx| {
8176            assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
8177            assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8178            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
8179            assert_eq!(pane.items_len(), 1);
8180            assert_eq!(
8181                pane.active_item().unwrap().item_id(),
8182                multi_buffer_with_both_files_id,
8183                "Should have only the multi buffer left in the pane"
8184            );
8185            assert!(
8186                dirty_multi_buffer_with_both.read(cx).is_dirty,
8187                "The multi buffer containing the unsaved buffer should still be dirty"
8188            );
8189        });
8190
8191        dirty_regular_buffer.update(cx, |buffer, cx| {
8192            buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
8193        });
8194
8195        let close_multi_buffer_task = pane
8196            .update_in(cx, |pane, window, cx| {
8197                pane.close_active_item(
8198                    &CloseActiveItem {
8199                        save_intent: Some(SaveIntent::Close),
8200                        close_pinned: false,
8201                    },
8202                    window,
8203                    cx,
8204                )
8205            })
8206            .expect("should have the multi buffer to close");
8207        cx.background_executor.run_until_parked();
8208        assert!(
8209            cx.has_pending_prompt(),
8210            "Dirty multi buffer should prompt a save dialog"
8211        );
8212        cx.simulate_prompt_answer("Save");
8213        cx.background_executor.run_until_parked();
8214        close_multi_buffer_task
8215            .await
8216            .expect("Closing the multi buffer failed");
8217        pane.update(cx, |pane, cx| {
8218            assert_eq!(
8219                dirty_multi_buffer_with_both.read(cx).save_count,
8220                1,
8221                "Multi buffer item should get be saved"
8222            );
8223            // Test impl does not save inner items, so we do not assert them
8224            assert_eq!(
8225                pane.items_len(),
8226                0,
8227                "No more items should be left in the pane"
8228            );
8229            assert!(pane.active_item().is_none());
8230        });
8231    }
8232
8233    #[gpui::test]
8234    async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
8235        cx: &mut TestAppContext,
8236    ) {
8237        init_test(cx);
8238
8239        let fs = FakeFs::new(cx.background_executor.clone());
8240        let project = Project::test(fs, [], cx).await;
8241        let (workspace, cx) =
8242            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8243        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8244
8245        let dirty_regular_buffer = cx.new(|cx| {
8246            TestItem::new(cx)
8247                .with_dirty(true)
8248                .with_label("1.txt")
8249                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8250        });
8251        let dirty_regular_buffer_2 = cx.new(|cx| {
8252            TestItem::new(cx)
8253                .with_dirty(true)
8254                .with_label("2.txt")
8255                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8256        });
8257        let clear_regular_buffer = cx.new(|cx| {
8258            TestItem::new(cx)
8259                .with_label("3.txt")
8260                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8261        });
8262
8263        let dirty_multi_buffer_with_both = cx.new(|cx| {
8264            TestItem::new(cx)
8265                .with_dirty(true)
8266                .with_singleton(false)
8267                .with_label("Fake Project Search")
8268                .with_project_items(&[
8269                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8270                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8271                    clear_regular_buffer.read(cx).project_items[0].clone(),
8272                ])
8273        });
8274        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8275        workspace.update_in(cx, |workspace, window, cx| {
8276            workspace.add_item(
8277                pane.clone(),
8278                Box::new(dirty_regular_buffer.clone()),
8279                None,
8280                false,
8281                false,
8282                window,
8283                cx,
8284            );
8285            workspace.add_item(
8286                pane.clone(),
8287                Box::new(dirty_multi_buffer_with_both.clone()),
8288                None,
8289                false,
8290                false,
8291                window,
8292                cx,
8293            );
8294        });
8295
8296        pane.update_in(cx, |pane, window, cx| {
8297            pane.activate_item(1, true, true, window, cx);
8298            assert_eq!(
8299                pane.active_item().unwrap().item_id(),
8300                multi_buffer_with_both_files_id,
8301                "Should select the multi buffer in the pane"
8302            );
8303        });
8304        let _close_multi_buffer_task = pane
8305            .update_in(cx, |pane, window, cx| {
8306                pane.close_active_item(
8307                    &CloseActiveItem {
8308                        save_intent: None,
8309                        close_pinned: false,
8310                    },
8311                    window,
8312                    cx,
8313                )
8314            })
8315            .expect("should have active multi buffer to close");
8316        cx.background_executor.run_until_parked();
8317        assert!(
8318            cx.has_pending_prompt(),
8319            "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
8320        );
8321    }
8322
8323    #[gpui::test]
8324    async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
8325        cx: &mut TestAppContext,
8326    ) {
8327        init_test(cx);
8328
8329        let fs = FakeFs::new(cx.background_executor.clone());
8330        let project = Project::test(fs, [], cx).await;
8331        let (workspace, cx) =
8332            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8333        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8334
8335        let dirty_regular_buffer = cx.new(|cx| {
8336            TestItem::new(cx)
8337                .with_dirty(true)
8338                .with_label("1.txt")
8339                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8340        });
8341        let dirty_regular_buffer_2 = cx.new(|cx| {
8342            TestItem::new(cx)
8343                .with_dirty(true)
8344                .with_label("2.txt")
8345                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8346        });
8347        let clear_regular_buffer = cx.new(|cx| {
8348            TestItem::new(cx)
8349                .with_label("3.txt")
8350                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8351        });
8352
8353        let dirty_multi_buffer = cx.new(|cx| {
8354            TestItem::new(cx)
8355                .with_dirty(true)
8356                .with_singleton(false)
8357                .with_label("Fake Project Search")
8358                .with_project_items(&[
8359                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8360                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8361                    clear_regular_buffer.read(cx).project_items[0].clone(),
8362                ])
8363        });
8364        workspace.update_in(cx, |workspace, window, cx| {
8365            workspace.add_item(
8366                pane.clone(),
8367                Box::new(dirty_regular_buffer.clone()),
8368                None,
8369                false,
8370                false,
8371                window,
8372                cx,
8373            );
8374            workspace.add_item(
8375                pane.clone(),
8376                Box::new(dirty_regular_buffer_2.clone()),
8377                None,
8378                false,
8379                false,
8380                window,
8381                cx,
8382            );
8383            workspace.add_item(
8384                pane.clone(),
8385                Box::new(dirty_multi_buffer.clone()),
8386                None,
8387                false,
8388                false,
8389                window,
8390                cx,
8391            );
8392        });
8393
8394        pane.update_in(cx, |pane, window, cx| {
8395            pane.activate_item(2, true, true, window, cx);
8396            assert_eq!(
8397                pane.active_item().unwrap().item_id(),
8398                dirty_multi_buffer.item_id(),
8399                "Should select the multi buffer in the pane"
8400            );
8401        });
8402        let close_multi_buffer_task = pane
8403            .update_in(cx, |pane, window, cx| {
8404                pane.close_active_item(
8405                    &CloseActiveItem {
8406                        save_intent: None,
8407                        close_pinned: false,
8408                    },
8409                    window,
8410                    cx,
8411                )
8412            })
8413            .expect("should have active multi buffer to close");
8414        cx.background_executor.run_until_parked();
8415        assert!(
8416            !cx.has_pending_prompt(),
8417            "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8418        );
8419        close_multi_buffer_task
8420            .await
8421            .expect("Closing multi buffer failed");
8422        pane.update(cx, |pane, cx| {
8423            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8424            assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8425            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8426            assert_eq!(
8427                pane.items()
8428                    .map(|item| item.item_id())
8429                    .sorted()
8430                    .collect::<Vec<_>>(),
8431                vec![
8432                    dirty_regular_buffer.item_id(),
8433                    dirty_regular_buffer_2.item_id(),
8434                ],
8435                "Should have no multi buffer left in the pane"
8436            );
8437            assert!(dirty_regular_buffer.read(cx).is_dirty);
8438            assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8439        });
8440    }
8441
8442    #[gpui::test]
8443    async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8444        init_test(cx);
8445        let fs = FakeFs::new(cx.executor());
8446        let project = Project::test(fs, [], cx).await;
8447        let (workspace, cx) =
8448            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8449
8450        // Add a new panel to the right dock, opening the dock and setting the
8451        // focus to the new panel.
8452        let panel = workspace.update_in(cx, |workspace, window, cx| {
8453            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8454            workspace.add_panel(panel.clone(), window, cx);
8455
8456            workspace
8457                .right_dock()
8458                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8459
8460            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8461
8462            panel
8463        });
8464
8465        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8466        // panel to the next valid position which, in this case, is the left
8467        // dock.
8468        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8469        workspace.update(cx, |workspace, cx| {
8470            assert!(workspace.left_dock().read(cx).is_open());
8471            assert_eq!(panel.read(cx).position, DockPosition::Left);
8472        });
8473
8474        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8475        // panel to the next valid position which, in this case, is the bottom
8476        // dock.
8477        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8478        workspace.update(cx, |workspace, cx| {
8479            assert!(workspace.bottom_dock().read(cx).is_open());
8480            assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8481        });
8482
8483        // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8484        // around moving the panel to its initial position, the right dock.
8485        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8486        workspace.update(cx, |workspace, cx| {
8487            assert!(workspace.right_dock().read(cx).is_open());
8488            assert_eq!(panel.read(cx).position, DockPosition::Right);
8489        });
8490
8491        // Remove focus from the panel, ensuring that, if the panel is not
8492        // focused, the `MoveFocusedPanelToNextPosition` action does not update
8493        // the panel's position, so the panel is still in the right dock.
8494        workspace.update_in(cx, |workspace, window, cx| {
8495            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8496        });
8497
8498        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8499        workspace.update(cx, |workspace, cx| {
8500            assert!(workspace.right_dock().read(cx).is_open());
8501            assert_eq!(panel.read(cx).position, DockPosition::Right);
8502        });
8503    }
8504
8505    mod register_project_item_tests {
8506
8507        use super::*;
8508
8509        // View
8510        struct TestPngItemView {
8511            focus_handle: FocusHandle,
8512        }
8513        // Model
8514        struct TestPngItem {}
8515
8516        impl project::ProjectItem for TestPngItem {
8517            fn try_open(
8518                _project: &Entity<Project>,
8519                path: &ProjectPath,
8520                cx: &mut App,
8521            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8522                if path.path.extension().unwrap() == "png" {
8523                    Some(cx.spawn(|mut cx| async move { cx.new(|_| TestPngItem {}) }))
8524                } else {
8525                    None
8526                }
8527            }
8528
8529            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8530                None
8531            }
8532
8533            fn project_path(&self, _: &App) -> Option<ProjectPath> {
8534                None
8535            }
8536
8537            fn is_dirty(&self) -> bool {
8538                false
8539            }
8540        }
8541
8542        impl Item for TestPngItemView {
8543            type Event = ();
8544        }
8545        impl EventEmitter<()> for TestPngItemView {}
8546        impl Focusable for TestPngItemView {
8547            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8548                self.focus_handle.clone()
8549            }
8550        }
8551
8552        impl Render for TestPngItemView {
8553            fn render(
8554                &mut self,
8555                _window: &mut Window,
8556                _cx: &mut Context<Self>,
8557            ) -> impl IntoElement {
8558                Empty
8559            }
8560        }
8561
8562        impl ProjectItem for TestPngItemView {
8563            type Item = TestPngItem;
8564
8565            fn for_project_item(
8566                _project: Entity<Project>,
8567                _item: Entity<Self::Item>,
8568                _: &mut Window,
8569                cx: &mut Context<Self>,
8570            ) -> Self
8571            where
8572                Self: Sized,
8573            {
8574                Self {
8575                    focus_handle: cx.focus_handle(),
8576                }
8577            }
8578        }
8579
8580        // View
8581        struct TestIpynbItemView {
8582            focus_handle: FocusHandle,
8583        }
8584        // Model
8585        struct TestIpynbItem {}
8586
8587        impl project::ProjectItem for TestIpynbItem {
8588            fn try_open(
8589                _project: &Entity<Project>,
8590                path: &ProjectPath,
8591                cx: &mut App,
8592            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8593                if path.path.extension().unwrap() == "ipynb" {
8594                    Some(cx.spawn(|mut cx| async move { cx.new(|_| TestIpynbItem {}) }))
8595                } else {
8596                    None
8597                }
8598            }
8599
8600            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8601                None
8602            }
8603
8604            fn project_path(&self, _: &App) -> Option<ProjectPath> {
8605                None
8606            }
8607
8608            fn is_dirty(&self) -> bool {
8609                false
8610            }
8611        }
8612
8613        impl Item for TestIpynbItemView {
8614            type Event = ();
8615        }
8616        impl EventEmitter<()> for TestIpynbItemView {}
8617        impl Focusable for TestIpynbItemView {
8618            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8619                self.focus_handle.clone()
8620            }
8621        }
8622
8623        impl Render for TestIpynbItemView {
8624            fn render(
8625                &mut self,
8626                _window: &mut Window,
8627                _cx: &mut Context<Self>,
8628            ) -> impl IntoElement {
8629                Empty
8630            }
8631        }
8632
8633        impl ProjectItem for TestIpynbItemView {
8634            type Item = TestIpynbItem;
8635
8636            fn for_project_item(
8637                _project: Entity<Project>,
8638                _item: Entity<Self::Item>,
8639                _: &mut Window,
8640                cx: &mut Context<Self>,
8641            ) -> Self
8642            where
8643                Self: Sized,
8644            {
8645                Self {
8646                    focus_handle: cx.focus_handle(),
8647                }
8648            }
8649        }
8650
8651        struct TestAlternatePngItemView {
8652            focus_handle: FocusHandle,
8653        }
8654
8655        impl Item for TestAlternatePngItemView {
8656            type Event = ();
8657        }
8658
8659        impl EventEmitter<()> for TestAlternatePngItemView {}
8660        impl Focusable for TestAlternatePngItemView {
8661            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8662                self.focus_handle.clone()
8663            }
8664        }
8665
8666        impl Render for TestAlternatePngItemView {
8667            fn render(
8668                &mut self,
8669                _window: &mut Window,
8670                _cx: &mut Context<Self>,
8671            ) -> impl IntoElement {
8672                Empty
8673            }
8674        }
8675
8676        impl ProjectItem for TestAlternatePngItemView {
8677            type Item = TestPngItem;
8678
8679            fn for_project_item(
8680                _project: Entity<Project>,
8681                _item: Entity<Self::Item>,
8682                _: &mut Window,
8683                cx: &mut Context<Self>,
8684            ) -> Self
8685            where
8686                Self: Sized,
8687            {
8688                Self {
8689                    focus_handle: cx.focus_handle(),
8690                }
8691            }
8692        }
8693
8694        #[gpui::test]
8695        async fn test_register_project_item(cx: &mut TestAppContext) {
8696            init_test(cx);
8697
8698            cx.update(|cx| {
8699                register_project_item::<TestPngItemView>(cx);
8700                register_project_item::<TestIpynbItemView>(cx);
8701            });
8702
8703            let fs = FakeFs::new(cx.executor());
8704            fs.insert_tree(
8705                "/root1",
8706                json!({
8707                    "one.png": "BINARYDATAHERE",
8708                    "two.ipynb": "{ totally a notebook }",
8709                    "three.txt": "editing text, sure why not?"
8710                }),
8711            )
8712            .await;
8713
8714            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8715            let (workspace, cx) =
8716                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8717
8718            let worktree_id = project.update(cx, |project, cx| {
8719                project.worktrees(cx).next().unwrap().read(cx).id()
8720            });
8721
8722            let handle = workspace
8723                .update_in(cx, |workspace, window, cx| {
8724                    let project_path = (worktree_id, "one.png");
8725                    workspace.open_path(project_path, None, true, window, cx)
8726                })
8727                .await
8728                .unwrap();
8729
8730            // Now we can check if the handle we got back errored or not
8731            assert_eq!(
8732                handle.to_any().entity_type(),
8733                TypeId::of::<TestPngItemView>()
8734            );
8735
8736            let handle = workspace
8737                .update_in(cx, |workspace, window, cx| {
8738                    let project_path = (worktree_id, "two.ipynb");
8739                    workspace.open_path(project_path, None, true, window, cx)
8740                })
8741                .await
8742                .unwrap();
8743
8744            assert_eq!(
8745                handle.to_any().entity_type(),
8746                TypeId::of::<TestIpynbItemView>()
8747            );
8748
8749            let handle = workspace
8750                .update_in(cx, |workspace, window, cx| {
8751                    let project_path = (worktree_id, "three.txt");
8752                    workspace.open_path(project_path, None, true, window, cx)
8753                })
8754                .await;
8755            assert!(handle.is_err());
8756        }
8757
8758        #[gpui::test]
8759        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8760            init_test(cx);
8761
8762            cx.update(|cx| {
8763                register_project_item::<TestPngItemView>(cx);
8764                register_project_item::<TestAlternatePngItemView>(cx);
8765            });
8766
8767            let fs = FakeFs::new(cx.executor());
8768            fs.insert_tree(
8769                "/root1",
8770                json!({
8771                    "one.png": "BINARYDATAHERE",
8772                    "two.ipynb": "{ totally a notebook }",
8773                    "three.txt": "editing text, sure why not?"
8774                }),
8775            )
8776            .await;
8777            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8778            let (workspace, cx) =
8779                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8780            let worktree_id = project.update(cx, |project, cx| {
8781                project.worktrees(cx).next().unwrap().read(cx).id()
8782            });
8783
8784            let handle = workspace
8785                .update_in(cx, |workspace, window, cx| {
8786                    let project_path = (worktree_id, "one.png");
8787                    workspace.open_path(project_path, None, true, window, cx)
8788                })
8789                .await
8790                .unwrap();
8791
8792            // This _must_ be the second item registered
8793            assert_eq!(
8794                handle.to_any().entity_type(),
8795                TypeId::of::<TestAlternatePngItemView>()
8796            );
8797
8798            let handle = workspace
8799                .update_in(cx, |workspace, window, cx| {
8800                    let project_path = (worktree_id, "three.txt");
8801                    workspace.open_path(project_path, None, true, window, cx)
8802                })
8803                .await;
8804            assert!(handle.is_err());
8805        }
8806    }
8807
8808    pub fn init_test(cx: &mut TestAppContext) {
8809        cx.update(|cx| {
8810            let settings_store = SettingsStore::test(cx);
8811            cx.set_global(settings_store);
8812            theme::init(theme::LoadThemes::JustBase, cx);
8813            language::init(cx);
8814            crate::init_settings(cx);
8815            Project::init_settings(cx);
8816        });
8817    }
8818
8819    fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
8820        let item = TestProjectItem::new(id, path, cx);
8821        item.update(cx, |item, _| {
8822            item.is_dirty = true;
8823        });
8824        item
8825    }
8826}