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