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