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