item.rs

   1use crate::{
   2    CollaboratorId, DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory,
   3    SerializableItemRegistry, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
   4    invalid_buffer_view::InvalidBufferView,
   5    pane::{self, Pane},
   6    persistence::model::ItemId,
   7    searchable::SearchableItemHandle,
   8    workspace_settings::{AutosaveSetting, WorkspaceSettings},
   9};
  10use anyhow::Result;
  11use client::{Client, proto};
  12use futures::{StreamExt, channel::mpsc};
  13use gpui::{
  14    Action, AnyElement, AnyView, App, Context, Entity, EntityId, EventEmitter, FocusHandle,
  15    Focusable, Font, HighlightStyle, Pixels, Point, Render, SharedString, Task, WeakEntity, Window,
  16};
  17use project::{Project, ProjectEntryId, ProjectPath};
  18pub use settings::{
  19    ActivateOnClose, ClosePosition, Settings, SettingsLocation, ShowCloseButton, ShowDiagnostics,
  20};
  21use smallvec::SmallVec;
  22use std::{
  23    any::{Any, TypeId},
  24    cell::RefCell,
  25    ops::Range,
  26    path::Path,
  27    rc::Rc,
  28    sync::Arc,
  29    time::Duration,
  30};
  31use theme::Theme;
  32use ui::{Color, Icon, IntoElement, Label, LabelCommon};
  33use util::{MergeFrom as _, ResultExt};
  34
  35pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200);
  36
  37#[derive(Clone, Copy, Debug)]
  38pub struct SaveOptions {
  39    pub format: bool,
  40    pub autosave: bool,
  41}
  42
  43impl Default for SaveOptions {
  44    fn default() -> Self {
  45        Self {
  46            format: true,
  47            autosave: false,
  48        }
  49    }
  50}
  51
  52pub struct ItemSettings {
  53    pub git_status: bool,
  54    pub close_position: ClosePosition,
  55    pub activate_on_close: ActivateOnClose,
  56    pub file_icons: bool,
  57    pub show_diagnostics: ShowDiagnostics,
  58    pub show_close_button: ShowCloseButton,
  59}
  60
  61pub struct PreviewTabsSettings {
  62    pub enabled: bool,
  63    pub enable_preview_from_file_finder: bool,
  64    pub enable_preview_from_code_navigation: bool,
  65}
  66
  67impl Settings for ItemSettings {
  68    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
  69        let tabs = content.tabs.as_ref().unwrap();
  70        Self {
  71            git_status: tabs.git_status.unwrap(),
  72            close_position: tabs.close_position.unwrap(),
  73            activate_on_close: tabs.activate_on_close.unwrap(),
  74            file_icons: tabs.file_icons.unwrap(),
  75            show_diagnostics: tabs.show_diagnostics.unwrap(),
  76            show_close_button: tabs.show_close_button.unwrap(),
  77        }
  78    }
  79
  80    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
  81        let Some(tabs) = content.tabs.as_ref() else {
  82            return;
  83        };
  84        self.git_status.merge_from(&tabs.git_status);
  85        self.close_position.merge_from(&tabs.close_position);
  86        self.activate_on_close.merge_from(&tabs.activate_on_close);
  87        self.file_icons.merge_from(&tabs.file_icons);
  88        self.show_diagnostics.merge_from(&tabs.show_diagnostics);
  89        self.show_close_button.merge_from(&tabs.show_close_button);
  90    }
  91
  92    fn import_from_vscode(
  93        vscode: &settings::VsCodeSettings,
  94        current: &mut settings::SettingsContent,
  95    ) {
  96        if let Some(b) = vscode.read_bool("workbench.editor.tabActionCloseVisibility") {
  97            current.tabs.get_or_insert_default().show_close_button = Some(if b {
  98                ShowCloseButton::Always
  99            } else {
 100                ShowCloseButton::Hidden
 101            })
 102        }
 103        if let Some(s) = vscode.read_enum("workbench.editor.tabActionLocation", |s| match s {
 104            "right" => Some(ClosePosition::Right),
 105            "left" => Some(ClosePosition::Left),
 106            _ => None,
 107        }) {
 108            current.tabs.get_or_insert_default().close_position = Some(s)
 109        }
 110        if let Some(b) = vscode.read_bool("workbench.editor.focusRecentEditorAfterClose") {
 111            current.tabs.get_or_insert_default().activate_on_close = Some(if b {
 112                ActivateOnClose::History
 113            } else {
 114                ActivateOnClose::LeftNeighbour
 115            })
 116        }
 117
 118        if let Some(b) = vscode.read_bool("workbench.editor.showIcons") {
 119            current.tabs.get_or_insert_default().file_icons = Some(b);
 120        };
 121        if let Some(b) = vscode.read_bool("git.decorations.enabled") {
 122            current.tabs.get_or_insert_default().git_status = Some(b);
 123        }
 124    }
 125}
 126
 127impl Settings for PreviewTabsSettings {
 128    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
 129        let preview_tabs = content.preview_tabs.as_ref().unwrap();
 130        Self {
 131            enabled: preview_tabs.enabled.unwrap(),
 132            enable_preview_from_file_finder: preview_tabs.enable_preview_from_file_finder.unwrap(),
 133            enable_preview_from_code_navigation: preview_tabs
 134                .enable_preview_from_code_navigation
 135                .unwrap(),
 136        }
 137    }
 138
 139    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
 140        let Some(preview_tabs) = content.preview_tabs.as_ref() else {
 141            return;
 142        };
 143
 144        self.enabled.merge_from(&preview_tabs.enabled);
 145        self.enable_preview_from_file_finder
 146            .merge_from(&preview_tabs.enable_preview_from_file_finder);
 147        self.enable_preview_from_code_navigation
 148            .merge_from(&preview_tabs.enable_preview_from_code_navigation);
 149    }
 150
 151    fn import_from_vscode(
 152        vscode: &settings::VsCodeSettings,
 153        current: &mut settings::SettingsContent,
 154    ) {
 155        if let Some(enabled) = vscode.read_bool("workbench.editor.enablePreview") {
 156            current.preview_tabs.get_or_insert_default().enabled = Some(enabled);
 157        }
 158        if let Some(enable_preview_from_code_navigation) =
 159            vscode.read_bool("workbench.editor.enablePreviewFromCodeNavigation")
 160        {
 161            current
 162                .preview_tabs
 163                .get_or_insert_default()
 164                .enable_preview_from_code_navigation = Some(enable_preview_from_code_navigation)
 165        }
 166        if let Some(enable_preview_from_file_finder) =
 167            vscode.read_bool("workbench.editor.enablePreviewFromQuickOpen")
 168        {
 169            current
 170                .preview_tabs
 171                .get_or_insert_default()
 172                .enable_preview_from_file_finder = Some(enable_preview_from_file_finder)
 173        }
 174    }
 175}
 176
 177#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
 178pub enum ItemEvent {
 179    CloseItem,
 180    UpdateTab,
 181    UpdateBreadcrumbs,
 182    Edit,
 183}
 184
 185// TODO: Combine this with existing HighlightedText struct?
 186pub struct BreadcrumbText {
 187    pub text: String,
 188    pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
 189    pub font: Option<Font>,
 190}
 191
 192#[derive(Clone, Copy, Default, Debug)]
 193pub struct TabContentParams {
 194    pub detail: Option<usize>,
 195    pub selected: bool,
 196    pub preview: bool,
 197    /// Tab content should be deemphasized when active pane does not have focus.
 198    pub deemphasized: bool,
 199}
 200
 201impl TabContentParams {
 202    /// Returns the text color to be used for the tab content.
 203    pub fn text_color(&self) -> Color {
 204        if self.deemphasized {
 205            if self.selected {
 206                Color::Muted
 207            } else {
 208                Color::Hidden
 209            }
 210        } else if self.selected {
 211            Color::Default
 212        } else {
 213            Color::Muted
 214        }
 215    }
 216}
 217
 218pub enum TabTooltipContent {
 219    Text(SharedString),
 220    Custom(Box<dyn Fn(&mut Window, &mut App) -> AnyView>),
 221}
 222
 223pub trait Item: Focusable + EventEmitter<Self::Event> + Render + Sized {
 224    type Event;
 225
 226    /// Returns the tab contents.
 227    ///
 228    /// By default this returns a [`Label`] that displays that text from
 229    /// `tab_content_text`.
 230    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
 231        let text = self.tab_content_text(params.detail.unwrap_or_default(), cx);
 232
 233        Label::new(text)
 234            .color(params.text_color())
 235            .into_any_element()
 236    }
 237
 238    /// Returns the textual contents of the tab.
 239    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString;
 240
 241    /// Returns the suggested filename for saving this item.
 242    /// By default, returns the tab content text.
 243    fn suggested_filename(&self, cx: &App) -> SharedString {
 244        self.tab_content_text(0, cx)
 245    }
 246
 247    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
 248        None
 249    }
 250
 251    /// Returns the tab tooltip text.
 252    ///
 253    /// Use this if you don't need to customize the tab tooltip content.
 254    fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
 255        None
 256    }
 257
 258    /// Returns the tab tooltip content.
 259    ///
 260    /// By default this returns a Tooltip text from
 261    /// `tab_tooltip_text`.
 262    fn tab_tooltip_content(&self, cx: &App) -> Option<TabTooltipContent> {
 263        self.tab_tooltip_text(cx).map(TabTooltipContent::Text)
 264    }
 265
 266    fn to_item_events(_event: &Self::Event, _f: impl FnMut(ItemEvent)) {}
 267
 268    fn deactivated(&mut self, _window: &mut Window, _: &mut Context<Self>) {}
 269    fn discarded(&self, _project: Entity<Project>, _window: &mut Window, _cx: &mut Context<Self>) {}
 270    fn on_removed(&self, _cx: &App) {}
 271    fn workspace_deactivated(&mut self, _window: &mut Window, _: &mut Context<Self>) {}
 272    fn navigate(&mut self, _: Box<dyn Any>, _window: &mut Window, _: &mut Context<Self>) -> bool {
 273        false
 274    }
 275
 276    fn telemetry_event_text(&self) -> Option<&'static str> {
 277        None
 278    }
 279
 280    /// (model id, Item)
 281    fn for_each_project_item(
 282        &self,
 283        _: &App,
 284        _: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
 285    ) {
 286    }
 287    fn is_singleton(&self, _cx: &App) -> bool {
 288        false
 289    }
 290    fn set_nav_history(&mut self, _: ItemNavHistory, _window: &mut Window, _: &mut Context<Self>) {}
 291    fn clone_on_split(
 292        &self,
 293        _workspace_id: Option<WorkspaceId>,
 294        _window: &mut Window,
 295        _: &mut Context<Self>,
 296    ) -> Option<Entity<Self>>
 297    where
 298        Self: Sized,
 299    {
 300        None
 301    }
 302    fn is_dirty(&self, _: &App) -> bool {
 303        false
 304    }
 305    fn has_deleted_file(&self, _: &App) -> bool {
 306        false
 307    }
 308    fn has_conflict(&self, _: &App) -> bool {
 309        false
 310    }
 311    fn can_save(&self, _cx: &App) -> bool {
 312        false
 313    }
 314    fn can_save_as(&self, _: &App) -> bool {
 315        false
 316    }
 317    fn save(
 318        &mut self,
 319        _options: SaveOptions,
 320        _project: Entity<Project>,
 321        _window: &mut Window,
 322        _cx: &mut Context<Self>,
 323    ) -> Task<Result<()>> {
 324        unimplemented!("save() must be implemented if can_save() returns true")
 325    }
 326    fn save_as(
 327        &mut self,
 328        _project: Entity<Project>,
 329        _path: ProjectPath,
 330        _window: &mut Window,
 331        _cx: &mut Context<Self>,
 332    ) -> Task<Result<()>> {
 333        unimplemented!("save_as() must be implemented if can_save() returns true")
 334    }
 335    fn reload(
 336        &mut self,
 337        _project: Entity<Project>,
 338        _window: &mut Window,
 339        _cx: &mut Context<Self>,
 340    ) -> Task<Result<()>> {
 341        unimplemented!("reload() must be implemented if can_save() returns true")
 342    }
 343
 344    fn act_as_type<'a>(
 345        &'a self,
 346        type_id: TypeId,
 347        self_handle: &'a Entity<Self>,
 348        _: &'a App,
 349    ) -> Option<AnyView> {
 350        if TypeId::of::<Self>() == type_id {
 351            Some(self_handle.clone().into())
 352        } else {
 353            None
 354        }
 355    }
 356
 357    fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
 358        None
 359    }
 360
 361    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
 362        ToolbarItemLocation::Hidden
 363    }
 364
 365    fn breadcrumbs(&self, _theme: &Theme, _cx: &App) -> Option<Vec<BreadcrumbText>> {
 366        None
 367    }
 368
 369    fn added_to_workspace(
 370        &mut self,
 371        _workspace: &mut Workspace,
 372        _window: &mut Window,
 373        _cx: &mut Context<Self>,
 374    ) {
 375    }
 376
 377    fn show_toolbar(&self) -> bool {
 378        true
 379    }
 380
 381    fn pixel_position_of_cursor(&self, _: &App) -> Option<Point<Pixels>> {
 382        None
 383    }
 384
 385    fn preserve_preview(&self, _cx: &App) -> bool {
 386        false
 387    }
 388
 389    fn include_in_nav_history() -> bool {
 390        true
 391    }
 392}
 393
 394pub trait SerializableItem: Item {
 395    fn serialized_item_kind() -> &'static str;
 396
 397    fn cleanup(
 398        workspace_id: WorkspaceId,
 399        alive_items: Vec<ItemId>,
 400        window: &mut Window,
 401        cx: &mut App,
 402    ) -> Task<Result<()>>;
 403
 404    fn deserialize(
 405        _project: Entity<Project>,
 406        _workspace: WeakEntity<Workspace>,
 407        _workspace_id: WorkspaceId,
 408        _item_id: ItemId,
 409        _window: &mut Window,
 410        _cx: &mut App,
 411    ) -> Task<Result<Entity<Self>>>;
 412
 413    fn serialize(
 414        &mut self,
 415        workspace: &mut Workspace,
 416        item_id: ItemId,
 417        closing: bool,
 418        window: &mut Window,
 419        cx: &mut Context<Self>,
 420    ) -> Option<Task<Result<()>>>;
 421
 422    fn should_serialize(&self, event: &Self::Event) -> bool;
 423}
 424
 425pub trait SerializableItemHandle: ItemHandle {
 426    fn serialized_item_kind(&self) -> &'static str;
 427    fn serialize(
 428        &self,
 429        workspace: &mut Workspace,
 430        closing: bool,
 431        window: &mut Window,
 432        cx: &mut App,
 433    ) -> Option<Task<Result<()>>>;
 434    fn should_serialize(&self, event: &dyn Any, cx: &App) -> bool;
 435}
 436
 437impl<T> SerializableItemHandle for Entity<T>
 438where
 439    T: SerializableItem,
 440{
 441    fn serialized_item_kind(&self) -> &'static str {
 442        T::serialized_item_kind()
 443    }
 444
 445    fn serialize(
 446        &self,
 447        workspace: &mut Workspace,
 448        closing: bool,
 449        window: &mut Window,
 450        cx: &mut App,
 451    ) -> Option<Task<Result<()>>> {
 452        self.update(cx, |this, cx| {
 453            this.serialize(workspace, cx.entity_id().as_u64(), closing, window, cx)
 454        })
 455    }
 456
 457    fn should_serialize(&self, event: &dyn Any, cx: &App) -> bool {
 458        event
 459            .downcast_ref::<T::Event>()
 460            .is_some_and(|event| self.read(cx).should_serialize(event))
 461    }
 462}
 463
 464pub trait ItemHandle: 'static + Send {
 465    fn item_focus_handle(&self, cx: &App) -> FocusHandle;
 466    fn subscribe_to_item_events(
 467        &self,
 468        window: &mut Window,
 469        cx: &mut App,
 470        handler: Box<dyn Fn(ItemEvent, &mut Window, &mut App)>,
 471    ) -> gpui::Subscription;
 472    fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement;
 473    fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString;
 474    fn suggested_filename(&self, cx: &App) -> SharedString;
 475    fn tab_icon(&self, window: &Window, cx: &App) -> Option<Icon>;
 476    fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString>;
 477    fn tab_tooltip_content(&self, cx: &App) -> Option<TabTooltipContent>;
 478    fn telemetry_event_text(&self, cx: &App) -> Option<&'static str>;
 479    fn dragged_tab_content(
 480        &self,
 481        params: TabContentParams,
 482        window: &Window,
 483        cx: &App,
 484    ) -> AnyElement;
 485    fn project_path(&self, cx: &App) -> Option<ProjectPath>;
 486    fn project_entry_ids(&self, cx: &App) -> SmallVec<[ProjectEntryId; 3]>;
 487    fn project_paths(&self, cx: &App) -> SmallVec<[ProjectPath; 3]>;
 488    fn project_item_model_ids(&self, cx: &App) -> SmallVec<[EntityId; 3]>;
 489    fn for_each_project_item(
 490        &self,
 491        _: &App,
 492        _: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
 493    );
 494    fn is_singleton(&self, cx: &App) -> bool;
 495    fn boxed_clone(&self) -> Box<dyn ItemHandle>;
 496    fn clone_on_split(
 497        &self,
 498        workspace_id: Option<WorkspaceId>,
 499        window: &mut Window,
 500        cx: &mut App,
 501    ) -> Option<Box<dyn ItemHandle>>;
 502    fn added_to_pane(
 503        &self,
 504        workspace: &mut Workspace,
 505        pane: Entity<Pane>,
 506        window: &mut Window,
 507        cx: &mut Context<Workspace>,
 508    );
 509    fn deactivated(&self, window: &mut Window, cx: &mut App);
 510    fn on_removed(&self, cx: &App);
 511    fn workspace_deactivated(&self, window: &mut Window, cx: &mut App);
 512    fn navigate(&self, data: Box<dyn Any>, window: &mut Window, cx: &mut App) -> bool;
 513    fn item_id(&self) -> EntityId;
 514    fn to_any(&self) -> AnyView;
 515    fn is_dirty(&self, cx: &App) -> bool;
 516    fn has_deleted_file(&self, cx: &App) -> bool;
 517    fn has_conflict(&self, cx: &App) -> bool;
 518    fn can_save(&self, cx: &App) -> bool;
 519    fn can_save_as(&self, cx: &App) -> bool;
 520    fn save(
 521        &self,
 522        options: SaveOptions,
 523        project: Entity<Project>,
 524        window: &mut Window,
 525        cx: &mut App,
 526    ) -> Task<Result<()>>;
 527    fn save_as(
 528        &self,
 529        project: Entity<Project>,
 530        path: ProjectPath,
 531        window: &mut Window,
 532        cx: &mut App,
 533    ) -> Task<Result<()>>;
 534    fn reload(
 535        &self,
 536        project: Entity<Project>,
 537        window: &mut Window,
 538        cx: &mut App,
 539    ) -> Task<Result<()>>;
 540    fn act_as_type(&self, type_id: TypeId, cx: &App) -> Option<AnyView>;
 541    fn to_followable_item_handle(&self, cx: &App) -> Option<Box<dyn FollowableItemHandle>>;
 542    fn to_serializable_item_handle(&self, cx: &App) -> Option<Box<dyn SerializableItemHandle>>;
 543    fn on_release(
 544        &self,
 545        cx: &mut App,
 546        callback: Box<dyn FnOnce(&mut App) + Send>,
 547    ) -> gpui::Subscription;
 548    fn to_searchable_item_handle(&self, cx: &App) -> Option<Box<dyn SearchableItemHandle>>;
 549    fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation;
 550    fn breadcrumbs(&self, theme: &Theme, cx: &App) -> Option<Vec<BreadcrumbText>>;
 551    fn show_toolbar(&self, cx: &App) -> bool;
 552    fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>>;
 553    fn downgrade_item(&self) -> Box<dyn WeakItemHandle>;
 554    fn workspace_settings<'a>(&self, cx: &'a App) -> &'a WorkspaceSettings;
 555    fn preserve_preview(&self, cx: &App) -> bool;
 556    fn include_in_nav_history(&self) -> bool;
 557    fn relay_action(&self, action: Box<dyn Action>, window: &mut Window, cx: &mut App);
 558    fn can_autosave(&self, cx: &App) -> bool {
 559        let is_deleted = self.project_entry_ids(cx).is_empty();
 560        self.is_dirty(cx) && !self.has_conflict(cx) && self.can_save(cx) && !is_deleted
 561    }
 562}
 563
 564pub trait WeakItemHandle: Send + Sync {
 565    fn id(&self) -> EntityId;
 566    fn boxed_clone(&self) -> Box<dyn WeakItemHandle>;
 567    fn upgrade(&self) -> Option<Box<dyn ItemHandle>>;
 568}
 569
 570impl dyn ItemHandle {
 571    pub fn downcast<V: 'static>(&self) -> Option<Entity<V>> {
 572        self.to_any().downcast().ok()
 573    }
 574
 575    pub fn act_as<V: 'static>(&self, cx: &App) -> Option<Entity<V>> {
 576        self.act_as_type(TypeId::of::<V>(), cx)
 577            .and_then(|t| t.downcast().ok())
 578    }
 579}
 580
 581impl<T: Item> ItemHandle for Entity<T> {
 582    fn subscribe_to_item_events(
 583        &self,
 584        window: &mut Window,
 585        cx: &mut App,
 586        handler: Box<dyn Fn(ItemEvent, &mut Window, &mut App)>,
 587    ) -> gpui::Subscription {
 588        window.subscribe(self, cx, move |_, event, window, cx| {
 589            T::to_item_events(event, |item_event| handler(item_event, window, cx));
 590        })
 591    }
 592
 593    fn item_focus_handle(&self, cx: &App) -> FocusHandle {
 594        self.read(cx).focus_handle(cx)
 595    }
 596
 597    fn telemetry_event_text(&self, cx: &App) -> Option<&'static str> {
 598        self.read(cx).telemetry_event_text()
 599    }
 600
 601    fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement {
 602        self.read(cx).tab_content(params, window, cx)
 603    }
 604    fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString {
 605        self.read(cx).tab_content_text(detail, cx)
 606    }
 607
 608    fn suggested_filename(&self, cx: &App) -> SharedString {
 609        self.read(cx).suggested_filename(cx)
 610    }
 611
 612    fn tab_icon(&self, window: &Window, cx: &App) -> Option<Icon> {
 613        self.read(cx).tab_icon(window, cx)
 614    }
 615
 616    fn tab_tooltip_content(&self, cx: &App) -> Option<TabTooltipContent> {
 617        self.read(cx).tab_tooltip_content(cx)
 618    }
 619
 620    fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
 621        self.read(cx).tab_tooltip_text(cx)
 622    }
 623
 624    fn dragged_tab_content(
 625        &self,
 626        params: TabContentParams,
 627        window: &Window,
 628        cx: &App,
 629    ) -> AnyElement {
 630        self.read(cx).tab_content(
 631            TabContentParams {
 632                selected: true,
 633                ..params
 634            },
 635            window,
 636            cx,
 637        )
 638    }
 639
 640    fn project_path(&self, cx: &App) -> Option<ProjectPath> {
 641        let this = self.read(cx);
 642        let mut result = None;
 643        if this.is_singleton(cx) {
 644            this.for_each_project_item(cx, &mut |_, item| {
 645                result = item.project_path(cx);
 646            });
 647        }
 648        result
 649    }
 650
 651    fn workspace_settings<'a>(&self, cx: &'a App) -> &'a WorkspaceSettings {
 652        if let Some(project_path) = self.project_path(cx) {
 653            WorkspaceSettings::get(
 654                Some(SettingsLocation {
 655                    worktree_id: project_path.worktree_id,
 656                    path: &project_path.path,
 657                }),
 658                cx,
 659            )
 660        } else {
 661            WorkspaceSettings::get_global(cx)
 662        }
 663    }
 664
 665    fn project_entry_ids(&self, cx: &App) -> SmallVec<[ProjectEntryId; 3]> {
 666        let mut result = SmallVec::new();
 667        self.read(cx).for_each_project_item(cx, &mut |_, item| {
 668            if let Some(id) = item.entry_id(cx) {
 669                result.push(id);
 670            }
 671        });
 672        result
 673    }
 674
 675    fn project_paths(&self, cx: &App) -> SmallVec<[ProjectPath; 3]> {
 676        let mut result = SmallVec::new();
 677        self.read(cx).for_each_project_item(cx, &mut |_, item| {
 678            if let Some(id) = item.project_path(cx) {
 679                result.push(id);
 680            }
 681        });
 682        result
 683    }
 684
 685    fn project_item_model_ids(&self, cx: &App) -> SmallVec<[EntityId; 3]> {
 686        let mut result = SmallVec::new();
 687        self.read(cx).for_each_project_item(cx, &mut |id, _| {
 688            result.push(id);
 689        });
 690        result
 691    }
 692
 693    fn for_each_project_item(
 694        &self,
 695        cx: &App,
 696        f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
 697    ) {
 698        self.read(cx).for_each_project_item(cx, f)
 699    }
 700
 701    fn is_singleton(&self, cx: &App) -> bool {
 702        self.read(cx).is_singleton(cx)
 703    }
 704
 705    fn boxed_clone(&self) -> Box<dyn ItemHandle> {
 706        Box::new(self.clone())
 707    }
 708
 709    fn clone_on_split(
 710        &self,
 711        workspace_id: Option<WorkspaceId>,
 712        window: &mut Window,
 713        cx: &mut App,
 714    ) -> Option<Box<dyn ItemHandle>> {
 715        self.update(cx, |item, cx| item.clone_on_split(workspace_id, window, cx))
 716            .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
 717    }
 718
 719    fn added_to_pane(
 720        &self,
 721        workspace: &mut Workspace,
 722        pane: Entity<Pane>,
 723        window: &mut Window,
 724        cx: &mut Context<Workspace>,
 725    ) {
 726        let weak_item = self.downgrade();
 727        let history = pane.read(cx).nav_history_for_item(self);
 728        self.update(cx, |this, cx| {
 729            this.set_nav_history(history, window, cx);
 730            this.added_to_workspace(workspace, window, cx);
 731        });
 732
 733        if let Some(serializable_item) = self.to_serializable_item_handle(cx) {
 734            workspace
 735                .enqueue_item_serialization(serializable_item)
 736                .log_err();
 737        }
 738
 739        if workspace
 740            .panes_by_item
 741            .insert(self.item_id(), pane.downgrade())
 742            .is_none()
 743        {
 744            let mut pending_autosave = DelayedDebouncedEditAction::new();
 745            let (pending_update_tx, mut pending_update_rx) = mpsc::unbounded();
 746            let pending_update = Rc::new(RefCell::new(None));
 747
 748            let mut send_follower_updates = None;
 749            if let Some(item) = self.to_followable_item_handle(cx) {
 750                let is_project_item = item.is_project_item(window, cx);
 751                let item = item.downgrade();
 752
 753                send_follower_updates = Some(cx.spawn_in(window, {
 754                    let pending_update = pending_update.clone();
 755                    async move |workspace, cx| {
 756                        while let Some(mut leader_id) = pending_update_rx.next().await {
 757                            while let Ok(Some(id)) = pending_update_rx.try_next() {
 758                                leader_id = id;
 759                            }
 760
 761                            workspace.update_in(cx, |workspace, window, cx| {
 762                                let Some(item) = item.upgrade() else { return };
 763                                workspace.update_followers(
 764                                    is_project_item,
 765                                    proto::update_followers::Variant::UpdateView(
 766                                        proto::UpdateView {
 767                                            id: item
 768                                                .remote_id(workspace.client(), window, cx)
 769                                                .and_then(|id| id.to_proto()),
 770                                            variant: pending_update.borrow_mut().take(),
 771                                            leader_id,
 772                                        },
 773                                    ),
 774                                    window,
 775                                    cx,
 776                                );
 777                            })?;
 778                            cx.background_executor().timer(LEADER_UPDATE_THROTTLE).await;
 779                        }
 780                        anyhow::Ok(())
 781                    }
 782                }));
 783            }
 784
 785            let mut event_subscription = Some(cx.subscribe_in(
 786                self,
 787                window,
 788                move |workspace, item: &Entity<T>, event, window, cx| {
 789                    let pane = if let Some(pane) = workspace
 790                        .panes_by_item
 791                        .get(&item.item_id())
 792                        .and_then(|pane| pane.upgrade())
 793                    {
 794                        pane
 795                    } else {
 796                        return;
 797                    };
 798
 799                    if let Some(item) = item.to_followable_item_handle(cx) {
 800                        let leader_id = workspace.leader_for_pane(&pane);
 801
 802                        if let Some(leader_id) = leader_id
 803                            && let Some(FollowEvent::Unfollow) = item.to_follow_event(event)
 804                        {
 805                            workspace.unfollow(leader_id, window, cx);
 806                        }
 807
 808                        if item.item_focus_handle(cx).contains_focused(window, cx) {
 809                            match leader_id {
 810                                Some(CollaboratorId::Agent) => {}
 811                                Some(CollaboratorId::PeerId(leader_peer_id)) => {
 812                                    item.add_event_to_update_proto(
 813                                        event,
 814                                        &mut pending_update.borrow_mut(),
 815                                        window,
 816                                        cx,
 817                                    );
 818                                    pending_update_tx.unbounded_send(Some(leader_peer_id)).ok();
 819                                }
 820                                None => {
 821                                    item.add_event_to_update_proto(
 822                                        event,
 823                                        &mut pending_update.borrow_mut(),
 824                                        window,
 825                                        cx,
 826                                    );
 827                                    pending_update_tx.unbounded_send(None).ok();
 828                                }
 829                            }
 830                        }
 831                    }
 832
 833                    if let Some(item) = item.to_serializable_item_handle(cx)
 834                        && item.should_serialize(event, cx)
 835                    {
 836                        workspace.enqueue_item_serialization(item).ok();
 837                    }
 838
 839                    T::to_item_events(event, |event| match event {
 840                        ItemEvent::CloseItem => {
 841                            pane.update(cx, |pane, cx| {
 842                                pane.close_item_by_id(
 843                                    item.item_id(),
 844                                    crate::SaveIntent::Close,
 845                                    window,
 846                                    cx,
 847                                )
 848                            })
 849                            .detach_and_log_err(cx);
 850                        }
 851
 852                        ItemEvent::UpdateTab => {
 853                            workspace.update_item_dirty_state(item, window, cx);
 854
 855                            if item.has_deleted_file(cx)
 856                                && !item.is_dirty(cx)
 857                                && item.workspace_settings(cx).close_on_file_delete
 858                            {
 859                                let item_id = item.item_id();
 860                                let close_item_task = pane.update(cx, |pane, cx| {
 861                                    pane.close_item_by_id(
 862                                        item_id,
 863                                        crate::SaveIntent::Close,
 864                                        window,
 865                                        cx,
 866                                    )
 867                                });
 868                                cx.spawn_in(window, {
 869                                    let pane = pane.clone();
 870                                    async move |_workspace, cx| {
 871                                        close_item_task.await?;
 872                                        pane.update(cx, |pane, _cx| {
 873                                            pane.nav_history_mut().remove_item(item_id);
 874                                        })
 875                                    }
 876                                })
 877                                .detach_and_log_err(cx);
 878                            } else {
 879                                pane.update(cx, |_, cx| {
 880                                    cx.emit(pane::Event::ChangeItemTitle);
 881                                    cx.notify();
 882                                });
 883                            }
 884                        }
 885
 886                        ItemEvent::Edit => {
 887                            let autosave = item.workspace_settings(cx).autosave;
 888
 889                            if let AutosaveSetting::AfterDelay { milliseconds } = autosave {
 890                                let delay = Duration::from_millis(milliseconds);
 891                                let item = item.clone();
 892                                pending_autosave.fire_new(
 893                                    delay,
 894                                    window,
 895                                    cx,
 896                                    move |workspace, window, cx| {
 897                                        Pane::autosave_item(
 898                                            &item,
 899                                            workspace.project().clone(),
 900                                            window,
 901                                            cx,
 902                                        )
 903                                    },
 904                                );
 905                            }
 906                            pane.update(cx, |pane, cx| pane.handle_item_edit(item.item_id(), cx));
 907                        }
 908
 909                        _ => {}
 910                    });
 911                },
 912            ));
 913
 914            cx.on_blur(
 915                &self.read(cx).focus_handle(cx),
 916                window,
 917                move |workspace, window, cx| {
 918                    if let Some(item) = weak_item.upgrade()
 919                        && item.workspace_settings(cx).autosave == AutosaveSetting::OnFocusChange
 920                    {
 921                        Pane::autosave_item(&item, workspace.project.clone(), window, cx)
 922                            .detach_and_log_err(cx);
 923                    }
 924                },
 925            )
 926            .detach();
 927
 928            let item_id = self.item_id();
 929            workspace.update_item_dirty_state(self, window, cx);
 930            cx.observe_release_in(self, window, move |workspace, _, _, _| {
 931                workspace.panes_by_item.remove(&item_id);
 932                event_subscription.take();
 933                send_follower_updates.take();
 934            })
 935            .detach();
 936        }
 937
 938        cx.defer_in(window, |workspace, window, cx| {
 939            workspace.serialize_workspace(window, cx);
 940        });
 941    }
 942
 943    fn deactivated(&self, window: &mut Window, cx: &mut App) {
 944        self.update(cx, |this, cx| this.deactivated(window, cx));
 945    }
 946
 947    fn on_removed(&self, cx: &App) {
 948        self.read(cx).on_removed(cx);
 949    }
 950
 951    fn workspace_deactivated(&self, window: &mut Window, cx: &mut App) {
 952        self.update(cx, |this, cx| this.workspace_deactivated(window, cx));
 953    }
 954
 955    fn navigate(&self, data: Box<dyn Any>, window: &mut Window, cx: &mut App) -> bool {
 956        self.update(cx, |this, cx| this.navigate(data, window, cx))
 957    }
 958
 959    fn item_id(&self) -> EntityId {
 960        self.entity_id()
 961    }
 962
 963    fn to_any(&self) -> AnyView {
 964        self.clone().into()
 965    }
 966
 967    fn is_dirty(&self, cx: &App) -> bool {
 968        self.read(cx).is_dirty(cx)
 969    }
 970
 971    fn has_deleted_file(&self, cx: &App) -> bool {
 972        self.read(cx).has_deleted_file(cx)
 973    }
 974
 975    fn has_conflict(&self, cx: &App) -> bool {
 976        self.read(cx).has_conflict(cx)
 977    }
 978
 979    fn can_save(&self, cx: &App) -> bool {
 980        self.read(cx).can_save(cx)
 981    }
 982
 983    fn can_save_as(&self, cx: &App) -> bool {
 984        self.read(cx).can_save_as(cx)
 985    }
 986
 987    fn save(
 988        &self,
 989        options: SaveOptions,
 990        project: Entity<Project>,
 991        window: &mut Window,
 992        cx: &mut App,
 993    ) -> Task<Result<()>> {
 994        self.update(cx, |item, cx| item.save(options, project, window, cx))
 995    }
 996
 997    fn save_as(
 998        &self,
 999        project: Entity<Project>,
1000        path: ProjectPath,
1001        window: &mut Window,
1002        cx: &mut App,
1003    ) -> Task<anyhow::Result<()>> {
1004        self.update(cx, |item, cx| item.save_as(project, path, window, cx))
1005    }
1006
1007    fn reload(
1008        &self,
1009        project: Entity<Project>,
1010        window: &mut Window,
1011        cx: &mut App,
1012    ) -> Task<Result<()>> {
1013        self.update(cx, |item, cx| item.reload(project, window, cx))
1014    }
1015
1016    fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a App) -> Option<AnyView> {
1017        self.read(cx).act_as_type(type_id, self, cx)
1018    }
1019
1020    fn to_followable_item_handle(&self, cx: &App) -> Option<Box<dyn FollowableItemHandle>> {
1021        FollowableViewRegistry::to_followable_view(self.clone(), cx)
1022    }
1023
1024    fn on_release(
1025        &self,
1026        cx: &mut App,
1027        callback: Box<dyn FnOnce(&mut App) + Send>,
1028    ) -> gpui::Subscription {
1029        cx.observe_release(self, move |_, cx| callback(cx))
1030    }
1031
1032    fn to_searchable_item_handle(&self, cx: &App) -> Option<Box<dyn SearchableItemHandle>> {
1033        self.read(cx).as_searchable(self)
1034    }
1035
1036    fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
1037        self.read(cx).breadcrumb_location(cx)
1038    }
1039
1040    fn breadcrumbs(&self, theme: &Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
1041        self.read(cx).breadcrumbs(theme, cx)
1042    }
1043
1044    fn show_toolbar(&self, cx: &App) -> bool {
1045        self.read(cx).show_toolbar()
1046    }
1047
1048    fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>> {
1049        self.read(cx).pixel_position_of_cursor(cx)
1050    }
1051
1052    fn downgrade_item(&self) -> Box<dyn WeakItemHandle> {
1053        Box::new(self.downgrade())
1054    }
1055
1056    fn to_serializable_item_handle(&self, cx: &App) -> Option<Box<dyn SerializableItemHandle>> {
1057        SerializableItemRegistry::view_to_serializable_item_handle(self.to_any(), cx)
1058    }
1059
1060    fn preserve_preview(&self, cx: &App) -> bool {
1061        self.read(cx).preserve_preview(cx)
1062    }
1063
1064    fn include_in_nav_history(&self) -> bool {
1065        T::include_in_nav_history()
1066    }
1067
1068    fn relay_action(&self, action: Box<dyn Action>, window: &mut Window, cx: &mut App) {
1069        self.update(cx, |this, cx| {
1070            this.focus_handle(cx).focus(window);
1071            window.dispatch_action(action, cx);
1072        })
1073    }
1074}
1075
1076impl From<Box<dyn ItemHandle>> for AnyView {
1077    fn from(val: Box<dyn ItemHandle>) -> Self {
1078        val.to_any()
1079    }
1080}
1081
1082impl From<&Box<dyn ItemHandle>> for AnyView {
1083    fn from(val: &Box<dyn ItemHandle>) -> Self {
1084        val.to_any()
1085    }
1086}
1087
1088impl Clone for Box<dyn ItemHandle> {
1089    fn clone(&self) -> Box<dyn ItemHandle> {
1090        self.boxed_clone()
1091    }
1092}
1093
1094impl<T: Item> WeakItemHandle for WeakEntity<T> {
1095    fn id(&self) -> EntityId {
1096        self.entity_id()
1097    }
1098
1099    fn boxed_clone(&self) -> Box<dyn WeakItemHandle> {
1100        Box::new(self.clone())
1101    }
1102
1103    fn upgrade(&self) -> Option<Box<dyn ItemHandle>> {
1104        self.upgrade().map(|v| Box::new(v) as Box<dyn ItemHandle>)
1105    }
1106}
1107
1108#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1109pub struct ProjectItemKind(pub &'static str);
1110
1111pub trait ProjectItem: Item {
1112    type Item: project::ProjectItem;
1113
1114    fn project_item_kind() -> Option<ProjectItemKind> {
1115        None
1116    }
1117
1118    fn for_project_item(
1119        project: Entity<Project>,
1120        pane: Option<&Pane>,
1121        item: Entity<Self::Item>,
1122        window: &mut Window,
1123        cx: &mut Context<Self>,
1124    ) -> Self
1125    where
1126        Self: Sized;
1127
1128    /// A fallback handler, which will be called after [`project::ProjectItem::try_open`] fails,
1129    /// with the error from that failure as an argument.
1130    /// Allows to open an item that can gracefully display and handle errors.
1131    fn for_broken_project_item(
1132        _abs_path: &Path,
1133        _is_local: bool,
1134        _e: &anyhow::Error,
1135        _window: &mut Window,
1136        _cx: &mut App,
1137    ) -> Option<InvalidBufferView>
1138    where
1139        Self: Sized,
1140    {
1141        None
1142    }
1143}
1144
1145#[derive(Debug)]
1146pub enum FollowEvent {
1147    Unfollow,
1148}
1149
1150pub enum Dedup {
1151    KeepExisting,
1152    ReplaceExisting,
1153}
1154
1155pub trait FollowableItem: Item {
1156    fn remote_id(&self) -> Option<ViewId>;
1157    fn to_state_proto(&self, window: &Window, cx: &App) -> Option<proto::view::Variant>;
1158    fn from_state_proto(
1159        project: Entity<Workspace>,
1160        id: ViewId,
1161        state: &mut Option<proto::view::Variant>,
1162        window: &mut Window,
1163        cx: &mut App,
1164    ) -> Option<Task<Result<Entity<Self>>>>;
1165    fn to_follow_event(event: &Self::Event) -> Option<FollowEvent>;
1166    fn add_event_to_update_proto(
1167        &self,
1168        event: &Self::Event,
1169        update: &mut Option<proto::update_view::Variant>,
1170        window: &Window,
1171        cx: &App,
1172    ) -> bool;
1173    fn apply_update_proto(
1174        &mut self,
1175        project: &Entity<Project>,
1176        message: proto::update_view::Variant,
1177        window: &mut Window,
1178        cx: &mut Context<Self>,
1179    ) -> Task<Result<()>>;
1180    fn is_project_item(&self, window: &Window, cx: &App) -> bool;
1181    fn set_leader_id(
1182        &mut self,
1183        leader_peer_id: Option<CollaboratorId>,
1184        window: &mut Window,
1185        cx: &mut Context<Self>,
1186    );
1187    fn dedup(&self, existing: &Self, window: &Window, cx: &App) -> Option<Dedup>;
1188    fn update_agent_location(
1189        &mut self,
1190        _location: language::Anchor,
1191        _window: &mut Window,
1192        _cx: &mut Context<Self>,
1193    ) {
1194    }
1195}
1196
1197pub trait FollowableItemHandle: ItemHandle {
1198    fn remote_id(&self, client: &Arc<Client>, window: &mut Window, cx: &mut App) -> Option<ViewId>;
1199    fn downgrade(&self) -> Box<dyn WeakFollowableItemHandle>;
1200    fn set_leader_id(
1201        &self,
1202        leader_peer_id: Option<CollaboratorId>,
1203        window: &mut Window,
1204        cx: &mut App,
1205    );
1206    fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant>;
1207    fn add_event_to_update_proto(
1208        &self,
1209        event: &dyn Any,
1210        update: &mut Option<proto::update_view::Variant>,
1211        window: &mut Window,
1212        cx: &mut App,
1213    ) -> bool;
1214    fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent>;
1215    fn apply_update_proto(
1216        &self,
1217        project: &Entity<Project>,
1218        message: proto::update_view::Variant,
1219        window: &mut Window,
1220        cx: &mut App,
1221    ) -> Task<Result<()>>;
1222    fn is_project_item(&self, window: &mut Window, cx: &mut App) -> bool;
1223    fn dedup(
1224        &self,
1225        existing: &dyn FollowableItemHandle,
1226        window: &mut Window,
1227        cx: &mut App,
1228    ) -> Option<Dedup>;
1229    fn update_agent_location(&self, location: language::Anchor, window: &mut Window, cx: &mut App);
1230}
1231
1232impl<T: FollowableItem> FollowableItemHandle for Entity<T> {
1233    fn remote_id(&self, client: &Arc<Client>, _: &mut Window, cx: &mut App) -> Option<ViewId> {
1234        self.read(cx).remote_id().or_else(|| {
1235            client.peer_id().map(|creator| ViewId {
1236                creator: CollaboratorId::PeerId(creator),
1237                id: self.item_id().as_u64(),
1238            })
1239        })
1240    }
1241
1242    fn downgrade(&self) -> Box<dyn WeakFollowableItemHandle> {
1243        Box::new(self.downgrade())
1244    }
1245
1246    fn set_leader_id(&self, leader_id: Option<CollaboratorId>, window: &mut Window, cx: &mut App) {
1247        self.update(cx, |this, cx| this.set_leader_id(leader_id, window, cx))
1248    }
1249
1250    fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant> {
1251        self.read(cx).to_state_proto(window, cx)
1252    }
1253
1254    fn add_event_to_update_proto(
1255        &self,
1256        event: &dyn Any,
1257        update: &mut Option<proto::update_view::Variant>,
1258        window: &mut Window,
1259        cx: &mut App,
1260    ) -> bool {
1261        if let Some(event) = event.downcast_ref() {
1262            self.read(cx)
1263                .add_event_to_update_proto(event, update, window, cx)
1264        } else {
1265            false
1266        }
1267    }
1268
1269    fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent> {
1270        T::to_follow_event(event.downcast_ref()?)
1271    }
1272
1273    fn apply_update_proto(
1274        &self,
1275        project: &Entity<Project>,
1276        message: proto::update_view::Variant,
1277        window: &mut Window,
1278        cx: &mut App,
1279    ) -> Task<Result<()>> {
1280        self.update(cx, |this, cx| {
1281            this.apply_update_proto(project, message, window, cx)
1282        })
1283    }
1284
1285    fn is_project_item(&self, window: &mut Window, cx: &mut App) -> bool {
1286        self.read(cx).is_project_item(window, cx)
1287    }
1288
1289    fn dedup(
1290        &self,
1291        existing: &dyn FollowableItemHandle,
1292        window: &mut Window,
1293        cx: &mut App,
1294    ) -> Option<Dedup> {
1295        let existing = existing.to_any().downcast::<T>().ok()?;
1296        self.read(cx).dedup(existing.read(cx), window, cx)
1297    }
1298
1299    fn update_agent_location(&self, location: language::Anchor, window: &mut Window, cx: &mut App) {
1300        self.update(cx, |this, cx| {
1301            this.update_agent_location(location, window, cx)
1302        })
1303    }
1304}
1305
1306pub trait WeakFollowableItemHandle: Send + Sync {
1307    fn upgrade(&self) -> Option<Box<dyn FollowableItemHandle>>;
1308}
1309
1310impl<T: FollowableItem> WeakFollowableItemHandle for WeakEntity<T> {
1311    fn upgrade(&self) -> Option<Box<dyn FollowableItemHandle>> {
1312        Some(Box::new(self.upgrade()?))
1313    }
1314}
1315
1316#[cfg(any(test, feature = "test-support"))]
1317pub mod test {
1318    use super::{Item, ItemEvent, SerializableItem, TabContentParams};
1319    use crate::{ItemId, ItemNavHistory, Workspace, WorkspaceId, item::SaveOptions};
1320    use gpui::{
1321        AnyElement, App, AppContext as _, Context, Entity, EntityId, EventEmitter, Focusable,
1322        InteractiveElement, IntoElement, Render, SharedString, Task, WeakEntity, Window,
1323    };
1324    use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
1325    use std::{any::Any, cell::Cell, path::Path};
1326
1327    pub struct TestProjectItem {
1328        pub entry_id: Option<ProjectEntryId>,
1329        pub project_path: Option<ProjectPath>,
1330        pub is_dirty: bool,
1331    }
1332
1333    pub struct TestItem {
1334        pub workspace_id: Option<WorkspaceId>,
1335        pub state: String,
1336        pub label: String,
1337        pub save_count: usize,
1338        pub save_as_count: usize,
1339        pub reload_count: usize,
1340        pub is_dirty: bool,
1341        pub is_singleton: bool,
1342        pub has_conflict: bool,
1343        pub has_deleted_file: bool,
1344        pub project_items: Vec<Entity<TestProjectItem>>,
1345        pub nav_history: Option<ItemNavHistory>,
1346        pub tab_descriptions: Option<Vec<&'static str>>,
1347        pub tab_detail: Cell<Option<usize>>,
1348        serialize: Option<Box<dyn Fn() -> Option<Task<anyhow::Result<()>>>>>,
1349        focus_handle: gpui::FocusHandle,
1350    }
1351
1352    impl project::ProjectItem for TestProjectItem {
1353        fn try_open(
1354            _project: &Entity<Project>,
1355            _path: &ProjectPath,
1356            _cx: &mut App,
1357        ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
1358            None
1359        }
1360        fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
1361            self.entry_id
1362        }
1363
1364        fn project_path(&self, _: &App) -> Option<ProjectPath> {
1365            self.project_path.clone()
1366        }
1367
1368        fn is_dirty(&self) -> bool {
1369            self.is_dirty
1370        }
1371    }
1372
1373    pub enum TestItemEvent {
1374        Edit,
1375    }
1376
1377    impl TestProjectItem {
1378        pub fn new(id: u64, path: &str, cx: &mut App) -> Entity<Self> {
1379            let entry_id = Some(ProjectEntryId::from_proto(id));
1380            let project_path = Some(ProjectPath {
1381                worktree_id: WorktreeId::from_usize(0),
1382                path: Path::new(path).into(),
1383            });
1384            cx.new(|_| Self {
1385                entry_id,
1386                project_path,
1387                is_dirty: false,
1388            })
1389        }
1390
1391        pub fn new_untitled(cx: &mut App) -> Entity<Self> {
1392            cx.new(|_| Self {
1393                project_path: None,
1394                entry_id: None,
1395                is_dirty: false,
1396            })
1397        }
1398
1399        pub fn new_dirty(id: u64, path: &str, cx: &mut App) -> Entity<Self> {
1400            let entry_id = Some(ProjectEntryId::from_proto(id));
1401            let project_path = Some(ProjectPath {
1402                worktree_id: WorktreeId::from_usize(0),
1403                path: Path::new(path).into(),
1404            });
1405            cx.new(|_| Self {
1406                entry_id,
1407                project_path,
1408                is_dirty: true,
1409            })
1410        }
1411    }
1412
1413    impl TestItem {
1414        pub fn new(cx: &mut Context<Self>) -> Self {
1415            Self {
1416                state: String::new(),
1417                label: String::new(),
1418                save_count: 0,
1419                save_as_count: 0,
1420                reload_count: 0,
1421                is_dirty: false,
1422                has_conflict: false,
1423                has_deleted_file: false,
1424                project_items: Vec::new(),
1425                is_singleton: true,
1426                nav_history: None,
1427                tab_descriptions: None,
1428                tab_detail: Default::default(),
1429                workspace_id: Default::default(),
1430                focus_handle: cx.focus_handle(),
1431                serialize: None,
1432            }
1433        }
1434
1435        pub fn new_deserialized(id: WorkspaceId, cx: &mut Context<Self>) -> Self {
1436            let mut this = Self::new(cx);
1437            this.workspace_id = Some(id);
1438            this
1439        }
1440
1441        pub fn with_label(mut self, state: &str) -> Self {
1442            self.label = state.to_string();
1443            self
1444        }
1445
1446        pub fn with_singleton(mut self, singleton: bool) -> Self {
1447            self.is_singleton = singleton;
1448            self
1449        }
1450
1451        pub fn set_has_deleted_file(&mut self, deleted: bool) {
1452            self.has_deleted_file = deleted;
1453        }
1454
1455        pub fn with_dirty(mut self, dirty: bool) -> Self {
1456            self.is_dirty = dirty;
1457            self
1458        }
1459
1460        pub fn with_conflict(mut self, has_conflict: bool) -> Self {
1461            self.has_conflict = has_conflict;
1462            self
1463        }
1464
1465        pub fn with_project_items(mut self, items: &[Entity<TestProjectItem>]) -> Self {
1466            self.project_items.clear();
1467            self.project_items.extend(items.iter().cloned());
1468            self
1469        }
1470
1471        pub fn with_serialize(
1472            mut self,
1473            serialize: impl Fn() -> Option<Task<anyhow::Result<()>>> + 'static,
1474        ) -> Self {
1475            self.serialize = Some(Box::new(serialize));
1476            self
1477        }
1478
1479        pub fn set_state(&mut self, state: String, cx: &mut Context<Self>) {
1480            self.push_to_nav_history(cx);
1481            self.state = state;
1482        }
1483
1484        fn push_to_nav_history(&mut self, cx: &mut Context<Self>) {
1485            if let Some(history) = &mut self.nav_history {
1486                history.push(Some(Box::new(self.state.clone())), cx);
1487            }
1488        }
1489    }
1490
1491    impl Render for TestItem {
1492        fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1493            gpui::div().track_focus(&self.focus_handle(cx))
1494        }
1495    }
1496
1497    impl EventEmitter<ItemEvent> for TestItem {}
1498
1499    impl Focusable for TestItem {
1500        fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
1501            self.focus_handle.clone()
1502        }
1503    }
1504
1505    impl Item for TestItem {
1506        type Event = ItemEvent;
1507
1508        fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
1509            f(*event)
1510        }
1511
1512        fn tab_content_text(&self, detail: usize, _cx: &App) -> SharedString {
1513            self.tab_descriptions
1514                .as_ref()
1515                .and_then(|descriptions| {
1516                    let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
1517                    description.into()
1518                })
1519                .unwrap_or_default()
1520                .into()
1521        }
1522
1523        fn telemetry_event_text(&self) -> Option<&'static str> {
1524            None
1525        }
1526
1527        fn tab_content(&self, params: TabContentParams, _window: &Window, _cx: &App) -> AnyElement {
1528            self.tab_detail.set(params.detail);
1529            gpui::div().into_any_element()
1530        }
1531
1532        fn for_each_project_item(
1533            &self,
1534            cx: &App,
1535            f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
1536        ) {
1537            self.project_items
1538                .iter()
1539                .for_each(|item| f(item.entity_id(), item.read(cx)))
1540        }
1541
1542        fn is_singleton(&self, _: &App) -> bool {
1543            self.is_singleton
1544        }
1545
1546        fn set_nav_history(
1547            &mut self,
1548            history: ItemNavHistory,
1549            _window: &mut Window,
1550            _: &mut Context<Self>,
1551        ) {
1552            self.nav_history = Some(history);
1553        }
1554
1555        fn navigate(
1556            &mut self,
1557            state: Box<dyn Any>,
1558            _window: &mut Window,
1559            _: &mut Context<Self>,
1560        ) -> bool {
1561            let state = *state.downcast::<String>().unwrap_or_default();
1562            if state != self.state {
1563                self.state = state;
1564                true
1565            } else {
1566                false
1567            }
1568        }
1569
1570        fn deactivated(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1571            self.push_to_nav_history(cx);
1572        }
1573
1574        fn clone_on_split(
1575            &self,
1576            _workspace_id: Option<WorkspaceId>,
1577            _: &mut Window,
1578            cx: &mut Context<Self>,
1579        ) -> Option<Entity<Self>>
1580        where
1581            Self: Sized,
1582        {
1583            Some(cx.new(|cx| Self {
1584                state: self.state.clone(),
1585                label: self.label.clone(),
1586                save_count: self.save_count,
1587                save_as_count: self.save_as_count,
1588                reload_count: self.reload_count,
1589                is_dirty: self.is_dirty,
1590                is_singleton: self.is_singleton,
1591                has_conflict: self.has_conflict,
1592                has_deleted_file: self.has_deleted_file,
1593                project_items: self.project_items.clone(),
1594                nav_history: None,
1595                tab_descriptions: None,
1596                tab_detail: Default::default(),
1597                workspace_id: self.workspace_id,
1598                focus_handle: cx.focus_handle(),
1599                serialize: None,
1600            }))
1601        }
1602
1603        fn is_dirty(&self, _: &App) -> bool {
1604            self.is_dirty
1605        }
1606
1607        fn has_conflict(&self, _: &App) -> bool {
1608            self.has_conflict
1609        }
1610
1611        fn has_deleted_file(&self, _: &App) -> bool {
1612            self.has_deleted_file
1613        }
1614
1615        fn can_save(&self, cx: &App) -> bool {
1616            !self.project_items.is_empty()
1617                && self
1618                    .project_items
1619                    .iter()
1620                    .all(|item| item.read(cx).entry_id.is_some())
1621        }
1622
1623        fn can_save_as(&self, _cx: &App) -> bool {
1624            self.is_singleton
1625        }
1626
1627        fn save(
1628            &mut self,
1629            _: SaveOptions,
1630            _: Entity<Project>,
1631            _window: &mut Window,
1632            cx: &mut Context<Self>,
1633        ) -> Task<anyhow::Result<()>> {
1634            self.save_count += 1;
1635            self.is_dirty = false;
1636            for item in &self.project_items {
1637                item.update(cx, |item, _| {
1638                    if item.is_dirty {
1639                        item.is_dirty = false;
1640                    }
1641                })
1642            }
1643            Task::ready(Ok(()))
1644        }
1645
1646        fn save_as(
1647            &mut self,
1648            _: Entity<Project>,
1649            _: ProjectPath,
1650            _window: &mut Window,
1651            _: &mut Context<Self>,
1652        ) -> Task<anyhow::Result<()>> {
1653            self.save_as_count += 1;
1654            self.is_dirty = false;
1655            Task::ready(Ok(()))
1656        }
1657
1658        fn reload(
1659            &mut self,
1660            _: Entity<Project>,
1661            _window: &mut Window,
1662            _: &mut Context<Self>,
1663        ) -> Task<anyhow::Result<()>> {
1664            self.reload_count += 1;
1665            self.is_dirty = false;
1666            Task::ready(Ok(()))
1667        }
1668    }
1669
1670    impl SerializableItem for TestItem {
1671        fn serialized_item_kind() -> &'static str {
1672            "TestItem"
1673        }
1674
1675        fn deserialize(
1676            _project: Entity<Project>,
1677            _workspace: WeakEntity<Workspace>,
1678            workspace_id: WorkspaceId,
1679            _item_id: ItemId,
1680            _window: &mut Window,
1681            cx: &mut App,
1682        ) -> Task<anyhow::Result<Entity<Self>>> {
1683            let entity = cx.new(|cx| Self::new_deserialized(workspace_id, cx));
1684            Task::ready(Ok(entity))
1685        }
1686
1687        fn cleanup(
1688            _workspace_id: WorkspaceId,
1689            _alive_items: Vec<ItemId>,
1690            _window: &mut Window,
1691            _cx: &mut App,
1692        ) -> Task<anyhow::Result<()>> {
1693            Task::ready(Ok(()))
1694        }
1695
1696        fn serialize(
1697            &mut self,
1698            _workspace: &mut Workspace,
1699            _item_id: ItemId,
1700            _closing: bool,
1701            _window: &mut Window,
1702            _cx: &mut Context<Self>,
1703        ) -> Option<Task<anyhow::Result<()>>> {
1704            if let Some(serialize) = self.serialize.take() {
1705                let result = serialize();
1706                self.serialize = Some(serialize);
1707                result
1708            } else {
1709                None
1710            }
1711        }
1712
1713        fn should_serialize(&self, _event: &Self::Event) -> bool {
1714            false
1715        }
1716    }
1717}