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