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