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