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