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