item.rs

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