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}