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