item.rs

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