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