workspace.rs

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