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