item.rs

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