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