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, _: &mut Window, cx: &mut Context<Self>) {
5495        if let Some((notification_id, _)) = self.notifications.pop() {
5496            dismiss_app_notification(&notification_id, cx);
5497            return;
5498        }
5499
5500        cx.emit(Event::ClearActivityIndicator);
5501        cx.propagate();
5502    }
5503}
5504
5505fn leader_border_for_pane(
5506    follower_states: &HashMap<CollaboratorId, FollowerState>,
5507    pane: &Entity<Pane>,
5508    _: &Window,
5509    cx: &App,
5510) -> Option<Div> {
5511    let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
5512        if state.pane() == pane {
5513            Some((*leader_id, state))
5514        } else {
5515            None
5516        }
5517    })?;
5518
5519    let mut leader_color = match leader_id {
5520        CollaboratorId::PeerId(leader_peer_id) => {
5521            let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
5522            let leader = room.remote_participant_for_peer_id(leader_peer_id)?;
5523
5524            cx.theme()
5525                .players()
5526                .color_for_participant(leader.participant_index.0)
5527                .cursor
5528        }
5529        CollaboratorId::Agent => cx.theme().players().agent().cursor,
5530    };
5531    leader_color.fade_out(0.3);
5532    Some(
5533        div()
5534            .absolute()
5535            .size_full()
5536            .left_0()
5537            .top_0()
5538            .border_2()
5539            .border_color(leader_color),
5540    )
5541}
5542
5543fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
5544    ZED_WINDOW_POSITION
5545        .zip(*ZED_WINDOW_SIZE)
5546        .map(|(position, size)| Bounds {
5547            origin: position,
5548            size,
5549        })
5550}
5551
5552fn open_items(
5553    serialized_workspace: Option<SerializedWorkspace>,
5554    mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
5555    window: &mut Window,
5556    cx: &mut Context<Workspace>,
5557) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
5558    let restored_items = serialized_workspace.map(|serialized_workspace| {
5559        Workspace::load_workspace(
5560            serialized_workspace,
5561            project_paths_to_open
5562                .iter()
5563                .map(|(_, project_path)| project_path)
5564                .cloned()
5565                .collect(),
5566            window,
5567            cx,
5568        )
5569    });
5570
5571    cx.spawn_in(window, async move |workspace, cx| {
5572        let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
5573
5574        if let Some(restored_items) = restored_items {
5575            let restored_items = restored_items.await?;
5576
5577            let restored_project_paths = restored_items
5578                .iter()
5579                .filter_map(|item| {
5580                    cx.update(|_, cx| item.as_ref()?.project_path(cx))
5581                        .ok()
5582                        .flatten()
5583                })
5584                .collect::<HashSet<_>>();
5585
5586            for restored_item in restored_items {
5587                opened_items.push(restored_item.map(Ok));
5588            }
5589
5590            project_paths_to_open
5591                .iter_mut()
5592                .for_each(|(_, project_path)| {
5593                    if let Some(project_path_to_open) = project_path {
5594                        if restored_project_paths.contains(project_path_to_open) {
5595                            *project_path = None;
5596                        }
5597                    }
5598                });
5599        } else {
5600            for _ in 0..project_paths_to_open.len() {
5601                opened_items.push(None);
5602            }
5603        }
5604        assert!(opened_items.len() == project_paths_to_open.len());
5605
5606        let tasks =
5607            project_paths_to_open
5608                .into_iter()
5609                .enumerate()
5610                .map(|(ix, (abs_path, project_path))| {
5611                    let workspace = workspace.clone();
5612                    cx.spawn(async move |cx| {
5613                        let file_project_path = project_path?;
5614                        let abs_path_task = workspace.update(cx, |workspace, cx| {
5615                            workspace.project().update(cx, |project, cx| {
5616                                project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
5617                            })
5618                        });
5619
5620                        // We only want to open file paths here. If one of the items
5621                        // here is a directory, it was already opened further above
5622                        // with a `find_or_create_worktree`.
5623                        if let Ok(task) = abs_path_task {
5624                            if task.await.map_or(true, |p| p.is_file()) {
5625                                return Some((
5626                                    ix,
5627                                    workspace
5628                                        .update_in(cx, |workspace, window, cx| {
5629                                            workspace.open_path(
5630                                                file_project_path,
5631                                                None,
5632                                                true,
5633                                                window,
5634                                                cx,
5635                                            )
5636                                        })
5637                                        .log_err()?
5638                                        .await,
5639                                ));
5640                            }
5641                        }
5642                        None
5643                    })
5644                });
5645
5646        let tasks = tasks.collect::<Vec<_>>();
5647
5648        let tasks = futures::future::join_all(tasks);
5649        for (ix, path_open_result) in tasks.await.into_iter().flatten() {
5650            opened_items[ix] = Some(path_open_result);
5651        }
5652
5653        Ok(opened_items)
5654    })
5655}
5656
5657enum ActivateInDirectionTarget {
5658    Pane(Entity<Pane>),
5659    Dock(Entity<Dock>),
5660}
5661
5662fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
5663    workspace
5664        .update(cx, |workspace, _, cx| {
5665            if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
5666                struct DatabaseFailedNotification;
5667
5668                workspace.show_notification(
5669                    NotificationId::unique::<DatabaseFailedNotification>(),
5670                    cx,
5671                    |cx| {
5672                        cx.new(|cx| {
5673                            MessageNotification::new("Failed to load the database file.", cx)
5674                                .primary_message("File an Issue")
5675                                .primary_icon(IconName::Plus)
5676                                .primary_on_click(|window, cx| {
5677                                    window.dispatch_action(Box::new(FileBugReport), cx)
5678                                })
5679                        })
5680                    },
5681                );
5682            }
5683        })
5684        .log_err();
5685}
5686
5687impl Focusable for Workspace {
5688    fn focus_handle(&self, cx: &App) -> FocusHandle {
5689        self.active_pane.focus_handle(cx)
5690    }
5691}
5692
5693#[derive(Clone)]
5694struct DraggedDock(DockPosition);
5695
5696impl Render for DraggedDock {
5697    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5698        gpui::Empty
5699    }
5700}
5701
5702impl Render for Workspace {
5703    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5704        let mut context = KeyContext::new_with_defaults();
5705        context.add("Workspace");
5706        context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
5707        let centered_layout = self.centered_layout
5708            && self.center.panes().len() == 1
5709            && self.active_item(cx).is_some();
5710        let render_padding = |size| {
5711            (size > 0.0).then(|| {
5712                div()
5713                    .h_full()
5714                    .w(relative(size))
5715                    .bg(cx.theme().colors().editor_background)
5716                    .border_color(cx.theme().colors().pane_group_border)
5717            })
5718        };
5719        let paddings = if centered_layout {
5720            let settings = WorkspaceSettings::get_global(cx).centered_layout;
5721            (
5722                render_padding(Self::adjust_padding(settings.left_padding)),
5723                render_padding(Self::adjust_padding(settings.right_padding)),
5724            )
5725        } else {
5726            (None, None)
5727        };
5728        let ui_font = theme::setup_ui_font(window, cx);
5729
5730        let theme = cx.theme().clone();
5731        let colors = theme.colors();
5732        let notification_entities = self
5733            .notifications
5734            .iter()
5735            .map(|(_, notification)| notification.entity_id())
5736            .collect::<Vec<_>>();
5737
5738        client_side_decorations(
5739            self.actions(div(), window, cx)
5740                .key_context(context)
5741                .relative()
5742                .size_full()
5743                .flex()
5744                .flex_col()
5745                .font(ui_font)
5746                .gap_0()
5747                .justify_start()
5748                .items_start()
5749                .text_color(colors.text)
5750                .overflow_hidden()
5751                .children(self.titlebar_item.clone())
5752                .on_modifiers_changed(move |_, _, cx| {
5753                    for &id in &notification_entities {
5754                        cx.notify(id);
5755                    }
5756                })
5757                .child(
5758                    div()
5759                        .size_full()
5760                        .relative()
5761                        .flex_1()
5762                        .flex()
5763                        .flex_col()
5764                        .child(
5765                            div()
5766                                .id("workspace")
5767                                .bg(colors.background)
5768                                .relative()
5769                                .flex_1()
5770                                .w_full()
5771                                .flex()
5772                                .flex_col()
5773                                .overflow_hidden()
5774                                .border_t_1()
5775                                .border_b_1()
5776                                .border_color(colors.border)
5777                                .child({
5778                                    let this = cx.entity().clone();
5779                                    canvas(
5780                                        move |bounds, window, cx| {
5781                                            this.update(cx, |this, cx| {
5782                                                let bounds_changed = this.bounds != bounds;
5783                                                this.bounds = bounds;
5784
5785                                                if bounds_changed {
5786                                                    this.left_dock.update(cx, |dock, cx| {
5787                                                        dock.clamp_panel_size(
5788                                                            bounds.size.width,
5789                                                            window,
5790                                                            cx,
5791                                                        )
5792                                                    });
5793
5794                                                    this.right_dock.update(cx, |dock, cx| {
5795                                                        dock.clamp_panel_size(
5796                                                            bounds.size.width,
5797                                                            window,
5798                                                            cx,
5799                                                        )
5800                                                    });
5801
5802                                                    this.bottom_dock.update(cx, |dock, cx| {
5803                                                        dock.clamp_panel_size(
5804                                                            bounds.size.height,
5805                                                            window,
5806                                                            cx,
5807                                                        )
5808                                                    });
5809                                                }
5810                                            })
5811                                        },
5812                                        |_, _, _, _| {},
5813                                    )
5814                                    .absolute()
5815                                    .size_full()
5816                                })
5817                                .when(self.zoomed.is_none(), |this| {
5818                                    this.on_drag_move(cx.listener(
5819                                        move |workspace,
5820                                              e: &DragMoveEvent<DraggedDock>,
5821                                              window,
5822                                              cx| {
5823                                            if workspace.previous_dock_drag_coordinates
5824                                                != Some(e.event.position)
5825                                            {
5826                                                workspace.previous_dock_drag_coordinates =
5827                                                    Some(e.event.position);
5828                                                match e.drag(cx).0 {
5829                                                    DockPosition::Left => {
5830                                                        resize_left_dock(
5831                                                            e.event.position.x
5832                                                                - workspace.bounds.left(),
5833                                                            workspace,
5834                                                            window,
5835                                                            cx,
5836                                                        );
5837                                                    }
5838                                                    DockPosition::Right => {
5839                                                        resize_right_dock(
5840                                                            workspace.bounds.right()
5841                                                                - e.event.position.x,
5842                                                            workspace,
5843                                                            window,
5844                                                            cx,
5845                                                        );
5846                                                    }
5847                                                    DockPosition::Bottom => {
5848                                                        resize_bottom_dock(
5849                                                            workspace.bounds.bottom()
5850                                                                - e.event.position.y,
5851                                                            workspace,
5852                                                            window,
5853                                                            cx,
5854                                                        );
5855                                                    }
5856                                                };
5857                                                workspace.serialize_workspace(window, cx);
5858                                            }
5859                                        },
5860                                    ))
5861                                })
5862                                .child({
5863                                    match self.bottom_dock_layout {
5864                                        BottomDockLayout::Full => div()
5865                                            .flex()
5866                                            .flex_col()
5867                                            .h_full()
5868                                            .child(
5869                                                div()
5870                                                    .flex()
5871                                                    .flex_row()
5872                                                    .flex_1()
5873                                                    .overflow_hidden()
5874                                                    .children(self.render_dock(
5875                                                        DockPosition::Left,
5876                                                        &self.left_dock,
5877                                                        window,
5878                                                        cx,
5879                                                    ))
5880                                                    .child(
5881                                                        div()
5882                                                            .flex()
5883                                                            .flex_col()
5884                                                            .flex_1()
5885                                                            .overflow_hidden()
5886                                                            .child(
5887                                                                h_flex()
5888                                                                    .flex_1()
5889                                                                    .when_some(
5890                                                                        paddings.0,
5891                                                                        |this, p| {
5892                                                                            this.child(
5893                                                                                p.border_r_1(),
5894                                                                            )
5895                                                                        },
5896                                                                    )
5897                                                                    .child(self.center.render(
5898                                                                        self.zoomed.as_ref(),
5899                                                                        &PaneRenderContext {
5900                                                                            follower_states:
5901                                                                                &self.follower_states,
5902                                                                            active_call: self.active_call(),
5903                                                                            active_pane: &self.active_pane,
5904                                                                            app_state: &self.app_state,
5905                                                                            project: &self.project,
5906                                                                            workspace: &self.weak_self,
5907                                                                        },
5908                                                                        window,
5909                                                                        cx,
5910                                                                    ))
5911                                                                    .when_some(
5912                                                                        paddings.1,
5913                                                                        |this, p| {
5914                                                                            this.child(
5915                                                                                p.border_l_1(),
5916                                                                            )
5917                                                                        },
5918                                                                    ),
5919                                                            ),
5920                                                    )
5921                                                    .children(self.render_dock(
5922                                                        DockPosition::Right,
5923                                                        &self.right_dock,
5924                                                        window,
5925                                                        cx,
5926                                                    )),
5927                                            )
5928                                            .child(div().w_full().children(self.render_dock(
5929                                                DockPosition::Bottom,
5930                                                &self.bottom_dock,
5931                                                window,
5932                                                cx
5933                                            ))),
5934
5935                                        BottomDockLayout::LeftAligned => div()
5936                                            .flex()
5937                                            .flex_row()
5938                                            .h_full()
5939                                            .child(
5940                                                div()
5941                                                    .flex()
5942                                                    .flex_col()
5943                                                    .flex_1()
5944                                                    .h_full()
5945                                                    .child(
5946                                                        div()
5947                                                            .flex()
5948                                                            .flex_row()
5949                                                            .flex_1()
5950                                                            .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx))
5951                                                            .child(
5952                                                                div()
5953                                                                    .flex()
5954                                                                    .flex_col()
5955                                                                    .flex_1()
5956                                                                    .overflow_hidden()
5957                                                                    .child(
5958                                                                        h_flex()
5959                                                                            .flex_1()
5960                                                                            .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
5961                                                                            .child(self.center.render(
5962                                                                                self.zoomed.as_ref(),
5963                                                                                &PaneRenderContext {
5964                                                                                    follower_states:
5965                                                                                        &self.follower_states,
5966                                                                                    active_call: self.active_call(),
5967                                                                                    active_pane: &self.active_pane,
5968                                                                                    app_state: &self.app_state,
5969                                                                                    project: &self.project,
5970                                                                                    workspace: &self.weak_self,
5971                                                                                },
5972                                                                                window,
5973                                                                                cx,
5974                                                                            ))
5975                                                                            .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
5976                                                                    )
5977                                                            )
5978                                                    )
5979                                                    .child(
5980                                                        div()
5981                                                            .w_full()
5982                                                            .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
5983                                                    ),
5984                                            )
5985                                            .children(self.render_dock(
5986                                                DockPosition::Right,
5987                                                &self.right_dock,
5988                                                window,
5989                                                cx,
5990                                            )),
5991
5992                                        BottomDockLayout::RightAligned => div()
5993                                            .flex()
5994                                            .flex_row()
5995                                            .h_full()
5996                                            .children(self.render_dock(
5997                                                DockPosition::Left,
5998                                                &self.left_dock,
5999                                                window,
6000                                                cx,
6001                                            ))
6002                                            .child(
6003                                                div()
6004                                                    .flex()
6005                                                    .flex_col()
6006                                                    .flex_1()
6007                                                    .h_full()
6008                                                    .child(
6009                                                        div()
6010                                                            .flex()
6011                                                            .flex_row()
6012                                                            .flex_1()
6013                                                            .child(
6014                                                                div()
6015                                                                    .flex()
6016                                                                    .flex_col()
6017                                                                    .flex_1()
6018                                                                    .overflow_hidden()
6019                                                                    .child(
6020                                                                        h_flex()
6021                                                                            .flex_1()
6022                                                                            .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
6023                                                                            .child(self.center.render(
6024                                                                                self.zoomed.as_ref(),
6025                                                                                &PaneRenderContext {
6026                                                                                    follower_states:
6027                                                                                        &self.follower_states,
6028                                                                                    active_call: self.active_call(),
6029                                                                                    active_pane: &self.active_pane,
6030                                                                                    app_state: &self.app_state,
6031                                                                                    project: &self.project,
6032                                                                                    workspace: &self.weak_self,
6033                                                                                },
6034                                                                                window,
6035                                                                                cx,
6036                                                                            ))
6037                                                                            .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
6038                                                                    )
6039                                                            )
6040                                                            .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx))
6041                                                    )
6042                                                    .child(
6043                                                        div()
6044                                                            .w_full()
6045                                                            .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
6046                                                    ),
6047                                            ),
6048
6049                                        BottomDockLayout::Contained => div()
6050                                            .flex()
6051                                            .flex_row()
6052                                            .h_full()
6053                                            .children(self.render_dock(
6054                                                DockPosition::Left,
6055                                                &self.left_dock,
6056                                                window,
6057                                                cx,
6058                                            ))
6059                                            .child(
6060                                                div()
6061                                                    .flex()
6062                                                    .flex_col()
6063                                                    .flex_1()
6064                                                    .overflow_hidden()
6065                                                    .child(
6066                                                        h_flex()
6067                                                            .flex_1()
6068                                                            .when_some(paddings.0, |this, p| {
6069                                                                this.child(p.border_r_1())
6070                                                            })
6071                                                            .child(self.center.render(
6072                                                                self.zoomed.as_ref(),
6073                                                                &PaneRenderContext {
6074                                                                    follower_states:
6075                                                                        &self.follower_states,
6076                                                                    active_call: self.active_call(),
6077                                                                    active_pane: &self.active_pane,
6078                                                                    app_state: &self.app_state,
6079                                                                    project: &self.project,
6080                                                                    workspace: &self.weak_self,
6081                                                                },
6082                                                                window,
6083                                                                cx,
6084                                                            ))
6085                                                            .when_some(paddings.1, |this, p| {
6086                                                                this.child(p.border_l_1())
6087                                                            }),
6088                                                    )
6089                                                    .children(self.render_dock(
6090                                                        DockPosition::Bottom,
6091                                                        &self.bottom_dock,
6092                                                        window,
6093                                                        cx,
6094                                                    )),
6095                                            )
6096                                            .children(self.render_dock(
6097                                                DockPosition::Right,
6098                                                &self.right_dock,
6099                                                window,
6100                                                cx,
6101                                            )),
6102                                    }
6103                                })
6104                                .children(self.zoomed.as_ref().and_then(|view| {
6105                                    let zoomed_view = view.upgrade()?;
6106                                    let div = div()
6107                                        .occlude()
6108                                        .absolute()
6109                                        .overflow_hidden()
6110                                        .border_color(colors.border)
6111                                        .bg(colors.background)
6112                                        .child(zoomed_view)
6113                                        .inset_0()
6114                                        .shadow_lg();
6115
6116                                    Some(match self.zoomed_position {
6117                                        Some(DockPosition::Left) => div.right_2().border_r_1(),
6118                                        Some(DockPosition::Right) => div.left_2().border_l_1(),
6119                                        Some(DockPosition::Bottom) => div.top_2().border_t_1(),
6120                                        None => {
6121                                            div.top_2().bottom_2().left_2().right_2().border_1()
6122                                        }
6123                                    })
6124                                }))
6125                                .children(self.render_notifications(window, cx)),
6126                        )
6127                        .child(self.status_bar.clone())
6128                        .child(self.modal_layer.clone())
6129                        .child(self.toast_layer.clone()),
6130                ),
6131            window,
6132            cx,
6133        )
6134    }
6135}
6136
6137fn resize_bottom_dock(
6138    new_size: Pixels,
6139    workspace: &mut Workspace,
6140    window: &mut Window,
6141    cx: &mut App,
6142) {
6143    let size =
6144        new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE - workspace.bounds.top());
6145    workspace.bottom_dock.update(cx, |bottom_dock, cx| {
6146        bottom_dock.resize_active_panel(Some(size), window, cx);
6147    });
6148}
6149
6150fn resize_right_dock(
6151    new_size: Pixels,
6152    workspace: &mut Workspace,
6153    window: &mut Window,
6154    cx: &mut App,
6155) {
6156    let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
6157    workspace.right_dock.update(cx, |right_dock, cx| {
6158        right_dock.resize_active_panel(Some(size), window, cx);
6159    });
6160}
6161
6162fn resize_left_dock(
6163    new_size: Pixels,
6164    workspace: &mut Workspace,
6165    window: &mut Window,
6166    cx: &mut App,
6167) {
6168    let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
6169
6170    workspace.left_dock.update(cx, |left_dock, cx| {
6171        left_dock.resize_active_panel(Some(size), window, cx);
6172    });
6173}
6174
6175impl WorkspaceStore {
6176    pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
6177        Self {
6178            workspaces: Default::default(),
6179            _subscriptions: vec![
6180                client.add_request_handler(cx.weak_entity(), Self::handle_follow),
6181                client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
6182            ],
6183            client,
6184        }
6185    }
6186
6187    pub fn update_followers(
6188        &self,
6189        project_id: Option<u64>,
6190        update: proto::update_followers::Variant,
6191        cx: &App,
6192    ) -> Option<()> {
6193        let active_call = ActiveCall::try_global(cx)?;
6194        let room_id = active_call.read(cx).room()?.read(cx).id();
6195        self.client
6196            .send(proto::UpdateFollowers {
6197                room_id,
6198                project_id,
6199                variant: Some(update),
6200            })
6201            .log_err()
6202    }
6203
6204    pub async fn handle_follow(
6205        this: Entity<Self>,
6206        envelope: TypedEnvelope<proto::Follow>,
6207        mut cx: AsyncApp,
6208    ) -> Result<proto::FollowResponse> {
6209        this.update(&mut cx, |this, cx| {
6210            let follower = Follower {
6211                project_id: envelope.payload.project_id,
6212                peer_id: envelope.original_sender_id()?,
6213            };
6214
6215            let mut response = proto::FollowResponse::default();
6216            this.workspaces.retain(|workspace| {
6217                workspace
6218                    .update(cx, |workspace, window, cx| {
6219                        let handler_response =
6220                            workspace.handle_follow(follower.project_id, window, cx);
6221                        if let Some(active_view) = handler_response.active_view.clone() {
6222                            if workspace.project.read(cx).remote_id() == follower.project_id {
6223                                response.active_view = Some(active_view)
6224                            }
6225                        }
6226                    })
6227                    .is_ok()
6228            });
6229
6230            Ok(response)
6231        })?
6232    }
6233
6234    async fn handle_update_followers(
6235        this: Entity<Self>,
6236        envelope: TypedEnvelope<proto::UpdateFollowers>,
6237        mut cx: AsyncApp,
6238    ) -> Result<()> {
6239        let leader_id = envelope.original_sender_id()?;
6240        let update = envelope.payload;
6241
6242        this.update(&mut cx, |this, cx| {
6243            this.workspaces.retain(|workspace| {
6244                workspace
6245                    .update(cx, |workspace, window, cx| {
6246                        let project_id = workspace.project.read(cx).remote_id();
6247                        if update.project_id != project_id && update.project_id.is_some() {
6248                            return;
6249                        }
6250                        workspace.handle_update_followers(leader_id, update.clone(), window, cx);
6251                    })
6252                    .is_ok()
6253            });
6254            Ok(())
6255        })?
6256    }
6257}
6258
6259impl ViewId {
6260    pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
6261        Ok(Self {
6262            creator: message
6263                .creator
6264                .map(CollaboratorId::PeerId)
6265                .ok_or_else(|| anyhow!("creator is missing"))?,
6266            id: message.id,
6267        })
6268    }
6269
6270    pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
6271        if let CollaboratorId::PeerId(peer_id) = self.creator {
6272            Some(proto::ViewId {
6273                creator: Some(peer_id),
6274                id: self.id,
6275            })
6276        } else {
6277            None
6278        }
6279    }
6280}
6281
6282impl FollowerState {
6283    fn pane(&self) -> &Entity<Pane> {
6284        self.dock_pane.as_ref().unwrap_or(&self.center_pane)
6285    }
6286}
6287
6288pub trait WorkspaceHandle {
6289    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
6290}
6291
6292impl WorkspaceHandle for Entity<Workspace> {
6293    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
6294        self.read(cx)
6295            .worktrees(cx)
6296            .flat_map(|worktree| {
6297                let worktree_id = worktree.read(cx).id();
6298                worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
6299                    worktree_id,
6300                    path: f.path.clone(),
6301                })
6302            })
6303            .collect::<Vec<_>>()
6304    }
6305}
6306
6307impl std::fmt::Debug for OpenPaths {
6308    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6309        f.debug_struct("OpenPaths")
6310            .field("paths", &self.paths)
6311            .finish()
6312    }
6313}
6314
6315pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
6316    DB.last_workspace().await.log_err().flatten()
6317}
6318
6319pub fn last_session_workspace_locations(
6320    last_session_id: &str,
6321    last_session_window_stack: Option<Vec<WindowId>>,
6322) -> Option<Vec<SerializedWorkspaceLocation>> {
6323    DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
6324        .log_err()
6325}
6326
6327actions!(
6328    collab,
6329    [
6330        OpenChannelNotes,
6331        Mute,
6332        Deafen,
6333        LeaveCall,
6334        ShareProject,
6335        ScreenShare
6336    ]
6337);
6338actions!(zed, [OpenLog]);
6339
6340async fn join_channel_internal(
6341    channel_id: ChannelId,
6342    app_state: &Arc<AppState>,
6343    requesting_window: Option<WindowHandle<Workspace>>,
6344    active_call: &Entity<ActiveCall>,
6345    cx: &mut AsyncApp,
6346) -> Result<bool> {
6347    let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
6348        let Some(room) = active_call.room().map(|room| room.read(cx)) else {
6349            return (false, None);
6350        };
6351
6352        let already_in_channel = room.channel_id() == Some(channel_id);
6353        let should_prompt = room.is_sharing_project()
6354            && !room.remote_participants().is_empty()
6355            && !already_in_channel;
6356        let open_room = if already_in_channel {
6357            active_call.room().cloned()
6358        } else {
6359            None
6360        };
6361        (should_prompt, open_room)
6362    })?;
6363
6364    if let Some(room) = open_room {
6365        let task = room.update(cx, |room, cx| {
6366            if let Some((project, host)) = room.most_active_project(cx) {
6367                return Some(join_in_room_project(project, host, app_state.clone(), cx));
6368            }
6369
6370            None
6371        })?;
6372        if let Some(task) = task {
6373            task.await?;
6374        }
6375        return anyhow::Ok(true);
6376    }
6377
6378    if should_prompt {
6379        if let Some(workspace) = requesting_window {
6380            let answer = workspace
6381                .update(cx, |_, window, cx| {
6382                    window.prompt(
6383                        PromptLevel::Warning,
6384                        "Do you want to switch channels?",
6385                        Some("Leaving this call will unshare your current project."),
6386                        &["Yes, Join Channel", "Cancel"],
6387                        cx,
6388                    )
6389                })?
6390                .await;
6391
6392            if answer == Ok(1) {
6393                return Ok(false);
6394            }
6395        } else {
6396            return Ok(false); // unreachable!() hopefully
6397        }
6398    }
6399
6400    let client = cx.update(|cx| active_call.read(cx).client())?;
6401
6402    let mut client_status = client.status();
6403
6404    // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
6405    'outer: loop {
6406        let Some(status) = client_status.recv().await else {
6407            return Err(anyhow!("error connecting"));
6408        };
6409
6410        match status {
6411            Status::Connecting
6412            | Status::Authenticating
6413            | Status::Reconnecting
6414            | Status::Reauthenticating => continue,
6415            Status::Connected { .. } => break 'outer,
6416            Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
6417            Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
6418            Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
6419                return Err(ErrorCode::Disconnected.into());
6420            }
6421        }
6422    }
6423
6424    let room = active_call
6425        .update(cx, |active_call, cx| {
6426            active_call.join_channel(channel_id, cx)
6427        })?
6428        .await?;
6429
6430    let Some(room) = room else {
6431        return anyhow::Ok(true);
6432    };
6433
6434    room.update(cx, |room, _| room.room_update_completed())?
6435        .await;
6436
6437    let task = room.update(cx, |room, cx| {
6438        if let Some((project, host)) = room.most_active_project(cx) {
6439            return Some(join_in_room_project(project, host, app_state.clone(), cx));
6440        }
6441
6442        // If you are the first to join a channel, see if you should share your project.
6443        if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
6444            if let Some(workspace) = requesting_window {
6445                let project = workspace.update(cx, |workspace, _, cx| {
6446                    let project = workspace.project.read(cx);
6447
6448                    if !CallSettings::get_global(cx).share_on_join {
6449                        return None;
6450                    }
6451
6452                    if (project.is_local() || project.is_via_ssh())
6453                        && project.visible_worktrees(cx).any(|tree| {
6454                            tree.read(cx)
6455                                .root_entry()
6456                                .map_or(false, |entry| entry.is_dir())
6457                        })
6458                    {
6459                        Some(workspace.project.clone())
6460                    } else {
6461                        None
6462                    }
6463                });
6464                if let Ok(Some(project)) = project {
6465                    return Some(cx.spawn(async move |room, cx| {
6466                        room.update(cx, |room, cx| room.share_project(project, cx))?
6467                            .await?;
6468                        Ok(())
6469                    }));
6470                }
6471            }
6472        }
6473
6474        None
6475    })?;
6476    if let Some(task) = task {
6477        task.await?;
6478        return anyhow::Ok(true);
6479    }
6480    anyhow::Ok(false)
6481}
6482
6483pub fn join_channel(
6484    channel_id: ChannelId,
6485    app_state: Arc<AppState>,
6486    requesting_window: Option<WindowHandle<Workspace>>,
6487    cx: &mut App,
6488) -> Task<Result<()>> {
6489    let active_call = ActiveCall::global(cx);
6490    cx.spawn(async move |cx| {
6491        let result = join_channel_internal(
6492            channel_id,
6493            &app_state,
6494            requesting_window,
6495            &active_call,
6496             cx,
6497        )
6498            .await;
6499
6500        // join channel succeeded, and opened a window
6501        if matches!(result, Ok(true)) {
6502            return anyhow::Ok(());
6503        }
6504
6505        // find an existing workspace to focus and show call controls
6506        let mut active_window =
6507            requesting_window.or_else(|| activate_any_workspace_window( cx));
6508        if active_window.is_none() {
6509            // no open workspaces, make one to show the error in (blergh)
6510            let (window_handle, _) = cx
6511                .update(|cx| {
6512                    Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
6513                })?
6514                .await?;
6515
6516            if result.is_ok() {
6517                cx.update(|cx| {
6518                    cx.dispatch_action(&OpenChannelNotes);
6519                }).log_err();
6520            }
6521
6522            active_window = Some(window_handle);
6523        }
6524
6525        if let Err(err) = result {
6526            log::error!("failed to join channel: {}", err);
6527            if let Some(active_window) = active_window {
6528                active_window
6529                    .update(cx, |_, window, cx| {
6530                        let detail: SharedString = match err.error_code() {
6531                            ErrorCode::SignedOut => {
6532                                "Please sign in to continue.".into()
6533                            }
6534                            ErrorCode::UpgradeRequired => {
6535                                "Your are running an unsupported version of Zed. Please update to continue.".into()
6536                            }
6537                            ErrorCode::NoSuchChannel => {
6538                                "No matching channel was found. Please check the link and try again.".into()
6539                            }
6540                            ErrorCode::Forbidden => {
6541                                "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
6542                            }
6543                            ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
6544                            _ => format!("{}\n\nPlease try again.", err).into(),
6545                        };
6546                        window.prompt(
6547                            PromptLevel::Critical,
6548                            "Failed to join channel",
6549                            Some(&detail),
6550                            &["Ok"],
6551                        cx)
6552                    })?
6553                    .await
6554                    .ok();
6555            }
6556        }
6557
6558        // return ok, we showed the error to the user.
6559        anyhow::Ok(())
6560    })
6561}
6562
6563pub async fn get_any_active_workspace(
6564    app_state: Arc<AppState>,
6565    mut cx: AsyncApp,
6566) -> anyhow::Result<WindowHandle<Workspace>> {
6567    // find an existing workspace to focus and show call controls
6568    let active_window = activate_any_workspace_window(&mut cx);
6569    if active_window.is_none() {
6570        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
6571            .await?;
6572    }
6573    activate_any_workspace_window(&mut cx).context("could not open zed")
6574}
6575
6576fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
6577    cx.update(|cx| {
6578        if let Some(workspace_window) = cx
6579            .active_window()
6580            .and_then(|window| window.downcast::<Workspace>())
6581        {
6582            return Some(workspace_window);
6583        }
6584
6585        for window in cx.windows() {
6586            if let Some(workspace_window) = window.downcast::<Workspace>() {
6587                workspace_window
6588                    .update(cx, |_, window, _| window.activate_window())
6589                    .ok();
6590                return Some(workspace_window);
6591            }
6592        }
6593        None
6594    })
6595    .ok()
6596    .flatten()
6597}
6598
6599pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
6600    cx.windows()
6601        .into_iter()
6602        .filter_map(|window| window.downcast::<Workspace>())
6603        .filter(|workspace| {
6604            workspace
6605                .read(cx)
6606                .is_ok_and(|workspace| workspace.project.read(cx).is_local())
6607        })
6608        .collect()
6609}
6610
6611#[derive(Default)]
6612pub struct OpenOptions {
6613    pub visible: Option<OpenVisible>,
6614    pub focus: Option<bool>,
6615    pub open_new_workspace: Option<bool>,
6616    pub replace_window: Option<WindowHandle<Workspace>>,
6617    pub env: Option<HashMap<String, String>>,
6618}
6619
6620#[allow(clippy::type_complexity)]
6621pub fn open_paths(
6622    abs_paths: &[PathBuf],
6623    app_state: Arc<AppState>,
6624    open_options: OpenOptions,
6625    cx: &mut App,
6626) -> Task<
6627    anyhow::Result<(
6628        WindowHandle<Workspace>,
6629        Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
6630    )>,
6631> {
6632    let abs_paths = abs_paths.to_vec();
6633    let mut existing = None;
6634    let mut best_match = None;
6635    let mut open_visible = OpenVisible::All;
6636
6637    cx.spawn(async move |cx| {
6638        if open_options.open_new_workspace != Some(true) {
6639            let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
6640            let all_metadatas = futures::future::join_all(all_paths)
6641                .await
6642                .into_iter()
6643                .filter_map(|result| result.ok().flatten())
6644                .collect::<Vec<_>>();
6645
6646            cx.update(|cx| {
6647                for window in local_workspace_windows(&cx) {
6648                    if let Ok(workspace) = window.read(&cx) {
6649                        let m = workspace.project.read(&cx).visibility_for_paths(
6650                            &abs_paths,
6651                            &all_metadatas,
6652                            open_options.open_new_workspace == None,
6653                            cx,
6654                        );
6655                        if m > best_match {
6656                            existing = Some(window);
6657                            best_match = m;
6658                        } else if best_match.is_none()
6659                            && open_options.open_new_workspace == Some(false)
6660                        {
6661                            existing = Some(window)
6662                        }
6663                    }
6664                }
6665            })?;
6666
6667            if open_options.open_new_workspace.is_none() && existing.is_none() {
6668                if all_metadatas.iter().all(|file| !file.is_dir) {
6669                    cx.update(|cx| {
6670                        if let Some(window) = cx
6671                            .active_window()
6672                            .and_then(|window| window.downcast::<Workspace>())
6673                        {
6674                            if let Ok(workspace) = window.read(cx) {
6675                                let project = workspace.project().read(cx);
6676                                if project.is_local() && !project.is_via_collab() {
6677                                    existing = Some(window);
6678                                    open_visible = OpenVisible::None;
6679                                    return;
6680                                }
6681                            }
6682                        }
6683                        for window in local_workspace_windows(cx) {
6684                            if let Ok(workspace) = window.read(cx) {
6685                                let project = workspace.project().read(cx);
6686                                if project.is_via_collab() {
6687                                    continue;
6688                                }
6689                                existing = Some(window);
6690                                open_visible = OpenVisible::None;
6691                                break;
6692                            }
6693                        }
6694                    })?;
6695                }
6696            }
6697        }
6698
6699        if let Some(existing) = existing {
6700            let open_task = existing
6701                .update(cx, |workspace, window, cx| {
6702                    window.activate_window();
6703                    workspace.open_paths(
6704                        abs_paths,
6705                        OpenOptions {
6706                            visible: Some(open_visible),
6707                            ..Default::default()
6708                        },
6709                        None,
6710                        window,
6711                        cx,
6712                    )
6713                })?
6714                .await;
6715
6716            _ = existing.update(cx, |workspace, _, cx| {
6717                for item in open_task.iter().flatten() {
6718                    if let Err(e) = item {
6719                        workspace.show_error(&e, cx);
6720                    }
6721                }
6722            });
6723
6724            Ok((existing, open_task))
6725        } else {
6726            cx.update(move |cx| {
6727                Workspace::new_local(
6728                    abs_paths,
6729                    app_state.clone(),
6730                    open_options.replace_window,
6731                    open_options.env,
6732                    cx,
6733                )
6734            })?
6735            .await
6736        }
6737    })
6738}
6739
6740pub fn open_new(
6741    open_options: OpenOptions,
6742    app_state: Arc<AppState>,
6743    cx: &mut App,
6744    init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6745) -> Task<anyhow::Result<()>> {
6746    let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6747    cx.spawn(async move |cx| {
6748        let (workspace, opened_paths) = task.await?;
6749        workspace.update(cx, |workspace, window, cx| {
6750            if opened_paths.is_empty() {
6751                init(workspace, window, cx)
6752            }
6753        })?;
6754        Ok(())
6755    })
6756}
6757
6758pub fn create_and_open_local_file(
6759    path: &'static Path,
6760    window: &mut Window,
6761    cx: &mut Context<Workspace>,
6762    default_content: impl 'static + Send + FnOnce() -> Rope,
6763) -> Task<Result<Box<dyn ItemHandle>>> {
6764    cx.spawn_in(window, async move |workspace, cx| {
6765        let fs = workspace.update(cx, |workspace, _| workspace.app_state().fs.clone())?;
6766        if !fs.is_file(path).await {
6767            fs.create_file(path, Default::default()).await?;
6768            fs.save(path, &default_content(), Default::default())
6769                .await?;
6770        }
6771
6772        let mut items = workspace
6773            .update_in(cx, |workspace, window, cx| {
6774                workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6775                    workspace.open_paths(
6776                        vec![path.to_path_buf()],
6777                        OpenOptions {
6778                            visible: Some(OpenVisible::None),
6779                            ..Default::default()
6780                        },
6781                        None,
6782                        window,
6783                        cx,
6784                    )
6785                })
6786            })?
6787            .await?
6788            .await;
6789
6790        let item = items.pop().flatten();
6791        item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
6792    })
6793}
6794
6795pub fn open_ssh_project_with_new_connection(
6796    window: WindowHandle<Workspace>,
6797    connection_options: SshConnectionOptions,
6798    cancel_rx: oneshot::Receiver<()>,
6799    delegate: Arc<dyn SshClientDelegate>,
6800    app_state: Arc<AppState>,
6801    paths: Vec<PathBuf>,
6802    cx: &mut App,
6803) -> Task<Result<()>> {
6804    cx.spawn(async move |cx| {
6805        let (serialized_ssh_project, workspace_id, serialized_workspace) =
6806            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6807
6808        let session = match cx
6809            .update(|cx| {
6810                remote::SshRemoteClient::new(
6811                    ConnectionIdentifier::Workspace(workspace_id.0),
6812                    connection_options,
6813                    cancel_rx,
6814                    delegate,
6815                    cx,
6816                )
6817            })?
6818            .await?
6819        {
6820            Some(result) => result,
6821            None => return Ok(()),
6822        };
6823
6824        let project = cx.update(|cx| {
6825            project::Project::ssh(
6826                session,
6827                app_state.client.clone(),
6828                app_state.node_runtime.clone(),
6829                app_state.user_store.clone(),
6830                app_state.languages.clone(),
6831                app_state.fs.clone(),
6832                cx,
6833            )
6834        })?;
6835
6836        open_ssh_project_inner(
6837            project,
6838            paths,
6839            serialized_ssh_project,
6840            workspace_id,
6841            serialized_workspace,
6842            app_state,
6843            window,
6844            cx,
6845        )
6846        .await
6847    })
6848}
6849
6850pub fn open_ssh_project_with_existing_connection(
6851    connection_options: SshConnectionOptions,
6852    project: Entity<Project>,
6853    paths: Vec<PathBuf>,
6854    app_state: Arc<AppState>,
6855    window: WindowHandle<Workspace>,
6856    cx: &mut AsyncApp,
6857) -> Task<Result<()>> {
6858    cx.spawn(async move |cx| {
6859        let (serialized_ssh_project, workspace_id, serialized_workspace) =
6860            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6861
6862        open_ssh_project_inner(
6863            project,
6864            paths,
6865            serialized_ssh_project,
6866            workspace_id,
6867            serialized_workspace,
6868            app_state,
6869            window,
6870            cx,
6871        )
6872        .await
6873    })
6874}
6875
6876async fn open_ssh_project_inner(
6877    project: Entity<Project>,
6878    paths: Vec<PathBuf>,
6879    serialized_ssh_project: SerializedSshProject,
6880    workspace_id: WorkspaceId,
6881    serialized_workspace: Option<SerializedWorkspace>,
6882    app_state: Arc<AppState>,
6883    window: WindowHandle<Workspace>,
6884    cx: &mut AsyncApp,
6885) -> Result<()> {
6886    let toolchains = DB.toolchains(workspace_id).await?;
6887    for (toolchain, worktree_id, path) in toolchains {
6888        project
6889            .update(cx, |this, cx| {
6890                this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
6891            })?
6892            .await;
6893    }
6894    let mut project_paths_to_open = vec![];
6895    let mut project_path_errors = vec![];
6896
6897    for path in paths {
6898        let result = cx
6899            .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6900            .await;
6901        match result {
6902            Ok((_, project_path)) => {
6903                project_paths_to_open.push((path.clone(), Some(project_path)));
6904            }
6905            Err(error) => {
6906                project_path_errors.push(error);
6907            }
6908        };
6909    }
6910
6911    if project_paths_to_open.is_empty() {
6912        return Err(project_path_errors
6913            .pop()
6914            .unwrap_or_else(|| anyhow!("no paths given")));
6915    }
6916
6917    cx.update_window(window.into(), |_, window, cx| {
6918        window.replace_root(cx, |window, cx| {
6919            telemetry::event!("SSH Project Opened");
6920
6921            let mut workspace =
6922                Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6923            workspace.set_serialized_ssh_project(serialized_ssh_project);
6924            workspace.update_history(cx);
6925            workspace
6926        });
6927    })?;
6928
6929    window
6930        .update(cx, |_, window, cx| {
6931            window.activate_window();
6932            open_items(serialized_workspace, project_paths_to_open, window, cx)
6933        })?
6934        .await?;
6935
6936    window.update(cx, |workspace, _, cx| {
6937        for error in project_path_errors {
6938            if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6939                if let Some(path) = error.error_tag("path") {
6940                    workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6941                }
6942            } else {
6943                workspace.show_error(&error, cx)
6944            }
6945        }
6946    })?;
6947
6948    Ok(())
6949}
6950
6951fn serialize_ssh_project(
6952    connection_options: SshConnectionOptions,
6953    paths: Vec<PathBuf>,
6954    cx: &AsyncApp,
6955) -> Task<
6956    Result<(
6957        SerializedSshProject,
6958        WorkspaceId,
6959        Option<SerializedWorkspace>,
6960    )>,
6961> {
6962    cx.background_spawn(async move {
6963        let serialized_ssh_project = persistence::DB
6964            .get_or_create_ssh_project(
6965                connection_options.host.clone(),
6966                connection_options.port,
6967                paths
6968                    .iter()
6969                    .map(|path| path.to_string_lossy().to_string())
6970                    .collect::<Vec<_>>(),
6971                connection_options.username.clone(),
6972            )
6973            .await?;
6974
6975        let serialized_workspace =
6976            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6977
6978        let workspace_id = if let Some(workspace_id) =
6979            serialized_workspace.as_ref().map(|workspace| workspace.id)
6980        {
6981            workspace_id
6982        } else {
6983            persistence::DB.next_id().await?
6984        };
6985
6986        Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6987    })
6988}
6989
6990pub fn join_in_room_project(
6991    project_id: u64,
6992    follow_user_id: u64,
6993    app_state: Arc<AppState>,
6994    cx: &mut App,
6995) -> Task<Result<()>> {
6996    let windows = cx.windows();
6997    cx.spawn(async move |cx| {
6998        let existing_workspace = windows.into_iter().find_map(|window_handle| {
6999            window_handle
7000                .downcast::<Workspace>()
7001                .and_then(|window_handle| {
7002                    window_handle
7003                        .update(cx, |workspace, _window, cx| {
7004                            if workspace.project().read(cx).remote_id() == Some(project_id) {
7005                                Some(window_handle)
7006                            } else {
7007                                None
7008                            }
7009                        })
7010                        .unwrap_or(None)
7011                })
7012        });
7013
7014        let workspace = if let Some(existing_workspace) = existing_workspace {
7015            existing_workspace
7016        } else {
7017            let active_call = cx.update(|cx| ActiveCall::global(cx))?;
7018            let room = active_call
7019                .read_with(cx, |call, _| call.room().cloned())?
7020                .ok_or_else(|| anyhow!("not in a call"))?;
7021            let project = room
7022                .update(cx, |room, cx| {
7023                    room.join_project(
7024                        project_id,
7025                        app_state.languages.clone(),
7026                        app_state.fs.clone(),
7027                        cx,
7028                    )
7029                })?
7030                .await?;
7031
7032            let window_bounds_override = window_bounds_env_override();
7033            cx.update(|cx| {
7034                let mut options = (app_state.build_window_options)(None, cx);
7035                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
7036                cx.open_window(options, |window, cx| {
7037                    cx.new(|cx| {
7038                        Workspace::new(Default::default(), project, app_state.clone(), window, cx)
7039                    })
7040                })
7041            })??
7042        };
7043
7044        workspace.update(cx, |workspace, window, cx| {
7045            cx.activate(true);
7046            window.activate_window();
7047
7048            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
7049                let follow_peer_id = room
7050                    .read(cx)
7051                    .remote_participants()
7052                    .iter()
7053                    .find(|(_, participant)| participant.user.id == follow_user_id)
7054                    .map(|(_, p)| p.peer_id)
7055                    .or_else(|| {
7056                        // If we couldn't follow the given user, follow the host instead.
7057                        let collaborator = workspace
7058                            .project()
7059                            .read(cx)
7060                            .collaborators()
7061                            .values()
7062                            .find(|collaborator| collaborator.is_host)?;
7063                        Some(collaborator.peer_id)
7064                    });
7065
7066                if let Some(follow_peer_id) = follow_peer_id {
7067                    workspace.follow(follow_peer_id, window, cx);
7068                }
7069            }
7070        })?;
7071
7072        anyhow::Ok(())
7073    })
7074}
7075
7076pub fn reload(reload: &Reload, cx: &mut App) {
7077    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
7078    let mut workspace_windows = cx
7079        .windows()
7080        .into_iter()
7081        .filter_map(|window| window.downcast::<Workspace>())
7082        .collect::<Vec<_>>();
7083
7084    // If multiple windows have unsaved changes, and need a save prompt,
7085    // prompt in the active window before switching to a different window.
7086    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
7087
7088    let mut prompt = None;
7089    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
7090        prompt = window
7091            .update(cx, |_, window, cx| {
7092                window.prompt(
7093                    PromptLevel::Info,
7094                    "Are you sure you want to restart?",
7095                    None,
7096                    &["Restart", "Cancel"],
7097                    cx,
7098                )
7099            })
7100            .ok();
7101    }
7102
7103    let binary_path = reload.binary_path.clone();
7104    cx.spawn(async move |cx| {
7105        if let Some(prompt) = prompt {
7106            let answer = prompt.await?;
7107            if answer != 0 {
7108                return Ok(());
7109            }
7110        }
7111
7112        // If the user cancels any save prompt, then keep the app open.
7113        for window in workspace_windows {
7114            if let Ok(should_close) = window.update(cx, |workspace, window, cx| {
7115                workspace.prepare_to_close(CloseIntent::Quit, window, cx)
7116            }) {
7117                if !should_close.await? {
7118                    return Ok(());
7119                }
7120            }
7121        }
7122
7123        cx.update(|cx| cx.restart(binary_path))
7124    })
7125    .detach_and_log_err(cx);
7126}
7127
7128fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
7129    let mut parts = value.split(',');
7130    let x: usize = parts.next()?.parse().ok()?;
7131    let y: usize = parts.next()?.parse().ok()?;
7132    Some(point(px(x as f32), px(y as f32)))
7133}
7134
7135fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
7136    let mut parts = value.split(',');
7137    let width: usize = parts.next()?.parse().ok()?;
7138    let height: usize = parts.next()?.parse().ok()?;
7139    Some(size(px(width as f32), px(height as f32)))
7140}
7141
7142pub fn client_side_decorations(
7143    element: impl IntoElement,
7144    window: &mut Window,
7145    cx: &mut App,
7146) -> Stateful<Div> {
7147    const BORDER_SIZE: Pixels = px(1.0);
7148    let decorations = window.window_decorations();
7149
7150    if matches!(decorations, Decorations::Client { .. }) {
7151        window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
7152    }
7153
7154    struct GlobalResizeEdge(ResizeEdge);
7155    impl Global for GlobalResizeEdge {}
7156
7157    div()
7158        .id("window-backdrop")
7159        .bg(transparent_black())
7160        .map(|div| match decorations {
7161            Decorations::Server => div,
7162            Decorations::Client { tiling, .. } => div
7163                .when(!(tiling.top || tiling.right), |div| {
7164                    div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7165                })
7166                .when(!(tiling.top || tiling.left), |div| {
7167                    div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7168                })
7169                .when(!(tiling.bottom || tiling.right), |div| {
7170                    div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7171                })
7172                .when(!(tiling.bottom || tiling.left), |div| {
7173                    div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7174                })
7175                .when(!tiling.top, |div| {
7176                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
7177                })
7178                .when(!tiling.bottom, |div| {
7179                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
7180                })
7181                .when(!tiling.left, |div| {
7182                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
7183                })
7184                .when(!tiling.right, |div| {
7185                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
7186                })
7187                .on_mouse_move(move |e, window, cx| {
7188                    let size = window.window_bounds().get_bounds().size;
7189                    let pos = e.position;
7190
7191                    let new_edge =
7192                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
7193
7194                    let edge = cx.try_global::<GlobalResizeEdge>();
7195                    if new_edge != edge.map(|edge| edge.0) {
7196                        window
7197                            .window_handle()
7198                            .update(cx, |workspace, _, cx| {
7199                                cx.notify(workspace.entity_id());
7200                            })
7201                            .ok();
7202                    }
7203                })
7204                .on_mouse_down(MouseButton::Left, move |e, window, _| {
7205                    let size = window.window_bounds().get_bounds().size;
7206                    let pos = e.position;
7207
7208                    let edge = match resize_edge(
7209                        pos,
7210                        theme::CLIENT_SIDE_DECORATION_SHADOW,
7211                        size,
7212                        tiling,
7213                    ) {
7214                        Some(value) => value,
7215                        None => return,
7216                    };
7217
7218                    window.start_window_resize(edge);
7219                }),
7220        })
7221        .size_full()
7222        .child(
7223            div()
7224                .cursor(CursorStyle::Arrow)
7225                .map(|div| match decorations {
7226                    Decorations::Server => div,
7227                    Decorations::Client { tiling } => div
7228                        .border_color(cx.theme().colors().border)
7229                        .when(!(tiling.top || tiling.right), |div| {
7230                            div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7231                        })
7232                        .when(!(tiling.top || tiling.left), |div| {
7233                            div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7234                        })
7235                        .when(!(tiling.bottom || tiling.right), |div| {
7236                            div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7237                        })
7238                        .when(!(tiling.bottom || tiling.left), |div| {
7239                            div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7240                        })
7241                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
7242                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
7243                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
7244                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
7245                        .when(!tiling.is_tiled(), |div| {
7246                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
7247                                color: Hsla {
7248                                    h: 0.,
7249                                    s: 0.,
7250                                    l: 0.,
7251                                    a: 0.4,
7252                                },
7253                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
7254                                spread_radius: px(0.),
7255                                offset: point(px(0.0), px(0.0)),
7256                            }])
7257                        }),
7258                })
7259                .on_mouse_move(|_e, _, cx| {
7260                    cx.stop_propagation();
7261                })
7262                .size_full()
7263                .child(element),
7264        )
7265        .map(|div| match decorations {
7266            Decorations::Server => div,
7267            Decorations::Client { tiling, .. } => div.child(
7268                canvas(
7269                    |_bounds, window, _| {
7270                        window.insert_hitbox(
7271                            Bounds::new(
7272                                point(px(0.0), px(0.0)),
7273                                window.window_bounds().get_bounds().size,
7274                            ),
7275                            false,
7276                        )
7277                    },
7278                    move |_bounds, hitbox, window, cx| {
7279                        let mouse = window.mouse_position();
7280                        let size = window.window_bounds().get_bounds().size;
7281                        let Some(edge) =
7282                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
7283                        else {
7284                            return;
7285                        };
7286                        cx.set_global(GlobalResizeEdge(edge));
7287                        window.set_cursor_style(
7288                            match edge {
7289                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
7290                                ResizeEdge::Left | ResizeEdge::Right => {
7291                                    CursorStyle::ResizeLeftRight
7292                                }
7293                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
7294                                    CursorStyle::ResizeUpLeftDownRight
7295                                }
7296                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
7297                                    CursorStyle::ResizeUpRightDownLeft
7298                                }
7299                            },
7300                            Some(&hitbox),
7301                        );
7302                    },
7303                )
7304                .size_full()
7305                .absolute(),
7306            ),
7307        })
7308}
7309
7310fn resize_edge(
7311    pos: Point<Pixels>,
7312    shadow_size: Pixels,
7313    window_size: Size<Pixels>,
7314    tiling: Tiling,
7315) -> Option<ResizeEdge> {
7316    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
7317    if bounds.contains(&pos) {
7318        return None;
7319    }
7320
7321    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
7322    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
7323    if !tiling.top && top_left_bounds.contains(&pos) {
7324        return Some(ResizeEdge::TopLeft);
7325    }
7326
7327    let top_right_bounds = Bounds::new(
7328        Point::new(window_size.width - corner_size.width, px(0.)),
7329        corner_size,
7330    );
7331    if !tiling.top && top_right_bounds.contains(&pos) {
7332        return Some(ResizeEdge::TopRight);
7333    }
7334
7335    let bottom_left_bounds = Bounds::new(
7336        Point::new(px(0.), window_size.height - corner_size.height),
7337        corner_size,
7338    );
7339    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
7340        return Some(ResizeEdge::BottomLeft);
7341    }
7342
7343    let bottom_right_bounds = Bounds::new(
7344        Point::new(
7345            window_size.width - corner_size.width,
7346            window_size.height - corner_size.height,
7347        ),
7348        corner_size,
7349    );
7350    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
7351        return Some(ResizeEdge::BottomRight);
7352    }
7353
7354    if !tiling.top && pos.y < shadow_size {
7355        Some(ResizeEdge::Top)
7356    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
7357        Some(ResizeEdge::Bottom)
7358    } else if !tiling.left && pos.x < shadow_size {
7359        Some(ResizeEdge::Left)
7360    } else if !tiling.right && pos.x > window_size.width - shadow_size {
7361        Some(ResizeEdge::Right)
7362    } else {
7363        None
7364    }
7365}
7366
7367fn join_pane_into_active(
7368    active_pane: &Entity<Pane>,
7369    pane: &Entity<Pane>,
7370    window: &mut Window,
7371    cx: &mut App,
7372) {
7373    if pane == active_pane {
7374        return;
7375    } else if pane.read(cx).items_len() == 0 {
7376        pane.update(cx, |_, cx| {
7377            cx.emit(pane::Event::Remove {
7378                focus_on_pane: None,
7379            });
7380        })
7381    } else {
7382        move_all_items(pane, active_pane, window, cx);
7383    }
7384}
7385
7386fn move_all_items(
7387    from_pane: &Entity<Pane>,
7388    to_pane: &Entity<Pane>,
7389    window: &mut Window,
7390    cx: &mut App,
7391) {
7392    let destination_is_different = from_pane != to_pane;
7393    let mut moved_items = 0;
7394    for (item_ix, item_handle) in from_pane
7395        .read(cx)
7396        .items()
7397        .enumerate()
7398        .map(|(ix, item)| (ix, item.clone()))
7399        .collect::<Vec<_>>()
7400    {
7401        let ix = item_ix - moved_items;
7402        if destination_is_different {
7403            // Close item from previous pane
7404            from_pane.update(cx, |source, cx| {
7405                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
7406            });
7407            moved_items += 1;
7408        }
7409
7410        // This automatically removes duplicate items in the pane
7411        to_pane.update(cx, |destination, cx| {
7412            destination.add_item(item_handle, true, true, None, window, cx);
7413            window.focus(&destination.focus_handle(cx))
7414        });
7415    }
7416}
7417
7418pub fn move_item(
7419    source: &Entity<Pane>,
7420    destination: &Entity<Pane>,
7421    item_id_to_move: EntityId,
7422    destination_index: usize,
7423    window: &mut Window,
7424    cx: &mut App,
7425) {
7426    let Some((item_ix, item_handle)) = source
7427        .read(cx)
7428        .items()
7429        .enumerate()
7430        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
7431        .map(|(ix, item)| (ix, item.clone()))
7432    else {
7433        // Tab was closed during drag
7434        return;
7435    };
7436
7437    if source != destination {
7438        // Close item from previous pane
7439        source.update(cx, |source, cx| {
7440            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
7441        });
7442    }
7443
7444    // This automatically removes duplicate items in the pane
7445    destination.update(cx, |destination, cx| {
7446        destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
7447        window.focus(&destination.focus_handle(cx))
7448    });
7449}
7450
7451pub fn move_active_item(
7452    source: &Entity<Pane>,
7453    destination: &Entity<Pane>,
7454    focus_destination: bool,
7455    close_if_empty: bool,
7456    window: &mut Window,
7457    cx: &mut App,
7458) {
7459    if source == destination {
7460        return;
7461    }
7462    let Some(active_item) = source.read(cx).active_item() else {
7463        return;
7464    };
7465    source.update(cx, |source_pane, cx| {
7466        let item_id = active_item.item_id();
7467        source_pane.remove_item(item_id, false, close_if_empty, window, cx);
7468        destination.update(cx, |target_pane, cx| {
7469            target_pane.add_item(
7470                active_item,
7471                focus_destination,
7472                focus_destination,
7473                Some(target_pane.items_len()),
7474                window,
7475                cx,
7476            );
7477        });
7478    });
7479}
7480
7481#[derive(Debug)]
7482pub struct WorkspacePosition {
7483    pub window_bounds: Option<WindowBounds>,
7484    pub display: Option<Uuid>,
7485    pub centered_layout: bool,
7486}
7487
7488pub fn ssh_workspace_position_from_db(
7489    host: String,
7490    port: Option<u16>,
7491    user: Option<String>,
7492    paths_to_open: &[PathBuf],
7493    cx: &App,
7494) -> Task<Result<WorkspacePosition>> {
7495    let paths = paths_to_open
7496        .iter()
7497        .map(|path| path.to_string_lossy().to_string())
7498        .collect::<Vec<_>>();
7499
7500    cx.background_spawn(async move {
7501        let serialized_ssh_project = persistence::DB
7502            .get_or_create_ssh_project(host, port, paths, user)
7503            .await
7504            .context("fetching serialized ssh project")?;
7505        let serialized_workspace =
7506            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
7507
7508        let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
7509            (Some(WindowBounds::Windowed(bounds)), None)
7510        } else {
7511            let restorable_bounds = serialized_workspace
7512                .as_ref()
7513                .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
7514                .or_else(|| {
7515                    let (display, window_bounds) = DB.last_window().log_err()?;
7516                    Some((display?, window_bounds?))
7517                });
7518
7519            if let Some((serialized_display, serialized_status)) = restorable_bounds {
7520                (Some(serialized_status.0), Some(serialized_display))
7521            } else {
7522                (None, None)
7523            }
7524        };
7525
7526        let centered_layout = serialized_workspace
7527            .as_ref()
7528            .map(|w| w.centered_layout)
7529            .unwrap_or(false);
7530
7531        Ok(WorkspacePosition {
7532            window_bounds,
7533            display,
7534            centered_layout,
7535        })
7536    })
7537}
7538
7539#[cfg(test)]
7540mod tests {
7541    use std::{cell::RefCell, rc::Rc};
7542
7543    use super::*;
7544    use crate::{
7545        dock::{PanelEvent, test::TestPanel},
7546        item::{
7547            ItemEvent,
7548            test::{TestItem, TestProjectItem},
7549        },
7550    };
7551    use fs::FakeFs;
7552    use gpui::{
7553        DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
7554        UpdateGlobal, VisualTestContext, px,
7555    };
7556    use project::{Project, ProjectEntryId};
7557    use serde_json::json;
7558    use settings::SettingsStore;
7559
7560    #[gpui::test]
7561    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
7562        init_test(cx);
7563
7564        let fs = FakeFs::new(cx.executor());
7565        let project = Project::test(fs, [], cx).await;
7566        let (workspace, cx) =
7567            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7568
7569        // Adding an item with no ambiguity renders the tab without detail.
7570        let item1 = cx.new(|cx| {
7571            let mut item = TestItem::new(cx);
7572            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
7573            item
7574        });
7575        workspace.update_in(cx, |workspace, window, cx| {
7576            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7577        });
7578        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
7579
7580        // Adding an item that creates ambiguity increases the level of detail on
7581        // both tabs.
7582        let item2 = cx.new_window_entity(|_window, cx| {
7583            let mut item = TestItem::new(cx);
7584            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7585            item
7586        });
7587        workspace.update_in(cx, |workspace, window, cx| {
7588            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7589        });
7590        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7591        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7592
7593        // Adding an item that creates ambiguity increases the level of detail only
7594        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
7595        // we stop at the highest detail available.
7596        let item3 = cx.new(|cx| {
7597            let mut item = TestItem::new(cx);
7598            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7599            item
7600        });
7601        workspace.update_in(cx, |workspace, window, cx| {
7602            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7603        });
7604        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7605        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7606        item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7607    }
7608
7609    #[gpui::test]
7610    async fn test_tracking_active_path(cx: &mut TestAppContext) {
7611        init_test(cx);
7612
7613        let fs = FakeFs::new(cx.executor());
7614        fs.insert_tree(
7615            "/root1",
7616            json!({
7617                "one.txt": "",
7618                "two.txt": "",
7619            }),
7620        )
7621        .await;
7622        fs.insert_tree(
7623            "/root2",
7624            json!({
7625                "three.txt": "",
7626            }),
7627        )
7628        .await;
7629
7630        let project = Project::test(fs, ["root1".as_ref()], cx).await;
7631        let (workspace, cx) =
7632            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7633        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7634        let worktree_id = project.update(cx, |project, cx| {
7635            project.worktrees(cx).next().unwrap().read(cx).id()
7636        });
7637
7638        let item1 = cx.new(|cx| {
7639            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
7640        });
7641        let item2 = cx.new(|cx| {
7642            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
7643        });
7644
7645        // Add an item to an empty pane
7646        workspace.update_in(cx, |workspace, window, cx| {
7647            workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
7648        });
7649        project.update(cx, |project, cx| {
7650            assert_eq!(
7651                project.active_entry(),
7652                project
7653                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7654                    .map(|e| e.id)
7655            );
7656        });
7657        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7658
7659        // Add a second item to a non-empty pane
7660        workspace.update_in(cx, |workspace, window, cx| {
7661            workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
7662        });
7663        assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
7664        project.update(cx, |project, cx| {
7665            assert_eq!(
7666                project.active_entry(),
7667                project
7668                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
7669                    .map(|e| e.id)
7670            );
7671        });
7672
7673        // Close the active item
7674        pane.update_in(cx, |pane, window, cx| {
7675            pane.close_active_item(&Default::default(), window, cx)
7676                .unwrap()
7677        })
7678        .await
7679        .unwrap();
7680        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7681        project.update(cx, |project, cx| {
7682            assert_eq!(
7683                project.active_entry(),
7684                project
7685                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7686                    .map(|e| e.id)
7687            );
7688        });
7689
7690        // Add a project folder
7691        project
7692            .update(cx, |project, cx| {
7693                project.find_or_create_worktree("root2", true, cx)
7694            })
7695            .await
7696            .unwrap();
7697        assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
7698
7699        // Remove a project folder
7700        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
7701        assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
7702    }
7703
7704    #[gpui::test]
7705    async fn test_close_window(cx: &mut TestAppContext) {
7706        init_test(cx);
7707
7708        let fs = FakeFs::new(cx.executor());
7709        fs.insert_tree("/root", json!({ "one": "" })).await;
7710
7711        let project = Project::test(fs, ["root".as_ref()], cx).await;
7712        let (workspace, cx) =
7713            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7714
7715        // When there are no dirty items, there's nothing to do.
7716        let item1 = cx.new(TestItem::new);
7717        workspace.update_in(cx, |w, window, cx| {
7718            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
7719        });
7720        let task = workspace.update_in(cx, |w, window, cx| {
7721            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7722        });
7723        assert!(task.await.unwrap());
7724
7725        // When there are dirty untitled items, prompt to save each one. If the user
7726        // cancels any prompt, then abort.
7727        let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
7728        let item3 = cx.new(|cx| {
7729            TestItem::new(cx)
7730                .with_dirty(true)
7731                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7732        });
7733        workspace.update_in(cx, |w, window, cx| {
7734            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7735            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7736        });
7737        let task = workspace.update_in(cx, |w, window, cx| {
7738            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7739        });
7740        cx.executor().run_until_parked();
7741        cx.simulate_prompt_answer("Cancel"); // cancel save all
7742        cx.executor().run_until_parked();
7743        assert!(!cx.has_pending_prompt());
7744        assert!(!task.await.unwrap());
7745    }
7746
7747    #[gpui::test]
7748    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
7749        init_test(cx);
7750
7751        // Register TestItem as a serializable item
7752        cx.update(|cx| {
7753            register_serializable_item::<TestItem>(cx);
7754        });
7755
7756        let fs = FakeFs::new(cx.executor());
7757        fs.insert_tree("/root", json!({ "one": "" })).await;
7758
7759        let project = Project::test(fs, ["root".as_ref()], cx).await;
7760        let (workspace, cx) =
7761            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7762
7763        // When there are dirty untitled items, but they can serialize, then there is no prompt.
7764        let item1 = cx.new(|cx| {
7765            TestItem::new(cx)
7766                .with_dirty(true)
7767                .with_serialize(|| Some(Task::ready(Ok(()))))
7768        });
7769        let item2 = cx.new(|cx| {
7770            TestItem::new(cx)
7771                .with_dirty(true)
7772                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7773                .with_serialize(|| Some(Task::ready(Ok(()))))
7774        });
7775        workspace.update_in(cx, |w, window, cx| {
7776            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7777            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7778        });
7779        let task = workspace.update_in(cx, |w, window, cx| {
7780            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7781        });
7782        assert!(task.await.unwrap());
7783    }
7784
7785    #[gpui::test]
7786    async fn test_close_pane_items(cx: &mut TestAppContext) {
7787        init_test(cx);
7788
7789        let fs = FakeFs::new(cx.executor());
7790
7791        let project = Project::test(fs, None, cx).await;
7792        let (workspace, cx) =
7793            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7794
7795        let item1 = cx.new(|cx| {
7796            TestItem::new(cx)
7797                .with_dirty(true)
7798                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7799        });
7800        let item2 = cx.new(|cx| {
7801            TestItem::new(cx)
7802                .with_dirty(true)
7803                .with_conflict(true)
7804                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7805        });
7806        let item3 = cx.new(|cx| {
7807            TestItem::new(cx)
7808                .with_dirty(true)
7809                .with_conflict(true)
7810                .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7811        });
7812        let item4 = cx.new(|cx| {
7813            TestItem::new(cx).with_dirty(true).with_project_items(&[{
7814                let project_item = TestProjectItem::new_untitled(cx);
7815                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7816                project_item
7817            }])
7818        });
7819        let pane = workspace.update_in(cx, |workspace, window, cx| {
7820            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7821            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7822            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7823            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7824            workspace.active_pane().clone()
7825        });
7826
7827        let close_items = pane.update_in(cx, |pane, window, cx| {
7828            pane.activate_item(1, true, true, window, cx);
7829            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7830            let item1_id = item1.item_id();
7831            let item3_id = item3.item_id();
7832            let item4_id = item4.item_id();
7833            pane.close_items(window, cx, SaveIntent::Close, move |id| {
7834                [item1_id, item3_id, item4_id].contains(&id)
7835            })
7836        });
7837        cx.executor().run_until_parked();
7838
7839        assert!(cx.has_pending_prompt());
7840        cx.simulate_prompt_answer("Save all");
7841
7842        cx.executor().run_until_parked();
7843
7844        // Item 1 is saved. There's a prompt to save item 3.
7845        pane.update(cx, |pane, cx| {
7846            assert_eq!(item1.read(cx).save_count, 1);
7847            assert_eq!(item1.read(cx).save_as_count, 0);
7848            assert_eq!(item1.read(cx).reload_count, 0);
7849            assert_eq!(pane.items_len(), 3);
7850            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7851        });
7852        assert!(cx.has_pending_prompt());
7853
7854        // Cancel saving item 3.
7855        cx.simulate_prompt_answer("Discard");
7856        cx.executor().run_until_parked();
7857
7858        // Item 3 is reloaded. There's a prompt to save item 4.
7859        pane.update(cx, |pane, cx| {
7860            assert_eq!(item3.read(cx).save_count, 0);
7861            assert_eq!(item3.read(cx).save_as_count, 0);
7862            assert_eq!(item3.read(cx).reload_count, 1);
7863            assert_eq!(pane.items_len(), 2);
7864            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7865        });
7866
7867        // There's a prompt for a path for item 4.
7868        cx.simulate_new_path_selection(|_| Some(Default::default()));
7869        close_items.await.unwrap();
7870
7871        // The requested items are closed.
7872        pane.update(cx, |pane, cx| {
7873            assert_eq!(item4.read(cx).save_count, 0);
7874            assert_eq!(item4.read(cx).save_as_count, 1);
7875            assert_eq!(item4.read(cx).reload_count, 0);
7876            assert_eq!(pane.items_len(), 1);
7877            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7878        });
7879    }
7880
7881    #[gpui::test]
7882    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7883        init_test(cx);
7884
7885        let fs = FakeFs::new(cx.executor());
7886        let project = Project::test(fs, [], cx).await;
7887        let (workspace, cx) =
7888            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7889
7890        // Create several workspace items with single project entries, and two
7891        // workspace items with multiple project entries.
7892        let single_entry_items = (0..=4)
7893            .map(|project_entry_id| {
7894                cx.new(|cx| {
7895                    TestItem::new(cx)
7896                        .with_dirty(true)
7897                        .with_project_items(&[dirty_project_item(
7898                            project_entry_id,
7899                            &format!("{project_entry_id}.txt"),
7900                            cx,
7901                        )])
7902                })
7903            })
7904            .collect::<Vec<_>>();
7905        let item_2_3 = cx.new(|cx| {
7906            TestItem::new(cx)
7907                .with_dirty(true)
7908                .with_singleton(false)
7909                .with_project_items(&[
7910                    single_entry_items[2].read(cx).project_items[0].clone(),
7911                    single_entry_items[3].read(cx).project_items[0].clone(),
7912                ])
7913        });
7914        let item_3_4 = cx.new(|cx| {
7915            TestItem::new(cx)
7916                .with_dirty(true)
7917                .with_singleton(false)
7918                .with_project_items(&[
7919                    single_entry_items[3].read(cx).project_items[0].clone(),
7920                    single_entry_items[4].read(cx).project_items[0].clone(),
7921                ])
7922        });
7923
7924        // Create two panes that contain the following project entries:
7925        //   left pane:
7926        //     multi-entry items:   (2, 3)
7927        //     single-entry items:  0, 2, 3, 4
7928        //   right pane:
7929        //     single-entry items:  4, 1
7930        //     multi-entry items:   (3, 4)
7931        let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
7932            let left_pane = workspace.active_pane().clone();
7933            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7934            workspace.add_item_to_active_pane(
7935                single_entry_items[0].boxed_clone(),
7936                None,
7937                true,
7938                window,
7939                cx,
7940            );
7941            workspace.add_item_to_active_pane(
7942                single_entry_items[2].boxed_clone(),
7943                None,
7944                true,
7945                window,
7946                cx,
7947            );
7948            workspace.add_item_to_active_pane(
7949                single_entry_items[3].boxed_clone(),
7950                None,
7951                true,
7952                window,
7953                cx,
7954            );
7955            workspace.add_item_to_active_pane(
7956                single_entry_items[4].boxed_clone(),
7957                None,
7958                true,
7959                window,
7960                cx,
7961            );
7962
7963            let right_pane = workspace
7964                .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7965                .unwrap();
7966
7967            right_pane.update(cx, |pane, cx| {
7968                pane.add_item(
7969                    single_entry_items[1].boxed_clone(),
7970                    true,
7971                    true,
7972                    None,
7973                    window,
7974                    cx,
7975                );
7976                pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7977            });
7978
7979            (left_pane, right_pane)
7980        });
7981
7982        cx.focus(&right_pane);
7983
7984        let mut close = right_pane.update_in(cx, |pane, window, cx| {
7985            pane.close_all_items(&CloseAllItems::default(), window, cx)
7986                .unwrap()
7987        });
7988        cx.executor().run_until_parked();
7989
7990        let msg = cx.pending_prompt().unwrap().0;
7991        assert!(msg.contains("1.txt"));
7992        assert!(!msg.contains("2.txt"));
7993        assert!(!msg.contains("3.txt"));
7994        assert!(!msg.contains("4.txt"));
7995
7996        cx.simulate_prompt_answer("Cancel");
7997        close.await.unwrap();
7998
7999        left_pane
8000            .update_in(cx, |left_pane, window, cx| {
8001                left_pane.close_item_by_id(
8002                    single_entry_items[3].entity_id(),
8003                    SaveIntent::Skip,
8004                    window,
8005                    cx,
8006                )
8007            })
8008            .await
8009            .unwrap();
8010
8011        close = right_pane.update_in(cx, |pane, window, cx| {
8012            pane.close_all_items(&CloseAllItems::default(), window, cx)
8013                .unwrap()
8014        });
8015        cx.executor().run_until_parked();
8016
8017        let details = cx.pending_prompt().unwrap().1;
8018        assert!(details.contains("1.txt"));
8019        assert!(!details.contains("2.txt"));
8020        assert!(details.contains("3.txt"));
8021        // ideally this assertion could be made, but today we can only
8022        // save whole items not project items, so the orphaned item 3 causes
8023        // 4 to be saved too.
8024        // assert!(!details.contains("4.txt"));
8025
8026        cx.simulate_prompt_answer("Save all");
8027
8028        cx.executor().run_until_parked();
8029        close.await.unwrap();
8030        right_pane.update(cx, |pane, _| {
8031            assert_eq!(pane.items_len(), 0);
8032        });
8033    }
8034
8035    #[gpui::test]
8036    async fn test_autosave(cx: &mut gpui::TestAppContext) {
8037        init_test(cx);
8038
8039        let fs = FakeFs::new(cx.executor());
8040        let project = Project::test(fs, [], cx).await;
8041        let (workspace, cx) =
8042            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8043        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8044
8045        let item = cx.new(|cx| {
8046            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
8047        });
8048        let item_id = item.entity_id();
8049        workspace.update_in(cx, |workspace, window, cx| {
8050            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8051        });
8052
8053        // Autosave on window change.
8054        item.update(cx, |item, cx| {
8055            SettingsStore::update_global(cx, |settings, cx| {
8056                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8057                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
8058                })
8059            });
8060            item.is_dirty = true;
8061        });
8062
8063        // Deactivating the window saves the file.
8064        cx.deactivate_window();
8065        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
8066
8067        // Re-activating the window doesn't save the file.
8068        cx.update(|window, _| window.activate_window());
8069        cx.executor().run_until_parked();
8070        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
8071
8072        // Autosave on focus change.
8073        item.update_in(cx, |item, window, cx| {
8074            cx.focus_self(window);
8075            SettingsStore::update_global(cx, |settings, cx| {
8076                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8077                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
8078                })
8079            });
8080            item.is_dirty = true;
8081        });
8082
8083        // Blurring the item saves the file.
8084        item.update_in(cx, |_, window, _| window.blur());
8085        cx.executor().run_until_parked();
8086        item.update(cx, |item, _| assert_eq!(item.save_count, 2));
8087
8088        // Deactivating the window still saves the file.
8089        item.update_in(cx, |item, window, cx| {
8090            cx.focus_self(window);
8091            item.is_dirty = true;
8092        });
8093        cx.deactivate_window();
8094        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
8095
8096        // Autosave after delay.
8097        item.update(cx, |item, cx| {
8098            SettingsStore::update_global(cx, |settings, cx| {
8099                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8100                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
8101                })
8102            });
8103            item.is_dirty = true;
8104            cx.emit(ItemEvent::Edit);
8105        });
8106
8107        // Delay hasn't fully expired, so the file is still dirty and unsaved.
8108        cx.executor().advance_clock(Duration::from_millis(250));
8109        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
8110
8111        // After delay expires, the file is saved.
8112        cx.executor().advance_clock(Duration::from_millis(250));
8113        item.update(cx, |item, _| assert_eq!(item.save_count, 4));
8114
8115        // Autosave on focus change, ensuring closing the tab counts as such.
8116        item.update(cx, |item, cx| {
8117            SettingsStore::update_global(cx, |settings, cx| {
8118                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8119                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
8120                })
8121            });
8122            item.is_dirty = true;
8123            for project_item in &mut item.project_items {
8124                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
8125            }
8126        });
8127
8128        pane.update_in(cx, |pane, window, cx| {
8129            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
8130        })
8131        .await
8132        .unwrap();
8133        assert!(!cx.has_pending_prompt());
8134        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
8135
8136        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
8137        workspace.update_in(cx, |workspace, window, cx| {
8138            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8139        });
8140        item.update_in(cx, |item, window, cx| {
8141            item.project_items[0].update(cx, |item, _| {
8142                item.entry_id = None;
8143            });
8144            item.is_dirty = true;
8145            window.blur();
8146        });
8147        cx.run_until_parked();
8148        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
8149
8150        // Ensure autosave is prevented for deleted files also when closing the buffer.
8151        let _close_items = pane.update_in(cx, |pane, window, cx| {
8152            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
8153        });
8154        cx.run_until_parked();
8155        assert!(cx.has_pending_prompt());
8156        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
8157    }
8158
8159    #[gpui::test]
8160    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
8161        init_test(cx);
8162
8163        let fs = FakeFs::new(cx.executor());
8164
8165        let project = Project::test(fs, [], cx).await;
8166        let (workspace, cx) =
8167            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8168
8169        let item = cx.new(|cx| {
8170            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
8171        });
8172        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8173        let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
8174        let toolbar_notify_count = Rc::new(RefCell::new(0));
8175
8176        workspace.update_in(cx, |workspace, window, cx| {
8177            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8178            let toolbar_notification_count = toolbar_notify_count.clone();
8179            cx.observe_in(&toolbar, window, move |_, _, _, _| {
8180                *toolbar_notification_count.borrow_mut() += 1
8181            })
8182            .detach();
8183        });
8184
8185        pane.update(cx, |pane, _| {
8186            assert!(!pane.can_navigate_backward());
8187            assert!(!pane.can_navigate_forward());
8188        });
8189
8190        item.update_in(cx, |item, _, cx| {
8191            item.set_state("one".to_string(), cx);
8192        });
8193
8194        // Toolbar must be notified to re-render the navigation buttons
8195        assert_eq!(*toolbar_notify_count.borrow(), 1);
8196
8197        pane.update(cx, |pane, _| {
8198            assert!(pane.can_navigate_backward());
8199            assert!(!pane.can_navigate_forward());
8200        });
8201
8202        workspace
8203            .update_in(cx, |workspace, window, cx| {
8204                workspace.go_back(pane.downgrade(), window, cx)
8205            })
8206            .await
8207            .unwrap();
8208
8209        assert_eq!(*toolbar_notify_count.borrow(), 2);
8210        pane.update(cx, |pane, _| {
8211            assert!(!pane.can_navigate_backward());
8212            assert!(pane.can_navigate_forward());
8213        });
8214    }
8215
8216    #[gpui::test]
8217    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
8218        init_test(cx);
8219        let fs = FakeFs::new(cx.executor());
8220
8221        let project = Project::test(fs, [], cx).await;
8222        let (workspace, cx) =
8223            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8224
8225        let panel = workspace.update_in(cx, |workspace, window, cx| {
8226            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8227            workspace.add_panel(panel.clone(), window, cx);
8228
8229            workspace
8230                .right_dock()
8231                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8232
8233            panel
8234        });
8235
8236        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8237        pane.update_in(cx, |pane, window, cx| {
8238            let item = cx.new(TestItem::new);
8239            pane.add_item(Box::new(item), true, true, None, window, cx);
8240        });
8241
8242        // Transfer focus from center to panel
8243        workspace.update_in(cx, |workspace, window, cx| {
8244            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8245        });
8246
8247        workspace.update_in(cx, |workspace, window, cx| {
8248            assert!(workspace.right_dock().read(cx).is_open());
8249            assert!(!panel.is_zoomed(window, cx));
8250            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8251        });
8252
8253        // Transfer focus from panel to center
8254        workspace.update_in(cx, |workspace, window, cx| {
8255            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8256        });
8257
8258        workspace.update_in(cx, |workspace, window, cx| {
8259            assert!(workspace.right_dock().read(cx).is_open());
8260            assert!(!panel.is_zoomed(window, cx));
8261            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8262        });
8263
8264        // Close the dock
8265        workspace.update_in(cx, |workspace, window, cx| {
8266            workspace.toggle_dock(DockPosition::Right, window, cx);
8267        });
8268
8269        workspace.update_in(cx, |workspace, window, cx| {
8270            assert!(!workspace.right_dock().read(cx).is_open());
8271            assert!(!panel.is_zoomed(window, cx));
8272            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8273        });
8274
8275        // Open the dock
8276        workspace.update_in(cx, |workspace, window, cx| {
8277            workspace.toggle_dock(DockPosition::Right, window, cx);
8278        });
8279
8280        workspace.update_in(cx, |workspace, window, cx| {
8281            assert!(workspace.right_dock().read(cx).is_open());
8282            assert!(!panel.is_zoomed(window, cx));
8283            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8284        });
8285
8286        // Focus and zoom panel
8287        panel.update_in(cx, |panel, window, cx| {
8288            cx.focus_self(window);
8289            panel.set_zoomed(true, window, cx)
8290        });
8291
8292        workspace.update_in(cx, |workspace, window, cx| {
8293            assert!(workspace.right_dock().read(cx).is_open());
8294            assert!(panel.is_zoomed(window, cx));
8295            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8296        });
8297
8298        // Transfer focus to the center closes the dock
8299        workspace.update_in(cx, |workspace, window, cx| {
8300            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8301        });
8302
8303        workspace.update_in(cx, |workspace, window, cx| {
8304            assert!(!workspace.right_dock().read(cx).is_open());
8305            assert!(panel.is_zoomed(window, cx));
8306            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8307        });
8308
8309        // Transferring focus back to the panel keeps it zoomed
8310        workspace.update_in(cx, |workspace, window, cx| {
8311            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8312        });
8313
8314        workspace.update_in(cx, |workspace, window, cx| {
8315            assert!(workspace.right_dock().read(cx).is_open());
8316            assert!(panel.is_zoomed(window, cx));
8317            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8318        });
8319
8320        // Close the dock while it is zoomed
8321        workspace.update_in(cx, |workspace, window, cx| {
8322            workspace.toggle_dock(DockPosition::Right, window, cx)
8323        });
8324
8325        workspace.update_in(cx, |workspace, window, cx| {
8326            assert!(!workspace.right_dock().read(cx).is_open());
8327            assert!(panel.is_zoomed(window, cx));
8328            assert!(workspace.zoomed.is_none());
8329            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8330        });
8331
8332        // Opening the dock, when it's zoomed, retains focus
8333        workspace.update_in(cx, |workspace, window, cx| {
8334            workspace.toggle_dock(DockPosition::Right, window, cx)
8335        });
8336
8337        workspace.update_in(cx, |workspace, window, cx| {
8338            assert!(workspace.right_dock().read(cx).is_open());
8339            assert!(panel.is_zoomed(window, cx));
8340            assert!(workspace.zoomed.is_some());
8341            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8342        });
8343
8344        // Unzoom and close the panel, zoom the active pane.
8345        panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
8346        workspace.update_in(cx, |workspace, window, cx| {
8347            workspace.toggle_dock(DockPosition::Right, window, cx)
8348        });
8349        pane.update_in(cx, |pane, window, cx| {
8350            pane.toggle_zoom(&Default::default(), window, cx)
8351        });
8352
8353        // Opening a dock unzooms the pane.
8354        workspace.update_in(cx, |workspace, window, cx| {
8355            workspace.toggle_dock(DockPosition::Right, window, cx)
8356        });
8357        workspace.update_in(cx, |workspace, window, cx| {
8358            let pane = pane.read(cx);
8359            assert!(!pane.is_zoomed());
8360            assert!(!pane.focus_handle(cx).is_focused(window));
8361            assert!(workspace.right_dock().read(cx).is_open());
8362            assert!(workspace.zoomed.is_none());
8363        });
8364    }
8365
8366    #[gpui::test]
8367    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
8368        init_test(cx);
8369
8370        let fs = FakeFs::new(cx.executor());
8371
8372        let project = Project::test(fs, None, cx).await;
8373        let (workspace, cx) =
8374            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8375
8376        // Let's arrange the panes like this:
8377        //
8378        // +-----------------------+
8379        // |         top           |
8380        // +------+--------+-------+
8381        // | left | center | right |
8382        // +------+--------+-------+
8383        // |        bottom         |
8384        // +-----------------------+
8385
8386        let top_item = cx.new(|cx| {
8387            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
8388        });
8389        let bottom_item = cx.new(|cx| {
8390            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
8391        });
8392        let left_item = cx.new(|cx| {
8393            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
8394        });
8395        let right_item = cx.new(|cx| {
8396            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
8397        });
8398        let center_item = cx.new(|cx| {
8399            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
8400        });
8401
8402        let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8403            let top_pane_id = workspace.active_pane().entity_id();
8404            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
8405            workspace.split_pane(
8406                workspace.active_pane().clone(),
8407                SplitDirection::Down,
8408                window,
8409                cx,
8410            );
8411            top_pane_id
8412        });
8413        let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8414            let bottom_pane_id = workspace.active_pane().entity_id();
8415            workspace.add_item_to_active_pane(
8416                Box::new(bottom_item.clone()),
8417                None,
8418                false,
8419                window,
8420                cx,
8421            );
8422            workspace.split_pane(
8423                workspace.active_pane().clone(),
8424                SplitDirection::Up,
8425                window,
8426                cx,
8427            );
8428            bottom_pane_id
8429        });
8430        let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8431            let left_pane_id = workspace.active_pane().entity_id();
8432            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
8433            workspace.split_pane(
8434                workspace.active_pane().clone(),
8435                SplitDirection::Right,
8436                window,
8437                cx,
8438            );
8439            left_pane_id
8440        });
8441        let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8442            let right_pane_id = workspace.active_pane().entity_id();
8443            workspace.add_item_to_active_pane(
8444                Box::new(right_item.clone()),
8445                None,
8446                false,
8447                window,
8448                cx,
8449            );
8450            workspace.split_pane(
8451                workspace.active_pane().clone(),
8452                SplitDirection::Left,
8453                window,
8454                cx,
8455            );
8456            right_pane_id
8457        });
8458        let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8459            let center_pane_id = workspace.active_pane().entity_id();
8460            workspace.add_item_to_active_pane(
8461                Box::new(center_item.clone()),
8462                None,
8463                false,
8464                window,
8465                cx,
8466            );
8467            center_pane_id
8468        });
8469        cx.executor().run_until_parked();
8470
8471        workspace.update_in(cx, |workspace, window, cx| {
8472            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
8473
8474            // Join into next from center pane into right
8475            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8476        });
8477
8478        workspace.update_in(cx, |workspace, window, cx| {
8479            let active_pane = workspace.active_pane();
8480            assert_eq!(right_pane_id, active_pane.entity_id());
8481            assert_eq!(2, active_pane.read(cx).items_len());
8482            let item_ids_in_pane =
8483                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8484            assert!(item_ids_in_pane.contains(&center_item.item_id()));
8485            assert!(item_ids_in_pane.contains(&right_item.item_id()));
8486
8487            // Join into next from right pane into bottom
8488            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8489        });
8490
8491        workspace.update_in(cx, |workspace, window, cx| {
8492            let active_pane = workspace.active_pane();
8493            assert_eq!(bottom_pane_id, active_pane.entity_id());
8494            assert_eq!(3, active_pane.read(cx).items_len());
8495            let item_ids_in_pane =
8496                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8497            assert!(item_ids_in_pane.contains(&center_item.item_id()));
8498            assert!(item_ids_in_pane.contains(&right_item.item_id()));
8499            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8500
8501            // Join into next from bottom pane into left
8502            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8503        });
8504
8505        workspace.update_in(cx, |workspace, window, cx| {
8506            let active_pane = workspace.active_pane();
8507            assert_eq!(left_pane_id, active_pane.entity_id());
8508            assert_eq!(4, active_pane.read(cx).items_len());
8509            let item_ids_in_pane =
8510                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8511            assert!(item_ids_in_pane.contains(&center_item.item_id()));
8512            assert!(item_ids_in_pane.contains(&right_item.item_id()));
8513            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8514            assert!(item_ids_in_pane.contains(&left_item.item_id()));
8515
8516            // Join into next from left pane into top
8517            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8518        });
8519
8520        workspace.update_in(cx, |workspace, window, cx| {
8521            let active_pane = workspace.active_pane();
8522            assert_eq!(top_pane_id, active_pane.entity_id());
8523            assert_eq!(5, active_pane.read(cx).items_len());
8524            let item_ids_in_pane =
8525                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8526            assert!(item_ids_in_pane.contains(&center_item.item_id()));
8527            assert!(item_ids_in_pane.contains(&right_item.item_id()));
8528            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8529            assert!(item_ids_in_pane.contains(&left_item.item_id()));
8530            assert!(item_ids_in_pane.contains(&top_item.item_id()));
8531
8532            // Single pane left: no-op
8533            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
8534        });
8535
8536        workspace.update(cx, |workspace, _cx| {
8537            let active_pane = workspace.active_pane();
8538            assert_eq!(top_pane_id, active_pane.entity_id());
8539        });
8540    }
8541
8542    fn add_an_item_to_active_pane(
8543        cx: &mut VisualTestContext,
8544        workspace: &Entity<Workspace>,
8545        item_id: u64,
8546    ) -> Entity<TestItem> {
8547        let item = cx.new(|cx| {
8548            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
8549                item_id,
8550                "item{item_id}.txt",
8551                cx,
8552            )])
8553        });
8554        workspace.update_in(cx, |workspace, window, cx| {
8555            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
8556        });
8557        return item;
8558    }
8559
8560    fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
8561        return workspace.update_in(cx, |workspace, window, cx| {
8562            let new_pane = workspace.split_pane(
8563                workspace.active_pane().clone(),
8564                SplitDirection::Right,
8565                window,
8566                cx,
8567            );
8568            new_pane
8569        });
8570    }
8571
8572    #[gpui::test]
8573    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
8574        init_test(cx);
8575        let fs = FakeFs::new(cx.executor());
8576        let project = Project::test(fs, None, cx).await;
8577        let (workspace, cx) =
8578            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8579
8580        add_an_item_to_active_pane(cx, &workspace, 1);
8581        split_pane(cx, &workspace);
8582        add_an_item_to_active_pane(cx, &workspace, 2);
8583        split_pane(cx, &workspace); // empty pane
8584        split_pane(cx, &workspace);
8585        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
8586
8587        cx.executor().run_until_parked();
8588
8589        workspace.update(cx, |workspace, cx| {
8590            let num_panes = workspace.panes().len();
8591            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8592            let active_item = workspace
8593                .active_pane()
8594                .read(cx)
8595                .active_item()
8596                .expect("item is in focus");
8597
8598            assert_eq!(num_panes, 4);
8599            assert_eq!(num_items_in_current_pane, 1);
8600            assert_eq!(active_item.item_id(), last_item.item_id());
8601        });
8602
8603        workspace.update_in(cx, |workspace, window, cx| {
8604            workspace.join_all_panes(window, cx);
8605        });
8606
8607        workspace.update(cx, |workspace, cx| {
8608            let num_panes = workspace.panes().len();
8609            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8610            let active_item = workspace
8611                .active_pane()
8612                .read(cx)
8613                .active_item()
8614                .expect("item is in focus");
8615
8616            assert_eq!(num_panes, 1);
8617            assert_eq!(num_items_in_current_pane, 3);
8618            assert_eq!(active_item.item_id(), last_item.item_id());
8619        });
8620    }
8621    struct TestModal(FocusHandle);
8622
8623    impl TestModal {
8624        fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
8625            Self(cx.focus_handle())
8626        }
8627    }
8628
8629    impl EventEmitter<DismissEvent> for TestModal {}
8630
8631    impl Focusable for TestModal {
8632        fn focus_handle(&self, _cx: &App) -> FocusHandle {
8633            self.0.clone()
8634        }
8635    }
8636
8637    impl ModalView for TestModal {}
8638
8639    impl Render for TestModal {
8640        fn render(
8641            &mut self,
8642            _window: &mut Window,
8643            _cx: &mut Context<TestModal>,
8644        ) -> impl IntoElement {
8645            div().track_focus(&self.0)
8646        }
8647    }
8648
8649    #[gpui::test]
8650    async fn test_panels(cx: &mut gpui::TestAppContext) {
8651        init_test(cx);
8652        let fs = FakeFs::new(cx.executor());
8653
8654        let project = Project::test(fs, [], cx).await;
8655        let (workspace, cx) =
8656            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8657
8658        let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
8659            let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
8660            workspace.add_panel(panel_1.clone(), window, cx);
8661            workspace.toggle_dock(DockPosition::Left, window, cx);
8662            let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8663            workspace.add_panel(panel_2.clone(), window, cx);
8664            workspace.toggle_dock(DockPosition::Right, window, cx);
8665
8666            let left_dock = workspace.left_dock();
8667            assert_eq!(
8668                left_dock.read(cx).visible_panel().unwrap().panel_id(),
8669                panel_1.panel_id()
8670            );
8671            assert_eq!(
8672                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8673                panel_1.size(window, cx)
8674            );
8675
8676            left_dock.update(cx, |left_dock, cx| {
8677                left_dock.resize_active_panel(Some(px(1337.)), window, cx)
8678            });
8679            assert_eq!(
8680                workspace
8681                    .right_dock()
8682                    .read(cx)
8683                    .visible_panel()
8684                    .unwrap()
8685                    .panel_id(),
8686                panel_2.panel_id(),
8687            );
8688
8689            (panel_1, panel_2)
8690        });
8691
8692        // Move panel_1 to the right
8693        panel_1.update_in(cx, |panel_1, window, cx| {
8694            panel_1.set_position(DockPosition::Right, window, cx)
8695        });
8696
8697        workspace.update_in(cx, |workspace, window, cx| {
8698            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
8699            // Since it was the only panel on the left, the left dock should now be closed.
8700            assert!(!workspace.left_dock().read(cx).is_open());
8701            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
8702            let right_dock = workspace.right_dock();
8703            assert_eq!(
8704                right_dock.read(cx).visible_panel().unwrap().panel_id(),
8705                panel_1.panel_id()
8706            );
8707            assert_eq!(
8708                right_dock.read(cx).active_panel_size(window, cx).unwrap(),
8709                px(1337.)
8710            );
8711
8712            // Now we move panel_2 to the left
8713            panel_2.set_position(DockPosition::Left, window, cx);
8714        });
8715
8716        workspace.update(cx, |workspace, cx| {
8717            // Since panel_2 was not visible on the right, we don't open the left dock.
8718            assert!(!workspace.left_dock().read(cx).is_open());
8719            // And the right dock is unaffected in its displaying of panel_1
8720            assert!(workspace.right_dock().read(cx).is_open());
8721            assert_eq!(
8722                workspace
8723                    .right_dock()
8724                    .read(cx)
8725                    .visible_panel()
8726                    .unwrap()
8727                    .panel_id(),
8728                panel_1.panel_id(),
8729            );
8730        });
8731
8732        // Move panel_1 back to the left
8733        panel_1.update_in(cx, |panel_1, window, cx| {
8734            panel_1.set_position(DockPosition::Left, window, cx)
8735        });
8736
8737        workspace.update_in(cx, |workspace, window, cx| {
8738            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
8739            let left_dock = workspace.left_dock();
8740            assert!(left_dock.read(cx).is_open());
8741            assert_eq!(
8742                left_dock.read(cx).visible_panel().unwrap().panel_id(),
8743                panel_1.panel_id()
8744            );
8745            assert_eq!(
8746                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8747                px(1337.)
8748            );
8749            // And the right dock should be closed as it no longer has any panels.
8750            assert!(!workspace.right_dock().read(cx).is_open());
8751
8752            // Now we move panel_1 to the bottom
8753            panel_1.set_position(DockPosition::Bottom, window, cx);
8754        });
8755
8756        workspace.update_in(cx, |workspace, window, cx| {
8757            // Since panel_1 was visible on the left, we close the left dock.
8758            assert!(!workspace.left_dock().read(cx).is_open());
8759            // The bottom dock is sized based on the panel's default size,
8760            // since the panel orientation changed from vertical to horizontal.
8761            let bottom_dock = workspace.bottom_dock();
8762            assert_eq!(
8763                bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
8764                panel_1.size(window, cx),
8765            );
8766            // Close bottom dock and move panel_1 back to the left.
8767            bottom_dock.update(cx, |bottom_dock, cx| {
8768                bottom_dock.set_open(false, window, cx)
8769            });
8770            panel_1.set_position(DockPosition::Left, window, cx);
8771        });
8772
8773        // Emit activated event on panel 1
8774        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
8775
8776        // Now the left dock is open and panel_1 is active and focused.
8777        workspace.update_in(cx, |workspace, window, cx| {
8778            let left_dock = workspace.left_dock();
8779            assert!(left_dock.read(cx).is_open());
8780            assert_eq!(
8781                left_dock.read(cx).visible_panel().unwrap().panel_id(),
8782                panel_1.panel_id(),
8783            );
8784            assert!(panel_1.focus_handle(cx).is_focused(window));
8785        });
8786
8787        // Emit closed event on panel 2, which is not active
8788        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8789
8790        // Wo don't close the left dock, because panel_2 wasn't the active panel
8791        workspace.update(cx, |workspace, cx| {
8792            let left_dock = workspace.left_dock();
8793            assert!(left_dock.read(cx).is_open());
8794            assert_eq!(
8795                left_dock.read(cx).visible_panel().unwrap().panel_id(),
8796                panel_1.panel_id(),
8797            );
8798        });
8799
8800        // Emitting a ZoomIn event shows the panel as zoomed.
8801        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
8802        workspace.update(cx, |workspace, _| {
8803            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8804            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
8805        });
8806
8807        // Move panel to another dock while it is zoomed
8808        panel_1.update_in(cx, |panel, window, cx| {
8809            panel.set_position(DockPosition::Right, window, cx)
8810        });
8811        workspace.update(cx, |workspace, _| {
8812            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8813
8814            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8815        });
8816
8817        // This is a helper for getting a:
8818        // - valid focus on an element,
8819        // - that isn't a part of the panes and panels system of the Workspace,
8820        // - and doesn't trigger the 'on_focus_lost' API.
8821        let focus_other_view = {
8822            let workspace = workspace.clone();
8823            move |cx: &mut VisualTestContext| {
8824                workspace.update_in(cx, |workspace, window, cx| {
8825                    if let Some(_) = workspace.active_modal::<TestModal>(cx) {
8826                        workspace.toggle_modal(window, cx, TestModal::new);
8827                        workspace.toggle_modal(window, cx, TestModal::new);
8828                    } else {
8829                        workspace.toggle_modal(window, cx, TestModal::new);
8830                    }
8831                })
8832            }
8833        };
8834
8835        // If focus is transferred to another view that's not a panel or another pane, we still show
8836        // the panel as zoomed.
8837        focus_other_view(cx);
8838        workspace.update(cx, |workspace, _| {
8839            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8840            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8841        });
8842
8843        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8844        workspace.update_in(cx, |_workspace, window, cx| {
8845            cx.focus_self(window);
8846        });
8847        workspace.update(cx, |workspace, _| {
8848            assert_eq!(workspace.zoomed, None);
8849            assert_eq!(workspace.zoomed_position, None);
8850        });
8851
8852        // If focus is transferred again to another view that's not a panel or a pane, we won't
8853        // show the panel as zoomed because it wasn't zoomed before.
8854        focus_other_view(cx);
8855        workspace.update(cx, |workspace, _| {
8856            assert_eq!(workspace.zoomed, None);
8857            assert_eq!(workspace.zoomed_position, None);
8858        });
8859
8860        // When the panel is activated, it is zoomed again.
8861        cx.dispatch_action(ToggleRightDock);
8862        workspace.update(cx, |workspace, _| {
8863            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8864            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8865        });
8866
8867        // Emitting a ZoomOut event unzooms the panel.
8868        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8869        workspace.update(cx, |workspace, _| {
8870            assert_eq!(workspace.zoomed, None);
8871            assert_eq!(workspace.zoomed_position, None);
8872        });
8873
8874        // Emit closed event on panel 1, which is active
8875        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8876
8877        // Now the left dock is closed, because panel_1 was the active panel
8878        workspace.update(cx, |workspace, cx| {
8879            let right_dock = workspace.right_dock();
8880            assert!(!right_dock.read(cx).is_open());
8881        });
8882    }
8883
8884    #[gpui::test]
8885    async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8886        init_test(cx);
8887
8888        let fs = FakeFs::new(cx.background_executor.clone());
8889        let project = Project::test(fs, [], cx).await;
8890        let (workspace, cx) =
8891            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8892        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8893
8894        let dirty_regular_buffer = cx.new(|cx| {
8895            TestItem::new(cx)
8896                .with_dirty(true)
8897                .with_label("1.txt")
8898                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8899        });
8900        let dirty_regular_buffer_2 = cx.new(|cx| {
8901            TestItem::new(cx)
8902                .with_dirty(true)
8903                .with_label("2.txt")
8904                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8905        });
8906        let dirty_multi_buffer_with_both = cx.new(|cx| {
8907            TestItem::new(cx)
8908                .with_dirty(true)
8909                .with_singleton(false)
8910                .with_label("Fake Project Search")
8911                .with_project_items(&[
8912                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8913                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8914                ])
8915        });
8916        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8917        workspace.update_in(cx, |workspace, window, cx| {
8918            workspace.add_item(
8919                pane.clone(),
8920                Box::new(dirty_regular_buffer.clone()),
8921                None,
8922                false,
8923                false,
8924                window,
8925                cx,
8926            );
8927            workspace.add_item(
8928                pane.clone(),
8929                Box::new(dirty_regular_buffer_2.clone()),
8930                None,
8931                false,
8932                false,
8933                window,
8934                cx,
8935            );
8936            workspace.add_item(
8937                pane.clone(),
8938                Box::new(dirty_multi_buffer_with_both.clone()),
8939                None,
8940                false,
8941                false,
8942                window,
8943                cx,
8944            );
8945        });
8946
8947        pane.update_in(cx, |pane, window, cx| {
8948            pane.activate_item(2, true, true, window, cx);
8949            assert_eq!(
8950                pane.active_item().unwrap().item_id(),
8951                multi_buffer_with_both_files_id,
8952                "Should select the multi buffer in the pane"
8953            );
8954        });
8955        let close_all_but_multi_buffer_task = pane
8956            .update_in(cx, |pane, window, cx| {
8957                pane.close_inactive_items(
8958                    &CloseInactiveItems {
8959                        save_intent: Some(SaveIntent::Save),
8960                        close_pinned: true,
8961                    },
8962                    window,
8963                    cx,
8964                )
8965            })
8966            .expect("should have inactive files to close");
8967        cx.background_executor.run_until_parked();
8968        assert!(!cx.has_pending_prompt());
8969        close_all_but_multi_buffer_task
8970            .await
8971            .expect("Closing all buffers but the multi buffer failed");
8972        pane.update(cx, |pane, cx| {
8973            assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
8974            assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8975            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
8976            assert_eq!(pane.items_len(), 1);
8977            assert_eq!(
8978                pane.active_item().unwrap().item_id(),
8979                multi_buffer_with_both_files_id,
8980                "Should have only the multi buffer left in the pane"
8981            );
8982            assert!(
8983                dirty_multi_buffer_with_both.read(cx).is_dirty,
8984                "The multi buffer containing the unsaved buffer should still be dirty"
8985            );
8986        });
8987
8988        dirty_regular_buffer.update(cx, |buffer, cx| {
8989            buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
8990        });
8991
8992        let close_multi_buffer_task = pane
8993            .update_in(cx, |pane, window, cx| {
8994                pane.close_active_item(
8995                    &CloseActiveItem {
8996                        save_intent: Some(SaveIntent::Close),
8997                        close_pinned: false,
8998                    },
8999                    window,
9000                    cx,
9001                )
9002            })
9003            .expect("should have the multi buffer to close");
9004        cx.background_executor.run_until_parked();
9005        assert!(
9006            cx.has_pending_prompt(),
9007            "Dirty multi buffer should prompt a save dialog"
9008        );
9009        cx.simulate_prompt_answer("Save");
9010        cx.background_executor.run_until_parked();
9011        close_multi_buffer_task
9012            .await
9013            .expect("Closing the multi buffer failed");
9014        pane.update(cx, |pane, cx| {
9015            assert_eq!(
9016                dirty_multi_buffer_with_both.read(cx).save_count,
9017                1,
9018                "Multi buffer item should get be saved"
9019            );
9020            // Test impl does not save inner items, so we do not assert them
9021            assert_eq!(
9022                pane.items_len(),
9023                0,
9024                "No more items should be left in the pane"
9025            );
9026            assert!(pane.active_item().is_none());
9027        });
9028    }
9029
9030    #[gpui::test]
9031    async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
9032        cx: &mut TestAppContext,
9033    ) {
9034        init_test(cx);
9035
9036        let fs = FakeFs::new(cx.background_executor.clone());
9037        let project = Project::test(fs, [], cx).await;
9038        let (workspace, cx) =
9039            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9040        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
9041
9042        let dirty_regular_buffer = cx.new(|cx| {
9043            TestItem::new(cx)
9044                .with_dirty(true)
9045                .with_label("1.txt")
9046                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9047        });
9048        let dirty_regular_buffer_2 = cx.new(|cx| {
9049            TestItem::new(cx)
9050                .with_dirty(true)
9051                .with_label("2.txt")
9052                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9053        });
9054        let clear_regular_buffer = cx.new(|cx| {
9055            TestItem::new(cx)
9056                .with_label("3.txt")
9057                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
9058        });
9059
9060        let dirty_multi_buffer_with_both = cx.new(|cx| {
9061            TestItem::new(cx)
9062                .with_dirty(true)
9063                .with_singleton(false)
9064                .with_label("Fake Project Search")
9065                .with_project_items(&[
9066                    dirty_regular_buffer.read(cx).project_items[0].clone(),
9067                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
9068                    clear_regular_buffer.read(cx).project_items[0].clone(),
9069                ])
9070        });
9071        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
9072        workspace.update_in(cx, |workspace, window, cx| {
9073            workspace.add_item(
9074                pane.clone(),
9075                Box::new(dirty_regular_buffer.clone()),
9076                None,
9077                false,
9078                false,
9079                window,
9080                cx,
9081            );
9082            workspace.add_item(
9083                pane.clone(),
9084                Box::new(dirty_multi_buffer_with_both.clone()),
9085                None,
9086                false,
9087                false,
9088                window,
9089                cx,
9090            );
9091        });
9092
9093        pane.update_in(cx, |pane, window, cx| {
9094            pane.activate_item(1, true, true, window, cx);
9095            assert_eq!(
9096                pane.active_item().unwrap().item_id(),
9097                multi_buffer_with_both_files_id,
9098                "Should select the multi buffer in the pane"
9099            );
9100        });
9101        let _close_multi_buffer_task = pane
9102            .update_in(cx, |pane, window, cx| {
9103                pane.close_active_item(
9104                    &CloseActiveItem {
9105                        save_intent: None,
9106                        close_pinned: false,
9107                    },
9108                    window,
9109                    cx,
9110                )
9111            })
9112            .expect("should have active multi buffer to close");
9113        cx.background_executor.run_until_parked();
9114        assert!(
9115            cx.has_pending_prompt(),
9116            "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
9117        );
9118    }
9119
9120    #[gpui::test]
9121    async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
9122        cx: &mut TestAppContext,
9123    ) {
9124        init_test(cx);
9125
9126        let fs = FakeFs::new(cx.background_executor.clone());
9127        let project = Project::test(fs, [], cx).await;
9128        let (workspace, cx) =
9129            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9130        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
9131
9132        let dirty_regular_buffer = cx.new(|cx| {
9133            TestItem::new(cx)
9134                .with_dirty(true)
9135                .with_label("1.txt")
9136                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9137        });
9138        let dirty_regular_buffer_2 = cx.new(|cx| {
9139            TestItem::new(cx)
9140                .with_dirty(true)
9141                .with_label("2.txt")
9142                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9143        });
9144        let clear_regular_buffer = cx.new(|cx| {
9145            TestItem::new(cx)
9146                .with_label("3.txt")
9147                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
9148        });
9149
9150        let dirty_multi_buffer = cx.new(|cx| {
9151            TestItem::new(cx)
9152                .with_dirty(true)
9153                .with_singleton(false)
9154                .with_label("Fake Project Search")
9155                .with_project_items(&[
9156                    dirty_regular_buffer.read(cx).project_items[0].clone(),
9157                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
9158                    clear_regular_buffer.read(cx).project_items[0].clone(),
9159                ])
9160        });
9161        workspace.update_in(cx, |workspace, window, cx| {
9162            workspace.add_item(
9163                pane.clone(),
9164                Box::new(dirty_regular_buffer.clone()),
9165                None,
9166                false,
9167                false,
9168                window,
9169                cx,
9170            );
9171            workspace.add_item(
9172                pane.clone(),
9173                Box::new(dirty_regular_buffer_2.clone()),
9174                None,
9175                false,
9176                false,
9177                window,
9178                cx,
9179            );
9180            workspace.add_item(
9181                pane.clone(),
9182                Box::new(dirty_multi_buffer.clone()),
9183                None,
9184                false,
9185                false,
9186                window,
9187                cx,
9188            );
9189        });
9190
9191        pane.update_in(cx, |pane, window, cx| {
9192            pane.activate_item(2, true, true, window, cx);
9193            assert_eq!(
9194                pane.active_item().unwrap().item_id(),
9195                dirty_multi_buffer.item_id(),
9196                "Should select the multi buffer in the pane"
9197            );
9198        });
9199        let close_multi_buffer_task = pane
9200            .update_in(cx, |pane, window, cx| {
9201                pane.close_active_item(
9202                    &CloseActiveItem {
9203                        save_intent: None,
9204                        close_pinned: false,
9205                    },
9206                    window,
9207                    cx,
9208                )
9209            })
9210            .expect("should have active multi buffer to close");
9211        cx.background_executor.run_until_parked();
9212        assert!(
9213            !cx.has_pending_prompt(),
9214            "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
9215        );
9216        close_multi_buffer_task
9217            .await
9218            .expect("Closing multi buffer failed");
9219        pane.update(cx, |pane, cx| {
9220            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
9221            assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
9222            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
9223            assert_eq!(
9224                pane.items()
9225                    .map(|item| item.item_id())
9226                    .sorted()
9227                    .collect::<Vec<_>>(),
9228                vec![
9229                    dirty_regular_buffer.item_id(),
9230                    dirty_regular_buffer_2.item_id(),
9231                ],
9232                "Should have no multi buffer left in the pane"
9233            );
9234            assert!(dirty_regular_buffer.read(cx).is_dirty);
9235            assert!(dirty_regular_buffer_2.read(cx).is_dirty);
9236        });
9237    }
9238
9239    #[gpui::test]
9240    async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
9241        init_test(cx);
9242        let fs = FakeFs::new(cx.executor());
9243        let project = Project::test(fs, [], cx).await;
9244        let (workspace, cx) =
9245            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9246
9247        // Add a new panel to the right dock, opening the dock and setting the
9248        // focus to the new panel.
9249        let panel = workspace.update_in(cx, |workspace, window, cx| {
9250            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
9251            workspace.add_panel(panel.clone(), window, cx);
9252
9253            workspace
9254                .right_dock()
9255                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
9256
9257            workspace.toggle_panel_focus::<TestPanel>(window, cx);
9258
9259            panel
9260        });
9261
9262        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
9263        // panel to the next valid position which, in this case, is the left
9264        // dock.
9265        cx.dispatch_action(MoveFocusedPanelToNextPosition);
9266        workspace.update(cx, |workspace, cx| {
9267            assert!(workspace.left_dock().read(cx).is_open());
9268            assert_eq!(panel.read(cx).position, DockPosition::Left);
9269        });
9270
9271        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
9272        // panel to the next valid position which, in this case, is the bottom
9273        // dock.
9274        cx.dispatch_action(MoveFocusedPanelToNextPosition);
9275        workspace.update(cx, |workspace, cx| {
9276            assert!(workspace.bottom_dock().read(cx).is_open());
9277            assert_eq!(panel.read(cx).position, DockPosition::Bottom);
9278        });
9279
9280        // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
9281        // around moving the panel to its initial position, the right dock.
9282        cx.dispatch_action(MoveFocusedPanelToNextPosition);
9283        workspace.update(cx, |workspace, cx| {
9284            assert!(workspace.right_dock().read(cx).is_open());
9285            assert_eq!(panel.read(cx).position, DockPosition::Right);
9286        });
9287
9288        // Remove focus from the panel, ensuring that, if the panel is not
9289        // focused, the `MoveFocusedPanelToNextPosition` action does not update
9290        // the panel's position, so the panel is still in the right dock.
9291        workspace.update_in(cx, |workspace, window, cx| {
9292            workspace.toggle_panel_focus::<TestPanel>(window, cx);
9293        });
9294
9295        cx.dispatch_action(MoveFocusedPanelToNextPosition);
9296        workspace.update(cx, |workspace, cx| {
9297            assert!(workspace.right_dock().read(cx).is_open());
9298            assert_eq!(panel.read(cx).position, DockPosition::Right);
9299        });
9300    }
9301
9302    mod register_project_item_tests {
9303
9304        use super::*;
9305
9306        // View
9307        struct TestPngItemView {
9308            focus_handle: FocusHandle,
9309        }
9310        // Model
9311        struct TestPngItem {}
9312
9313        impl project::ProjectItem for TestPngItem {
9314            fn try_open(
9315                _project: &Entity<Project>,
9316                path: &ProjectPath,
9317                cx: &mut App,
9318            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
9319                if path.path.extension().unwrap() == "png" {
9320                    Some(cx.spawn(async move |cx| cx.new(|_| TestPngItem {})))
9321                } else {
9322                    None
9323                }
9324            }
9325
9326            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
9327                None
9328            }
9329
9330            fn project_path(&self, _: &App) -> Option<ProjectPath> {
9331                None
9332            }
9333
9334            fn is_dirty(&self) -> bool {
9335                false
9336            }
9337        }
9338
9339        impl Item for TestPngItemView {
9340            type Event = ();
9341            fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
9342                "".into()
9343            }
9344        }
9345        impl EventEmitter<()> for TestPngItemView {}
9346        impl Focusable for TestPngItemView {
9347            fn focus_handle(&self, _cx: &App) -> FocusHandle {
9348                self.focus_handle.clone()
9349            }
9350        }
9351
9352        impl Render for TestPngItemView {
9353            fn render(
9354                &mut self,
9355                _window: &mut Window,
9356                _cx: &mut Context<Self>,
9357            ) -> impl IntoElement {
9358                Empty
9359            }
9360        }
9361
9362        impl ProjectItem for TestPngItemView {
9363            type Item = TestPngItem;
9364
9365            fn for_project_item(
9366                _project: Entity<Project>,
9367                _pane: Option<&Pane>,
9368                _item: Entity<Self::Item>,
9369                _: &mut Window,
9370                cx: &mut Context<Self>,
9371            ) -> Self
9372            where
9373                Self: Sized,
9374            {
9375                Self {
9376                    focus_handle: cx.focus_handle(),
9377                }
9378            }
9379        }
9380
9381        // View
9382        struct TestIpynbItemView {
9383            focus_handle: FocusHandle,
9384        }
9385        // Model
9386        struct TestIpynbItem {}
9387
9388        impl project::ProjectItem for TestIpynbItem {
9389            fn try_open(
9390                _project: &Entity<Project>,
9391                path: &ProjectPath,
9392                cx: &mut App,
9393            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
9394                if path.path.extension().unwrap() == "ipynb" {
9395                    Some(cx.spawn(async move |cx| cx.new(|_| TestIpynbItem {})))
9396                } else {
9397                    None
9398                }
9399            }
9400
9401            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
9402                None
9403            }
9404
9405            fn project_path(&self, _: &App) -> Option<ProjectPath> {
9406                None
9407            }
9408
9409            fn is_dirty(&self) -> bool {
9410                false
9411            }
9412        }
9413
9414        impl Item for TestIpynbItemView {
9415            type Event = ();
9416            fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
9417                "".into()
9418            }
9419        }
9420        impl EventEmitter<()> for TestIpynbItemView {}
9421        impl Focusable for TestIpynbItemView {
9422            fn focus_handle(&self, _cx: &App) -> FocusHandle {
9423                self.focus_handle.clone()
9424            }
9425        }
9426
9427        impl Render for TestIpynbItemView {
9428            fn render(
9429                &mut self,
9430                _window: &mut Window,
9431                _cx: &mut Context<Self>,
9432            ) -> impl IntoElement {
9433                Empty
9434            }
9435        }
9436
9437        impl ProjectItem for TestIpynbItemView {
9438            type Item = TestIpynbItem;
9439
9440            fn for_project_item(
9441                _project: Entity<Project>,
9442                _pane: Option<&Pane>,
9443                _item: Entity<Self::Item>,
9444                _: &mut Window,
9445                cx: &mut Context<Self>,
9446            ) -> Self
9447            where
9448                Self: Sized,
9449            {
9450                Self {
9451                    focus_handle: cx.focus_handle(),
9452                }
9453            }
9454        }
9455
9456        struct TestAlternatePngItemView {
9457            focus_handle: FocusHandle,
9458        }
9459
9460        impl Item for TestAlternatePngItemView {
9461            type Event = ();
9462            fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
9463                "".into()
9464            }
9465        }
9466
9467        impl EventEmitter<()> for TestAlternatePngItemView {}
9468        impl Focusable for TestAlternatePngItemView {
9469            fn focus_handle(&self, _cx: &App) -> FocusHandle {
9470                self.focus_handle.clone()
9471            }
9472        }
9473
9474        impl Render for TestAlternatePngItemView {
9475            fn render(
9476                &mut self,
9477                _window: &mut Window,
9478                _cx: &mut Context<Self>,
9479            ) -> impl IntoElement {
9480                Empty
9481            }
9482        }
9483
9484        impl ProjectItem for TestAlternatePngItemView {
9485            type Item = TestPngItem;
9486
9487            fn for_project_item(
9488                _project: Entity<Project>,
9489                _pane: Option<&Pane>,
9490                _item: Entity<Self::Item>,
9491                _: &mut Window,
9492                cx: &mut Context<Self>,
9493            ) -> Self
9494            where
9495                Self: Sized,
9496            {
9497                Self {
9498                    focus_handle: cx.focus_handle(),
9499                }
9500            }
9501        }
9502
9503        #[gpui::test]
9504        async fn test_register_project_item(cx: &mut TestAppContext) {
9505            init_test(cx);
9506
9507            cx.update(|cx| {
9508                register_project_item::<TestPngItemView>(cx);
9509                register_project_item::<TestIpynbItemView>(cx);
9510            });
9511
9512            let fs = FakeFs::new(cx.executor());
9513            fs.insert_tree(
9514                "/root1",
9515                json!({
9516                    "one.png": "BINARYDATAHERE",
9517                    "two.ipynb": "{ totally a notebook }",
9518                    "three.txt": "editing text, sure why not?"
9519                }),
9520            )
9521            .await;
9522
9523            let project = Project::test(fs, ["root1".as_ref()], cx).await;
9524            let (workspace, cx) =
9525                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9526
9527            let worktree_id = project.update(cx, |project, cx| {
9528                project.worktrees(cx).next().unwrap().read(cx).id()
9529            });
9530
9531            let handle = workspace
9532                .update_in(cx, |workspace, window, cx| {
9533                    let project_path = (worktree_id, "one.png");
9534                    workspace.open_path(project_path, None, true, window, cx)
9535                })
9536                .await
9537                .unwrap();
9538
9539            // Now we can check if the handle we got back errored or not
9540            assert_eq!(
9541                handle.to_any().entity_type(),
9542                TypeId::of::<TestPngItemView>()
9543            );
9544
9545            let handle = workspace
9546                .update_in(cx, |workspace, window, cx| {
9547                    let project_path = (worktree_id, "two.ipynb");
9548                    workspace.open_path(project_path, None, true, window, cx)
9549                })
9550                .await
9551                .unwrap();
9552
9553            assert_eq!(
9554                handle.to_any().entity_type(),
9555                TypeId::of::<TestIpynbItemView>()
9556            );
9557
9558            let handle = workspace
9559                .update_in(cx, |workspace, window, cx| {
9560                    let project_path = (worktree_id, "three.txt");
9561                    workspace.open_path(project_path, None, true, window, cx)
9562                })
9563                .await;
9564            assert!(handle.is_err());
9565        }
9566
9567        #[gpui::test]
9568        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
9569            init_test(cx);
9570
9571            cx.update(|cx| {
9572                register_project_item::<TestPngItemView>(cx);
9573                register_project_item::<TestAlternatePngItemView>(cx);
9574            });
9575
9576            let fs = FakeFs::new(cx.executor());
9577            fs.insert_tree(
9578                "/root1",
9579                json!({
9580                    "one.png": "BINARYDATAHERE",
9581                    "two.ipynb": "{ totally a notebook }",
9582                    "three.txt": "editing text, sure why not?"
9583                }),
9584            )
9585            .await;
9586            let project = Project::test(fs, ["root1".as_ref()], cx).await;
9587            let (workspace, cx) =
9588                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9589            let worktree_id = project.update(cx, |project, cx| {
9590                project.worktrees(cx).next().unwrap().read(cx).id()
9591            });
9592
9593            let handle = workspace
9594                .update_in(cx, |workspace, window, cx| {
9595                    let project_path = (worktree_id, "one.png");
9596                    workspace.open_path(project_path, None, true, window, cx)
9597                })
9598                .await
9599                .unwrap();
9600
9601            // This _must_ be the second item registered
9602            assert_eq!(
9603                handle.to_any().entity_type(),
9604                TypeId::of::<TestAlternatePngItemView>()
9605            );
9606
9607            let handle = workspace
9608                .update_in(cx, |workspace, window, cx| {
9609                    let project_path = (worktree_id, "three.txt");
9610                    workspace.open_path(project_path, None, true, window, cx)
9611                })
9612                .await;
9613            assert!(handle.is_err());
9614        }
9615    }
9616
9617    pub fn init_test(cx: &mut TestAppContext) {
9618        cx.update(|cx| {
9619            let settings_store = SettingsStore::test(cx);
9620            cx.set_global(settings_store);
9621            theme::init(theme::LoadThemes::JustBase, cx);
9622            language::init(cx);
9623            crate::init_settings(cx);
9624            Project::init_settings(cx);
9625        });
9626    }
9627
9628    fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
9629        let item = TestProjectItem::new(id, path, cx);
9630        item.update(cx, |item, _| {
9631            item.is_dirty = true;
9632        });
9633        item
9634    }
9635}