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