item.rs

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