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