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