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