workspace.rs

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