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