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