item.rs

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