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