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    #[allow(clippy::too_many_arguments)]
2700    pub fn add_item(
2701        &mut self,
2702        pane: Entity<Pane>,
2703        item: Box<dyn ItemHandle>,
2704        destination_index: Option<usize>,
2705        activate_pane: bool,
2706        focus_item: bool,
2707        window: &mut Window,
2708        cx: &mut App,
2709    ) {
2710        if let Some(text) = item.telemetry_event_text(cx) {
2711            telemetry::event!(text);
2712        }
2713
2714        pane.update(cx, |pane, cx| {
2715            pane.add_item(
2716                item,
2717                activate_pane,
2718                focus_item,
2719                destination_index,
2720                window,
2721                cx,
2722            )
2723        });
2724    }
2725
2726    pub fn split_item(
2727        &mut self,
2728        split_direction: SplitDirection,
2729        item: Box<dyn ItemHandle>,
2730        window: &mut Window,
2731        cx: &mut Context<Self>,
2732    ) {
2733        let new_pane = self.split_pane(self.active_pane.clone(), split_direction, window, cx);
2734        self.add_item(new_pane, item, None, true, true, window, cx);
2735    }
2736
2737    pub fn open_abs_path(
2738        &mut self,
2739        abs_path: PathBuf,
2740        visible: bool,
2741        window: &mut Window,
2742        cx: &mut Context<Self>,
2743    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2744        cx.spawn_in(window, |workspace, mut cx| async move {
2745            let open_paths_task_result = workspace
2746                .update_in(&mut cx, |workspace, window, cx| {
2747                    workspace.open_paths(
2748                        vec![abs_path.clone()],
2749                        if visible {
2750                            OpenVisible::All
2751                        } else {
2752                            OpenVisible::None
2753                        },
2754                        None,
2755                        window,
2756                        cx,
2757                    )
2758                })
2759                .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
2760                .await;
2761            anyhow::ensure!(
2762                open_paths_task_result.len() == 1,
2763                "open abs path {abs_path:?} task returned incorrect number of results"
2764            );
2765            match open_paths_task_result
2766                .into_iter()
2767                .next()
2768                .expect("ensured single task result")
2769            {
2770                Some(open_result) => {
2771                    open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
2772                }
2773                None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
2774            }
2775        })
2776    }
2777
2778    pub fn split_abs_path(
2779        &mut self,
2780        abs_path: PathBuf,
2781        visible: bool,
2782        window: &mut Window,
2783        cx: &mut Context<Self>,
2784    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2785        let project_path_task =
2786            Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
2787        cx.spawn_in(window, |this, mut cx| async move {
2788            let (_, path) = project_path_task.await?;
2789            this.update_in(&mut cx, |this, window, cx| {
2790                this.split_path(path, window, cx)
2791            })?
2792            .await
2793        })
2794    }
2795
2796    pub fn open_path(
2797        &mut self,
2798        path: impl Into<ProjectPath>,
2799        pane: Option<WeakEntity<Pane>>,
2800        focus_item: bool,
2801        window: &mut Window,
2802        cx: &mut App,
2803    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2804        self.open_path_preview(path, pane, focus_item, false, window, cx)
2805    }
2806
2807    pub fn open_path_preview(
2808        &mut self,
2809        path: impl Into<ProjectPath>,
2810        pane: Option<WeakEntity<Pane>>,
2811        focus_item: bool,
2812        allow_preview: bool,
2813        window: &mut Window,
2814        cx: &mut App,
2815    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2816        let pane = pane.unwrap_or_else(|| {
2817            self.last_active_center_pane.clone().unwrap_or_else(|| {
2818                self.panes
2819                    .first()
2820                    .expect("There must be an active pane")
2821                    .downgrade()
2822            })
2823        });
2824
2825        let task = self.load_path(path.into(), window, cx);
2826        window.spawn(cx, move |mut cx| async move {
2827            let (project_entry_id, build_item) = task.await?;
2828            let result = pane.update_in(&mut cx, |pane, window, cx| {
2829                let result = pane.open_item(
2830                    project_entry_id,
2831                    focus_item,
2832                    allow_preview,
2833                    None,
2834                    window,
2835                    cx,
2836                    build_item,
2837                );
2838
2839                result
2840            });
2841            result
2842        })
2843    }
2844
2845    pub fn split_path(
2846        &mut self,
2847        path: impl Into<ProjectPath>,
2848        window: &mut Window,
2849        cx: &mut Context<Self>,
2850    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2851        self.split_path_preview(path, false, None, window, cx)
2852    }
2853
2854    pub fn split_path_preview(
2855        &mut self,
2856        path: impl Into<ProjectPath>,
2857        allow_preview: bool,
2858        split_direction: Option<SplitDirection>,
2859        window: &mut Window,
2860        cx: &mut Context<Self>,
2861    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2862        let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
2863            self.panes
2864                .first()
2865                .expect("There must be an active pane")
2866                .downgrade()
2867        });
2868
2869        if let Member::Pane(center_pane) = &self.center.root {
2870            if center_pane.read(cx).items_len() == 0 {
2871                return self.open_path(path, Some(pane), true, window, cx);
2872            }
2873        }
2874
2875        let task = self.load_path(path.into(), window, cx);
2876        cx.spawn_in(window, |this, mut cx| async move {
2877            let (project_entry_id, build_item) = task.await?;
2878            this.update_in(&mut cx, move |this, window, cx| -> Option<_> {
2879                let pane = pane.upgrade()?;
2880                let new_pane = this.split_pane(
2881                    pane,
2882                    split_direction.unwrap_or(SplitDirection::Right),
2883                    window,
2884                    cx,
2885                );
2886                new_pane.update(cx, |new_pane, cx| {
2887                    Some(new_pane.open_item(
2888                        project_entry_id,
2889                        true,
2890                        allow_preview,
2891                        None,
2892                        window,
2893                        cx,
2894                        build_item,
2895                    ))
2896                })
2897            })
2898            .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
2899        })
2900    }
2901
2902    fn load_path(
2903        &mut self,
2904        path: ProjectPath,
2905        window: &mut Window,
2906        cx: &mut App,
2907    ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
2908        let project = self.project().clone();
2909        let project_item_builders = cx.default_global::<ProjectItemOpeners>().clone();
2910        let Some(open_project_item) = project_item_builders
2911            .iter()
2912            .rev()
2913            .find_map(|open_project_item| open_project_item(&project, &path, window, cx))
2914        else {
2915            return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
2916        };
2917        open_project_item
2918    }
2919
2920    pub fn find_project_item<T>(
2921        &self,
2922        pane: &Entity<Pane>,
2923        project_item: &Entity<T::Item>,
2924        cx: &App,
2925    ) -> Option<Entity<T>>
2926    where
2927        T: ProjectItem,
2928    {
2929        use project::ProjectItem as _;
2930        let project_item = project_item.read(cx);
2931        let entry_id = project_item.entry_id(cx);
2932        let project_path = project_item.project_path(cx);
2933
2934        let mut item = None;
2935        if let Some(entry_id) = entry_id {
2936            item = pane.read(cx).item_for_entry(entry_id, cx);
2937        }
2938        if item.is_none() {
2939            if let Some(project_path) = project_path {
2940                item = pane.read(cx).item_for_path(project_path, cx);
2941            }
2942        }
2943
2944        item.and_then(|item| item.downcast::<T>())
2945    }
2946
2947    pub fn is_project_item_open<T>(
2948        &self,
2949        pane: &Entity<Pane>,
2950        project_item: &Entity<T::Item>,
2951        cx: &App,
2952    ) -> bool
2953    where
2954        T: ProjectItem,
2955    {
2956        self.find_project_item::<T>(pane, project_item, cx)
2957            .is_some()
2958    }
2959
2960    pub fn open_project_item<T>(
2961        &mut self,
2962        pane: Entity<Pane>,
2963        project_item: Entity<T::Item>,
2964        activate_pane: bool,
2965        focus_item: bool,
2966        window: &mut Window,
2967        cx: &mut Context<Self>,
2968    ) -> Entity<T>
2969    where
2970        T: ProjectItem,
2971    {
2972        if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
2973            self.activate_item(&item, activate_pane, focus_item, window, cx);
2974            return item;
2975        }
2976
2977        let item =
2978            cx.new(|cx| T::for_project_item(self.project().clone(), project_item, window, cx));
2979        let item_id = item.item_id();
2980        let mut destination_index = None;
2981        pane.update(cx, |pane, cx| {
2982            if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
2983                if let Some(preview_item_id) = pane.preview_item_id() {
2984                    if preview_item_id != item_id {
2985                        destination_index = pane.close_current_preview_item(window, cx);
2986                    }
2987                }
2988            }
2989            pane.set_preview_item_id(Some(item.item_id()), cx)
2990        });
2991
2992        self.add_item(
2993            pane,
2994            Box::new(item.clone()),
2995            destination_index,
2996            activate_pane,
2997            focus_item,
2998            window,
2999            cx,
3000        );
3001        item
3002    }
3003
3004    pub fn open_shared_screen(
3005        &mut self,
3006        peer_id: PeerId,
3007        window: &mut Window,
3008        cx: &mut Context<Self>,
3009    ) {
3010        if let Some(shared_screen) =
3011            self.shared_screen_for_peer(peer_id, &self.active_pane, window, cx)
3012        {
3013            self.active_pane.update(cx, |pane, cx| {
3014                pane.add_item(Box::new(shared_screen), false, true, None, window, cx)
3015            });
3016        }
3017    }
3018
3019    pub fn activate_item(
3020        &mut self,
3021        item: &dyn ItemHandle,
3022        activate_pane: bool,
3023        focus_item: bool,
3024        window: &mut Window,
3025        cx: &mut App,
3026    ) -> bool {
3027        let result = self.panes.iter().find_map(|pane| {
3028            pane.read(cx)
3029                .index_for_item(item)
3030                .map(|ix| (pane.clone(), ix))
3031        });
3032        if let Some((pane, ix)) = result {
3033            pane.update(cx, |pane, cx| {
3034                pane.activate_item(ix, activate_pane, focus_item, window, cx)
3035            });
3036            true
3037        } else {
3038            false
3039        }
3040    }
3041
3042    fn activate_pane_at_index(
3043        &mut self,
3044        action: &ActivatePane,
3045        window: &mut Window,
3046        cx: &mut Context<Self>,
3047    ) {
3048        let panes = self.center.panes();
3049        if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
3050            window.focus(&pane.focus_handle(cx));
3051        } else {
3052            self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx);
3053        }
3054    }
3055
3056    fn move_item_to_pane_at_index(
3057        &mut self,
3058        action: &MoveItemToPane,
3059        window: &mut Window,
3060        cx: &mut Context<Self>,
3061    ) {
3062        let Some(&target_pane) = self.center.panes().get(action.destination) else {
3063            return;
3064        };
3065        move_active_item(
3066            &self.active_pane,
3067            target_pane,
3068            action.focus,
3069            true,
3070            window,
3071            cx,
3072        );
3073    }
3074
3075    pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) {
3076        let panes = self.center.panes();
3077        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
3078            let next_ix = (ix + 1) % panes.len();
3079            let next_pane = panes[next_ix].clone();
3080            window.focus(&next_pane.focus_handle(cx));
3081        }
3082    }
3083
3084    pub fn activate_previous_pane(&mut self, window: &mut Window, cx: &mut App) {
3085        let panes = self.center.panes();
3086        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
3087            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
3088            let prev_pane = panes[prev_ix].clone();
3089            window.focus(&prev_pane.focus_handle(cx));
3090        }
3091    }
3092
3093    pub fn activate_pane_in_direction(
3094        &mut self,
3095        direction: SplitDirection,
3096        window: &mut Window,
3097        cx: &mut App,
3098    ) {
3099        use ActivateInDirectionTarget as Target;
3100        enum Origin {
3101            LeftDock,
3102            RightDock,
3103            BottomDock,
3104            Center,
3105        }
3106
3107        let origin: Origin = [
3108            (&self.left_dock, Origin::LeftDock),
3109            (&self.right_dock, Origin::RightDock),
3110            (&self.bottom_dock, Origin::BottomDock),
3111        ]
3112        .into_iter()
3113        .find_map(|(dock, origin)| {
3114            if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() {
3115                Some(origin)
3116            } else {
3117                None
3118            }
3119        })
3120        .unwrap_or(Origin::Center);
3121
3122        let get_last_active_pane = || {
3123            let pane = self
3124                .last_active_center_pane
3125                .clone()
3126                .unwrap_or_else(|| {
3127                    self.panes
3128                        .first()
3129                        .expect("There must be an active pane")
3130                        .downgrade()
3131                })
3132                .upgrade()?;
3133            (pane.read(cx).items_len() != 0).then_some(pane)
3134        };
3135
3136        let try_dock =
3137            |dock: &Entity<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
3138
3139        let target = match (origin, direction) {
3140            // We're in the center, so we first try to go to a different pane,
3141            // otherwise try to go to a dock.
3142            (Origin::Center, direction) => {
3143                if let Some(pane) = self.find_pane_in_direction(direction, cx) {
3144                    Some(Target::Pane(pane))
3145                } else {
3146                    match direction {
3147                        SplitDirection::Up => None,
3148                        SplitDirection::Down => try_dock(&self.bottom_dock),
3149                        SplitDirection::Left => try_dock(&self.left_dock),
3150                        SplitDirection::Right => try_dock(&self.right_dock),
3151                    }
3152                }
3153            }
3154
3155            (Origin::LeftDock, SplitDirection::Right) => {
3156                if let Some(last_active_pane) = get_last_active_pane() {
3157                    Some(Target::Pane(last_active_pane))
3158                } else {
3159                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
3160                }
3161            }
3162
3163            (Origin::LeftDock, SplitDirection::Down)
3164            | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
3165
3166            (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
3167            (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
3168            (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
3169
3170            (Origin::RightDock, SplitDirection::Left) => {
3171                if let Some(last_active_pane) = get_last_active_pane() {
3172                    Some(Target::Pane(last_active_pane))
3173                } else {
3174                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
3175                }
3176            }
3177
3178            _ => None,
3179        };
3180
3181        match target {
3182            Some(ActivateInDirectionTarget::Pane(pane)) => {
3183                window.focus(&pane.focus_handle(cx));
3184            }
3185            Some(ActivateInDirectionTarget::Dock(dock)) => {
3186                // Defer this to avoid a panic when the dock's active panel is already on the stack.
3187                window.defer(cx, move |window, cx| {
3188                    let dock = dock.read(cx);
3189                    if let Some(panel) = dock.active_panel() {
3190                        panel.panel_focus_handle(cx).focus(window);
3191                    } else {
3192                        log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
3193                    }
3194                })
3195            }
3196            None => {}
3197        }
3198    }
3199
3200    pub fn move_item_to_pane_in_direction(
3201        &mut self,
3202        action: &MoveItemToPaneInDirection,
3203        window: &mut Window,
3204        cx: &mut App,
3205    ) {
3206        if let Some(destination) = self.find_pane_in_direction(action.direction, cx) {
3207            move_active_item(
3208                &self.active_pane,
3209                &destination,
3210                action.focus,
3211                true,
3212                window,
3213                cx,
3214            );
3215        }
3216    }
3217
3218    pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
3219        self.center.bounding_box_for_pane(pane)
3220    }
3221
3222    pub fn find_pane_in_direction(
3223        &mut self,
3224        direction: SplitDirection,
3225        cx: &App,
3226    ) -> Option<Entity<Pane>> {
3227        self.center
3228            .find_pane_in_direction(&self.active_pane, direction, cx)
3229            .cloned()
3230    }
3231
3232    pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
3233        if let Some(to) = self.find_pane_in_direction(direction, cx) {
3234            self.center.swap(&self.active_pane, &to);
3235            cx.notify();
3236        }
3237    }
3238
3239    pub fn resize_pane(
3240        &mut self,
3241        axis: gpui::Axis,
3242        amount: Pixels,
3243        window: &mut Window,
3244        cx: &mut Context<Self>,
3245    ) {
3246        let docks = self.all_docks();
3247        let active_dock = docks
3248            .into_iter()
3249            .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
3250
3251        if let Some(dock) = active_dock {
3252            let Some(panel_size) = dock.read(cx).active_panel_size(window, cx) else {
3253                return;
3254            };
3255            match dock.read(cx).position() {
3256                DockPosition::Left => resize_left_dock(panel_size + amount, self, window, cx),
3257                DockPosition::Bottom => resize_bottom_dock(panel_size + amount, self, window, cx),
3258                DockPosition::Right => resize_right_dock(panel_size + amount, self, window, cx),
3259            }
3260        } else {
3261            self.center
3262                .resize(&self.active_pane, axis, amount, &self.bounds);
3263        }
3264        cx.notify();
3265    }
3266
3267    pub fn reset_pane_sizes(&mut self, cx: &mut Context<Self>) {
3268        self.center.reset_pane_sizes();
3269        cx.notify();
3270    }
3271
3272    fn handle_pane_focused(
3273        &mut self,
3274        pane: Entity<Pane>,
3275        window: &mut Window,
3276        cx: &mut Context<Self>,
3277    ) {
3278        // This is explicitly hoisted out of the following check for pane identity as
3279        // terminal panel panes are not registered as a center panes.
3280        self.status_bar.update(cx, |status_bar, cx| {
3281            status_bar.set_active_pane(&pane, window, cx);
3282        });
3283        if self.active_pane != pane {
3284            self.set_active_pane(&pane, window, cx);
3285        }
3286
3287        if self.last_active_center_pane.is_none() {
3288            self.last_active_center_pane = Some(pane.downgrade());
3289        }
3290
3291        self.dismiss_zoomed_items_to_reveal(None, window, cx);
3292        if pane.read(cx).is_zoomed() {
3293            self.zoomed = Some(pane.downgrade().into());
3294        } else {
3295            self.zoomed = None;
3296        }
3297        self.zoomed_position = None;
3298        cx.emit(Event::ZoomChanged);
3299        self.update_active_view_for_followers(window, cx);
3300        pane.update(cx, |pane, _| {
3301            pane.track_alternate_file_items();
3302        });
3303
3304        cx.notify();
3305    }
3306
3307    fn set_active_pane(
3308        &mut self,
3309        pane: &Entity<Pane>,
3310        window: &mut Window,
3311        cx: &mut Context<Self>,
3312    ) {
3313        self.active_pane = pane.clone();
3314        self.active_item_path_changed(window, cx);
3315        self.last_active_center_pane = Some(pane.downgrade());
3316    }
3317
3318    fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3319        self.update_active_view_for_followers(window, cx);
3320    }
3321
3322    fn handle_pane_event(
3323        &mut self,
3324        pane: &Entity<Pane>,
3325        event: &pane::Event,
3326        window: &mut Window,
3327        cx: &mut Context<Self>,
3328    ) {
3329        let mut serialize_workspace = true;
3330        match event {
3331            pane::Event::AddItem { item } => {
3332                item.added_to_pane(self, pane.clone(), window, cx);
3333                cx.emit(Event::ItemAdded {
3334                    item: item.boxed_clone(),
3335                });
3336            }
3337            pane::Event::Split(direction) => {
3338                self.split_and_clone(pane.clone(), *direction, window, cx);
3339            }
3340            pane::Event::JoinIntoNext => {
3341                self.join_pane_into_next(pane.clone(), window, cx);
3342            }
3343            pane::Event::JoinAll => {
3344                self.join_all_panes(window, cx);
3345            }
3346            pane::Event::Remove { focus_on_pane } => {
3347                self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx);
3348            }
3349            pane::Event::ActivateItem {
3350                local,
3351                focus_changed,
3352            } => {
3353                cx.on_next_frame(window, |_, window, _| {
3354                    window.invalidate_character_coordinates();
3355                });
3356
3357                pane.update(cx, |pane, _| {
3358                    pane.track_alternate_file_items();
3359                });
3360                if *local {
3361                    self.unfollow_in_pane(&pane, window, cx);
3362                }
3363                if pane == self.active_pane() {
3364                    self.active_item_path_changed(window, cx);
3365                    self.update_active_view_for_followers(window, cx);
3366                }
3367                serialize_workspace = *focus_changed || pane != self.active_pane();
3368            }
3369            pane::Event::UserSavedItem { item, save_intent } => {
3370                cx.emit(Event::UserSavedItem {
3371                    pane: pane.downgrade(),
3372                    item: item.boxed_clone(),
3373                    save_intent: *save_intent,
3374                });
3375                serialize_workspace = false;
3376            }
3377            pane::Event::ChangeItemTitle => {
3378                if *pane == self.active_pane {
3379                    self.active_item_path_changed(window, cx);
3380                }
3381                self.update_window_edited(window, cx);
3382                serialize_workspace = false;
3383            }
3384            pane::Event::RemoveItem { .. } => {}
3385            pane::Event::RemovedItem { item_id } => {
3386                cx.emit(Event::ActiveItemChanged);
3387                self.update_window_edited(window, cx);
3388                if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
3389                    if entry.get().entity_id() == pane.entity_id() {
3390                        entry.remove();
3391                    }
3392                }
3393            }
3394            pane::Event::Focus => {
3395                cx.on_next_frame(window, |_, window, _| {
3396                    window.invalidate_character_coordinates();
3397                });
3398                self.handle_pane_focused(pane.clone(), window, cx);
3399            }
3400            pane::Event::ZoomIn => {
3401                if *pane == self.active_pane {
3402                    pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
3403                    if pane.read(cx).has_focus(window, cx) {
3404                        self.zoomed = Some(pane.downgrade().into());
3405                        self.zoomed_position = None;
3406                        cx.emit(Event::ZoomChanged);
3407                    }
3408                    cx.notify();
3409                }
3410            }
3411            pane::Event::ZoomOut => {
3412                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3413                if self.zoomed_position.is_none() {
3414                    self.zoomed = None;
3415                    cx.emit(Event::ZoomChanged);
3416                }
3417                cx.notify();
3418            }
3419        }
3420
3421        if serialize_workspace {
3422            self.serialize_workspace(window, cx);
3423        }
3424    }
3425
3426    pub fn unfollow_in_pane(
3427        &mut self,
3428        pane: &Entity<Pane>,
3429        window: &mut Window,
3430        cx: &mut Context<Workspace>,
3431    ) -> Option<PeerId> {
3432        let leader_id = self.leader_for_pane(pane)?;
3433        self.unfollow(leader_id, window, cx);
3434        Some(leader_id)
3435    }
3436
3437    pub fn split_pane(
3438        &mut self,
3439        pane_to_split: Entity<Pane>,
3440        split_direction: SplitDirection,
3441        window: &mut Window,
3442        cx: &mut Context<Self>,
3443    ) -> Entity<Pane> {
3444        let new_pane = self.add_pane(window, cx);
3445        self.center
3446            .split(&pane_to_split, &new_pane, split_direction)
3447            .unwrap();
3448        cx.notify();
3449        new_pane
3450    }
3451
3452    pub fn split_and_clone(
3453        &mut self,
3454        pane: Entity<Pane>,
3455        direction: SplitDirection,
3456        window: &mut Window,
3457        cx: &mut Context<Self>,
3458    ) -> Option<Entity<Pane>> {
3459        let item = pane.read(cx).active_item()?;
3460        let maybe_pane_handle =
3461            if let Some(clone) = item.clone_on_split(self.database_id(), window, cx) {
3462                let new_pane = self.add_pane(window, cx);
3463                new_pane.update(cx, |pane, cx| {
3464                    pane.add_item(clone, true, true, None, window, cx)
3465                });
3466                self.center.split(&pane, &new_pane, direction).unwrap();
3467                Some(new_pane)
3468            } else {
3469                None
3470            };
3471        cx.notify();
3472        maybe_pane_handle
3473    }
3474
3475    pub fn split_pane_with_item(
3476        &mut self,
3477        pane_to_split: WeakEntity<Pane>,
3478        split_direction: SplitDirection,
3479        from: WeakEntity<Pane>,
3480        item_id_to_move: EntityId,
3481        window: &mut Window,
3482        cx: &mut Context<Self>,
3483    ) {
3484        let Some(pane_to_split) = pane_to_split.upgrade() else {
3485            return;
3486        };
3487        let Some(from) = from.upgrade() else {
3488            return;
3489        };
3490
3491        let new_pane = self.add_pane(window, cx);
3492        move_item(&from, &new_pane, item_id_to_move, 0, window, cx);
3493        self.center
3494            .split(&pane_to_split, &new_pane, split_direction)
3495            .unwrap();
3496        cx.notify();
3497    }
3498
3499    pub fn split_pane_with_project_entry(
3500        &mut self,
3501        pane_to_split: WeakEntity<Pane>,
3502        split_direction: SplitDirection,
3503        project_entry: ProjectEntryId,
3504        window: &mut Window,
3505        cx: &mut Context<Self>,
3506    ) -> Option<Task<Result<()>>> {
3507        let pane_to_split = pane_to_split.upgrade()?;
3508        let new_pane = self.add_pane(window, cx);
3509        self.center
3510            .split(&pane_to_split, &new_pane, split_direction)
3511            .unwrap();
3512
3513        let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
3514        let task = self.open_path(path, Some(new_pane.downgrade()), true, window, cx);
3515        Some(cx.foreground_executor().spawn(async move {
3516            task.await?;
3517            Ok(())
3518        }))
3519    }
3520
3521    pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3522        let active_item = self.active_pane.read(cx).active_item();
3523        for pane in &self.panes {
3524            join_pane_into_active(&self.active_pane, pane, window, cx);
3525        }
3526        if let Some(active_item) = active_item {
3527            self.activate_item(active_item.as_ref(), true, true, window, cx);
3528        }
3529        cx.notify();
3530    }
3531
3532    pub fn join_pane_into_next(
3533        &mut self,
3534        pane: Entity<Pane>,
3535        window: &mut Window,
3536        cx: &mut Context<Self>,
3537    ) {
3538        let next_pane = self
3539            .find_pane_in_direction(SplitDirection::Right, cx)
3540            .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
3541            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3542            .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
3543        let Some(next_pane) = next_pane else {
3544            return;
3545        };
3546        move_all_items(&pane, &next_pane, window, cx);
3547        cx.notify();
3548    }
3549
3550    fn remove_pane(
3551        &mut self,
3552        pane: Entity<Pane>,
3553        focus_on: Option<Entity<Pane>>,
3554        window: &mut Window,
3555        cx: &mut Context<Self>,
3556    ) {
3557        if self.center.remove(&pane).unwrap() {
3558            self.force_remove_pane(&pane, &focus_on, window, cx);
3559            self.unfollow_in_pane(&pane, window, cx);
3560            self.last_leaders_by_pane.remove(&pane.downgrade());
3561            for removed_item in pane.read(cx).items() {
3562                self.panes_by_item.remove(&removed_item.item_id());
3563            }
3564
3565            cx.notify();
3566        } else {
3567            self.active_item_path_changed(window, cx);
3568        }
3569        cx.emit(Event::PaneRemoved);
3570    }
3571
3572    pub fn panes(&self) -> &[Entity<Pane>] {
3573        &self.panes
3574    }
3575
3576    pub fn active_pane(&self) -> &Entity<Pane> {
3577        &self.active_pane
3578    }
3579
3580    pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> {
3581        for dock in self.all_docks() {
3582            if dock.focus_handle(cx).contains_focused(window, cx) {
3583                if let Some(pane) = dock
3584                    .read(cx)
3585                    .active_panel()
3586                    .and_then(|panel| panel.pane(cx))
3587                {
3588                    return pane;
3589                }
3590            }
3591        }
3592        self.active_pane().clone()
3593    }
3594
3595    pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
3596        self.find_pane_in_direction(SplitDirection::Right, cx)
3597            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3598            .unwrap_or_else(|| {
3599                self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx)
3600            })
3601            .clone()
3602    }
3603
3604    pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> {
3605        let weak_pane = self.panes_by_item.get(&handle.item_id())?;
3606        weak_pane.upgrade()
3607    }
3608
3609    fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
3610        self.follower_states.retain(|leader_id, state| {
3611            if *leader_id == peer_id {
3612                for item in state.items_by_leader_view_id.values() {
3613                    item.view.set_leader_peer_id(None, window, cx);
3614                }
3615                false
3616            } else {
3617                true
3618            }
3619        });
3620        cx.notify();
3621    }
3622
3623    pub fn start_following(
3624        &mut self,
3625        leader_id: PeerId,
3626        window: &mut Window,
3627        cx: &mut Context<Self>,
3628    ) -> Option<Task<Result<()>>> {
3629        let pane = self.active_pane().clone();
3630
3631        self.last_leaders_by_pane
3632            .insert(pane.downgrade(), leader_id);
3633        self.unfollow(leader_id, window, cx);
3634        self.unfollow_in_pane(&pane, window, cx);
3635        self.follower_states.insert(
3636            leader_id,
3637            FollowerState {
3638                center_pane: pane.clone(),
3639                dock_pane: None,
3640                active_view_id: None,
3641                items_by_leader_view_id: Default::default(),
3642            },
3643        );
3644        cx.notify();
3645
3646        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3647        let project_id = self.project.read(cx).remote_id();
3648        let request = self.app_state.client.request(proto::Follow {
3649            room_id,
3650            project_id,
3651            leader_id: Some(leader_id),
3652        });
3653
3654        Some(cx.spawn_in(window, |this, mut cx| async move {
3655            let response = request.await?;
3656            this.update(&mut cx, |this, _| {
3657                let state = this
3658                    .follower_states
3659                    .get_mut(&leader_id)
3660                    .ok_or_else(|| anyhow!("following interrupted"))?;
3661                state.active_view_id = response
3662                    .active_view
3663                    .as_ref()
3664                    .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3665                Ok::<_, anyhow::Error>(())
3666            })??;
3667            if let Some(view) = response.active_view {
3668                Self::add_view_from_leader(this.clone(), leader_id, &view, &mut cx).await?;
3669            }
3670            this.update_in(&mut cx, |this, window, cx| {
3671                this.leader_updated(leader_id, window, cx)
3672            })?;
3673            Ok(())
3674        }))
3675    }
3676
3677    pub fn follow_next_collaborator(
3678        &mut self,
3679        _: &FollowNextCollaborator,
3680        window: &mut Window,
3681        cx: &mut Context<Self>,
3682    ) {
3683        let collaborators = self.project.read(cx).collaborators();
3684        let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
3685            let mut collaborators = collaborators.keys().copied();
3686            for peer_id in collaborators.by_ref() {
3687                if peer_id == leader_id {
3688                    break;
3689                }
3690            }
3691            collaborators.next()
3692        } else if let Some(last_leader_id) =
3693            self.last_leaders_by_pane.get(&self.active_pane.downgrade())
3694        {
3695            if collaborators.contains_key(last_leader_id) {
3696                Some(*last_leader_id)
3697            } else {
3698                None
3699            }
3700        } else {
3701            None
3702        };
3703
3704        let pane = self.active_pane.clone();
3705        let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
3706        else {
3707            return;
3708        };
3709        if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
3710            return;
3711        }
3712        if let Some(task) = self.start_following(leader_id, window, cx) {
3713            task.detach_and_log_err(cx)
3714        }
3715    }
3716
3717    pub fn follow(&mut self, leader_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
3718        let Some(room) = ActiveCall::global(cx).read(cx).room() else {
3719            return;
3720        };
3721        let room = room.read(cx);
3722        let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
3723            return;
3724        };
3725
3726        let project = self.project.read(cx);
3727
3728        let other_project_id = match remote_participant.location {
3729            call::ParticipantLocation::External => None,
3730            call::ParticipantLocation::UnsharedProject => None,
3731            call::ParticipantLocation::SharedProject { project_id } => {
3732                if Some(project_id) == project.remote_id() {
3733                    None
3734                } else {
3735                    Some(project_id)
3736                }
3737            }
3738        };
3739
3740        // if they are active in another project, follow there.
3741        if let Some(project_id) = other_project_id {
3742            let app_state = self.app_state.clone();
3743            crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
3744                .detach_and_log_err(cx);
3745        }
3746
3747        // if you're already following, find the right pane and focus it.
3748        if let Some(follower_state) = self.follower_states.get(&leader_id) {
3749            window.focus(&follower_state.pane().focus_handle(cx));
3750
3751            return;
3752        }
3753
3754        // Otherwise, follow.
3755        if let Some(task) = self.start_following(leader_id, window, cx) {
3756            task.detach_and_log_err(cx)
3757        }
3758    }
3759
3760    pub fn unfollow(
3761        &mut self,
3762        leader_id: PeerId,
3763        window: &mut Window,
3764        cx: &mut Context<Self>,
3765    ) -> Option<()> {
3766        cx.notify();
3767        let state = self.follower_states.remove(&leader_id)?;
3768        for (_, item) in state.items_by_leader_view_id {
3769            item.view.set_leader_peer_id(None, window, cx);
3770        }
3771
3772        let project_id = self.project.read(cx).remote_id();
3773        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3774        self.app_state
3775            .client
3776            .send(proto::Unfollow {
3777                room_id,
3778                project_id,
3779                leader_id: Some(leader_id),
3780            })
3781            .log_err();
3782
3783        Some(())
3784    }
3785
3786    pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
3787        self.follower_states.contains_key(&peer_id)
3788    }
3789
3790    fn active_item_path_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3791        cx.emit(Event::ActiveItemChanged);
3792        let active_entry = self.active_project_path(cx);
3793        self.project
3794            .update(cx, |project, cx| project.set_active_path(active_entry, cx));
3795
3796        self.update_window_title(window, cx);
3797    }
3798
3799    fn update_window_title(&mut self, window: &mut Window, cx: &mut App) {
3800        let project = self.project().read(cx);
3801        let mut title = String::new();
3802
3803        for (i, name) in project.worktree_root_names(cx).enumerate() {
3804            if i > 0 {
3805                title.push_str(", ");
3806            }
3807            title.push_str(name);
3808        }
3809
3810        if title.is_empty() {
3811            title = "empty project".to_string();
3812        }
3813
3814        if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
3815            let filename = path
3816                .path
3817                .file_name()
3818                .map(|s| s.to_string_lossy())
3819                .or_else(|| {
3820                    Some(Cow::Borrowed(
3821                        project
3822                            .worktree_for_id(path.worktree_id, cx)?
3823                            .read(cx)
3824                            .root_name(),
3825                    ))
3826                });
3827
3828            if let Some(filename) = filename {
3829                title.push_str("");
3830                title.push_str(filename.as_ref());
3831            }
3832        }
3833
3834        if project.is_via_collab() {
3835            title.push_str("");
3836        } else if project.is_shared() {
3837            title.push_str("");
3838        }
3839
3840        window.set_window_title(&title);
3841    }
3842
3843    fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
3844        let is_edited = !self.project.read(cx).is_disconnected(cx)
3845            && self
3846                .items(cx)
3847                .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
3848        if is_edited != self.window_edited {
3849            self.window_edited = is_edited;
3850            window.set_window_edited(self.window_edited)
3851        }
3852    }
3853
3854    fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
3855        if self.notifications.is_empty() {
3856            None
3857        } else {
3858            Some(
3859                div()
3860                    .absolute()
3861                    .right_3()
3862                    .bottom_3()
3863                    .w_112()
3864                    .h_full()
3865                    .flex()
3866                    .flex_col()
3867                    .justify_end()
3868                    .gap_2()
3869                    .children(
3870                        self.notifications
3871                            .iter()
3872                            .map(|(_, notification)| notification.clone().into_any()),
3873                    ),
3874            )
3875        }
3876    }
3877
3878    // RPC handlers
3879
3880    fn active_view_for_follower(
3881        &self,
3882        follower_project_id: Option<u64>,
3883        window: &mut Window,
3884        cx: &mut Context<Self>,
3885    ) -> Option<proto::View> {
3886        let (item, panel_id) = self.active_item_for_followers(window, cx);
3887        let item = item?;
3888        let leader_id = self
3889            .pane_for(&*item)
3890            .and_then(|pane| self.leader_for_pane(&pane));
3891
3892        let item_handle = item.to_followable_item_handle(cx)?;
3893        let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
3894        let variant = item_handle.to_state_proto(window, cx)?;
3895
3896        if item_handle.is_project_item(window, cx)
3897            && (follower_project_id.is_none()
3898                || follower_project_id != self.project.read(cx).remote_id())
3899        {
3900            return None;
3901        }
3902
3903        Some(proto::View {
3904            id: Some(id.to_proto()),
3905            leader_id,
3906            variant: Some(variant),
3907            panel_id: panel_id.map(|id| id as i32),
3908        })
3909    }
3910
3911    fn handle_follow(
3912        &mut self,
3913        follower_project_id: Option<u64>,
3914        window: &mut Window,
3915        cx: &mut Context<Self>,
3916    ) -> proto::FollowResponse {
3917        let active_view = self.active_view_for_follower(follower_project_id, window, cx);
3918
3919        cx.notify();
3920        proto::FollowResponse {
3921            // TODO: Remove after version 0.145.x stabilizes.
3922            active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
3923            views: active_view.iter().cloned().collect(),
3924            active_view,
3925        }
3926    }
3927
3928    fn handle_update_followers(
3929        &mut self,
3930        leader_id: PeerId,
3931        message: proto::UpdateFollowers,
3932        _window: &mut Window,
3933        _cx: &mut Context<Self>,
3934    ) {
3935        self.leader_updates_tx
3936            .unbounded_send((leader_id, message))
3937            .ok();
3938    }
3939
3940    async fn process_leader_update(
3941        this: &WeakEntity<Self>,
3942        leader_id: PeerId,
3943        update: proto::UpdateFollowers,
3944        cx: &mut AsyncWindowContext,
3945    ) -> Result<()> {
3946        match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
3947            proto::update_followers::Variant::CreateView(view) => {
3948                let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
3949                let should_add_view = this.update(cx, |this, _| {
3950                    if let Some(state) = this.follower_states.get_mut(&leader_id) {
3951                        anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
3952                    } else {
3953                        anyhow::Ok(false)
3954                    }
3955                })??;
3956
3957                if should_add_view {
3958                    Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3959                }
3960            }
3961            proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
3962                let should_add_view = this.update(cx, |this, _| {
3963                    if let Some(state) = this.follower_states.get_mut(&leader_id) {
3964                        state.active_view_id = update_active_view
3965                            .view
3966                            .as_ref()
3967                            .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3968
3969                        if state.active_view_id.is_some_and(|view_id| {
3970                            !state.items_by_leader_view_id.contains_key(&view_id)
3971                        }) {
3972                            anyhow::Ok(true)
3973                        } else {
3974                            anyhow::Ok(false)
3975                        }
3976                    } else {
3977                        anyhow::Ok(false)
3978                    }
3979                })??;
3980
3981                if should_add_view {
3982                    if let Some(view) = update_active_view.view {
3983                        Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3984                    }
3985                }
3986            }
3987            proto::update_followers::Variant::UpdateView(update_view) => {
3988                let variant = update_view
3989                    .variant
3990                    .ok_or_else(|| anyhow!("missing update view variant"))?;
3991                let id = update_view
3992                    .id
3993                    .ok_or_else(|| anyhow!("missing update view id"))?;
3994                let mut tasks = Vec::new();
3995                this.update_in(cx, |this, window, cx| {
3996                    let project = this.project.clone();
3997                    if let Some(state) = this.follower_states.get(&leader_id) {
3998                        let view_id = ViewId::from_proto(id.clone())?;
3999                        if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
4000                            tasks.push(item.view.apply_update_proto(
4001                                &project,
4002                                variant.clone(),
4003                                window,
4004                                cx,
4005                            ));
4006                        }
4007                    }
4008                    anyhow::Ok(())
4009                })??;
4010                try_join_all(tasks).await.log_err();
4011            }
4012        }
4013        this.update_in(cx, |this, window, cx| {
4014            this.leader_updated(leader_id, window, cx)
4015        })?;
4016        Ok(())
4017    }
4018
4019    async fn add_view_from_leader(
4020        this: WeakEntity<Self>,
4021        leader_id: PeerId,
4022        view: &proto::View,
4023        cx: &mut AsyncWindowContext,
4024    ) -> Result<()> {
4025        let this = this.upgrade().context("workspace dropped")?;
4026
4027        let Some(id) = view.id.clone() else {
4028            return Err(anyhow!("no id for view"));
4029        };
4030        let id = ViewId::from_proto(id)?;
4031        let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
4032
4033        let pane = this.update(cx, |this, _cx| {
4034            let state = this
4035                .follower_states
4036                .get(&leader_id)
4037                .context("stopped following")?;
4038            anyhow::Ok(state.pane().clone())
4039        })??;
4040        let existing_item = pane.update_in(cx, |pane, window, cx| {
4041            let client = this.read(cx).client().clone();
4042            pane.items().find_map(|item| {
4043                let item = item.to_followable_item_handle(cx)?;
4044                if item.remote_id(&client, window, cx) == Some(id) {
4045                    Some(item)
4046                } else {
4047                    None
4048                }
4049            })
4050        })?;
4051        let item = if let Some(existing_item) = existing_item {
4052            existing_item
4053        } else {
4054            let variant = view.variant.clone();
4055            if variant.is_none() {
4056                Err(anyhow!("missing view variant"))?;
4057            }
4058
4059            let task = cx.update(|window, cx| {
4060                FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
4061            })?;
4062
4063            let Some(task) = task else {
4064                return Err(anyhow!(
4065                    "failed to construct view from leader (maybe from a different version of zed?)"
4066                ));
4067            };
4068
4069            let mut new_item = task.await?;
4070            pane.update_in(cx, |pane, window, cx| {
4071                let mut item_to_remove = None;
4072                for (ix, item) in pane.items().enumerate() {
4073                    if let Some(item) = item.to_followable_item_handle(cx) {
4074                        match new_item.dedup(item.as_ref(), window, cx) {
4075                            Some(item::Dedup::KeepExisting) => {
4076                                new_item =
4077                                    item.boxed_clone().to_followable_item_handle(cx).unwrap();
4078                                break;
4079                            }
4080                            Some(item::Dedup::ReplaceExisting) => {
4081                                item_to_remove = Some((ix, item.item_id()));
4082                                break;
4083                            }
4084                            None => {}
4085                        }
4086                    }
4087                }
4088
4089                if let Some((ix, id)) = item_to_remove {
4090                    pane.remove_item(id, false, false, window, cx);
4091                    pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
4092                }
4093            })?;
4094
4095            new_item
4096        };
4097
4098        this.update_in(cx, |this, window, cx| {
4099            let state = this.follower_states.get_mut(&leader_id)?;
4100            item.set_leader_peer_id(Some(leader_id), window, cx);
4101            state.items_by_leader_view_id.insert(
4102                id,
4103                FollowerView {
4104                    view: item,
4105                    location: panel_id,
4106                },
4107            );
4108
4109            Some(())
4110        })?;
4111
4112        Ok(())
4113    }
4114
4115    pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
4116        let mut is_project_item = true;
4117        let mut update = proto::UpdateActiveView::default();
4118        if window.is_window_active() {
4119            let (active_item, panel_id) = self.active_item_for_followers(window, cx);
4120
4121            if let Some(item) = active_item {
4122                if item.item_focus_handle(cx).contains_focused(window, cx) {
4123                    let leader_id = self
4124                        .pane_for(&*item)
4125                        .and_then(|pane| self.leader_for_pane(&pane));
4126
4127                    if let Some(item) = item.to_followable_item_handle(cx) {
4128                        let id = item
4129                            .remote_id(&self.app_state.client, window, cx)
4130                            .map(|id| id.to_proto());
4131
4132                        if let Some(id) = id.clone() {
4133                            if let Some(variant) = item.to_state_proto(window, cx) {
4134                                let view = Some(proto::View {
4135                                    id: Some(id.clone()),
4136                                    leader_id,
4137                                    variant: Some(variant),
4138                                    panel_id: panel_id.map(|id| id as i32),
4139                                });
4140
4141                                is_project_item = item.is_project_item(window, cx);
4142                                update = proto::UpdateActiveView {
4143                                    view,
4144                                    // TODO: Remove after version 0.145.x stabilizes.
4145                                    id: Some(id.clone()),
4146                                    leader_id,
4147                                };
4148                            }
4149                        };
4150                    }
4151                }
4152            }
4153        }
4154
4155        let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
4156        if active_view_id != self.last_active_view_id.as_ref() {
4157            self.last_active_view_id = active_view_id.cloned();
4158            self.update_followers(
4159                is_project_item,
4160                proto::update_followers::Variant::UpdateActiveView(update),
4161                window,
4162                cx,
4163            );
4164        }
4165    }
4166
4167    fn active_item_for_followers(
4168        &self,
4169        window: &mut Window,
4170        cx: &mut App,
4171    ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
4172        let mut active_item = None;
4173        let mut panel_id = None;
4174        for dock in self.all_docks() {
4175            if dock.focus_handle(cx).contains_focused(window, cx) {
4176                if let Some(panel) = dock.read(cx).active_panel() {
4177                    if let Some(pane) = panel.pane(cx) {
4178                        if let Some(item) = pane.read(cx).active_item() {
4179                            active_item = Some(item);
4180                            panel_id = panel.remote_id();
4181                            break;
4182                        }
4183                    }
4184                }
4185            }
4186        }
4187
4188        if active_item.is_none() {
4189            active_item = self.active_pane().read(cx).active_item();
4190        }
4191        (active_item, panel_id)
4192    }
4193
4194    fn update_followers(
4195        &self,
4196        project_only: bool,
4197        update: proto::update_followers::Variant,
4198        _: &mut Window,
4199        cx: &mut App,
4200    ) -> Option<()> {
4201        // If this update only applies to for followers in the current project,
4202        // then skip it unless this project is shared. If it applies to all
4203        // followers, regardless of project, then set `project_id` to none,
4204        // indicating that it goes to all followers.
4205        let project_id = if project_only {
4206            Some(self.project.read(cx).remote_id()?)
4207        } else {
4208            None
4209        };
4210        self.app_state().workspace_store.update(cx, |store, cx| {
4211            store.update_followers(project_id, update, cx)
4212        })
4213    }
4214
4215    pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<PeerId> {
4216        self.follower_states.iter().find_map(|(leader_id, state)| {
4217            if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
4218                Some(*leader_id)
4219            } else {
4220                None
4221            }
4222        })
4223    }
4224
4225    fn leader_updated(
4226        &mut self,
4227        leader_id: PeerId,
4228        window: &mut Window,
4229        cx: &mut Context<Self>,
4230    ) -> Option<()> {
4231        cx.notify();
4232
4233        let call = self.active_call()?;
4234        let room = call.read(cx).room()?.read(cx);
4235        let participant = room.remote_participant_for_peer_id(leader_id)?;
4236
4237        let leader_in_this_app;
4238        let leader_in_this_project;
4239        match participant.location {
4240            call::ParticipantLocation::SharedProject { project_id } => {
4241                leader_in_this_app = true;
4242                leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
4243            }
4244            call::ParticipantLocation::UnsharedProject => {
4245                leader_in_this_app = true;
4246                leader_in_this_project = false;
4247            }
4248            call::ParticipantLocation::External => {
4249                leader_in_this_app = false;
4250                leader_in_this_project = false;
4251            }
4252        };
4253
4254        let state = self.follower_states.get(&leader_id)?;
4255        let mut item_to_activate = None;
4256        if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
4257            if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
4258                if leader_in_this_project || !item.view.is_project_item(window, cx) {
4259                    item_to_activate = Some((item.location, item.view.boxed_clone()));
4260                }
4261            }
4262        } else if let Some(shared_screen) =
4263            self.shared_screen_for_peer(leader_id, &state.center_pane, window, cx)
4264        {
4265            item_to_activate = Some((None, Box::new(shared_screen)));
4266        }
4267
4268        let (panel_id, item) = item_to_activate?;
4269
4270        let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
4271        let pane;
4272        if let Some(panel_id) = panel_id {
4273            pane = self
4274                .activate_panel_for_proto_id(panel_id, window, cx)?
4275                .pane(cx)?;
4276            let state = self.follower_states.get_mut(&leader_id)?;
4277            state.dock_pane = Some(pane.clone());
4278        } else {
4279            pane = state.center_pane.clone();
4280            let state = self.follower_states.get_mut(&leader_id)?;
4281            if let Some(dock_pane) = state.dock_pane.take() {
4282                transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
4283            }
4284        }
4285
4286        pane.update(cx, |pane, cx| {
4287            let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
4288            if let Some(index) = pane.index_for_item(item.as_ref()) {
4289                pane.activate_item(index, false, false, window, cx);
4290            } else {
4291                pane.add_item(item.boxed_clone(), false, false, None, window, cx)
4292            }
4293
4294            if focus_active_item {
4295                pane.focus_active_item(window, cx)
4296            }
4297        });
4298
4299        None
4300    }
4301
4302    #[cfg(target_os = "windows")]
4303    fn shared_screen_for_peer(
4304        &self,
4305        _peer_id: PeerId,
4306        _pane: &Entity<Pane>,
4307        _window: &mut Window,
4308        _cx: &mut App,
4309    ) -> Option<Entity<SharedScreen>> {
4310        None
4311    }
4312
4313    #[cfg(not(target_os = "windows"))]
4314    fn shared_screen_for_peer(
4315        &self,
4316        peer_id: PeerId,
4317        pane: &Entity<Pane>,
4318        window: &mut Window,
4319        cx: &mut App,
4320    ) -> Option<Entity<SharedScreen>> {
4321        let call = self.active_call()?;
4322        let room = call.read(cx).room()?.read(cx);
4323        let participant = room.remote_participant_for_peer_id(peer_id)?;
4324        let track = participant.video_tracks.values().next()?.clone();
4325        let user = participant.user.clone();
4326
4327        for item in pane.read(cx).items_of_type::<SharedScreen>() {
4328            if item.read(cx).peer_id == peer_id {
4329                return Some(item);
4330            }
4331        }
4332
4333        Some(cx.new(|cx| SharedScreen::new(track, peer_id, user.clone(), window, cx)))
4334    }
4335
4336    pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4337        if window.is_window_active() {
4338            self.update_active_view_for_followers(window, cx);
4339
4340            if let Some(database_id) = self.database_id {
4341                cx.background_spawn(persistence::DB.update_timestamp(database_id))
4342                    .detach();
4343            }
4344        } else {
4345            for pane in &self.panes {
4346                pane.update(cx, |pane, cx| {
4347                    if let Some(item) = pane.active_item() {
4348                        item.workspace_deactivated(window, cx);
4349                    }
4350                    for item in pane.items() {
4351                        if matches!(
4352                            item.workspace_settings(cx).autosave,
4353                            AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
4354                        ) {
4355                            Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
4356                                .detach_and_log_err(cx);
4357                        }
4358                    }
4359                });
4360            }
4361        }
4362    }
4363
4364    pub fn active_call(&self) -> Option<&Entity<ActiveCall>> {
4365        self.active_call.as_ref().map(|(call, _)| call)
4366    }
4367
4368    fn on_active_call_event(
4369        &mut self,
4370        _: &Entity<ActiveCall>,
4371        event: &call::room::Event,
4372        window: &mut Window,
4373        cx: &mut Context<Self>,
4374    ) {
4375        match event {
4376            call::room::Event::ParticipantLocationChanged { participant_id }
4377            | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4378                self.leader_updated(*participant_id, window, cx);
4379            }
4380            _ => {}
4381        }
4382    }
4383
4384    pub fn database_id(&self) -> Option<WorkspaceId> {
4385        self.database_id
4386    }
4387
4388    fn local_paths(&self, cx: &App) -> Option<Vec<Arc<Path>>> {
4389        let project = self.project().read(cx);
4390
4391        if project.is_local() {
4392            Some(
4393                project
4394                    .visible_worktrees(cx)
4395                    .map(|worktree| worktree.read(cx).abs_path())
4396                    .collect::<Vec<_>>(),
4397            )
4398        } else {
4399            None
4400        }
4401    }
4402
4403    fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
4404        match member {
4405            Member::Axis(PaneAxis { members, .. }) => {
4406                for child in members.iter() {
4407                    self.remove_panes(child.clone(), window, cx)
4408                }
4409            }
4410            Member::Pane(pane) => {
4411                self.force_remove_pane(&pane, &None, window, cx);
4412            }
4413        }
4414    }
4415
4416    fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
4417        self.session_id.take();
4418        self.serialize_workspace_internal(window, cx)
4419    }
4420
4421    fn force_remove_pane(
4422        &mut self,
4423        pane: &Entity<Pane>,
4424        focus_on: &Option<Entity<Pane>>,
4425        window: &mut Window,
4426        cx: &mut Context<Workspace>,
4427    ) {
4428        self.panes.retain(|p| p != pane);
4429        if let Some(focus_on) = focus_on {
4430            focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4431        } else {
4432            if self.active_pane() == pane {
4433                self.panes
4434                    .last()
4435                    .unwrap()
4436                    .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4437            }
4438        }
4439        if self.last_active_center_pane == Some(pane.downgrade()) {
4440            self.last_active_center_pane = None;
4441        }
4442        cx.notify();
4443    }
4444
4445    fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4446        if self._schedule_serialize.is_none() {
4447            self._schedule_serialize = Some(cx.spawn_in(window, |this, mut cx| async move {
4448                cx.background_executor()
4449                    .timer(Duration::from_millis(100))
4450                    .await;
4451                this.update_in(&mut cx, |this, window, cx| {
4452                    this.serialize_workspace_internal(window, cx).detach();
4453                    this._schedule_serialize.take();
4454                })
4455                .log_err();
4456            }));
4457        }
4458    }
4459
4460    fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
4461        let Some(database_id) = self.database_id() else {
4462            return Task::ready(());
4463        };
4464
4465        fn serialize_pane_handle(
4466            pane_handle: &Entity<Pane>,
4467            window: &mut Window,
4468            cx: &mut App,
4469        ) -> SerializedPane {
4470            let (items, active, pinned_count) = {
4471                let pane = pane_handle.read(cx);
4472                let active_item_id = pane.active_item().map(|item| item.item_id());
4473                (
4474                    pane.items()
4475                        .filter_map(|handle| {
4476                            let handle = handle.to_serializable_item_handle(cx)?;
4477
4478                            Some(SerializedItem {
4479                                kind: Arc::from(handle.serialized_item_kind()),
4480                                item_id: handle.item_id().as_u64(),
4481                                active: Some(handle.item_id()) == active_item_id,
4482                                preview: pane.is_active_preview_item(handle.item_id()),
4483                            })
4484                        })
4485                        .collect::<Vec<_>>(),
4486                    pane.has_focus(window, cx),
4487                    pane.pinned_count(),
4488                )
4489            };
4490
4491            SerializedPane::new(items, active, pinned_count)
4492        }
4493
4494        fn build_serialized_pane_group(
4495            pane_group: &Member,
4496            window: &mut Window,
4497            cx: &mut App,
4498        ) -> SerializedPaneGroup {
4499            match pane_group {
4500                Member::Axis(PaneAxis {
4501                    axis,
4502                    members,
4503                    flexes,
4504                    bounding_boxes: _,
4505                }) => SerializedPaneGroup::Group {
4506                    axis: SerializedAxis(*axis),
4507                    children: members
4508                        .iter()
4509                        .map(|member| build_serialized_pane_group(member, window, cx))
4510                        .collect::<Vec<_>>(),
4511                    flexes: Some(flexes.lock().clone()),
4512                },
4513                Member::Pane(pane_handle) => {
4514                    SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
4515                }
4516            }
4517        }
4518
4519        fn build_serialized_docks(
4520            this: &Workspace,
4521            window: &mut Window,
4522            cx: &mut App,
4523        ) -> DockStructure {
4524            let left_dock = this.left_dock.read(cx);
4525            let left_visible = left_dock.is_open();
4526            let left_active_panel = left_dock
4527                .active_panel()
4528                .map(|panel| panel.persistent_name().to_string());
4529            let left_dock_zoom = left_dock
4530                .active_panel()
4531                .map(|panel| panel.is_zoomed(window, cx))
4532                .unwrap_or(false);
4533
4534            let right_dock = this.right_dock.read(cx);
4535            let right_visible = right_dock.is_open();
4536            let right_active_panel = right_dock
4537                .active_panel()
4538                .map(|panel| panel.persistent_name().to_string());
4539            let right_dock_zoom = right_dock
4540                .active_panel()
4541                .map(|panel| panel.is_zoomed(window, cx))
4542                .unwrap_or(false);
4543
4544            let bottom_dock = this.bottom_dock.read(cx);
4545            let bottom_visible = bottom_dock.is_open();
4546            let bottom_active_panel = bottom_dock
4547                .active_panel()
4548                .map(|panel| panel.persistent_name().to_string());
4549            let bottom_dock_zoom = bottom_dock
4550                .active_panel()
4551                .map(|panel| panel.is_zoomed(window, cx))
4552                .unwrap_or(false);
4553
4554            DockStructure {
4555                left: DockData {
4556                    visible: left_visible,
4557                    active_panel: left_active_panel,
4558                    zoom: left_dock_zoom,
4559                },
4560                right: DockData {
4561                    visible: right_visible,
4562                    active_panel: right_active_panel,
4563                    zoom: right_dock_zoom,
4564                },
4565                bottom: DockData {
4566                    visible: bottom_visible,
4567                    active_panel: bottom_active_panel,
4568                    zoom: bottom_dock_zoom,
4569                },
4570            }
4571        }
4572
4573        let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4574            Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4575        } else if let Some(local_paths) = self.local_paths(cx) {
4576            if !local_paths.is_empty() {
4577                Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4578            } else {
4579                None
4580            }
4581        } else {
4582            None
4583        };
4584
4585        if let Some(location) = location {
4586            let center_group = build_serialized_pane_group(&self.center.root, window, cx);
4587            let docks = build_serialized_docks(self, window, cx);
4588            let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
4589            let serialized_workspace = SerializedWorkspace {
4590                id: database_id,
4591                location,
4592                center_group,
4593                window_bounds,
4594                display: Default::default(),
4595                docks,
4596                centered_layout: self.centered_layout,
4597                session_id: self.session_id.clone(),
4598                window_id: Some(window.window_handle().window_id().as_u64()),
4599            };
4600            return window.spawn(cx, |_| persistence::DB.save_workspace(serialized_workspace));
4601        }
4602        Task::ready(())
4603    }
4604
4605    async fn serialize_items(
4606        this: &WeakEntity<Self>,
4607        items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4608        cx: &mut AsyncWindowContext,
4609    ) -> Result<()> {
4610        const CHUNK_SIZE: usize = 200;
4611
4612        let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4613
4614        while let Some(items_received) = serializable_items.next().await {
4615            let unique_items =
4616                items_received
4617                    .into_iter()
4618                    .fold(HashMap::default(), |mut acc, item| {
4619                        acc.entry(item.item_id()).or_insert(item);
4620                        acc
4621                    });
4622
4623            // We use into_iter() here so that the references to the items are moved into
4624            // the tasks and not kept alive while we're sleeping.
4625            for (_, item) in unique_items.into_iter() {
4626                if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
4627                    item.serialize(workspace, false, window, cx)
4628                }) {
4629                    cx.background_spawn(async move { task.await.log_err() })
4630                        .detach();
4631                }
4632            }
4633
4634            cx.background_executor()
4635                .timer(SERIALIZATION_THROTTLE_TIME)
4636                .await;
4637        }
4638
4639        Ok(())
4640    }
4641
4642    pub(crate) fn enqueue_item_serialization(
4643        &mut self,
4644        item: Box<dyn SerializableItemHandle>,
4645    ) -> Result<()> {
4646        self.serializable_items_tx
4647            .unbounded_send(item)
4648            .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4649    }
4650
4651    pub(crate) fn load_workspace(
4652        serialized_workspace: SerializedWorkspace,
4653        paths_to_open: Vec<Option<ProjectPath>>,
4654        window: &mut Window,
4655        cx: &mut Context<Workspace>,
4656    ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4657        cx.spawn_in(window, |workspace, mut cx| async move {
4658            let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4659
4660            let mut center_group = None;
4661            let mut center_items = None;
4662
4663            // Traverse the splits tree and add to things
4664            if let Some((group, active_pane, items)) = serialized_workspace
4665                .center_group
4666                .deserialize(
4667                    &project,
4668                    serialized_workspace.id,
4669                    workspace.clone(),
4670                    &mut cx,
4671                )
4672                .await
4673            {
4674                center_items = Some(items);
4675                center_group = Some((group, active_pane))
4676            }
4677
4678            let mut items_by_project_path = HashMap::default();
4679            let mut item_ids_by_kind = HashMap::default();
4680            let mut all_deserialized_items = Vec::default();
4681            cx.update(|_, cx| {
4682                for item in center_items.unwrap_or_default().into_iter().flatten() {
4683                    if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4684                        item_ids_by_kind
4685                            .entry(serializable_item_handle.serialized_item_kind())
4686                            .or_insert(Vec::new())
4687                            .push(item.item_id().as_u64() as ItemId);
4688                    }
4689
4690                    if let Some(project_path) = item.project_path(cx) {
4691                        items_by_project_path.insert(project_path, item.clone());
4692                    }
4693                    all_deserialized_items.push(item);
4694                }
4695            })?;
4696
4697            let opened_items = paths_to_open
4698                .into_iter()
4699                .map(|path_to_open| {
4700                    path_to_open
4701                        .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4702                })
4703                .collect::<Vec<_>>();
4704
4705            // Remove old panes from workspace panes list
4706            workspace.update_in(&mut cx, |workspace, window, cx| {
4707                if let Some((center_group, active_pane)) = center_group {
4708                    workspace.remove_panes(workspace.center.root.clone(), window, cx);
4709
4710                    // Swap workspace center group
4711                    workspace.center = PaneGroup::with_root(center_group);
4712                    if let Some(active_pane) = active_pane {
4713                        workspace.set_active_pane(&active_pane, window, cx);
4714                        cx.focus_self(window);
4715                    } else {
4716                        workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
4717                    }
4718                }
4719
4720                let docks = serialized_workspace.docks;
4721
4722                for (dock, serialized_dock) in [
4723                    (&mut workspace.right_dock, docks.right),
4724                    (&mut workspace.left_dock, docks.left),
4725                    (&mut workspace.bottom_dock, docks.bottom),
4726                ]
4727                .iter_mut()
4728                {
4729                    dock.update(cx, |dock, cx| {
4730                        dock.serialized_dock = Some(serialized_dock.clone());
4731                        dock.restore_state(window, cx);
4732                    });
4733                }
4734
4735                cx.notify();
4736            })?;
4737
4738            // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4739            // after loading the items, we might have different items and in order to avoid
4740            // the database filling up, we delete items that haven't been loaded now.
4741            //
4742            // The items that have been loaded, have been saved after they've been added to the workspace.
4743            let clean_up_tasks = workspace.update_in(&mut cx, |_, window, cx| {
4744                item_ids_by_kind
4745                    .into_iter()
4746                    .map(|(item_kind, loaded_items)| {
4747                        SerializableItemRegistry::cleanup(
4748                            item_kind,
4749                            serialized_workspace.id,
4750                            loaded_items,
4751                            window,
4752                            cx,
4753                        )
4754                        .log_err()
4755                    })
4756                    .collect::<Vec<_>>()
4757            })?;
4758
4759            futures::future::join_all(clean_up_tasks).await;
4760
4761            workspace
4762                .update_in(&mut cx, |workspace, window, cx| {
4763                    // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4764                    workspace.serialize_workspace_internal(window, cx).detach();
4765
4766                    // Ensure that we mark the window as edited if we did load dirty items
4767                    workspace.update_window_edited(window, cx);
4768                })
4769                .ok();
4770
4771            Ok(opened_items)
4772        })
4773    }
4774
4775    fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
4776        self.add_workspace_actions_listeners(div, window, cx)
4777            .on_action(cx.listener(Self::close_inactive_items_and_panes))
4778            .on_action(cx.listener(Self::close_all_items_and_panes))
4779            .on_action(cx.listener(Self::save_all))
4780            .on_action(cx.listener(Self::send_keystrokes))
4781            .on_action(cx.listener(Self::add_folder_to_project))
4782            .on_action(cx.listener(Self::follow_next_collaborator))
4783            .on_action(cx.listener(Self::close_window))
4784            .on_action(cx.listener(Self::activate_pane_at_index))
4785            .on_action(cx.listener(Self::move_item_to_pane_at_index))
4786            .on_action(cx.listener(Self::move_focused_panel_to_next_position))
4787            .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
4788                let pane = workspace.active_pane().clone();
4789                workspace.unfollow_in_pane(&pane, window, cx);
4790            }))
4791            .on_action(cx.listener(|workspace, action: &Save, window, cx| {
4792                workspace
4793                    .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
4794                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4795            }))
4796            .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
4797                workspace
4798                    .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
4799                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4800            }))
4801            .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
4802                workspace
4803                    .save_active_item(SaveIntent::SaveAs, window, cx)
4804                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4805            }))
4806            .on_action(
4807                cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
4808                    workspace.activate_previous_pane(window, cx)
4809                }),
4810            )
4811            .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4812                workspace.activate_next_pane(window, cx)
4813            }))
4814            .on_action(
4815                cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
4816                    workspace.activate_next_window(cx)
4817                }),
4818            )
4819            .on_action(
4820                cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
4821                    workspace.activate_previous_window(cx)
4822                }),
4823            )
4824            .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
4825                workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
4826            }))
4827            .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
4828                workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
4829            }))
4830            .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
4831                workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
4832            }))
4833            .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
4834                workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
4835            }))
4836            .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4837                workspace.activate_next_pane(window, cx)
4838            }))
4839            .on_action(cx.listener(
4840                |workspace, action: &MoveItemToPaneInDirection, window, cx| {
4841                    workspace.move_item_to_pane_in_direction(action, window, cx)
4842                },
4843            ))
4844            .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
4845                workspace.swap_pane_in_direction(SplitDirection::Left, cx)
4846            }))
4847            .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
4848                workspace.swap_pane_in_direction(SplitDirection::Right, cx)
4849            }))
4850            .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
4851                workspace.swap_pane_in_direction(SplitDirection::Up, cx)
4852            }))
4853            .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
4854                workspace.swap_pane_in_direction(SplitDirection::Down, cx)
4855            }))
4856            .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
4857                this.toggle_dock(DockPosition::Left, window, cx);
4858            }))
4859            .on_action(cx.listener(
4860                |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
4861                    workspace.toggle_dock(DockPosition::Right, window, cx);
4862                },
4863            ))
4864            .on_action(cx.listener(
4865                |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
4866                    workspace.toggle_dock(DockPosition::Bottom, window, cx);
4867                },
4868            ))
4869            .on_action(
4870                cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
4871                    workspace.close_all_docks(window, cx);
4872                }),
4873            )
4874            .on_action(cx.listener(
4875                |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
4876                    workspace.clear_all_notifications(cx);
4877                },
4878            ))
4879            .on_action(cx.listener(
4880                |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
4881                    workspace.reopen_closed_item(window, cx).detach();
4882                },
4883            ))
4884            .on_action(cx.listener(Workspace::toggle_centered_layout))
4885    }
4886
4887    #[cfg(any(test, feature = "test-support"))]
4888    pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
4889        use node_runtime::NodeRuntime;
4890        use session::Session;
4891
4892        let client = project.read(cx).client();
4893        let user_store = project.read(cx).user_store();
4894
4895        let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
4896        let session = cx.new(|cx| AppSession::new(Session::test(), cx));
4897        window.activate_window();
4898        let app_state = Arc::new(AppState {
4899            languages: project.read(cx).languages().clone(),
4900            workspace_store,
4901            client,
4902            user_store,
4903            fs: project.read(cx).fs().clone(),
4904            build_window_options: |_, _| Default::default(),
4905            node_runtime: NodeRuntime::unavailable(),
4906            session,
4907        });
4908        let workspace = Self::new(Default::default(), project, app_state, window, cx);
4909        workspace
4910            .active_pane
4911            .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4912        workspace
4913    }
4914
4915    pub fn register_action<A: Action>(
4916        &mut self,
4917        callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
4918    ) -> &mut Self {
4919        let callback = Arc::new(callback);
4920
4921        self.workspace_actions.push(Box::new(move |div, _, cx| {
4922            let callback = callback.clone();
4923            div.on_action(cx.listener(move |workspace, event, window, cx| {
4924                (callback.clone())(workspace, event, window, cx)
4925            }))
4926        }));
4927        self
4928    }
4929
4930    fn add_workspace_actions_listeners(
4931        &self,
4932        mut div: Div,
4933        window: &mut Window,
4934        cx: &mut Context<Self>,
4935    ) -> Div {
4936        for action in self.workspace_actions.iter() {
4937            div = (action)(div, window, cx)
4938        }
4939        div
4940    }
4941
4942    pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
4943        self.modal_layer.read(cx).has_active_modal()
4944    }
4945
4946    pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
4947        self.modal_layer.read(cx).active_modal()
4948    }
4949
4950    pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
4951    where
4952        B: FnOnce(&mut Window, &mut Context<V>) -> V,
4953    {
4954        self.modal_layer.update(cx, |modal_layer, cx| {
4955            modal_layer.toggle_modal(window, cx, build)
4956        })
4957    }
4958
4959    pub fn toggle_centered_layout(
4960        &mut self,
4961        _: &ToggleCenteredLayout,
4962        _: &mut Window,
4963        cx: &mut Context<Self>,
4964    ) {
4965        self.centered_layout = !self.centered_layout;
4966        if let Some(database_id) = self.database_id() {
4967            cx.background_spawn(DB.set_centered_layout(database_id, self.centered_layout))
4968                .detach_and_log_err(cx);
4969        }
4970        cx.notify();
4971    }
4972
4973    fn adjust_padding(padding: Option<f32>) -> f32 {
4974        padding
4975            .unwrap_or(Self::DEFAULT_PADDING)
4976            .clamp(0.0, Self::MAX_PADDING)
4977    }
4978
4979    fn render_dock(
4980        &self,
4981        position: DockPosition,
4982        dock: &Entity<Dock>,
4983        window: &mut Window,
4984        cx: &mut App,
4985    ) -> Option<Div> {
4986        if self.zoomed_position == Some(position) {
4987            return None;
4988        }
4989
4990        let leader_border = dock.read(cx).active_panel().and_then(|panel| {
4991            let pane = panel.pane(cx)?;
4992            let follower_states = &self.follower_states;
4993            leader_border_for_pane(follower_states, &pane, window, cx)
4994        });
4995
4996        Some(
4997            div()
4998                .flex()
4999                .flex_none()
5000                .overflow_hidden()
5001                .child(dock.clone())
5002                .children(leader_border),
5003        )
5004    }
5005
5006    pub fn for_window(window: &mut Window, _: &mut App) -> Option<Entity<Workspace>> {
5007        window.root().flatten()
5008    }
5009
5010    pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
5011        self.zoomed.as_ref()
5012    }
5013
5014    pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
5015        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5016            return;
5017        };
5018        let windows = cx.windows();
5019        let Some(next_window) = windows
5020            .iter()
5021            .cycle()
5022            .skip_while(|window| window.window_id() != current_window_id)
5023            .nth(1)
5024        else {
5025            return;
5026        };
5027        next_window
5028            .update(cx, |_, window, _| window.activate_window())
5029            .ok();
5030    }
5031
5032    pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
5033        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5034            return;
5035        };
5036        let windows = cx.windows();
5037        let Some(prev_window) = windows
5038            .iter()
5039            .rev()
5040            .cycle()
5041            .skip_while(|window| window.window_id() != current_window_id)
5042            .nth(1)
5043        else {
5044            return;
5045        };
5046        prev_window
5047            .update(cx, |_, window, _| window.activate_window())
5048            .ok();
5049    }
5050}
5051
5052fn leader_border_for_pane(
5053    follower_states: &HashMap<PeerId, FollowerState>,
5054    pane: &Entity<Pane>,
5055    _: &Window,
5056    cx: &App,
5057) -> Option<Div> {
5058    let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
5059        if state.pane() == pane {
5060            Some((*leader_id, state))
5061        } else {
5062            None
5063        }
5064    })?;
5065
5066    let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
5067    let leader = room.remote_participant_for_peer_id(leader_id)?;
5068
5069    let mut leader_color = cx
5070        .theme()
5071        .players()
5072        .color_for_participant(leader.participant_index.0)
5073        .cursor;
5074    leader_color.fade_out(0.3);
5075    Some(
5076        div()
5077            .absolute()
5078            .size_full()
5079            .left_0()
5080            .top_0()
5081            .border_2()
5082            .border_color(leader_color),
5083    )
5084}
5085
5086fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
5087    ZED_WINDOW_POSITION
5088        .zip(*ZED_WINDOW_SIZE)
5089        .map(|(position, size)| Bounds {
5090            origin: position,
5091            size,
5092        })
5093}
5094
5095fn open_items(
5096    serialized_workspace: Option<SerializedWorkspace>,
5097    mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
5098    window: &mut Window,
5099    cx: &mut Context<Workspace>,
5100) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
5101    let restored_items = serialized_workspace.map(|serialized_workspace| {
5102        Workspace::load_workspace(
5103            serialized_workspace,
5104            project_paths_to_open
5105                .iter()
5106                .map(|(_, project_path)| project_path)
5107                .cloned()
5108                .collect(),
5109            window,
5110            cx,
5111        )
5112    });
5113
5114    cx.spawn_in(window, |workspace, mut cx| async move {
5115        let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
5116
5117        if let Some(restored_items) = restored_items {
5118            let restored_items = restored_items.await?;
5119
5120            let restored_project_paths = restored_items
5121                .iter()
5122                .filter_map(|item| {
5123                    cx.update(|_, cx| item.as_ref()?.project_path(cx))
5124                        .ok()
5125                        .flatten()
5126                })
5127                .collect::<HashSet<_>>();
5128
5129            for restored_item in restored_items {
5130                opened_items.push(restored_item.map(Ok));
5131            }
5132
5133            project_paths_to_open
5134                .iter_mut()
5135                .for_each(|(_, project_path)| {
5136                    if let Some(project_path_to_open) = project_path {
5137                        if restored_project_paths.contains(project_path_to_open) {
5138                            *project_path = None;
5139                        }
5140                    }
5141                });
5142        } else {
5143            for _ in 0..project_paths_to_open.len() {
5144                opened_items.push(None);
5145            }
5146        }
5147        assert!(opened_items.len() == project_paths_to_open.len());
5148
5149        let tasks =
5150            project_paths_to_open
5151                .into_iter()
5152                .enumerate()
5153                .map(|(ix, (abs_path, project_path))| {
5154                    let workspace = workspace.clone();
5155                    cx.spawn(|mut cx| async move {
5156                        let file_project_path = project_path?;
5157                        let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
5158                            workspace.project().update(cx, |project, cx| {
5159                                project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
5160                            })
5161                        });
5162
5163                        // We only want to open file paths here. If one of the items
5164                        // here is a directory, it was already opened further above
5165                        // with a `find_or_create_worktree`.
5166                        if let Ok(task) = abs_path_task {
5167                            if task.await.map_or(true, |p| p.is_file()) {
5168                                return Some((
5169                                    ix,
5170                                    workspace
5171                                        .update_in(&mut cx, |workspace, window, cx| {
5172                                            workspace.open_path(
5173                                                file_project_path,
5174                                                None,
5175                                                true,
5176                                                window,
5177                                                cx,
5178                                            )
5179                                        })
5180                                        .log_err()?
5181                                        .await,
5182                                ));
5183                            }
5184                        }
5185                        None
5186                    })
5187                });
5188
5189        let tasks = tasks.collect::<Vec<_>>();
5190
5191        let tasks = futures::future::join_all(tasks);
5192        for (ix, path_open_result) in tasks.await.into_iter().flatten() {
5193            opened_items[ix] = Some(path_open_result);
5194        }
5195
5196        Ok(opened_items)
5197    })
5198}
5199
5200enum ActivateInDirectionTarget {
5201    Pane(Entity<Pane>),
5202    Dock(Entity<Dock>),
5203}
5204
5205fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
5206    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";
5207
5208    workspace
5209        .update(cx, |workspace, _, cx| {
5210            if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
5211                struct DatabaseFailedNotification;
5212
5213                workspace.show_notification(
5214                    NotificationId::unique::<DatabaseFailedNotification>(),
5215                    cx,
5216                    |cx| {
5217                        cx.new(|_| {
5218                            MessageNotification::new("Failed to load the database file.")
5219                                .primary_message("File an Issue")
5220                                .primary_icon(IconName::Plus)
5221                                .primary_on_click(|_window, cx| cx.open_url(REPORT_ISSUE_URL))
5222                        })
5223                    },
5224                );
5225            }
5226        })
5227        .log_err();
5228}
5229
5230impl Focusable for Workspace {
5231    fn focus_handle(&self, cx: &App) -> FocusHandle {
5232        self.active_pane.focus_handle(cx)
5233    }
5234}
5235
5236#[derive(Clone)]
5237struct DraggedDock(DockPosition);
5238
5239impl Render for DraggedDock {
5240    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5241        gpui::Empty
5242    }
5243}
5244
5245impl Render for Workspace {
5246    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5247        let mut context = KeyContext::new_with_defaults();
5248        context.add("Workspace");
5249        context.set("keyboard_layout", cx.keyboard_layout().clone());
5250        let centered_layout = self.centered_layout
5251            && self.center.panes().len() == 1
5252            && self.active_item(cx).is_some();
5253        let render_padding = |size| {
5254            (size > 0.0).then(|| {
5255                div()
5256                    .h_full()
5257                    .w(relative(size))
5258                    .bg(cx.theme().colors().editor_background)
5259                    .border_color(cx.theme().colors().pane_group_border)
5260            })
5261        };
5262        let paddings = if centered_layout {
5263            let settings = WorkspaceSettings::get_global(cx).centered_layout;
5264            (
5265                render_padding(Self::adjust_padding(settings.left_padding)),
5266                render_padding(Self::adjust_padding(settings.right_padding)),
5267            )
5268        } else {
5269            (None, None)
5270        };
5271        let ui_font = theme::setup_ui_font(window, cx);
5272
5273        let theme = cx.theme().clone();
5274        let colors = theme.colors();
5275
5276        client_side_decorations(
5277            self.actions(div(), window, cx)
5278                .key_context(context)
5279                .relative()
5280                .size_full()
5281                .flex()
5282                .flex_col()
5283                .font(ui_font)
5284                .gap_0()
5285                .justify_start()
5286                .items_start()
5287                .text_color(colors.text)
5288                .overflow_hidden()
5289                .children(self.titlebar_item.clone())
5290                .child(
5291                    div()
5292                        .size_full()
5293                        .relative()
5294                        .flex_1()
5295                        .flex()
5296                        .flex_col()
5297                        .child(
5298                            div()
5299                                .id("workspace")
5300                                .bg(colors.background)
5301                                .relative()
5302                                .flex_1()
5303                                .w_full()
5304                                .flex()
5305                                .flex_col()
5306                                .overflow_hidden()
5307                                .border_t_1()
5308                                .border_b_1()
5309                                .border_color(colors.border)
5310                                .child({
5311                                    let this = cx.entity().clone();
5312                                    canvas(
5313                                        move |bounds, window, cx| {
5314                                            this.update(cx, |this, cx| {
5315                                                let bounds_changed = this.bounds != bounds;
5316                                                this.bounds = bounds;
5317
5318                                                if bounds_changed {
5319                                                    this.left_dock.update(cx, |dock, cx| {
5320                                                        dock.clamp_panel_size(
5321                                                            bounds.size.width,
5322                                                            window,
5323                                                            cx,
5324                                                        )
5325                                                    });
5326
5327                                                    this.right_dock.update(cx, |dock, cx| {
5328                                                        dock.clamp_panel_size(
5329                                                            bounds.size.width,
5330                                                            window,
5331                                                            cx,
5332                                                        )
5333                                                    });
5334
5335                                                    this.bottom_dock.update(cx, |dock, cx| {
5336                                                        dock.clamp_panel_size(
5337                                                            bounds.size.height,
5338                                                            window,
5339                                                            cx,
5340                                                        )
5341                                                    });
5342                                                }
5343                                            })
5344                                        },
5345                                        |_, _, _, _| {},
5346                                    )
5347                                    .absolute()
5348                                    .size_full()
5349                                })
5350                                .when(self.zoomed.is_none(), |this| {
5351                                    this.on_drag_move(cx.listener(
5352                                        move |workspace,
5353                                              e: &DragMoveEvent<DraggedDock>,
5354                                              window,
5355                                              cx| {
5356                                            if workspace.previous_dock_drag_coordinates
5357                                                != Some(e.event.position)
5358                                            {
5359                                                workspace.previous_dock_drag_coordinates =
5360                                                    Some(e.event.position);
5361                                                match e.drag(cx).0 {
5362                                                    DockPosition::Left => {
5363                                                        resize_left_dock(
5364                                                            e.event.position.x
5365                                                                - workspace.bounds.left(),
5366                                                            workspace,
5367                                                            window,
5368                                                            cx,
5369                                                        );
5370                                                    }
5371                                                    DockPosition::Right => {
5372                                                        resize_right_dock(
5373                                                            workspace.bounds.right()
5374                                                                - e.event.position.x,
5375                                                            workspace,
5376                                                            window,
5377                                                            cx,
5378                                                        );
5379                                                    }
5380                                                    DockPosition::Bottom => {
5381                                                        resize_bottom_dock(
5382                                                            workspace.bounds.bottom()
5383                                                                - e.event.position.y,
5384                                                            workspace,
5385                                                            window,
5386                                                            cx,
5387                                                        );
5388                                                    }
5389                                                };
5390                                                workspace.serialize_workspace(window, cx);
5391                                            }
5392                                        },
5393                                    ))
5394                                })
5395                                .child(
5396                                    div()
5397                                        .flex()
5398                                        .flex_row()
5399                                        .h_full()
5400                                        // Left Dock
5401                                        .children(self.render_dock(
5402                                            DockPosition::Left,
5403                                            &self.left_dock,
5404                                            window,
5405                                            cx,
5406                                        ))
5407                                        // Panes
5408                                        .child(
5409                                            div()
5410                                                .flex()
5411                                                .flex_col()
5412                                                .flex_1()
5413                                                .overflow_hidden()
5414                                                .child(
5415                                                    h_flex()
5416                                                        .flex_1()
5417                                                        .when_some(paddings.0, |this, p| {
5418                                                            this.child(p.border_r_1())
5419                                                        })
5420                                                        .child(self.center.render(
5421                                                            &self.project,
5422                                                            &self.follower_states,
5423                                                            self.active_call(),
5424                                                            &self.active_pane,
5425                                                            self.zoomed.as_ref(),
5426                                                            &self.app_state,
5427                                                            window,
5428                                                            cx,
5429                                                        ))
5430                                                        .when_some(paddings.1, |this, p| {
5431                                                            this.child(p.border_l_1())
5432                                                        }),
5433                                                )
5434                                                .children(self.render_dock(
5435                                                    DockPosition::Bottom,
5436                                                    &self.bottom_dock,
5437                                                    window,
5438                                                    cx,
5439                                                )),
5440                                        )
5441                                        // Right Dock
5442                                        .children(self.render_dock(
5443                                            DockPosition::Right,
5444                                            &self.right_dock,
5445                                            window,
5446                                            cx,
5447                                        )),
5448                                )
5449                                .children(self.zoomed.as_ref().and_then(|view| {
5450                                    let zoomed_view = view.upgrade()?;
5451                                    let div = div()
5452                                        .occlude()
5453                                        .absolute()
5454                                        .overflow_hidden()
5455                                        .border_color(colors.border)
5456                                        .bg(colors.background)
5457                                        .child(zoomed_view)
5458                                        .inset_0()
5459                                        .shadow_lg();
5460
5461                                    Some(match self.zoomed_position {
5462                                        Some(DockPosition::Left) => div.right_2().border_r_1(),
5463                                        Some(DockPosition::Right) => div.left_2().border_l_1(),
5464                                        Some(DockPosition::Bottom) => div.top_2().border_t_1(),
5465                                        None => {
5466                                            div.top_2().bottom_2().left_2().right_2().border_1()
5467                                        }
5468                                    })
5469                                }))
5470                                .children(self.render_notifications(window, cx)),
5471                        )
5472                        .child(self.status_bar.clone())
5473                        .child(self.modal_layer.clone()),
5474                ),
5475            window,
5476            cx,
5477        )
5478    }
5479}
5480
5481fn resize_bottom_dock(
5482    new_size: Pixels,
5483    workspace: &mut Workspace,
5484    window: &mut Window,
5485    cx: &mut App,
5486) {
5487    let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
5488    workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5489        bottom_dock.resize_active_panel(Some(size), window, cx);
5490    });
5491}
5492
5493fn resize_right_dock(
5494    new_size: Pixels,
5495    workspace: &mut Workspace,
5496    window: &mut Window,
5497    cx: &mut App,
5498) {
5499    let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
5500    workspace.right_dock.update(cx, |right_dock, cx| {
5501        right_dock.resize_active_panel(Some(size), window, cx);
5502    });
5503}
5504
5505fn resize_left_dock(
5506    new_size: Pixels,
5507    workspace: &mut Workspace,
5508    window: &mut Window,
5509    cx: &mut App,
5510) {
5511    let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5512
5513    workspace.left_dock.update(cx, |left_dock, cx| {
5514        left_dock.resize_active_panel(Some(size), window, cx);
5515    });
5516}
5517
5518impl WorkspaceStore {
5519    pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
5520        Self {
5521            workspaces: Default::default(),
5522            _subscriptions: vec![
5523                client.add_request_handler(cx.weak_entity(), Self::handle_follow),
5524                client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
5525            ],
5526            client,
5527        }
5528    }
5529
5530    pub fn update_followers(
5531        &self,
5532        project_id: Option<u64>,
5533        update: proto::update_followers::Variant,
5534        cx: &App,
5535    ) -> Option<()> {
5536        let active_call = ActiveCall::try_global(cx)?;
5537        let room_id = active_call.read(cx).room()?.read(cx).id();
5538        self.client
5539            .send(proto::UpdateFollowers {
5540                room_id,
5541                project_id,
5542                variant: Some(update),
5543            })
5544            .log_err()
5545    }
5546
5547    pub async fn handle_follow(
5548        this: Entity<Self>,
5549        envelope: TypedEnvelope<proto::Follow>,
5550        mut cx: AsyncApp,
5551    ) -> Result<proto::FollowResponse> {
5552        this.update(&mut cx, |this, cx| {
5553            let follower = Follower {
5554                project_id: envelope.payload.project_id,
5555                peer_id: envelope.original_sender_id()?,
5556            };
5557
5558            let mut response = proto::FollowResponse::default();
5559            this.workspaces.retain(|workspace| {
5560                workspace
5561                    .update(cx, |workspace, window, cx| {
5562                        let handler_response =
5563                            workspace.handle_follow(follower.project_id, window, cx);
5564                        if let Some(active_view) = handler_response.active_view.clone() {
5565                            if workspace.project.read(cx).remote_id() == follower.project_id {
5566                                response.active_view = Some(active_view)
5567                            }
5568                        }
5569                    })
5570                    .is_ok()
5571            });
5572
5573            Ok(response)
5574        })?
5575    }
5576
5577    async fn handle_update_followers(
5578        this: Entity<Self>,
5579        envelope: TypedEnvelope<proto::UpdateFollowers>,
5580        mut cx: AsyncApp,
5581    ) -> Result<()> {
5582        let leader_id = envelope.original_sender_id()?;
5583        let update = envelope.payload;
5584
5585        this.update(&mut cx, |this, cx| {
5586            this.workspaces.retain(|workspace| {
5587                workspace
5588                    .update(cx, |workspace, window, cx| {
5589                        let project_id = workspace.project.read(cx).remote_id();
5590                        if update.project_id != project_id && update.project_id.is_some() {
5591                            return;
5592                        }
5593                        workspace.handle_update_followers(leader_id, update.clone(), window, cx);
5594                    })
5595                    .is_ok()
5596            });
5597            Ok(())
5598        })?
5599    }
5600}
5601
5602impl ViewId {
5603    pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5604        Ok(Self {
5605            creator: message
5606                .creator
5607                .ok_or_else(|| anyhow!("creator is missing"))?,
5608            id: message.id,
5609        })
5610    }
5611
5612    pub(crate) fn to_proto(self) -> proto::ViewId {
5613        proto::ViewId {
5614            creator: Some(self.creator),
5615            id: self.id,
5616        }
5617    }
5618}
5619
5620impl FollowerState {
5621    fn pane(&self) -> &Entity<Pane> {
5622        self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5623    }
5624}
5625
5626pub trait WorkspaceHandle {
5627    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
5628}
5629
5630impl WorkspaceHandle for Entity<Workspace> {
5631    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
5632        self.read(cx)
5633            .worktrees(cx)
5634            .flat_map(|worktree| {
5635                let worktree_id = worktree.read(cx).id();
5636                worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5637                    worktree_id,
5638                    path: f.path.clone(),
5639                })
5640            })
5641            .collect::<Vec<_>>()
5642    }
5643}
5644
5645impl std::fmt::Debug for OpenPaths {
5646    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5647        f.debug_struct("OpenPaths")
5648            .field("paths", &self.paths)
5649            .finish()
5650    }
5651}
5652
5653pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5654    DB.last_workspace().await.log_err().flatten()
5655}
5656
5657pub fn last_session_workspace_locations(
5658    last_session_id: &str,
5659    last_session_window_stack: Option<Vec<WindowId>>,
5660) -> Option<Vec<SerializedWorkspaceLocation>> {
5661    DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5662        .log_err()
5663}
5664
5665actions!(collab, [OpenChannelNotes]);
5666actions!(zed, [OpenLog]);
5667
5668async fn join_channel_internal(
5669    channel_id: ChannelId,
5670    app_state: &Arc<AppState>,
5671    requesting_window: Option<WindowHandle<Workspace>>,
5672    active_call: &Entity<ActiveCall>,
5673    cx: &mut AsyncApp,
5674) -> Result<bool> {
5675    let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5676        let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5677            return (false, None);
5678        };
5679
5680        let already_in_channel = room.channel_id() == Some(channel_id);
5681        let should_prompt = room.is_sharing_project()
5682            && !room.remote_participants().is_empty()
5683            && !already_in_channel;
5684        let open_room = if already_in_channel {
5685            active_call.room().cloned()
5686        } else {
5687            None
5688        };
5689        (should_prompt, open_room)
5690    })?;
5691
5692    if let Some(room) = open_room {
5693        let task = room.update(cx, |room, cx| {
5694            if let Some((project, host)) = room.most_active_project(cx) {
5695                return Some(join_in_room_project(project, host, app_state.clone(), cx));
5696            }
5697
5698            None
5699        })?;
5700        if let Some(task) = task {
5701            task.await?;
5702        }
5703        return anyhow::Ok(true);
5704    }
5705
5706    if should_prompt {
5707        if let Some(workspace) = requesting_window {
5708            let answer = workspace
5709                .update(cx, |_, window, cx| {
5710                    window.prompt(
5711                        PromptLevel::Warning,
5712                        "Do you want to switch channels?",
5713                        Some("Leaving this call will unshare your current project."),
5714                        &["Yes, Join Channel", "Cancel"],
5715                        cx,
5716                    )
5717                })?
5718                .await;
5719
5720            if answer == Ok(1) {
5721                return Ok(false);
5722            }
5723        } else {
5724            return Ok(false); // unreachable!() hopefully
5725        }
5726    }
5727
5728    let client = cx.update(|cx| active_call.read(cx).client())?;
5729
5730    let mut client_status = client.status();
5731
5732    // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5733    'outer: loop {
5734        let Some(status) = client_status.recv().await else {
5735            return Err(anyhow!("error connecting"));
5736        };
5737
5738        match status {
5739            Status::Connecting
5740            | Status::Authenticating
5741            | Status::Reconnecting
5742            | Status::Reauthenticating => continue,
5743            Status::Connected { .. } => break 'outer,
5744            Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5745            Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5746            Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5747                return Err(ErrorCode::Disconnected.into());
5748            }
5749        }
5750    }
5751
5752    let room = active_call
5753        .update(cx, |active_call, cx| {
5754            active_call.join_channel(channel_id, cx)
5755        })?
5756        .await?;
5757
5758    let Some(room) = room else {
5759        return anyhow::Ok(true);
5760    };
5761
5762    room.update(cx, |room, _| room.room_update_completed())?
5763        .await;
5764
5765    let task = room.update(cx, |room, cx| {
5766        if let Some((project, host)) = room.most_active_project(cx) {
5767            return Some(join_in_room_project(project, host, app_state.clone(), cx));
5768        }
5769
5770        // If you are the first to join a channel, see if you should share your project.
5771        if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5772            if let Some(workspace) = requesting_window {
5773                let project = workspace.update(cx, |workspace, _, cx| {
5774                    let project = workspace.project.read(cx);
5775
5776                    if !CallSettings::get_global(cx).share_on_join {
5777                        return None;
5778                    }
5779
5780                    if (project.is_local() || project.is_via_ssh())
5781                        && project.visible_worktrees(cx).any(|tree| {
5782                            tree.read(cx)
5783                                .root_entry()
5784                                .map_or(false, |entry| entry.is_dir())
5785                        })
5786                    {
5787                        Some(workspace.project.clone())
5788                    } else {
5789                        None
5790                    }
5791                });
5792                if let Ok(Some(project)) = project {
5793                    return Some(cx.spawn(|room, mut cx| async move {
5794                        room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5795                            .await?;
5796                        Ok(())
5797                    }));
5798                }
5799            }
5800        }
5801
5802        None
5803    })?;
5804    if let Some(task) = task {
5805        task.await?;
5806        return anyhow::Ok(true);
5807    }
5808    anyhow::Ok(false)
5809}
5810
5811pub fn join_channel(
5812    channel_id: ChannelId,
5813    app_state: Arc<AppState>,
5814    requesting_window: Option<WindowHandle<Workspace>>,
5815    cx: &mut App,
5816) -> Task<Result<()>> {
5817    let active_call = ActiveCall::global(cx);
5818    cx.spawn(|mut cx| async move {
5819        let result = join_channel_internal(
5820            channel_id,
5821            &app_state,
5822            requesting_window,
5823            &active_call,
5824            &mut cx,
5825        )
5826            .await;
5827
5828        // join channel succeeded, and opened a window
5829        if matches!(result, Ok(true)) {
5830            return anyhow::Ok(());
5831        }
5832
5833        // find an existing workspace to focus and show call controls
5834        let mut active_window =
5835            requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5836        if active_window.is_none() {
5837            // no open workspaces, make one to show the error in (blergh)
5838            let (window_handle, _) = cx
5839                .update(|cx| {
5840                    Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5841                })?
5842                .await?;
5843
5844            if result.is_ok() {
5845                cx.update(|cx| {
5846                    cx.dispatch_action(&OpenChannelNotes);
5847                }).log_err();
5848            }
5849
5850            active_window = Some(window_handle);
5851        }
5852
5853        if let Err(err) = result {
5854            log::error!("failed to join channel: {}", err);
5855            if let Some(active_window) = active_window {
5856                active_window
5857                    .update(&mut cx, |_, window, cx| {
5858                        let detail: SharedString = match err.error_code() {
5859                            ErrorCode::SignedOut => {
5860                                "Please sign in to continue.".into()
5861                            }
5862                            ErrorCode::UpgradeRequired => {
5863                                "Your are running an unsupported version of Zed. Please update to continue.".into()
5864                            }
5865                            ErrorCode::NoSuchChannel => {
5866                                "No matching channel was found. Please check the link and try again.".into()
5867                            }
5868                            ErrorCode::Forbidden => {
5869                                "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5870                            }
5871                            ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5872                            _ => format!("{}\n\nPlease try again.", err).into(),
5873                        };
5874                        window.prompt(
5875                            PromptLevel::Critical,
5876                            "Failed to join channel",
5877                            Some(&detail),
5878                            &["Ok"],
5879                        cx)
5880                    })?
5881                    .await
5882                    .ok();
5883            }
5884        }
5885
5886        // return ok, we showed the error to the user.
5887        anyhow::Ok(())
5888    })
5889}
5890
5891pub async fn get_any_active_workspace(
5892    app_state: Arc<AppState>,
5893    mut cx: AsyncApp,
5894) -> anyhow::Result<WindowHandle<Workspace>> {
5895    // find an existing workspace to focus and show call controls
5896    let active_window = activate_any_workspace_window(&mut cx);
5897    if active_window.is_none() {
5898        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5899            .await?;
5900    }
5901    activate_any_workspace_window(&mut cx).context("could not open zed")
5902}
5903
5904fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
5905    cx.update(|cx| {
5906        if let Some(workspace_window) = cx
5907            .active_window()
5908            .and_then(|window| window.downcast::<Workspace>())
5909        {
5910            return Some(workspace_window);
5911        }
5912
5913        for window in cx.windows() {
5914            if let Some(workspace_window) = window.downcast::<Workspace>() {
5915                workspace_window
5916                    .update(cx, |_, window, _| window.activate_window())
5917                    .ok();
5918                return Some(workspace_window);
5919            }
5920        }
5921        None
5922    })
5923    .ok()
5924    .flatten()
5925}
5926
5927pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
5928    cx.windows()
5929        .into_iter()
5930        .filter_map(|window| window.downcast::<Workspace>())
5931        .filter(|workspace| {
5932            workspace
5933                .read(cx)
5934                .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5935        })
5936        .collect()
5937}
5938
5939#[derive(Default)]
5940pub struct OpenOptions {
5941    pub open_new_workspace: Option<bool>,
5942    pub replace_window: Option<WindowHandle<Workspace>>,
5943    pub env: Option<HashMap<String, String>>,
5944}
5945#[allow(clippy::type_complexity)]
5946pub fn open_paths(
5947    abs_paths: &[PathBuf],
5948    app_state: Arc<AppState>,
5949    open_options: OpenOptions,
5950    cx: &mut App,
5951) -> Task<
5952    anyhow::Result<(
5953        WindowHandle<Workspace>,
5954        Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5955    )>,
5956> {
5957    let abs_paths = abs_paths.to_vec();
5958    let mut existing = None;
5959    let mut best_match = None;
5960    let mut open_visible = OpenVisible::All;
5961
5962    cx.spawn(move |mut cx| async move {
5963        if open_options.open_new_workspace != Some(true) {
5964            let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5965            let all_metadatas = futures::future::join_all(all_paths)
5966                .await
5967                .into_iter()
5968                .filter_map(|result| result.ok().flatten())
5969                .collect::<Vec<_>>();
5970
5971            cx.update(|cx| {
5972                for window in local_workspace_windows(&cx) {
5973                    if let Ok(workspace) = window.read(&cx) {
5974                        let m = workspace.project.read(&cx).visibility_for_paths(
5975                            &abs_paths,
5976                            &all_metadatas,
5977                            open_options.open_new_workspace == None,
5978                            cx,
5979                        );
5980                        if m > best_match {
5981                            existing = Some(window);
5982                            best_match = m;
5983                        } else if best_match.is_none()
5984                            && open_options.open_new_workspace == Some(false)
5985                        {
5986                            existing = Some(window)
5987                        }
5988                    }
5989                }
5990            })?;
5991
5992            if open_options.open_new_workspace.is_none() && existing.is_none() {
5993                if all_metadatas.iter().all(|file| !file.is_dir) {
5994                    cx.update(|cx| {
5995                        if let Some(window) = cx
5996                            .active_window()
5997                            .and_then(|window| window.downcast::<Workspace>())
5998                        {
5999                            if let Ok(workspace) = window.read(cx) {
6000                                let project = workspace.project().read(cx);
6001                                if project.is_local() && !project.is_via_collab() {
6002                                    existing = Some(window);
6003                                    open_visible = OpenVisible::None;
6004                                    return;
6005                                }
6006                            }
6007                        }
6008                        for window in local_workspace_windows(cx) {
6009                            if let Ok(workspace) = window.read(cx) {
6010                                let project = workspace.project().read(cx);
6011                                if project.is_via_collab() {
6012                                    continue;
6013                                }
6014                                existing = Some(window);
6015                                open_visible = OpenVisible::None;
6016                                break;
6017                            }
6018                        }
6019                    })?;
6020                }
6021            }
6022        }
6023
6024        if let Some(existing) = existing {
6025            let open_task = existing
6026                .update(&mut cx, |workspace, window, cx| {
6027                    window.activate_window();
6028                    workspace.open_paths(abs_paths, open_visible, None, window, cx)
6029                })?
6030                .await;
6031
6032            _ = existing.update(&mut cx, |workspace, _, cx| {
6033                for item in open_task.iter().flatten() {
6034                    if let Err(e) = item {
6035                        workspace.show_error(&e, cx);
6036                    }
6037                }
6038            });
6039
6040            Ok((existing, open_task))
6041        } else {
6042            cx.update(move |cx| {
6043                Workspace::new_local(
6044                    abs_paths,
6045                    app_state.clone(),
6046                    open_options.replace_window,
6047                    open_options.env,
6048                    cx,
6049                )
6050            })?
6051            .await
6052        }
6053    })
6054}
6055
6056pub fn open_new(
6057    open_options: OpenOptions,
6058    app_state: Arc<AppState>,
6059    cx: &mut App,
6060    init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6061) -> Task<anyhow::Result<()>> {
6062    let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6063    cx.spawn(|mut cx| async move {
6064        let (workspace, opened_paths) = task.await?;
6065        workspace.update(&mut cx, |workspace, window, cx| {
6066            if opened_paths.is_empty() {
6067                init(workspace, window, cx)
6068            }
6069        })?;
6070        Ok(())
6071    })
6072}
6073
6074pub fn create_and_open_local_file(
6075    path: &'static Path,
6076    window: &mut Window,
6077    cx: &mut Context<Workspace>,
6078    default_content: impl 'static + Send + FnOnce() -> Rope,
6079) -> Task<Result<Box<dyn ItemHandle>>> {
6080    cx.spawn_in(window, |workspace, mut cx| async move {
6081        let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
6082        if !fs.is_file(path).await {
6083            fs.create_file(path, Default::default()).await?;
6084            fs.save(path, &default_content(), Default::default())
6085                .await?;
6086        }
6087
6088        let mut items = workspace
6089            .update_in(&mut cx, |workspace, window, cx| {
6090                workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6091                    workspace.open_paths(
6092                        vec![path.to_path_buf()],
6093                        OpenVisible::None,
6094                        None,
6095                        window,
6096                        cx,
6097                    )
6098                })
6099            })?
6100            .await?
6101            .await;
6102
6103        let item = items.pop().flatten();
6104        item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
6105    })
6106}
6107
6108pub fn open_ssh_project(
6109    window: WindowHandle<Workspace>,
6110    connection_options: SshConnectionOptions,
6111    cancel_rx: oneshot::Receiver<()>,
6112    delegate: Arc<dyn SshClientDelegate>,
6113    app_state: Arc<AppState>,
6114    paths: Vec<PathBuf>,
6115    cx: &mut App,
6116) -> Task<Result<()>> {
6117    cx.spawn(|mut cx| async move {
6118        let (serialized_ssh_project, workspace_id, serialized_workspace) =
6119            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6120
6121        let session = match cx
6122            .update(|cx| {
6123                remote::SshRemoteClient::new(
6124                    ConnectionIdentifier::Workspace(workspace_id.0),
6125                    connection_options,
6126                    cancel_rx,
6127                    delegate,
6128                    cx,
6129                )
6130            })?
6131            .await?
6132        {
6133            Some(result) => result,
6134            None => return Ok(()),
6135        };
6136
6137        let project = cx.update(|cx| {
6138            project::Project::ssh(
6139                session,
6140                app_state.client.clone(),
6141                app_state.node_runtime.clone(),
6142                app_state.user_store.clone(),
6143                app_state.languages.clone(),
6144                app_state.fs.clone(),
6145                cx,
6146            )
6147        })?;
6148
6149        let toolchains = DB.toolchains(workspace_id).await?;
6150        for (toolchain, worktree_id) in toolchains {
6151            project
6152                .update(&mut cx, |this, cx| {
6153                    this.activate_toolchain(worktree_id, toolchain, cx)
6154                })?
6155                .await;
6156        }
6157        let mut project_paths_to_open = vec![];
6158        let mut project_path_errors = vec![];
6159
6160        for path in paths {
6161            let result = cx
6162                .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6163                .await;
6164            match result {
6165                Ok((_, project_path)) => {
6166                    project_paths_to_open.push((path.clone(), Some(project_path)));
6167                }
6168                Err(error) => {
6169                    project_path_errors.push(error);
6170                }
6171            };
6172        }
6173
6174        if project_paths_to_open.is_empty() {
6175            return Err(project_path_errors
6176                .pop()
6177                .unwrap_or_else(|| anyhow!("no paths given")));
6178        }
6179
6180        cx.update_window(window.into(), |_, window, cx| {
6181            window.replace_root(cx, |window, cx| {
6182                telemetry::event!("SSH Project Opened");
6183
6184                let mut workspace =
6185                    Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6186                workspace.set_serialized_ssh_project(serialized_ssh_project);
6187                workspace
6188            });
6189        })?;
6190
6191        window
6192            .update(&mut cx, |_, window, cx| {
6193                window.activate_window();
6194
6195                open_items(serialized_workspace, project_paths_to_open, window, cx)
6196            })?
6197            .await?;
6198
6199        window.update(&mut cx, |workspace, _, cx| {
6200            for error in project_path_errors {
6201                if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6202                    if let Some(path) = error.error_tag("path") {
6203                        workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6204                    }
6205                } else {
6206                    workspace.show_error(&error, cx)
6207                }
6208            }
6209        })
6210    })
6211}
6212
6213fn serialize_ssh_project(
6214    connection_options: SshConnectionOptions,
6215    paths: Vec<PathBuf>,
6216    cx: &AsyncApp,
6217) -> Task<
6218    Result<(
6219        SerializedSshProject,
6220        WorkspaceId,
6221        Option<SerializedWorkspace>,
6222    )>,
6223> {
6224    cx.background_spawn(async move {
6225        let serialized_ssh_project = persistence::DB
6226            .get_or_create_ssh_project(
6227                connection_options.host.clone(),
6228                connection_options.port,
6229                paths
6230                    .iter()
6231                    .map(|path| path.to_string_lossy().to_string())
6232                    .collect::<Vec<_>>(),
6233                connection_options.username.clone(),
6234            )
6235            .await?;
6236
6237        let serialized_workspace =
6238            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6239
6240        let workspace_id = if let Some(workspace_id) =
6241            serialized_workspace.as_ref().map(|workspace| workspace.id)
6242        {
6243            workspace_id
6244        } else {
6245            persistence::DB.next_id().await?
6246        };
6247
6248        Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6249    })
6250}
6251
6252pub fn join_in_room_project(
6253    project_id: u64,
6254    follow_user_id: u64,
6255    app_state: Arc<AppState>,
6256    cx: &mut App,
6257) -> Task<Result<()>> {
6258    let windows = cx.windows();
6259    cx.spawn(|mut cx| async move {
6260        let existing_workspace = windows.into_iter().find_map(|window_handle| {
6261            window_handle
6262                .downcast::<Workspace>()
6263                .and_then(|window_handle| {
6264                    window_handle
6265                        .update(&mut cx, |workspace, _window, cx| {
6266                            if workspace.project().read(cx).remote_id() == Some(project_id) {
6267                                Some(window_handle)
6268                            } else {
6269                                None
6270                            }
6271                        })
6272                        .unwrap_or(None)
6273                })
6274        });
6275
6276        let workspace = if let Some(existing_workspace) = existing_workspace {
6277            existing_workspace
6278        } else {
6279            let active_call = cx.update(|cx| ActiveCall::global(cx))?;
6280            let room = active_call
6281                .read_with(&cx, |call, _| call.room().cloned())?
6282                .ok_or_else(|| anyhow!("not in a call"))?;
6283            let project = room
6284                .update(&mut cx, |room, cx| {
6285                    room.join_project(
6286                        project_id,
6287                        app_state.languages.clone(),
6288                        app_state.fs.clone(),
6289                        cx,
6290                    )
6291                })?
6292                .await?;
6293
6294            let window_bounds_override = window_bounds_env_override();
6295            cx.update(|cx| {
6296                let mut options = (app_state.build_window_options)(None, cx);
6297                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
6298                cx.open_window(options, |window, cx| {
6299                    cx.new(|cx| {
6300                        Workspace::new(Default::default(), project, app_state.clone(), window, cx)
6301                    })
6302                })
6303            })??
6304        };
6305
6306        workspace.update(&mut cx, |workspace, window, cx| {
6307            cx.activate(true);
6308            window.activate_window();
6309
6310            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
6311                let follow_peer_id = room
6312                    .read(cx)
6313                    .remote_participants()
6314                    .iter()
6315                    .find(|(_, participant)| participant.user.id == follow_user_id)
6316                    .map(|(_, p)| p.peer_id)
6317                    .or_else(|| {
6318                        // If we couldn't follow the given user, follow the host instead.
6319                        let collaborator = workspace
6320                            .project()
6321                            .read(cx)
6322                            .collaborators()
6323                            .values()
6324                            .find(|collaborator| collaborator.is_host)?;
6325                        Some(collaborator.peer_id)
6326                    });
6327
6328                if let Some(follow_peer_id) = follow_peer_id {
6329                    workspace.follow(follow_peer_id, window, cx);
6330                }
6331            }
6332        })?;
6333
6334        anyhow::Ok(())
6335    })
6336}
6337
6338pub fn reload(reload: &Reload, cx: &mut App) {
6339    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
6340    let mut workspace_windows = cx
6341        .windows()
6342        .into_iter()
6343        .filter_map(|window| window.downcast::<Workspace>())
6344        .collect::<Vec<_>>();
6345
6346    // If multiple windows have unsaved changes, and need a save prompt,
6347    // prompt in the active window before switching to a different window.
6348    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
6349
6350    let mut prompt = None;
6351    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6352        prompt = window
6353            .update(cx, |_, window, cx| {
6354                window.prompt(
6355                    PromptLevel::Info,
6356                    "Are you sure you want to restart?",
6357                    None,
6358                    &["Restart", "Cancel"],
6359                    cx,
6360                )
6361            })
6362            .ok();
6363    }
6364
6365    let binary_path = reload.binary_path.clone();
6366    cx.spawn(|mut cx| async move {
6367        if let Some(prompt) = prompt {
6368            let answer = prompt.await?;
6369            if answer != 0 {
6370                return Ok(());
6371            }
6372        }
6373
6374        // If the user cancels any save prompt, then keep the app open.
6375        for window in workspace_windows {
6376            if let Ok(should_close) = window.update(&mut cx, |workspace, window, cx| {
6377                workspace.prepare_to_close(CloseIntent::Quit, window, cx)
6378            }) {
6379                if !should_close.await? {
6380                    return Ok(());
6381                }
6382            }
6383        }
6384
6385        cx.update(|cx| cx.restart(binary_path))
6386    })
6387    .detach_and_log_err(cx);
6388}
6389
6390fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6391    let mut parts = value.split(',');
6392    let x: usize = parts.next()?.parse().ok()?;
6393    let y: usize = parts.next()?.parse().ok()?;
6394    Some(point(px(x as f32), px(y as f32)))
6395}
6396
6397fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6398    let mut parts = value.split(',');
6399    let width: usize = parts.next()?.parse().ok()?;
6400    let height: usize = parts.next()?.parse().ok()?;
6401    Some(size(px(width as f32), px(height as f32)))
6402}
6403
6404pub fn client_side_decorations(
6405    element: impl IntoElement,
6406    window: &mut Window,
6407    cx: &mut App,
6408) -> Stateful<Div> {
6409    const BORDER_SIZE: Pixels = px(1.0);
6410    let decorations = window.window_decorations();
6411
6412    if matches!(decorations, Decorations::Client { .. }) {
6413        window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6414    }
6415
6416    struct GlobalResizeEdge(ResizeEdge);
6417    impl Global for GlobalResizeEdge {}
6418
6419    div()
6420        .id("window-backdrop")
6421        .bg(transparent_black())
6422        .map(|div| match decorations {
6423            Decorations::Server => div,
6424            Decorations::Client { tiling, .. } => div
6425                .when(!(tiling.top || tiling.right), |div| {
6426                    div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6427                })
6428                .when(!(tiling.top || tiling.left), |div| {
6429                    div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6430                })
6431                .when(!(tiling.bottom || tiling.right), |div| {
6432                    div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6433                })
6434                .when(!(tiling.bottom || tiling.left), |div| {
6435                    div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6436                })
6437                .when(!tiling.top, |div| {
6438                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6439                })
6440                .when(!tiling.bottom, |div| {
6441                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6442                })
6443                .when(!tiling.left, |div| {
6444                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6445                })
6446                .when(!tiling.right, |div| {
6447                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6448                })
6449                .on_mouse_move(move |e, window, cx| {
6450                    let size = window.window_bounds().get_bounds().size;
6451                    let pos = e.position;
6452
6453                    let new_edge =
6454                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6455
6456                    let edge = cx.try_global::<GlobalResizeEdge>();
6457                    if new_edge != edge.map(|edge| edge.0) {
6458                        window
6459                            .window_handle()
6460                            .update(cx, |workspace, _, cx| {
6461                                cx.notify(workspace.entity_id());
6462                            })
6463                            .ok();
6464                    }
6465                })
6466                .on_mouse_down(MouseButton::Left, move |e, window, _| {
6467                    let size = window.window_bounds().get_bounds().size;
6468                    let pos = e.position;
6469
6470                    let edge = match resize_edge(
6471                        pos,
6472                        theme::CLIENT_SIDE_DECORATION_SHADOW,
6473                        size,
6474                        tiling,
6475                    ) {
6476                        Some(value) => value,
6477                        None => return,
6478                    };
6479
6480                    window.start_window_resize(edge);
6481                }),
6482        })
6483        .size_full()
6484        .child(
6485            div()
6486                .cursor(CursorStyle::Arrow)
6487                .map(|div| match decorations {
6488                    Decorations::Server => div,
6489                    Decorations::Client { tiling } => div
6490                        .border_color(cx.theme().colors().border)
6491                        .when(!(tiling.top || tiling.right), |div| {
6492                            div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6493                        })
6494                        .when(!(tiling.top || tiling.left), |div| {
6495                            div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6496                        })
6497                        .when(!(tiling.bottom || tiling.right), |div| {
6498                            div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6499                        })
6500                        .when(!(tiling.bottom || tiling.left), |div| {
6501                            div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6502                        })
6503                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6504                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6505                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6506                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6507                        .when(!tiling.is_tiled(), |div| {
6508                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
6509                                color: Hsla {
6510                                    h: 0.,
6511                                    s: 0.,
6512                                    l: 0.,
6513                                    a: 0.4,
6514                                },
6515                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6516                                spread_radius: px(0.),
6517                                offset: point(px(0.0), px(0.0)),
6518                            }])
6519                        }),
6520                })
6521                .on_mouse_move(|_e, _, cx| {
6522                    cx.stop_propagation();
6523                })
6524                .size_full()
6525                .child(element),
6526        )
6527        .map(|div| match decorations {
6528            Decorations::Server => div,
6529            Decorations::Client { tiling, .. } => div.child(
6530                canvas(
6531                    |_bounds, window, _| {
6532                        window.insert_hitbox(
6533                            Bounds::new(
6534                                point(px(0.0), px(0.0)),
6535                                window.window_bounds().get_bounds().size,
6536                            ),
6537                            false,
6538                        )
6539                    },
6540                    move |_bounds, hitbox, window, cx| {
6541                        let mouse = window.mouse_position();
6542                        let size = window.window_bounds().get_bounds().size;
6543                        let Some(edge) =
6544                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6545                        else {
6546                            return;
6547                        };
6548                        cx.set_global(GlobalResizeEdge(edge));
6549                        window.set_cursor_style(
6550                            match edge {
6551                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6552                                ResizeEdge::Left | ResizeEdge::Right => {
6553                                    CursorStyle::ResizeLeftRight
6554                                }
6555                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6556                                    CursorStyle::ResizeUpLeftDownRight
6557                                }
6558                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6559                                    CursorStyle::ResizeUpRightDownLeft
6560                                }
6561                            },
6562                            &hitbox,
6563                        );
6564                    },
6565                )
6566                .size_full()
6567                .absolute(),
6568            ),
6569        })
6570}
6571
6572fn resize_edge(
6573    pos: Point<Pixels>,
6574    shadow_size: Pixels,
6575    window_size: Size<Pixels>,
6576    tiling: Tiling,
6577) -> Option<ResizeEdge> {
6578    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6579    if bounds.contains(&pos) {
6580        return None;
6581    }
6582
6583    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6584    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6585    if !tiling.top && top_left_bounds.contains(&pos) {
6586        return Some(ResizeEdge::TopLeft);
6587    }
6588
6589    let top_right_bounds = Bounds::new(
6590        Point::new(window_size.width - corner_size.width, px(0.)),
6591        corner_size,
6592    );
6593    if !tiling.top && top_right_bounds.contains(&pos) {
6594        return Some(ResizeEdge::TopRight);
6595    }
6596
6597    let bottom_left_bounds = Bounds::new(
6598        Point::new(px(0.), window_size.height - corner_size.height),
6599        corner_size,
6600    );
6601    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6602        return Some(ResizeEdge::BottomLeft);
6603    }
6604
6605    let bottom_right_bounds = Bounds::new(
6606        Point::new(
6607            window_size.width - corner_size.width,
6608            window_size.height - corner_size.height,
6609        ),
6610        corner_size,
6611    );
6612    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6613        return Some(ResizeEdge::BottomRight);
6614    }
6615
6616    if !tiling.top && pos.y < shadow_size {
6617        Some(ResizeEdge::Top)
6618    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6619        Some(ResizeEdge::Bottom)
6620    } else if !tiling.left && pos.x < shadow_size {
6621        Some(ResizeEdge::Left)
6622    } else if !tiling.right && pos.x > window_size.width - shadow_size {
6623        Some(ResizeEdge::Right)
6624    } else {
6625        None
6626    }
6627}
6628
6629fn join_pane_into_active(
6630    active_pane: &Entity<Pane>,
6631    pane: &Entity<Pane>,
6632    window: &mut Window,
6633    cx: &mut App,
6634) {
6635    if pane == active_pane {
6636        return;
6637    } else if pane.read(cx).items_len() == 0 {
6638        pane.update(cx, |_, cx| {
6639            cx.emit(pane::Event::Remove {
6640                focus_on_pane: None,
6641            });
6642        })
6643    } else {
6644        move_all_items(pane, active_pane, window, cx);
6645    }
6646}
6647
6648fn move_all_items(
6649    from_pane: &Entity<Pane>,
6650    to_pane: &Entity<Pane>,
6651    window: &mut Window,
6652    cx: &mut App,
6653) {
6654    let destination_is_different = from_pane != to_pane;
6655    let mut moved_items = 0;
6656    for (item_ix, item_handle) in from_pane
6657        .read(cx)
6658        .items()
6659        .enumerate()
6660        .map(|(ix, item)| (ix, item.clone()))
6661        .collect::<Vec<_>>()
6662    {
6663        let ix = item_ix - moved_items;
6664        if destination_is_different {
6665            // Close item from previous pane
6666            from_pane.update(cx, |source, cx| {
6667                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
6668            });
6669            moved_items += 1;
6670        }
6671
6672        // This automatically removes duplicate items in the pane
6673        to_pane.update(cx, |destination, cx| {
6674            destination.add_item(item_handle, true, true, None, window, cx);
6675            window.focus(&destination.focus_handle(cx))
6676        });
6677    }
6678}
6679
6680pub fn move_item(
6681    source: &Entity<Pane>,
6682    destination: &Entity<Pane>,
6683    item_id_to_move: EntityId,
6684    destination_index: usize,
6685    window: &mut Window,
6686    cx: &mut App,
6687) {
6688    let Some((item_ix, item_handle)) = source
6689        .read(cx)
6690        .items()
6691        .enumerate()
6692        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6693        .map(|(ix, item)| (ix, item.clone()))
6694    else {
6695        // Tab was closed during drag
6696        return;
6697    };
6698
6699    if source != destination {
6700        // Close item from previous pane
6701        source.update(cx, |source, cx| {
6702            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
6703        });
6704    }
6705
6706    // This automatically removes duplicate items in the pane
6707    destination.update(cx, |destination, cx| {
6708        destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
6709        window.focus(&destination.focus_handle(cx))
6710    });
6711}
6712
6713pub fn move_active_item(
6714    source: &Entity<Pane>,
6715    destination: &Entity<Pane>,
6716    focus_destination: bool,
6717    close_if_empty: bool,
6718    window: &mut Window,
6719    cx: &mut App,
6720) {
6721    if source == destination {
6722        return;
6723    }
6724    let Some(active_item) = source.read(cx).active_item() else {
6725        return;
6726    };
6727    source.update(cx, |source_pane, cx| {
6728        let item_id = active_item.item_id();
6729        source_pane.remove_item(item_id, false, close_if_empty, window, cx);
6730        destination.update(cx, |target_pane, cx| {
6731            target_pane.add_item(
6732                active_item,
6733                focus_destination,
6734                focus_destination,
6735                Some(target_pane.items_len()),
6736                window,
6737                cx,
6738            );
6739        });
6740    });
6741}
6742
6743#[cfg(test)]
6744mod tests {
6745    use std::{cell::RefCell, rc::Rc};
6746
6747    use super::*;
6748    use crate::{
6749        dock::{test::TestPanel, PanelEvent},
6750        item::{
6751            test::{TestItem, TestProjectItem},
6752            ItemEvent,
6753        },
6754    };
6755    use fs::FakeFs;
6756    use gpui::{
6757        px, DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
6758        UpdateGlobal, VisualTestContext,
6759    };
6760    use project::{Project, ProjectEntryId};
6761    use serde_json::json;
6762    use settings::SettingsStore;
6763
6764    #[gpui::test]
6765    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6766        init_test(cx);
6767
6768        let fs = FakeFs::new(cx.executor());
6769        let project = Project::test(fs, [], cx).await;
6770        let (workspace, cx) =
6771            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6772
6773        // Adding an item with no ambiguity renders the tab without detail.
6774        let item1 = cx.new(|cx| {
6775            let mut item = TestItem::new(cx);
6776            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6777            item
6778        });
6779        workspace.update_in(cx, |workspace, window, cx| {
6780            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6781        });
6782        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6783
6784        // Adding an item that creates ambiguity increases the level of detail on
6785        // both tabs.
6786        let item2 = cx.new_window_entity(|_window, cx| {
6787            let mut item = TestItem::new(cx);
6788            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6789            item
6790        });
6791        workspace.update_in(cx, |workspace, window, cx| {
6792            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6793        });
6794        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6795        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6796
6797        // Adding an item that creates ambiguity increases the level of detail only
6798        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6799        // we stop at the highest detail available.
6800        let item3 = cx.new(|cx| {
6801            let mut item = TestItem::new(cx);
6802            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6803            item
6804        });
6805        workspace.update_in(cx, |workspace, window, cx| {
6806            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6807        });
6808        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6809        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6810        item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6811    }
6812
6813    #[gpui::test]
6814    async fn test_tracking_active_path(cx: &mut TestAppContext) {
6815        init_test(cx);
6816
6817        let fs = FakeFs::new(cx.executor());
6818        fs.insert_tree(
6819            "/root1",
6820            json!({
6821                "one.txt": "",
6822                "two.txt": "",
6823            }),
6824        )
6825        .await;
6826        fs.insert_tree(
6827            "/root2",
6828            json!({
6829                "three.txt": "",
6830            }),
6831        )
6832        .await;
6833
6834        let project = Project::test(fs, ["root1".as_ref()], cx).await;
6835        let (workspace, cx) =
6836            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6837        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6838        let worktree_id = project.update(cx, |project, cx| {
6839            project.worktrees(cx).next().unwrap().read(cx).id()
6840        });
6841
6842        let item1 = cx.new(|cx| {
6843            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6844        });
6845        let item2 = cx.new(|cx| {
6846            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6847        });
6848
6849        // Add an item to an empty pane
6850        workspace.update_in(cx, |workspace, window, cx| {
6851            workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
6852        });
6853        project.update(cx, |project, cx| {
6854            assert_eq!(
6855                project.active_entry(),
6856                project
6857                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6858                    .map(|e| e.id)
6859            );
6860        });
6861        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6862
6863        // Add a second item to a non-empty pane
6864        workspace.update_in(cx, |workspace, window, cx| {
6865            workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
6866        });
6867        assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6868        project.update(cx, |project, cx| {
6869            assert_eq!(
6870                project.active_entry(),
6871                project
6872                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6873                    .map(|e| e.id)
6874            );
6875        });
6876
6877        // Close the active item
6878        pane.update_in(cx, |pane, window, cx| {
6879            pane.close_active_item(&Default::default(), window, cx)
6880                .unwrap()
6881        })
6882        .await
6883        .unwrap();
6884        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6885        project.update(cx, |project, cx| {
6886            assert_eq!(
6887                project.active_entry(),
6888                project
6889                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6890                    .map(|e| e.id)
6891            );
6892        });
6893
6894        // Add a project folder
6895        project
6896            .update(cx, |project, cx| {
6897                project.find_or_create_worktree("root2", true, cx)
6898            })
6899            .await
6900            .unwrap();
6901        assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6902
6903        // Remove a project folder
6904        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6905        assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6906    }
6907
6908    #[gpui::test]
6909    async fn test_close_window(cx: &mut TestAppContext) {
6910        init_test(cx);
6911
6912        let fs = FakeFs::new(cx.executor());
6913        fs.insert_tree("/root", json!({ "one": "" })).await;
6914
6915        let project = Project::test(fs, ["root".as_ref()], cx).await;
6916        let (workspace, cx) =
6917            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6918
6919        // When there are no dirty items, there's nothing to do.
6920        let item1 = cx.new(TestItem::new);
6921        workspace.update_in(cx, |w, window, cx| {
6922            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
6923        });
6924        let task = workspace.update_in(cx, |w, window, cx| {
6925            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6926        });
6927        assert!(task.await.unwrap());
6928
6929        // When there are dirty untitled items, prompt to save each one. If the user
6930        // cancels any prompt, then abort.
6931        let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
6932        let item3 = cx.new(|cx| {
6933            TestItem::new(cx)
6934                .with_dirty(true)
6935                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6936        });
6937        workspace.update_in(cx, |w, window, cx| {
6938            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6939            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6940        });
6941        let task = workspace.update_in(cx, |w, window, cx| {
6942            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6943        });
6944        cx.executor().run_until_parked();
6945        cx.simulate_prompt_answer("Cancel"); // cancel save all
6946        cx.executor().run_until_parked();
6947        assert!(!cx.has_pending_prompt());
6948        assert!(!task.await.unwrap());
6949    }
6950
6951    #[gpui::test]
6952    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6953        init_test(cx);
6954
6955        // Register TestItem as a serializable item
6956        cx.update(|cx| {
6957            register_serializable_item::<TestItem>(cx);
6958        });
6959
6960        let fs = FakeFs::new(cx.executor());
6961        fs.insert_tree("/root", json!({ "one": "" })).await;
6962
6963        let project = Project::test(fs, ["root".as_ref()], cx).await;
6964        let (workspace, cx) =
6965            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6966
6967        // When there are dirty untitled items, but they can serialize, then there is no prompt.
6968        let item1 = cx.new(|cx| {
6969            TestItem::new(cx)
6970                .with_dirty(true)
6971                .with_serialize(|| Some(Task::ready(Ok(()))))
6972        });
6973        let item2 = cx.new(|cx| {
6974            TestItem::new(cx)
6975                .with_dirty(true)
6976                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6977                .with_serialize(|| Some(Task::ready(Ok(()))))
6978        });
6979        workspace.update_in(cx, |w, window, cx| {
6980            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6981            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6982        });
6983        let task = workspace.update_in(cx, |w, window, cx| {
6984            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6985        });
6986        assert!(task.await.unwrap());
6987    }
6988
6989    #[gpui::test]
6990    async fn test_close_pane_items(cx: &mut TestAppContext) {
6991        init_test(cx);
6992
6993        let fs = FakeFs::new(cx.executor());
6994
6995        let project = Project::test(fs, None, cx).await;
6996        let (workspace, cx) =
6997            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6998
6999        let item1 = cx.new(|cx| {
7000            TestItem::new(cx)
7001                .with_dirty(true)
7002                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7003        });
7004        let item2 = cx.new(|cx| {
7005            TestItem::new(cx)
7006                .with_dirty(true)
7007                .with_conflict(true)
7008                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7009        });
7010        let item3 = cx.new(|cx| {
7011            TestItem::new(cx)
7012                .with_dirty(true)
7013                .with_conflict(true)
7014                .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7015        });
7016        let item4 = cx.new(|cx| {
7017            TestItem::new(cx).with_dirty(true).with_project_items(&[{
7018                let project_item = TestProjectItem::new_untitled(cx);
7019                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7020                project_item
7021            }])
7022        });
7023        let pane = workspace.update_in(cx, |workspace, window, cx| {
7024            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7025            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7026            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7027            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7028            workspace.active_pane().clone()
7029        });
7030
7031        let close_items = pane.update_in(cx, |pane, window, cx| {
7032            pane.activate_item(1, true, true, window, cx);
7033            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7034            let item1_id = item1.item_id();
7035            let item3_id = item3.item_id();
7036            let item4_id = item4.item_id();
7037            pane.close_items(window, cx, SaveIntent::Close, move |id| {
7038                [item1_id, item3_id, item4_id].contains(&id)
7039            })
7040        });
7041        cx.executor().run_until_parked();
7042
7043        assert!(cx.has_pending_prompt());
7044        cx.simulate_prompt_answer("Save all");
7045
7046        cx.executor().run_until_parked();
7047
7048        // Item 1 is saved. There's a prompt to save item 3.
7049        pane.update(cx, |pane, cx| {
7050            assert_eq!(item1.read(cx).save_count, 1);
7051            assert_eq!(item1.read(cx).save_as_count, 0);
7052            assert_eq!(item1.read(cx).reload_count, 0);
7053            assert_eq!(pane.items_len(), 3);
7054            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7055        });
7056        assert!(cx.has_pending_prompt());
7057
7058        // Cancel saving item 3.
7059        cx.simulate_prompt_answer("Discard");
7060        cx.executor().run_until_parked();
7061
7062        // Item 3 is reloaded. There's a prompt to save item 4.
7063        pane.update(cx, |pane, cx| {
7064            assert_eq!(item3.read(cx).save_count, 0);
7065            assert_eq!(item3.read(cx).save_as_count, 0);
7066            assert_eq!(item3.read(cx).reload_count, 1);
7067            assert_eq!(pane.items_len(), 2);
7068            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7069        });
7070
7071        // There's a prompt for a path for item 4.
7072        cx.simulate_new_path_selection(|_| Some(Default::default()));
7073        close_items.await.unwrap();
7074
7075        // The requested items are closed.
7076        pane.update(cx, |pane, cx| {
7077            assert_eq!(item4.read(cx).save_count, 0);
7078            assert_eq!(item4.read(cx).save_as_count, 1);
7079            assert_eq!(item4.read(cx).reload_count, 0);
7080            assert_eq!(pane.items_len(), 1);
7081            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7082        });
7083    }
7084
7085    #[gpui::test]
7086    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7087        init_test(cx);
7088
7089        let fs = FakeFs::new(cx.executor());
7090        let project = Project::test(fs, [], cx).await;
7091        let (workspace, cx) =
7092            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7093
7094        // Create several workspace items with single project entries, and two
7095        // workspace items with multiple project entries.
7096        let single_entry_items = (0..=4)
7097            .map(|project_entry_id| {
7098                cx.new(|cx| {
7099                    TestItem::new(cx)
7100                        .with_dirty(true)
7101                        .with_project_items(&[dirty_project_item(
7102                            project_entry_id,
7103                            &format!("{project_entry_id}.txt"),
7104                            cx,
7105                        )])
7106                })
7107            })
7108            .collect::<Vec<_>>();
7109        let item_2_3 = cx.new(|cx| {
7110            TestItem::new(cx)
7111                .with_dirty(true)
7112                .with_singleton(false)
7113                .with_project_items(&[
7114                    single_entry_items[2].read(cx).project_items[0].clone(),
7115                    single_entry_items[3].read(cx).project_items[0].clone(),
7116                ])
7117        });
7118        let item_3_4 = cx.new(|cx| {
7119            TestItem::new(cx)
7120                .with_dirty(true)
7121                .with_singleton(false)
7122                .with_project_items(&[
7123                    single_entry_items[3].read(cx).project_items[0].clone(),
7124                    single_entry_items[4].read(cx).project_items[0].clone(),
7125                ])
7126        });
7127
7128        // Create two panes that contain the following project entries:
7129        //   left pane:
7130        //     multi-entry items:   (2, 3)
7131        //     single-entry items:  0, 2, 3, 4
7132        //   right pane:
7133        //     single-entry items:  4, 1
7134        //     multi-entry items:   (3, 4)
7135        let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
7136            let left_pane = workspace.active_pane().clone();
7137            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7138            workspace.add_item_to_active_pane(
7139                single_entry_items[0].boxed_clone(),
7140                None,
7141                true,
7142                window,
7143                cx,
7144            );
7145            workspace.add_item_to_active_pane(
7146                single_entry_items[2].boxed_clone(),
7147                None,
7148                true,
7149                window,
7150                cx,
7151            );
7152            workspace.add_item_to_active_pane(
7153                single_entry_items[3].boxed_clone(),
7154                None,
7155                true,
7156                window,
7157                cx,
7158            );
7159            workspace.add_item_to_active_pane(
7160                single_entry_items[4].boxed_clone(),
7161                None,
7162                true,
7163                window,
7164                cx,
7165            );
7166
7167            let right_pane = workspace
7168                .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7169                .unwrap();
7170
7171            right_pane.update(cx, |pane, cx| {
7172                pane.add_item(
7173                    single_entry_items[1].boxed_clone(),
7174                    true,
7175                    true,
7176                    None,
7177                    window,
7178                    cx,
7179                );
7180                pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7181            });
7182
7183            (left_pane, right_pane)
7184        });
7185
7186        cx.focus(&right_pane);
7187
7188        let mut close = right_pane.update_in(cx, |pane, window, cx| {
7189            pane.close_all_items(&CloseAllItems::default(), window, cx)
7190                .unwrap()
7191        });
7192        cx.executor().run_until_parked();
7193
7194        let msg = cx.pending_prompt().unwrap().0;
7195        assert!(msg.contains("1.txt"));
7196        assert!(!msg.contains("2.txt"));
7197        assert!(!msg.contains("3.txt"));
7198        assert!(!msg.contains("4.txt"));
7199
7200        cx.simulate_prompt_answer("Cancel");
7201        close.await.unwrap();
7202
7203        left_pane
7204            .update_in(cx, |left_pane, window, cx| {
7205                left_pane.close_item_by_id(
7206                    single_entry_items[3].entity_id(),
7207                    SaveIntent::Skip,
7208                    window,
7209                    cx,
7210                )
7211            })
7212            .await
7213            .unwrap();
7214
7215        close = right_pane.update_in(cx, |pane, window, cx| {
7216            pane.close_all_items(&CloseAllItems::default(), window, cx)
7217                .unwrap()
7218        });
7219        cx.executor().run_until_parked();
7220
7221        let details = cx.pending_prompt().unwrap().1;
7222        assert!(details.contains("1.txt"));
7223        assert!(!details.contains("2.txt"));
7224        assert!(details.contains("3.txt"));
7225        // ideally this assertion could be made, but today we can only
7226        // save whole items not project items, so the orphaned item 3 causes
7227        // 4 to be saved too.
7228        // assert!(!details.contains("4.txt"));
7229
7230        cx.simulate_prompt_answer("Save all");
7231
7232        cx.executor().run_until_parked();
7233        close.await.unwrap();
7234        right_pane.update(cx, |pane, _| {
7235            assert_eq!(pane.items_len(), 0);
7236        });
7237    }
7238
7239    #[gpui::test]
7240    async fn test_autosave(cx: &mut gpui::TestAppContext) {
7241        init_test(cx);
7242
7243        let fs = FakeFs::new(cx.executor());
7244        let project = Project::test(fs, [], cx).await;
7245        let (workspace, cx) =
7246            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7247        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7248
7249        let item = cx.new(|cx| {
7250            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7251        });
7252        let item_id = item.entity_id();
7253        workspace.update_in(cx, |workspace, window, cx| {
7254            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7255        });
7256
7257        // Autosave on window change.
7258        item.update(cx, |item, cx| {
7259            SettingsStore::update_global(cx, |settings, cx| {
7260                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7261                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
7262                })
7263            });
7264            item.is_dirty = true;
7265        });
7266
7267        // Deactivating the window saves the file.
7268        cx.deactivate_window();
7269        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7270
7271        // Re-activating the window doesn't save the file.
7272        cx.update(|window, _| window.activate_window());
7273        cx.executor().run_until_parked();
7274        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7275
7276        // Autosave on focus change.
7277        item.update_in(cx, |item, window, cx| {
7278            cx.focus_self(window);
7279            SettingsStore::update_global(cx, |settings, cx| {
7280                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7281                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
7282                })
7283            });
7284            item.is_dirty = true;
7285        });
7286
7287        // Blurring the item saves the file.
7288        item.update_in(cx, |_, window, _| window.blur());
7289        cx.executor().run_until_parked();
7290        item.update(cx, |item, _| assert_eq!(item.save_count, 2));
7291
7292        // Deactivating the window still saves the file.
7293        item.update_in(cx, |item, window, cx| {
7294            cx.focus_self(window);
7295            item.is_dirty = true;
7296        });
7297        cx.deactivate_window();
7298        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7299
7300        // Autosave after delay.
7301        item.update(cx, |item, cx| {
7302            SettingsStore::update_global(cx, |settings, cx| {
7303                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7304                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
7305                })
7306            });
7307            item.is_dirty = true;
7308            cx.emit(ItemEvent::Edit);
7309        });
7310
7311        // Delay hasn't fully expired, so the file is still dirty and unsaved.
7312        cx.executor().advance_clock(Duration::from_millis(250));
7313        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7314
7315        // After delay expires, the file is saved.
7316        cx.executor().advance_clock(Duration::from_millis(250));
7317        item.update(cx, |item, _| assert_eq!(item.save_count, 4));
7318
7319        // Autosave on focus change, ensuring closing the tab counts as such.
7320        item.update(cx, |item, cx| {
7321            SettingsStore::update_global(cx, |settings, cx| {
7322                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7323                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
7324                })
7325            });
7326            item.is_dirty = true;
7327            for project_item in &mut item.project_items {
7328                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7329            }
7330        });
7331
7332        pane.update_in(cx, |pane, window, cx| {
7333            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7334        })
7335        .await
7336        .unwrap();
7337        assert!(!cx.has_pending_prompt());
7338        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7339
7340        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
7341        workspace.update_in(cx, |workspace, window, cx| {
7342            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7343        });
7344        item.update_in(cx, |item, window, cx| {
7345            item.project_items[0].update(cx, |item, _| {
7346                item.entry_id = None;
7347            });
7348            item.is_dirty = true;
7349            window.blur();
7350        });
7351        cx.run_until_parked();
7352        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7353
7354        // Ensure autosave is prevented for deleted files also when closing the buffer.
7355        let _close_items = pane.update_in(cx, |pane, window, cx| {
7356            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7357        });
7358        cx.run_until_parked();
7359        assert!(cx.has_pending_prompt());
7360        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7361    }
7362
7363    #[gpui::test]
7364    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
7365        init_test(cx);
7366
7367        let fs = FakeFs::new(cx.executor());
7368
7369        let project = Project::test(fs, [], cx).await;
7370        let (workspace, cx) =
7371            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7372
7373        let item = cx.new(|cx| {
7374            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7375        });
7376        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7377        let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
7378        let toolbar_notify_count = Rc::new(RefCell::new(0));
7379
7380        workspace.update_in(cx, |workspace, window, cx| {
7381            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7382            let toolbar_notification_count = toolbar_notify_count.clone();
7383            cx.observe_in(&toolbar, window, move |_, _, _, _| {
7384                *toolbar_notification_count.borrow_mut() += 1
7385            })
7386            .detach();
7387        });
7388
7389        pane.update(cx, |pane, _| {
7390            assert!(!pane.can_navigate_backward());
7391            assert!(!pane.can_navigate_forward());
7392        });
7393
7394        item.update_in(cx, |item, _, cx| {
7395            item.set_state("one".to_string(), cx);
7396        });
7397
7398        // Toolbar must be notified to re-render the navigation buttons
7399        assert_eq!(*toolbar_notify_count.borrow(), 1);
7400
7401        pane.update(cx, |pane, _| {
7402            assert!(pane.can_navigate_backward());
7403            assert!(!pane.can_navigate_forward());
7404        });
7405
7406        workspace
7407            .update_in(cx, |workspace, window, cx| {
7408                workspace.go_back(pane.downgrade(), window, cx)
7409            })
7410            .await
7411            .unwrap();
7412
7413        assert_eq!(*toolbar_notify_count.borrow(), 2);
7414        pane.update(cx, |pane, _| {
7415            assert!(!pane.can_navigate_backward());
7416            assert!(pane.can_navigate_forward());
7417        });
7418    }
7419
7420    #[gpui::test]
7421    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7422        init_test(cx);
7423        let fs = FakeFs::new(cx.executor());
7424
7425        let project = Project::test(fs, [], cx).await;
7426        let (workspace, cx) =
7427            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7428
7429        let panel = workspace.update_in(cx, |workspace, window, cx| {
7430            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7431            workspace.add_panel(panel.clone(), window, cx);
7432
7433            workspace
7434                .right_dock()
7435                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
7436
7437            panel
7438        });
7439
7440        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7441        pane.update_in(cx, |pane, window, cx| {
7442            let item = cx.new(TestItem::new);
7443            pane.add_item(Box::new(item), true, true, None, window, cx);
7444        });
7445
7446        // Transfer focus from center to panel
7447        workspace.update_in(cx, |workspace, window, cx| {
7448            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7449        });
7450
7451        workspace.update_in(cx, |workspace, window, cx| {
7452            assert!(workspace.right_dock().read(cx).is_open());
7453            assert!(!panel.is_zoomed(window, cx));
7454            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7455        });
7456
7457        // Transfer focus from panel to center
7458        workspace.update_in(cx, |workspace, window, cx| {
7459            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7460        });
7461
7462        workspace.update_in(cx, |workspace, window, cx| {
7463            assert!(workspace.right_dock().read(cx).is_open());
7464            assert!(!panel.is_zoomed(window, cx));
7465            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7466        });
7467
7468        // Close the dock
7469        workspace.update_in(cx, |workspace, window, cx| {
7470            workspace.toggle_dock(DockPosition::Right, window, cx);
7471        });
7472
7473        workspace.update_in(cx, |workspace, window, cx| {
7474            assert!(!workspace.right_dock().read(cx).is_open());
7475            assert!(!panel.is_zoomed(window, cx));
7476            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7477        });
7478
7479        // Open the dock
7480        workspace.update_in(cx, |workspace, window, cx| {
7481            workspace.toggle_dock(DockPosition::Right, window, cx);
7482        });
7483
7484        workspace.update_in(cx, |workspace, window, cx| {
7485            assert!(workspace.right_dock().read(cx).is_open());
7486            assert!(!panel.is_zoomed(window, cx));
7487            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7488        });
7489
7490        // Focus and zoom panel
7491        panel.update_in(cx, |panel, window, cx| {
7492            cx.focus_self(window);
7493            panel.set_zoomed(true, window, cx)
7494        });
7495
7496        workspace.update_in(cx, |workspace, window, cx| {
7497            assert!(workspace.right_dock().read(cx).is_open());
7498            assert!(panel.is_zoomed(window, cx));
7499            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7500        });
7501
7502        // Transfer focus to the center closes the dock
7503        workspace.update_in(cx, |workspace, window, cx| {
7504            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7505        });
7506
7507        workspace.update_in(cx, |workspace, window, cx| {
7508            assert!(!workspace.right_dock().read(cx).is_open());
7509            assert!(panel.is_zoomed(window, cx));
7510            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7511        });
7512
7513        // Transferring focus back to the panel keeps it zoomed
7514        workspace.update_in(cx, |workspace, window, cx| {
7515            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7516        });
7517
7518        workspace.update_in(cx, |workspace, window, cx| {
7519            assert!(workspace.right_dock().read(cx).is_open());
7520            assert!(panel.is_zoomed(window, cx));
7521            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7522        });
7523
7524        // Close the dock while it is zoomed
7525        workspace.update_in(cx, |workspace, window, cx| {
7526            workspace.toggle_dock(DockPosition::Right, window, cx)
7527        });
7528
7529        workspace.update_in(cx, |workspace, window, cx| {
7530            assert!(!workspace.right_dock().read(cx).is_open());
7531            assert!(panel.is_zoomed(window, cx));
7532            assert!(workspace.zoomed.is_none());
7533            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7534        });
7535
7536        // Opening the dock, when it's zoomed, retains focus
7537        workspace.update_in(cx, |workspace, window, cx| {
7538            workspace.toggle_dock(DockPosition::Right, window, cx)
7539        });
7540
7541        workspace.update_in(cx, |workspace, window, cx| {
7542            assert!(workspace.right_dock().read(cx).is_open());
7543            assert!(panel.is_zoomed(window, cx));
7544            assert!(workspace.zoomed.is_some());
7545            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7546        });
7547
7548        // Unzoom and close the panel, zoom the active pane.
7549        panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
7550        workspace.update_in(cx, |workspace, window, cx| {
7551            workspace.toggle_dock(DockPosition::Right, window, cx)
7552        });
7553        pane.update_in(cx, |pane, window, cx| {
7554            pane.toggle_zoom(&Default::default(), window, cx)
7555        });
7556
7557        // Opening a dock unzooms the pane.
7558        workspace.update_in(cx, |workspace, window, cx| {
7559            workspace.toggle_dock(DockPosition::Right, window, cx)
7560        });
7561        workspace.update_in(cx, |workspace, window, cx| {
7562            let pane = pane.read(cx);
7563            assert!(!pane.is_zoomed());
7564            assert!(!pane.focus_handle(cx).is_focused(window));
7565            assert!(workspace.right_dock().read(cx).is_open());
7566            assert!(workspace.zoomed.is_none());
7567        });
7568    }
7569
7570    #[gpui::test]
7571    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7572        init_test(cx);
7573
7574        let fs = FakeFs::new(cx.executor());
7575
7576        let project = Project::test(fs, None, cx).await;
7577        let (workspace, cx) =
7578            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7579
7580        // Let's arrange the panes like this:
7581        //
7582        // +-----------------------+
7583        // |         top           |
7584        // +------+--------+-------+
7585        // | left | center | right |
7586        // +------+--------+-------+
7587        // |        bottom         |
7588        // +-----------------------+
7589
7590        let top_item = cx.new(|cx| {
7591            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7592        });
7593        let bottom_item = cx.new(|cx| {
7594            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7595        });
7596        let left_item = cx.new(|cx| {
7597            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7598        });
7599        let right_item = cx.new(|cx| {
7600            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7601        });
7602        let center_item = cx.new(|cx| {
7603            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7604        });
7605
7606        let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7607            let top_pane_id = workspace.active_pane().entity_id();
7608            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
7609            workspace.split_pane(
7610                workspace.active_pane().clone(),
7611                SplitDirection::Down,
7612                window,
7613                cx,
7614            );
7615            top_pane_id
7616        });
7617        let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7618            let bottom_pane_id = workspace.active_pane().entity_id();
7619            workspace.add_item_to_active_pane(
7620                Box::new(bottom_item.clone()),
7621                None,
7622                false,
7623                window,
7624                cx,
7625            );
7626            workspace.split_pane(
7627                workspace.active_pane().clone(),
7628                SplitDirection::Up,
7629                window,
7630                cx,
7631            );
7632            bottom_pane_id
7633        });
7634        let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7635            let left_pane_id = workspace.active_pane().entity_id();
7636            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
7637            workspace.split_pane(
7638                workspace.active_pane().clone(),
7639                SplitDirection::Right,
7640                window,
7641                cx,
7642            );
7643            left_pane_id
7644        });
7645        let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7646            let right_pane_id = workspace.active_pane().entity_id();
7647            workspace.add_item_to_active_pane(
7648                Box::new(right_item.clone()),
7649                None,
7650                false,
7651                window,
7652                cx,
7653            );
7654            workspace.split_pane(
7655                workspace.active_pane().clone(),
7656                SplitDirection::Left,
7657                window,
7658                cx,
7659            );
7660            right_pane_id
7661        });
7662        let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7663            let center_pane_id = workspace.active_pane().entity_id();
7664            workspace.add_item_to_active_pane(
7665                Box::new(center_item.clone()),
7666                None,
7667                false,
7668                window,
7669                cx,
7670            );
7671            center_pane_id
7672        });
7673        cx.executor().run_until_parked();
7674
7675        workspace.update_in(cx, |workspace, window, cx| {
7676            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7677
7678            // Join into next from center pane into right
7679            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7680        });
7681
7682        workspace.update_in(cx, |workspace, window, cx| {
7683            let active_pane = workspace.active_pane();
7684            assert_eq!(right_pane_id, active_pane.entity_id());
7685            assert_eq!(2, active_pane.read(cx).items_len());
7686            let item_ids_in_pane =
7687                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7688            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7689            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7690
7691            // Join into next from right pane into bottom
7692            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7693        });
7694
7695        workspace.update_in(cx, |workspace, window, cx| {
7696            let active_pane = workspace.active_pane();
7697            assert_eq!(bottom_pane_id, active_pane.entity_id());
7698            assert_eq!(3, active_pane.read(cx).items_len());
7699            let item_ids_in_pane =
7700                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7701            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7702            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7703            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7704
7705            // Join into next from bottom pane into left
7706            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7707        });
7708
7709        workspace.update_in(cx, |workspace, window, cx| {
7710            let active_pane = workspace.active_pane();
7711            assert_eq!(left_pane_id, active_pane.entity_id());
7712            assert_eq!(4, active_pane.read(cx).items_len());
7713            let item_ids_in_pane =
7714                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7715            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7716            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7717            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7718            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7719
7720            // Join into next from left pane into top
7721            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7722        });
7723
7724        workspace.update_in(cx, |workspace, window, cx| {
7725            let active_pane = workspace.active_pane();
7726            assert_eq!(top_pane_id, active_pane.entity_id());
7727            assert_eq!(5, active_pane.read(cx).items_len());
7728            let item_ids_in_pane =
7729                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7730            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7731            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7732            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7733            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7734            assert!(item_ids_in_pane.contains(&top_item.item_id()));
7735
7736            // Single pane left: no-op
7737            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
7738        });
7739
7740        workspace.update(cx, |workspace, _cx| {
7741            let active_pane = workspace.active_pane();
7742            assert_eq!(top_pane_id, active_pane.entity_id());
7743        });
7744    }
7745
7746    fn add_an_item_to_active_pane(
7747        cx: &mut VisualTestContext,
7748        workspace: &Entity<Workspace>,
7749        item_id: u64,
7750    ) -> Entity<TestItem> {
7751        let item = cx.new(|cx| {
7752            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7753                item_id,
7754                "item{item_id}.txt",
7755                cx,
7756            )])
7757        });
7758        workspace.update_in(cx, |workspace, window, cx| {
7759            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
7760        });
7761        return item;
7762    }
7763
7764    fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
7765        return workspace.update_in(cx, |workspace, window, cx| {
7766            let new_pane = workspace.split_pane(
7767                workspace.active_pane().clone(),
7768                SplitDirection::Right,
7769                window,
7770                cx,
7771            );
7772            new_pane
7773        });
7774    }
7775
7776    #[gpui::test]
7777    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7778        init_test(cx);
7779        let fs = FakeFs::new(cx.executor());
7780        let project = Project::test(fs, None, cx).await;
7781        let (workspace, cx) =
7782            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7783
7784        add_an_item_to_active_pane(cx, &workspace, 1);
7785        split_pane(cx, &workspace);
7786        add_an_item_to_active_pane(cx, &workspace, 2);
7787        split_pane(cx, &workspace); // empty pane
7788        split_pane(cx, &workspace);
7789        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7790
7791        cx.executor().run_until_parked();
7792
7793        workspace.update(cx, |workspace, cx| {
7794            let num_panes = workspace.panes().len();
7795            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7796            let active_item = workspace
7797                .active_pane()
7798                .read(cx)
7799                .active_item()
7800                .expect("item is in focus");
7801
7802            assert_eq!(num_panes, 4);
7803            assert_eq!(num_items_in_current_pane, 1);
7804            assert_eq!(active_item.item_id(), last_item.item_id());
7805        });
7806
7807        workspace.update_in(cx, |workspace, window, cx| {
7808            workspace.join_all_panes(window, cx);
7809        });
7810
7811        workspace.update(cx, |workspace, cx| {
7812            let num_panes = workspace.panes().len();
7813            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7814            let active_item = workspace
7815                .active_pane()
7816                .read(cx)
7817                .active_item()
7818                .expect("item is in focus");
7819
7820            assert_eq!(num_panes, 1);
7821            assert_eq!(num_items_in_current_pane, 3);
7822            assert_eq!(active_item.item_id(), last_item.item_id());
7823        });
7824    }
7825    struct TestModal(FocusHandle);
7826
7827    impl TestModal {
7828        fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
7829            Self(cx.focus_handle())
7830        }
7831    }
7832
7833    impl EventEmitter<DismissEvent> for TestModal {}
7834
7835    impl Focusable for TestModal {
7836        fn focus_handle(&self, _cx: &App) -> FocusHandle {
7837            self.0.clone()
7838        }
7839    }
7840
7841    impl ModalView for TestModal {}
7842
7843    impl Render for TestModal {
7844        fn render(
7845            &mut self,
7846            _window: &mut Window,
7847            _cx: &mut Context<TestModal>,
7848        ) -> impl IntoElement {
7849            div().track_focus(&self.0)
7850        }
7851    }
7852
7853    #[gpui::test]
7854    async fn test_panels(cx: &mut gpui::TestAppContext) {
7855        init_test(cx);
7856        let fs = FakeFs::new(cx.executor());
7857
7858        let project = Project::test(fs, [], cx).await;
7859        let (workspace, cx) =
7860            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7861
7862        let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
7863            let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
7864            workspace.add_panel(panel_1.clone(), window, cx);
7865            workspace.toggle_dock(DockPosition::Left, window, cx);
7866            let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7867            workspace.add_panel(panel_2.clone(), window, cx);
7868            workspace.toggle_dock(DockPosition::Right, window, cx);
7869
7870            let left_dock = workspace.left_dock();
7871            assert_eq!(
7872                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7873                panel_1.panel_id()
7874            );
7875            assert_eq!(
7876                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7877                panel_1.size(window, cx)
7878            );
7879
7880            left_dock.update(cx, |left_dock, cx| {
7881                left_dock.resize_active_panel(Some(px(1337.)), window, cx)
7882            });
7883            assert_eq!(
7884                workspace
7885                    .right_dock()
7886                    .read(cx)
7887                    .visible_panel()
7888                    .unwrap()
7889                    .panel_id(),
7890                panel_2.panel_id(),
7891            );
7892
7893            (panel_1, panel_2)
7894        });
7895
7896        // Move panel_1 to the right
7897        panel_1.update_in(cx, |panel_1, window, cx| {
7898            panel_1.set_position(DockPosition::Right, window, cx)
7899        });
7900
7901        workspace.update_in(cx, |workspace, window, cx| {
7902            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7903            // Since it was the only panel on the left, the left dock should now be closed.
7904            assert!(!workspace.left_dock().read(cx).is_open());
7905            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7906            let right_dock = workspace.right_dock();
7907            assert_eq!(
7908                right_dock.read(cx).visible_panel().unwrap().panel_id(),
7909                panel_1.panel_id()
7910            );
7911            assert_eq!(
7912                right_dock.read(cx).active_panel_size(window, cx).unwrap(),
7913                px(1337.)
7914            );
7915
7916            // Now we move panel_2 to the left
7917            panel_2.set_position(DockPosition::Left, window, cx);
7918        });
7919
7920        workspace.update(cx, |workspace, cx| {
7921            // Since panel_2 was not visible on the right, we don't open the left dock.
7922            assert!(!workspace.left_dock().read(cx).is_open());
7923            // And the right dock is unaffected in its displaying of panel_1
7924            assert!(workspace.right_dock().read(cx).is_open());
7925            assert_eq!(
7926                workspace
7927                    .right_dock()
7928                    .read(cx)
7929                    .visible_panel()
7930                    .unwrap()
7931                    .panel_id(),
7932                panel_1.panel_id(),
7933            );
7934        });
7935
7936        // Move panel_1 back to the left
7937        panel_1.update_in(cx, |panel_1, window, cx| {
7938            panel_1.set_position(DockPosition::Left, window, cx)
7939        });
7940
7941        workspace.update_in(cx, |workspace, window, cx| {
7942            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7943            let left_dock = workspace.left_dock();
7944            assert!(left_dock.read(cx).is_open());
7945            assert_eq!(
7946                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7947                panel_1.panel_id()
7948            );
7949            assert_eq!(
7950                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7951                px(1337.)
7952            );
7953            // And the right dock should be closed as it no longer has any panels.
7954            assert!(!workspace.right_dock().read(cx).is_open());
7955
7956            // Now we move panel_1 to the bottom
7957            panel_1.set_position(DockPosition::Bottom, window, cx);
7958        });
7959
7960        workspace.update_in(cx, |workspace, window, cx| {
7961            // Since panel_1 was visible on the left, we close the left dock.
7962            assert!(!workspace.left_dock().read(cx).is_open());
7963            // The bottom dock is sized based on the panel's default size,
7964            // since the panel orientation changed from vertical to horizontal.
7965            let bottom_dock = workspace.bottom_dock();
7966            assert_eq!(
7967                bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
7968                panel_1.size(window, cx),
7969            );
7970            // Close bottom dock and move panel_1 back to the left.
7971            bottom_dock.update(cx, |bottom_dock, cx| {
7972                bottom_dock.set_open(false, window, cx)
7973            });
7974            panel_1.set_position(DockPosition::Left, window, cx);
7975        });
7976
7977        // Emit activated event on panel 1
7978        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7979
7980        // Now the left dock is open and panel_1 is active and focused.
7981        workspace.update_in(cx, |workspace, window, cx| {
7982            let left_dock = workspace.left_dock();
7983            assert!(left_dock.read(cx).is_open());
7984            assert_eq!(
7985                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7986                panel_1.panel_id(),
7987            );
7988            assert!(panel_1.focus_handle(cx).is_focused(window));
7989        });
7990
7991        // Emit closed event on panel 2, which is not active
7992        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7993
7994        // Wo don't close the left dock, because panel_2 wasn't the active panel
7995        workspace.update(cx, |workspace, cx| {
7996            let left_dock = workspace.left_dock();
7997            assert!(left_dock.read(cx).is_open());
7998            assert_eq!(
7999                left_dock.read(cx).visible_panel().unwrap().panel_id(),
8000                panel_1.panel_id(),
8001            );
8002        });
8003
8004        // Emitting a ZoomIn event shows the panel as zoomed.
8005        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
8006        workspace.update(cx, |workspace, _| {
8007            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8008            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
8009        });
8010
8011        // Move panel to another dock while it is zoomed
8012        panel_1.update_in(cx, |panel, window, cx| {
8013            panel.set_position(DockPosition::Right, window, cx)
8014        });
8015        workspace.update(cx, |workspace, _| {
8016            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8017
8018            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8019        });
8020
8021        // This is a helper for getting a:
8022        // - valid focus on an element,
8023        // - that isn't a part of the panes and panels system of the Workspace,
8024        // - and doesn't trigger the 'on_focus_lost' API.
8025        let focus_other_view = {
8026            let workspace = workspace.clone();
8027            move |cx: &mut VisualTestContext| {
8028                workspace.update_in(cx, |workspace, window, cx| {
8029                    if let Some(_) = workspace.active_modal::<TestModal>(cx) {
8030                        workspace.toggle_modal(window, cx, TestModal::new);
8031                        workspace.toggle_modal(window, cx, TestModal::new);
8032                    } else {
8033                        workspace.toggle_modal(window, cx, TestModal::new);
8034                    }
8035                })
8036            }
8037        };
8038
8039        // If focus is transferred to another view that's not a panel or another pane, we still show
8040        // the panel as zoomed.
8041        focus_other_view(cx);
8042        workspace.update(cx, |workspace, _| {
8043            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8044            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8045        });
8046
8047        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8048        workspace.update_in(cx, |_workspace, window, cx| {
8049            cx.focus_self(window);
8050        });
8051        workspace.update(cx, |workspace, _| {
8052            assert_eq!(workspace.zoomed, None);
8053            assert_eq!(workspace.zoomed_position, None);
8054        });
8055
8056        // If focus is transferred again to another view that's not a panel or a pane, we won't
8057        // show the panel as zoomed because it wasn't zoomed before.
8058        focus_other_view(cx);
8059        workspace.update(cx, |workspace, _| {
8060            assert_eq!(workspace.zoomed, None);
8061            assert_eq!(workspace.zoomed_position, None);
8062        });
8063
8064        // When the panel is activated, it is zoomed again.
8065        cx.dispatch_action(ToggleRightDock);
8066        workspace.update(cx, |workspace, _| {
8067            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8068            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8069        });
8070
8071        // Emitting a ZoomOut event unzooms the panel.
8072        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8073        workspace.update(cx, |workspace, _| {
8074            assert_eq!(workspace.zoomed, None);
8075            assert_eq!(workspace.zoomed_position, None);
8076        });
8077
8078        // Emit closed event on panel 1, which is active
8079        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8080
8081        // Now the left dock is closed, because panel_1 was the active panel
8082        workspace.update(cx, |workspace, cx| {
8083            let right_dock = workspace.right_dock();
8084            assert!(!right_dock.read(cx).is_open());
8085        });
8086    }
8087
8088    #[gpui::test]
8089    async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8090        init_test(cx);
8091
8092        let fs = FakeFs::new(cx.background_executor.clone());
8093        let project = Project::test(fs, [], cx).await;
8094        let (workspace, cx) =
8095            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8096        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8097
8098        let dirty_regular_buffer = cx.new(|cx| {
8099            TestItem::new(cx)
8100                .with_dirty(true)
8101                .with_label("1.txt")
8102                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8103        });
8104        let dirty_regular_buffer_2 = cx.new(|cx| {
8105            TestItem::new(cx)
8106                .with_dirty(true)
8107                .with_label("2.txt")
8108                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8109        });
8110        let dirty_multi_buffer_with_both = cx.new(|cx| {
8111            TestItem::new(cx)
8112                .with_dirty(true)
8113                .with_singleton(false)
8114                .with_label("Fake Project Search")
8115                .with_project_items(&[
8116                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8117                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8118                ])
8119        });
8120        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8121        workspace.update_in(cx, |workspace, window, cx| {
8122            workspace.add_item(
8123                pane.clone(),
8124                Box::new(dirty_regular_buffer.clone()),
8125                None,
8126                false,
8127                false,
8128                window,
8129                cx,
8130            );
8131            workspace.add_item(
8132                pane.clone(),
8133                Box::new(dirty_regular_buffer_2.clone()),
8134                None,
8135                false,
8136                false,
8137                window,
8138                cx,
8139            );
8140            workspace.add_item(
8141                pane.clone(),
8142                Box::new(dirty_multi_buffer_with_both.clone()),
8143                None,
8144                false,
8145                false,
8146                window,
8147                cx,
8148            );
8149        });
8150
8151        pane.update_in(cx, |pane, window, cx| {
8152            pane.activate_item(2, true, true, window, cx);
8153            assert_eq!(
8154                pane.active_item().unwrap().item_id(),
8155                multi_buffer_with_both_files_id,
8156                "Should select the multi buffer in the pane"
8157            );
8158        });
8159        let close_all_but_multi_buffer_task = pane
8160            .update_in(cx, |pane, window, cx| {
8161                pane.close_inactive_items(
8162                    &CloseInactiveItems {
8163                        save_intent: Some(SaveIntent::Save),
8164                        close_pinned: true,
8165                    },
8166                    window,
8167                    cx,
8168                )
8169            })
8170            .expect("should have inactive files to close");
8171        cx.background_executor.run_until_parked();
8172        assert!(!cx.has_pending_prompt());
8173        close_all_but_multi_buffer_task
8174            .await
8175            .expect("Closing all buffers but the multi buffer failed");
8176        pane.update(cx, |pane, cx| {
8177            assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
8178            assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8179            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
8180            assert_eq!(pane.items_len(), 1);
8181            assert_eq!(
8182                pane.active_item().unwrap().item_id(),
8183                multi_buffer_with_both_files_id,
8184                "Should have only the multi buffer left in the pane"
8185            );
8186            assert!(
8187                dirty_multi_buffer_with_both.read(cx).is_dirty,
8188                "The multi buffer containing the unsaved buffer should still be dirty"
8189            );
8190        });
8191
8192        dirty_regular_buffer.update(cx, |buffer, cx| {
8193            buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
8194        });
8195
8196        let close_multi_buffer_task = pane
8197            .update_in(cx, |pane, window, cx| {
8198                pane.close_active_item(
8199                    &CloseActiveItem {
8200                        save_intent: Some(SaveIntent::Close),
8201                        close_pinned: false,
8202                    },
8203                    window,
8204                    cx,
8205                )
8206            })
8207            .expect("should have the multi buffer to close");
8208        cx.background_executor.run_until_parked();
8209        assert!(
8210            cx.has_pending_prompt(),
8211            "Dirty multi buffer should prompt a save dialog"
8212        );
8213        cx.simulate_prompt_answer("Save");
8214        cx.background_executor.run_until_parked();
8215        close_multi_buffer_task
8216            .await
8217            .expect("Closing the multi buffer failed");
8218        pane.update(cx, |pane, cx| {
8219            assert_eq!(
8220                dirty_multi_buffer_with_both.read(cx).save_count,
8221                1,
8222                "Multi buffer item should get be saved"
8223            );
8224            // Test impl does not save inner items, so we do not assert them
8225            assert_eq!(
8226                pane.items_len(),
8227                0,
8228                "No more items should be left in the pane"
8229            );
8230            assert!(pane.active_item().is_none());
8231        });
8232    }
8233
8234    #[gpui::test]
8235    async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
8236        cx: &mut TestAppContext,
8237    ) {
8238        init_test(cx);
8239
8240        let fs = FakeFs::new(cx.background_executor.clone());
8241        let project = Project::test(fs, [], cx).await;
8242        let (workspace, cx) =
8243            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8244        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8245
8246        let dirty_regular_buffer = cx.new(|cx| {
8247            TestItem::new(cx)
8248                .with_dirty(true)
8249                .with_label("1.txt")
8250                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8251        });
8252        let dirty_regular_buffer_2 = cx.new(|cx| {
8253            TestItem::new(cx)
8254                .with_dirty(true)
8255                .with_label("2.txt")
8256                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8257        });
8258        let clear_regular_buffer = cx.new(|cx| {
8259            TestItem::new(cx)
8260                .with_label("3.txt")
8261                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8262        });
8263
8264        let dirty_multi_buffer_with_both = cx.new(|cx| {
8265            TestItem::new(cx)
8266                .with_dirty(true)
8267                .with_singleton(false)
8268                .with_label("Fake Project Search")
8269                .with_project_items(&[
8270                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8271                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8272                    clear_regular_buffer.read(cx).project_items[0].clone(),
8273                ])
8274        });
8275        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8276        workspace.update_in(cx, |workspace, window, cx| {
8277            workspace.add_item(
8278                pane.clone(),
8279                Box::new(dirty_regular_buffer.clone()),
8280                None,
8281                false,
8282                false,
8283                window,
8284                cx,
8285            );
8286            workspace.add_item(
8287                pane.clone(),
8288                Box::new(dirty_multi_buffer_with_both.clone()),
8289                None,
8290                false,
8291                false,
8292                window,
8293                cx,
8294            );
8295        });
8296
8297        pane.update_in(cx, |pane, window, cx| {
8298            pane.activate_item(1, true, true, window, cx);
8299            assert_eq!(
8300                pane.active_item().unwrap().item_id(),
8301                multi_buffer_with_both_files_id,
8302                "Should select the multi buffer in the pane"
8303            );
8304        });
8305        let _close_multi_buffer_task = pane
8306            .update_in(cx, |pane, window, cx| {
8307                pane.close_active_item(
8308                    &CloseActiveItem {
8309                        save_intent: None,
8310                        close_pinned: false,
8311                    },
8312                    window,
8313                    cx,
8314                )
8315            })
8316            .expect("should have active multi buffer to close");
8317        cx.background_executor.run_until_parked();
8318        assert!(
8319            cx.has_pending_prompt(),
8320            "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
8321        );
8322    }
8323
8324    #[gpui::test]
8325    async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
8326        cx: &mut TestAppContext,
8327    ) {
8328        init_test(cx);
8329
8330        let fs = FakeFs::new(cx.background_executor.clone());
8331        let project = Project::test(fs, [], cx).await;
8332        let (workspace, cx) =
8333            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8334        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8335
8336        let dirty_regular_buffer = cx.new(|cx| {
8337            TestItem::new(cx)
8338                .with_dirty(true)
8339                .with_label("1.txt")
8340                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8341        });
8342        let dirty_regular_buffer_2 = cx.new(|cx| {
8343            TestItem::new(cx)
8344                .with_dirty(true)
8345                .with_label("2.txt")
8346                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8347        });
8348        let clear_regular_buffer = cx.new(|cx| {
8349            TestItem::new(cx)
8350                .with_label("3.txt")
8351                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8352        });
8353
8354        let dirty_multi_buffer = cx.new(|cx| {
8355            TestItem::new(cx)
8356                .with_dirty(true)
8357                .with_singleton(false)
8358                .with_label("Fake Project Search")
8359                .with_project_items(&[
8360                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8361                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8362                    clear_regular_buffer.read(cx).project_items[0].clone(),
8363                ])
8364        });
8365        workspace.update_in(cx, |workspace, window, cx| {
8366            workspace.add_item(
8367                pane.clone(),
8368                Box::new(dirty_regular_buffer.clone()),
8369                None,
8370                false,
8371                false,
8372                window,
8373                cx,
8374            );
8375            workspace.add_item(
8376                pane.clone(),
8377                Box::new(dirty_regular_buffer_2.clone()),
8378                None,
8379                false,
8380                false,
8381                window,
8382                cx,
8383            );
8384            workspace.add_item(
8385                pane.clone(),
8386                Box::new(dirty_multi_buffer.clone()),
8387                None,
8388                false,
8389                false,
8390                window,
8391                cx,
8392            );
8393        });
8394
8395        pane.update_in(cx, |pane, window, cx| {
8396            pane.activate_item(2, true, true, window, cx);
8397            assert_eq!(
8398                pane.active_item().unwrap().item_id(),
8399                dirty_multi_buffer.item_id(),
8400                "Should select the multi buffer in the pane"
8401            );
8402        });
8403        let close_multi_buffer_task = pane
8404            .update_in(cx, |pane, window, cx| {
8405                pane.close_active_item(
8406                    &CloseActiveItem {
8407                        save_intent: None,
8408                        close_pinned: false,
8409                    },
8410                    window,
8411                    cx,
8412                )
8413            })
8414            .expect("should have active multi buffer to close");
8415        cx.background_executor.run_until_parked();
8416        assert!(
8417            !cx.has_pending_prompt(),
8418            "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8419        );
8420        close_multi_buffer_task
8421            .await
8422            .expect("Closing multi buffer failed");
8423        pane.update(cx, |pane, cx| {
8424            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8425            assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8426            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8427            assert_eq!(
8428                pane.items()
8429                    .map(|item| item.item_id())
8430                    .sorted()
8431                    .collect::<Vec<_>>(),
8432                vec![
8433                    dirty_regular_buffer.item_id(),
8434                    dirty_regular_buffer_2.item_id(),
8435                ],
8436                "Should have no multi buffer left in the pane"
8437            );
8438            assert!(dirty_regular_buffer.read(cx).is_dirty);
8439            assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8440        });
8441    }
8442
8443    #[gpui::test]
8444    async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8445        init_test(cx);
8446        let fs = FakeFs::new(cx.executor());
8447        let project = Project::test(fs, [], cx).await;
8448        let (workspace, cx) =
8449            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8450
8451        // Add a new panel to the right dock, opening the dock and setting the
8452        // focus to the new panel.
8453        let panel = workspace.update_in(cx, |workspace, window, cx| {
8454            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8455            workspace.add_panel(panel.clone(), window, cx);
8456
8457            workspace
8458                .right_dock()
8459                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8460
8461            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8462
8463            panel
8464        });
8465
8466        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8467        // panel to the next valid position which, in this case, is the left
8468        // dock.
8469        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8470        workspace.update(cx, |workspace, cx| {
8471            assert!(workspace.left_dock().read(cx).is_open());
8472            assert_eq!(panel.read(cx).position, DockPosition::Left);
8473        });
8474
8475        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8476        // panel to the next valid position which, in this case, is the bottom
8477        // dock.
8478        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8479        workspace.update(cx, |workspace, cx| {
8480            assert!(workspace.bottom_dock().read(cx).is_open());
8481            assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8482        });
8483
8484        // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8485        // around moving the panel to its initial position, the right dock.
8486        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8487        workspace.update(cx, |workspace, cx| {
8488            assert!(workspace.right_dock().read(cx).is_open());
8489            assert_eq!(panel.read(cx).position, DockPosition::Right);
8490        });
8491
8492        // Remove focus from the panel, ensuring that, if the panel is not
8493        // focused, the `MoveFocusedPanelToNextPosition` action does not update
8494        // the panel's position, so the panel is still in the right dock.
8495        workspace.update_in(cx, |workspace, window, cx| {
8496            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8497        });
8498
8499        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8500        workspace.update(cx, |workspace, cx| {
8501            assert!(workspace.right_dock().read(cx).is_open());
8502            assert_eq!(panel.read(cx).position, DockPosition::Right);
8503        });
8504    }
8505
8506    mod register_project_item_tests {
8507
8508        use super::*;
8509
8510        // View
8511        struct TestPngItemView {
8512            focus_handle: FocusHandle,
8513        }
8514        // Model
8515        struct TestPngItem {}
8516
8517        impl project::ProjectItem for TestPngItem {
8518            fn try_open(
8519                _project: &Entity<Project>,
8520                path: &ProjectPath,
8521                cx: &mut App,
8522            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8523                if path.path.extension().unwrap() == "png" {
8524                    Some(cx.spawn(|mut cx| async move { cx.new(|_| TestPngItem {}) }))
8525                } else {
8526                    None
8527                }
8528            }
8529
8530            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8531                None
8532            }
8533
8534            fn project_path(&self, _: &App) -> Option<ProjectPath> {
8535                None
8536            }
8537
8538            fn is_dirty(&self) -> bool {
8539                false
8540            }
8541        }
8542
8543        impl Item for TestPngItemView {
8544            type Event = ();
8545        }
8546        impl EventEmitter<()> for TestPngItemView {}
8547        impl Focusable for TestPngItemView {
8548            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8549                self.focus_handle.clone()
8550            }
8551        }
8552
8553        impl Render for TestPngItemView {
8554            fn render(
8555                &mut self,
8556                _window: &mut Window,
8557                _cx: &mut Context<Self>,
8558            ) -> impl IntoElement {
8559                Empty
8560            }
8561        }
8562
8563        impl ProjectItem for TestPngItemView {
8564            type Item = TestPngItem;
8565
8566            fn for_project_item(
8567                _project: Entity<Project>,
8568                _item: Entity<Self::Item>,
8569                _: &mut Window,
8570                cx: &mut Context<Self>,
8571            ) -> Self
8572            where
8573                Self: Sized,
8574            {
8575                Self {
8576                    focus_handle: cx.focus_handle(),
8577                }
8578            }
8579        }
8580
8581        // View
8582        struct TestIpynbItemView {
8583            focus_handle: FocusHandle,
8584        }
8585        // Model
8586        struct TestIpynbItem {}
8587
8588        impl project::ProjectItem for TestIpynbItem {
8589            fn try_open(
8590                _project: &Entity<Project>,
8591                path: &ProjectPath,
8592                cx: &mut App,
8593            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8594                if path.path.extension().unwrap() == "ipynb" {
8595                    Some(cx.spawn(|mut cx| async move { cx.new(|_| TestIpynbItem {}) }))
8596                } else {
8597                    None
8598                }
8599            }
8600
8601            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8602                None
8603            }
8604
8605            fn project_path(&self, _: &App) -> Option<ProjectPath> {
8606                None
8607            }
8608
8609            fn is_dirty(&self) -> bool {
8610                false
8611            }
8612        }
8613
8614        impl Item for TestIpynbItemView {
8615            type Event = ();
8616        }
8617        impl EventEmitter<()> for TestIpynbItemView {}
8618        impl Focusable for TestIpynbItemView {
8619            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8620                self.focus_handle.clone()
8621            }
8622        }
8623
8624        impl Render for TestIpynbItemView {
8625            fn render(
8626                &mut self,
8627                _window: &mut Window,
8628                _cx: &mut Context<Self>,
8629            ) -> impl IntoElement {
8630                Empty
8631            }
8632        }
8633
8634        impl ProjectItem for TestIpynbItemView {
8635            type Item = TestIpynbItem;
8636
8637            fn for_project_item(
8638                _project: Entity<Project>,
8639                _item: Entity<Self::Item>,
8640                _: &mut Window,
8641                cx: &mut Context<Self>,
8642            ) -> Self
8643            where
8644                Self: Sized,
8645            {
8646                Self {
8647                    focus_handle: cx.focus_handle(),
8648                }
8649            }
8650        }
8651
8652        struct TestAlternatePngItemView {
8653            focus_handle: FocusHandle,
8654        }
8655
8656        impl Item for TestAlternatePngItemView {
8657            type Event = ();
8658        }
8659
8660        impl EventEmitter<()> for TestAlternatePngItemView {}
8661        impl Focusable for TestAlternatePngItemView {
8662            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8663                self.focus_handle.clone()
8664            }
8665        }
8666
8667        impl Render for TestAlternatePngItemView {
8668            fn render(
8669                &mut self,
8670                _window: &mut Window,
8671                _cx: &mut Context<Self>,
8672            ) -> impl IntoElement {
8673                Empty
8674            }
8675        }
8676
8677        impl ProjectItem for TestAlternatePngItemView {
8678            type Item = TestPngItem;
8679
8680            fn for_project_item(
8681                _project: Entity<Project>,
8682                _item: Entity<Self::Item>,
8683                _: &mut Window,
8684                cx: &mut Context<Self>,
8685            ) -> Self
8686            where
8687                Self: Sized,
8688            {
8689                Self {
8690                    focus_handle: cx.focus_handle(),
8691                }
8692            }
8693        }
8694
8695        #[gpui::test]
8696        async fn test_register_project_item(cx: &mut TestAppContext) {
8697            init_test(cx);
8698
8699            cx.update(|cx| {
8700                register_project_item::<TestPngItemView>(cx);
8701                register_project_item::<TestIpynbItemView>(cx);
8702            });
8703
8704            let fs = FakeFs::new(cx.executor());
8705            fs.insert_tree(
8706                "/root1",
8707                json!({
8708                    "one.png": "BINARYDATAHERE",
8709                    "two.ipynb": "{ totally a notebook }",
8710                    "three.txt": "editing text, sure why not?"
8711                }),
8712            )
8713            .await;
8714
8715            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8716            let (workspace, cx) =
8717                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8718
8719            let worktree_id = project.update(cx, |project, cx| {
8720                project.worktrees(cx).next().unwrap().read(cx).id()
8721            });
8722
8723            let handle = workspace
8724                .update_in(cx, |workspace, window, cx| {
8725                    let project_path = (worktree_id, "one.png");
8726                    workspace.open_path(project_path, None, true, window, cx)
8727                })
8728                .await
8729                .unwrap();
8730
8731            // Now we can check if the handle we got back errored or not
8732            assert_eq!(
8733                handle.to_any().entity_type(),
8734                TypeId::of::<TestPngItemView>()
8735            );
8736
8737            let handle = workspace
8738                .update_in(cx, |workspace, window, cx| {
8739                    let project_path = (worktree_id, "two.ipynb");
8740                    workspace.open_path(project_path, None, true, window, cx)
8741                })
8742                .await
8743                .unwrap();
8744
8745            assert_eq!(
8746                handle.to_any().entity_type(),
8747                TypeId::of::<TestIpynbItemView>()
8748            );
8749
8750            let handle = workspace
8751                .update_in(cx, |workspace, window, cx| {
8752                    let project_path = (worktree_id, "three.txt");
8753                    workspace.open_path(project_path, None, true, window, cx)
8754                })
8755                .await;
8756            assert!(handle.is_err());
8757        }
8758
8759        #[gpui::test]
8760        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8761            init_test(cx);
8762
8763            cx.update(|cx| {
8764                register_project_item::<TestPngItemView>(cx);
8765                register_project_item::<TestAlternatePngItemView>(cx);
8766            });
8767
8768            let fs = FakeFs::new(cx.executor());
8769            fs.insert_tree(
8770                "/root1",
8771                json!({
8772                    "one.png": "BINARYDATAHERE",
8773                    "two.ipynb": "{ totally a notebook }",
8774                    "three.txt": "editing text, sure why not?"
8775                }),
8776            )
8777            .await;
8778            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8779            let (workspace, cx) =
8780                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8781            let worktree_id = project.update(cx, |project, cx| {
8782                project.worktrees(cx).next().unwrap().read(cx).id()
8783            });
8784
8785            let handle = workspace
8786                .update_in(cx, |workspace, window, cx| {
8787                    let project_path = (worktree_id, "one.png");
8788                    workspace.open_path(project_path, None, true, window, cx)
8789                })
8790                .await
8791                .unwrap();
8792
8793            // This _must_ be the second item registered
8794            assert_eq!(
8795                handle.to_any().entity_type(),
8796                TypeId::of::<TestAlternatePngItemView>()
8797            );
8798
8799            let handle = workspace
8800                .update_in(cx, |workspace, window, cx| {
8801                    let project_path = (worktree_id, "three.txt");
8802                    workspace.open_path(project_path, None, true, window, cx)
8803                })
8804                .await;
8805            assert!(handle.is_err());
8806        }
8807    }
8808
8809    pub fn init_test(cx: &mut TestAppContext) {
8810        cx.update(|cx| {
8811            let settings_store = SettingsStore::test(cx);
8812            cx.set_global(settings_store);
8813            theme::init(theme::LoadThemes::JustBase, cx);
8814            language::init(cx);
8815            crate::init_settings(cx);
8816            Project::init_settings(cx);
8817        });
8818    }
8819
8820    fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
8821        let item = TestProjectItem::new(id, path, cx);
8822        item.update(cx, |item, _| {
8823            item.is_dirty = true;
8824        });
8825        item
8826    }
8827}