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