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