1use crate::{
2 pane::{self, Pane},
3 persistence::model::ItemId,
4 searchable::SearchableItemHandle,
5 workspace_settings::{AutosaveSetting, WorkspaceSettings},
6 DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, SerializableItemRegistry,
7 ToolbarItemLocation, 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 Font, 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, SettingsLocation, SettingsSources};
24use smallvec::SmallVec;
25use std::{
26 any::{Any, TypeId},
27 cell::RefCell,
28 ops::Range,
29 rc::Rc,
30 sync::Arc,
31 time::Duration,
32};
33use theme::Theme;
34use ui::{Color, Element as _, Icon, IntoElement, Label, LabelCommon};
35use util::ResultExt;
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 pub activate_on_close: ActivateOnClose,
44 pub file_icons: bool,
45 pub show_diagnostics: ShowDiagnostics,
46 pub always_show_close_button: bool,
47}
48
49#[derive(Deserialize)]
50pub struct PreviewTabsSettings {
51 pub enabled: bool,
52 pub enable_preview_from_file_finder: bool,
53 pub enable_preview_from_code_navigation: bool,
54}
55
56#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
57#[serde(rename_all = "lowercase")]
58pub enum ClosePosition {
59 Left,
60 #[default]
61 Right,
62}
63
64#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
65#[serde(rename_all = "snake_case")]
66pub enum ShowDiagnostics {
67 #[default]
68 Off,
69 Errors,
70 All,
71}
72
73#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
74#[serde(rename_all = "snake_case")]
75pub enum ActivateOnClose {
76 #[default]
77 History,
78 Neighbour,
79 LeftNeighbour,
80}
81
82#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
83pub struct ItemSettingsContent {
84 /// Whether to show the Git file status on a tab item.
85 ///
86 /// Default: false
87 git_status: Option<bool>,
88 /// Position of the close button in a tab.
89 ///
90 /// Default: right
91 close_position: Option<ClosePosition>,
92 /// Whether to show the file icon for a tab.
93 ///
94 /// Default: false
95 file_icons: Option<bool>,
96 /// What to do after closing the current tab.
97 ///
98 /// Default: history
99 pub activate_on_close: Option<ActivateOnClose>,
100 /// Which files containing diagnostic errors/warnings to mark in the tabs.
101 /// This setting can take the following three values:
102 ///
103 /// Default: off
104 show_diagnostics: Option<ShowDiagnostics>,
105 /// Whether to always show the close button on tabs.
106 ///
107 /// Default: false
108 always_show_close_button: Option<bool>,
109}
110
111#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
112pub struct PreviewTabsSettingsContent {
113 /// Whether to show opened editors as preview tabs.
114 /// Preview tabs do not stay open, are reused until explicitly set to be kept open opened (via double-click or editing) and show file names in italic.
115 ///
116 /// Default: true
117 enabled: Option<bool>,
118 /// Whether to open tabs in preview mode when selected from the file finder.
119 ///
120 /// Default: false
121 enable_preview_from_file_finder: Option<bool>,
122 /// Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.
123 ///
124 /// Default: false
125 enable_preview_from_code_navigation: Option<bool>,
126}
127
128impl Settings for ItemSettings {
129 const KEY: Option<&'static str> = Some("tabs");
130
131 type FileContent = ItemSettingsContent;
132
133 fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
134 sources.json_merge()
135 }
136}
137
138impl Settings for PreviewTabsSettings {
139 const KEY: Option<&'static str> = Some("preview_tabs");
140
141 type FileContent = PreviewTabsSettingsContent;
142
143 fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
144 sources.json_merge()
145 }
146}
147
148#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
149pub enum ItemEvent {
150 CloseItem,
151 UpdateTab,
152 UpdateBreadcrumbs,
153 Edit,
154}
155
156// TODO: Combine this with existing HighlightedText struct?
157pub struct BreadcrumbText {
158 pub text: String,
159 pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
160 pub font: Option<Font>,
161}
162
163#[derive(Debug, Clone, Copy)]
164pub struct TabContentParams {
165 pub detail: Option<usize>,
166 pub selected: bool,
167 pub preview: bool,
168}
169
170impl TabContentParams {
171 /// Returns the text color to be used for the tab content.
172 pub fn text_color(&self) -> Color {
173 if self.selected {
174 Color::Default
175 } else {
176 Color::Muted
177 }
178 }
179}
180
181pub trait Item: FocusableView + EventEmitter<Self::Event> {
182 type Event;
183
184 /// Returns the tab contents.
185 ///
186 /// By default this returns a [`Label`] that displays that text from
187 /// `tab_content_text`.
188 fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
189 let Some(text) = self.tab_content_text(cx) else {
190 return gpui::Empty.into_any();
191 };
192
193 Label::new(text)
194 .color(params.text_color())
195 .into_any_element()
196 }
197
198 /// Returns the textual contents of the tab.
199 ///
200 /// Use this if you don't need to customize the tab contents.
201 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
202 None
203 }
204
205 fn tab_icon(&self, _cx: &WindowContext) -> Option<Icon> {
206 None
207 }
208
209 fn to_item_events(_event: &Self::Event, _f: impl FnMut(ItemEvent)) {}
210
211 fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
212 fn discarded(&self, _project: Model<Project>, _cx: &mut ViewContext<Self>) {}
213 fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
214 fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
215 false
216 }
217 fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
218 None
219 }
220 fn tab_description(&self, _: usize, _: &AppContext) -> Option<SharedString> {
221 None
222 }
223
224 fn telemetry_event_text(&self) -> Option<&'static str> {
225 None
226 }
227
228 /// (model id, Item)
229 fn for_each_project_item(
230 &self,
231 _: &AppContext,
232 _: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
233 ) {
234 }
235 fn is_singleton(&self, _cx: &AppContext) -> bool {
236 false
237 }
238 fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
239 fn clone_on_split(
240 &self,
241 _workspace_id: Option<WorkspaceId>,
242 _: &mut ViewContext<Self>,
243 ) -> Option<View<Self>>
244 where
245 Self: Sized,
246 {
247 None
248 }
249 fn is_dirty(&self, _: &AppContext) -> bool {
250 false
251 }
252 fn has_deleted_file(&self, _: &AppContext) -> bool {
253 false
254 }
255 fn has_conflict(&self, _: &AppContext) -> bool {
256 false
257 }
258 fn can_save(&self, _cx: &AppContext) -> bool {
259 false
260 }
261 fn save(
262 &mut self,
263 _format: bool,
264 _project: Model<Project>,
265 _cx: &mut ViewContext<Self>,
266 ) -> Task<Result<()>> {
267 unimplemented!("save() must be implemented if can_save() returns true")
268 }
269 fn save_as(
270 &mut self,
271 _project: Model<Project>,
272 _path: ProjectPath,
273 _cx: &mut ViewContext<Self>,
274 ) -> Task<Result<()>> {
275 unimplemented!("save_as() must be implemented if can_save() returns true")
276 }
277 fn reload(
278 &mut self,
279 _project: Model<Project>,
280 _cx: &mut ViewContext<Self>,
281 ) -> Task<Result<()>> {
282 unimplemented!("reload() must be implemented if can_save() returns true")
283 }
284
285 fn act_as_type<'a>(
286 &'a self,
287 type_id: TypeId,
288 self_handle: &'a View<Self>,
289 _: &'a AppContext,
290 ) -> Option<AnyView> {
291 if TypeId::of::<Self>() == type_id {
292 Some(self_handle.clone().into())
293 } else {
294 None
295 }
296 }
297
298 fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
299 None
300 }
301
302 fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation {
303 ToolbarItemLocation::Hidden
304 }
305
306 fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
307 None
308 }
309
310 fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext<Self>) {}
311
312 fn show_toolbar(&self) -> bool {
313 true
314 }
315
316 fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Point<Pixels>> {
317 None
318 }
319
320 fn preserve_preview(&self, _cx: &AppContext) -> bool {
321 false
322 }
323}
324
325pub trait SerializableItem: Item {
326 fn serialized_item_kind() -> &'static str;
327
328 fn cleanup(
329 workspace_id: WorkspaceId,
330 alive_items: Vec<ItemId>,
331 cx: &mut WindowContext,
332 ) -> Task<Result<()>>;
333
334 fn deserialize(
335 _project: Model<Project>,
336 _workspace: WeakView<Workspace>,
337 _workspace_id: WorkspaceId,
338 _item_id: ItemId,
339 _cx: &mut WindowContext,
340 ) -> Task<Result<View<Self>>>;
341
342 fn serialize(
343 &mut self,
344 workspace: &mut Workspace,
345 item_id: ItemId,
346 closing: bool,
347 cx: &mut ViewContext<Self>,
348 ) -> Option<Task<Result<()>>>;
349
350 fn should_serialize(&self, event: &Self::Event) -> bool;
351}
352
353pub trait SerializableItemHandle: ItemHandle {
354 fn serialized_item_kind(&self) -> &'static str;
355 fn serialize(
356 &self,
357 workspace: &mut Workspace,
358 closing: bool,
359 cx: &mut WindowContext,
360 ) -> Option<Task<Result<()>>>;
361 fn should_serialize(&self, event: &dyn Any, cx: &AppContext) -> bool;
362}
363
364impl<T> SerializableItemHandle for View<T>
365where
366 T: SerializableItem,
367{
368 fn serialized_item_kind(&self) -> &'static str {
369 T::serialized_item_kind()
370 }
371
372 fn serialize(
373 &self,
374 workspace: &mut Workspace,
375 closing: bool,
376 cx: &mut WindowContext,
377 ) -> Option<Task<Result<()>>> {
378 self.update(cx, |this, cx| {
379 this.serialize(workspace, cx.entity_id().as_u64(), closing, cx)
380 })
381 }
382
383 fn should_serialize(&self, event: &dyn Any, cx: &AppContext) -> bool {
384 event
385 .downcast_ref::<T::Event>()
386 .map_or(false, |event| self.read(cx).should_serialize(event))
387 }
388}
389
390pub trait ItemHandle: 'static + Send {
391 fn subscribe_to_item_events(
392 &self,
393 cx: &mut WindowContext,
394 handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
395 ) -> gpui::Subscription;
396 fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
397 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
398 fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString>;
399 fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement;
400 fn tab_icon(&self, cx: &WindowContext) -> Option<Icon>;
401 fn telemetry_event_text(&self, cx: &WindowContext) -> Option<&'static str>;
402 fn dragged_tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement;
403 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
404 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
405 fn project_paths(&self, cx: &AppContext) -> SmallVec<[ProjectPath; 3]>;
406 fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[EntityId; 3]>;
407 fn for_each_project_item(
408 &self,
409 _: &AppContext,
410 _: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
411 );
412 fn is_singleton(&self, cx: &AppContext) -> bool;
413 fn boxed_clone(&self) -> Box<dyn ItemHandle>;
414 fn clone_on_split(
415 &self,
416 workspace_id: Option<WorkspaceId>,
417 cx: &mut WindowContext,
418 ) -> Option<Box<dyn ItemHandle>>;
419 fn added_to_pane(
420 &self,
421 workspace: &mut Workspace,
422 pane: View<Pane>,
423 cx: &mut ViewContext<Workspace>,
424 );
425 fn deactivated(&self, cx: &mut WindowContext);
426 fn discarded(&self, project: Model<Project>, cx: &mut WindowContext);
427 fn workspace_deactivated(&self, cx: &mut WindowContext);
428 fn navigate(&self, data: Box<dyn Any>, cx: &mut WindowContext) -> bool;
429 fn item_id(&self) -> EntityId;
430 fn to_any(&self) -> AnyView;
431 fn is_dirty(&self, cx: &AppContext) -> bool;
432 fn has_deleted_file(&self, cx: &AppContext) -> bool;
433 fn has_conflict(&self, cx: &AppContext) -> bool;
434 fn can_save(&self, cx: &AppContext) -> bool;
435 fn save(
436 &self,
437 format: bool,
438 project: Model<Project>,
439 cx: &mut WindowContext,
440 ) -> Task<Result<()>>;
441 fn save_as(
442 &self,
443 project: Model<Project>,
444 path: ProjectPath,
445 cx: &mut WindowContext,
446 ) -> Task<Result<()>>;
447 fn reload(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
448 fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyView>;
449 fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
450 fn to_serializable_item_handle(
451 &self,
452 cx: &AppContext,
453 ) -> Option<Box<dyn SerializableItemHandle>>;
454 fn on_release(
455 &self,
456 cx: &mut AppContext,
457 callback: Box<dyn FnOnce(&mut AppContext) + Send>,
458 ) -> gpui::Subscription;
459 fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
460 fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
461 fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>>;
462 fn show_toolbar(&self, cx: &AppContext) -> bool;
463 fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>>;
464 fn downgrade_item(&self) -> Box<dyn WeakItemHandle>;
465 fn workspace_settings<'a>(&self, cx: &'a AppContext) -> &'a WorkspaceSettings;
466 fn preserve_preview(&self, cx: &AppContext) -> bool;
467}
468
469pub trait WeakItemHandle: Send + Sync {
470 fn id(&self) -> EntityId;
471 fn boxed_clone(&self) -> Box<dyn WeakItemHandle>;
472 fn upgrade(&self) -> Option<Box<dyn ItemHandle>>;
473}
474
475impl dyn ItemHandle {
476 pub fn downcast<V: 'static>(&self) -> Option<View<V>> {
477 self.to_any().downcast().ok()
478 }
479
480 pub fn act_as<V: 'static>(&self, cx: &AppContext) -> Option<View<V>> {
481 self.act_as_type(TypeId::of::<V>(), cx)
482 .and_then(|t| t.downcast().ok())
483 }
484}
485
486impl<T: Item> ItemHandle for View<T> {
487 fn subscribe_to_item_events(
488 &self,
489 cx: &mut WindowContext,
490 handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
491 ) -> gpui::Subscription {
492 cx.subscribe(self, move |_, event, cx| {
493 T::to_item_events(event, |item_event| handler(item_event, cx));
494 })
495 }
496
497 fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
498 self.focus_handle(cx)
499 }
500
501 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
502 self.read(cx).tab_tooltip_text(cx)
503 }
504
505 fn telemetry_event_text(&self, cx: &WindowContext) -> Option<&'static str> {
506 self.read(cx).telemetry_event_text()
507 }
508
509 fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString> {
510 self.read(cx).tab_description(detail, cx)
511 }
512
513 fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
514 self.read(cx).tab_content(params, cx)
515 }
516
517 fn tab_icon(&self, cx: &WindowContext) -> Option<Icon> {
518 self.read(cx).tab_icon(cx)
519 }
520
521 fn dragged_tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
522 self.read(cx).tab_content(
523 TabContentParams {
524 selected: true,
525 ..params
526 },
527 cx,
528 )
529 }
530
531 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
532 let this = self.read(cx);
533 let mut result = None;
534 if this.is_singleton(cx) {
535 this.for_each_project_item(cx, &mut |_, item| {
536 result = item.project_path(cx);
537 });
538 }
539 result
540 }
541
542 fn workspace_settings<'a>(&self, cx: &'a AppContext) -> &'a WorkspaceSettings {
543 if let Some(project_path) = self.project_path(cx) {
544 WorkspaceSettings::get(
545 Some(SettingsLocation {
546 worktree_id: project_path.worktree_id,
547 path: &project_path.path,
548 }),
549 cx,
550 )
551 } else {
552 WorkspaceSettings::get_global(cx)
553 }
554 }
555
556 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
557 let mut result = SmallVec::new();
558 self.read(cx).for_each_project_item(cx, &mut |_, item| {
559 if let Some(id) = item.entry_id(cx) {
560 result.push(id);
561 }
562 });
563 result
564 }
565
566 fn project_paths(&self, cx: &AppContext) -> SmallVec<[ProjectPath; 3]> {
567 let mut result = SmallVec::new();
568 self.read(cx).for_each_project_item(cx, &mut |_, item| {
569 if let Some(id) = item.project_path(cx) {
570 result.push(id);
571 }
572 });
573 result
574 }
575
576 fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[EntityId; 3]> {
577 let mut result = SmallVec::new();
578 self.read(cx).for_each_project_item(cx, &mut |id, _| {
579 result.push(id);
580 });
581 result
582 }
583
584 fn for_each_project_item(
585 &self,
586 cx: &AppContext,
587 f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
588 ) {
589 self.read(cx).for_each_project_item(cx, f)
590 }
591
592 fn is_singleton(&self, cx: &AppContext) -> bool {
593 self.read(cx).is_singleton(cx)
594 }
595
596 fn boxed_clone(&self) -> Box<dyn ItemHandle> {
597 Box::new(self.clone())
598 }
599
600 fn clone_on_split(
601 &self,
602 workspace_id: Option<WorkspaceId>,
603 cx: &mut WindowContext,
604 ) -> Option<Box<dyn ItemHandle>> {
605 self.update(cx, |item, cx| item.clone_on_split(workspace_id, cx))
606 .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
607 }
608
609 fn added_to_pane(
610 &self,
611 workspace: &mut Workspace,
612 pane: View<Pane>,
613 cx: &mut ViewContext<Workspace>,
614 ) {
615 let weak_item = self.downgrade();
616 let history = pane.read(cx).nav_history_for_item(self);
617 self.update(cx, |this, cx| {
618 this.set_nav_history(history, cx);
619 this.added_to_workspace(workspace, cx);
620 });
621
622 if let Some(serializable_item) = self.to_serializable_item_handle(cx) {
623 workspace
624 .enqueue_item_serialization(serializable_item)
625 .log_err();
626 }
627
628 if workspace
629 .panes_by_item
630 .insert(self.item_id(), pane.downgrade())
631 .is_none()
632 {
633 let mut pending_autosave = DelayedDebouncedEditAction::new();
634 let (pending_update_tx, mut pending_update_rx) = mpsc::unbounded();
635 let pending_update = Rc::new(RefCell::new(None));
636
637 let mut send_follower_updates = None;
638 if let Some(item) = self.to_followable_item_handle(cx) {
639 let is_project_item = item.is_project_item(cx);
640 let item = item.downgrade();
641
642 send_follower_updates = Some(cx.spawn({
643 let pending_update = pending_update.clone();
644 |workspace, mut cx| async move {
645 while let Some(mut leader_id) = pending_update_rx.next().await {
646 while let Ok(Some(id)) = pending_update_rx.try_next() {
647 leader_id = id;
648 }
649
650 workspace.update(&mut cx, |workspace, cx| {
651 let Some(item) = item.upgrade() else { return };
652 workspace.update_followers(
653 is_project_item,
654 proto::update_followers::Variant::UpdateView(
655 proto::UpdateView {
656 id: item
657 .remote_id(workspace.client(), cx)
658 .map(|id| id.to_proto()),
659 variant: pending_update.borrow_mut().take(),
660 leader_id,
661 },
662 ),
663 cx,
664 );
665 })?;
666 cx.background_executor().timer(LEADER_UPDATE_THROTTLE).await;
667 }
668 anyhow::Ok(())
669 }
670 }));
671 }
672
673 let mut event_subscription = Some(cx.subscribe(
674 self,
675 move |workspace, item: View<T>, event, cx| {
676 let pane = if let Some(pane) = workspace
677 .panes_by_item
678 .get(&item.item_id())
679 .and_then(|pane| pane.upgrade())
680 {
681 pane
682 } else {
683 return;
684 };
685
686 if let Some(item) = item.to_followable_item_handle(cx) {
687 let leader_id = workspace.leader_for_pane(&pane);
688
689 if let Some(leader_id) = leader_id {
690 if let Some(FollowEvent::Unfollow) = item.to_follow_event(event) {
691 workspace.unfollow(leader_id, cx);
692 }
693 }
694
695 if item.focus_handle(cx).contains_focused(cx) {
696 item.add_event_to_update_proto(
697 event,
698 &mut pending_update.borrow_mut(),
699 cx,
700 );
701 pending_update_tx.unbounded_send(leader_id).ok();
702 }
703 }
704
705 if let Some(item) = item.to_serializable_item_handle(cx) {
706 if item.should_serialize(event, cx) {
707 workspace.enqueue_item_serialization(item).ok();
708 }
709 }
710
711 T::to_item_events(event, |event| match event {
712 ItemEvent::CloseItem => {
713 pane.update(cx, |pane, cx| {
714 pane.close_item_by_id(item.item_id(), crate::SaveIntent::Close, cx)
715 })
716 .detach_and_log_err(cx);
717 }
718
719 ItemEvent::UpdateTab => {
720 pane.update(cx, |_, cx| {
721 cx.emit(pane::Event::ChangeItemTitle);
722 cx.notify();
723 });
724 }
725
726 ItemEvent::Edit => {
727 let autosave = item.workspace_settings(cx).autosave;
728
729 if let AutosaveSetting::AfterDelay { milliseconds } = autosave {
730 let delay = Duration::from_millis(milliseconds);
731 let item = item.clone();
732 pending_autosave.fire_new(delay, cx, move |workspace, cx| {
733 Pane::autosave_item(&item, workspace.project().clone(), cx)
734 });
735 }
736 pane.update(cx, |pane, cx| pane.handle_item_edit(item.item_id(), cx));
737 }
738
739 _ => {}
740 });
741 },
742 ));
743
744 cx.on_blur(&self.focus_handle(cx), move |workspace, cx| {
745 if let Some(item) = weak_item.upgrade() {
746 if item.workspace_settings(cx).autosave == AutosaveSetting::OnFocusChange {
747 Pane::autosave_item(&item, workspace.project.clone(), cx)
748 .detach_and_log_err(cx);
749 }
750 }
751 })
752 .detach();
753
754 let item_id = self.item_id();
755 cx.observe_release(self, move |workspace, _, _| {
756 workspace.panes_by_item.remove(&item_id);
757 event_subscription.take();
758 send_follower_updates.take();
759 })
760 .detach();
761 }
762
763 cx.defer(|workspace, cx| {
764 workspace.serialize_workspace(cx);
765 });
766 }
767
768 fn discarded(&self, project: Model<Project>, cx: &mut WindowContext) {
769 self.update(cx, |this, cx| this.discarded(project, cx));
770 }
771
772 fn deactivated(&self, cx: &mut WindowContext) {
773 self.update(cx, |this, cx| this.deactivated(cx));
774 }
775
776 fn workspace_deactivated(&self, cx: &mut WindowContext) {
777 self.update(cx, |this, cx| this.workspace_deactivated(cx));
778 }
779
780 fn navigate(&self, data: Box<dyn Any>, cx: &mut WindowContext) -> bool {
781 self.update(cx, |this, cx| this.navigate(data, cx))
782 }
783
784 fn item_id(&self) -> EntityId {
785 self.entity_id()
786 }
787
788 fn to_any(&self) -> AnyView {
789 self.clone().into()
790 }
791
792 fn is_dirty(&self, cx: &AppContext) -> bool {
793 self.read(cx).is_dirty(cx)
794 }
795
796 fn has_deleted_file(&self, cx: &AppContext) -> bool {
797 self.read(cx).has_deleted_file(cx)
798 }
799
800 fn has_conflict(&self, cx: &AppContext) -> bool {
801 self.read(cx).has_conflict(cx)
802 }
803
804 fn can_save(&self, cx: &AppContext) -> bool {
805 self.read(cx).can_save(cx)
806 }
807
808 fn save(
809 &self,
810 format: bool,
811 project: Model<Project>,
812 cx: &mut WindowContext,
813 ) -> Task<Result<()>> {
814 self.update(cx, |item, cx| item.save(format, project, cx))
815 }
816
817 fn save_as(
818 &self,
819 project: Model<Project>,
820 path: ProjectPath,
821 cx: &mut WindowContext,
822 ) -> Task<anyhow::Result<()>> {
823 self.update(cx, |item, cx| item.save_as(project, path, cx))
824 }
825
826 fn reload(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>> {
827 self.update(cx, |item, cx| item.reload(project, cx))
828 }
829
830 fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<AnyView> {
831 self.read(cx).act_as_type(type_id, self, cx)
832 }
833
834 fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
835 FollowableViewRegistry::to_followable_view(self.clone(), cx)
836 }
837
838 fn on_release(
839 &self,
840 cx: &mut AppContext,
841 callback: Box<dyn FnOnce(&mut AppContext) + Send>,
842 ) -> gpui::Subscription {
843 cx.observe_release(self, move |_, cx| callback(cx))
844 }
845
846 fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
847 self.read(cx).as_searchable(self)
848 }
849
850 fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation {
851 self.read(cx).breadcrumb_location(cx)
852 }
853
854 fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
855 self.read(cx).breadcrumbs(theme, cx)
856 }
857
858 fn show_toolbar(&self, cx: &AppContext) -> bool {
859 self.read(cx).show_toolbar()
860 }
861
862 fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
863 self.read(cx).pixel_position_of_cursor(cx)
864 }
865
866 fn downgrade_item(&self) -> Box<dyn WeakItemHandle> {
867 Box::new(self.downgrade())
868 }
869
870 fn to_serializable_item_handle(
871 &self,
872 cx: &AppContext,
873 ) -> Option<Box<dyn SerializableItemHandle>> {
874 SerializableItemRegistry::view_to_serializable_item_handle(self.to_any(), cx)
875 }
876
877 fn preserve_preview(&self, cx: &AppContext) -> bool {
878 self.read(cx).preserve_preview(cx)
879 }
880}
881
882impl From<Box<dyn ItemHandle>> for AnyView {
883 fn from(val: Box<dyn ItemHandle>) -> Self {
884 val.to_any()
885 }
886}
887
888impl From<&Box<dyn ItemHandle>> for AnyView {
889 fn from(val: &Box<dyn ItemHandle>) -> Self {
890 val.to_any()
891 }
892}
893
894impl Clone for Box<dyn ItemHandle> {
895 fn clone(&self) -> Box<dyn ItemHandle> {
896 self.boxed_clone()
897 }
898}
899
900impl<T: Item> WeakItemHandle for WeakView<T> {
901 fn id(&self) -> EntityId {
902 self.entity_id()
903 }
904
905 fn boxed_clone(&self) -> Box<dyn WeakItemHandle> {
906 Box::new(self.clone())
907 }
908
909 fn upgrade(&self) -> Option<Box<dyn ItemHandle>> {
910 self.upgrade().map(|v| Box::new(v) as Box<dyn ItemHandle>)
911 }
912}
913
914pub trait ProjectItem: Item {
915 type Item: project::ProjectItem;
916
917 fn for_project_item(
918 project: Model<Project>,
919 item: Model<Self::Item>,
920 cx: &mut ViewContext<Self>,
921 ) -> Self
922 where
923 Self: Sized;
924}
925
926#[derive(Debug)]
927pub enum FollowEvent {
928 Unfollow,
929}
930
931pub enum Dedup {
932 KeepExisting,
933 ReplaceExisting,
934}
935
936pub trait FollowableItem: Item {
937 fn remote_id(&self) -> Option<ViewId>;
938 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
939 fn from_state_proto(
940 project: View<Workspace>,
941 id: ViewId,
942 state: &mut Option<proto::view::Variant>,
943 cx: &mut WindowContext,
944 ) -> Option<Task<Result<View<Self>>>>;
945 fn to_follow_event(event: &Self::Event) -> Option<FollowEvent>;
946 fn add_event_to_update_proto(
947 &self,
948 event: &Self::Event,
949 update: &mut Option<proto::update_view::Variant>,
950 cx: &WindowContext,
951 ) -> bool;
952 fn apply_update_proto(
953 &mut self,
954 project: &Model<Project>,
955 message: proto::update_view::Variant,
956 cx: &mut ViewContext<Self>,
957 ) -> Task<Result<()>>;
958 fn is_project_item(&self, cx: &WindowContext) -> bool;
959 fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
960 fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup>;
961}
962
963pub trait FollowableItemHandle: ItemHandle {
964 fn remote_id(&self, client: &Arc<Client>, cx: &WindowContext) -> Option<ViewId>;
965 fn downgrade(&self) -> Box<dyn WeakFollowableItemHandle>;
966 fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext);
967 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
968 fn add_event_to_update_proto(
969 &self,
970 event: &dyn Any,
971 update: &mut Option<proto::update_view::Variant>,
972 cx: &WindowContext,
973 ) -> bool;
974 fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent>;
975 fn apply_update_proto(
976 &self,
977 project: &Model<Project>,
978 message: proto::update_view::Variant,
979 cx: &mut WindowContext,
980 ) -> Task<Result<()>>;
981 fn is_project_item(&self, cx: &WindowContext) -> bool;
982 fn dedup(&self, existing: &dyn FollowableItemHandle, cx: &WindowContext) -> Option<Dedup>;
983}
984
985impl<T: FollowableItem> FollowableItemHandle for View<T> {
986 fn remote_id(&self, client: &Arc<Client>, cx: &WindowContext) -> Option<ViewId> {
987 self.read(cx).remote_id().or_else(|| {
988 client.peer_id().map(|creator| ViewId {
989 creator,
990 id: self.item_id().as_u64(),
991 })
992 })
993 }
994
995 fn downgrade(&self) -> Box<dyn WeakFollowableItemHandle> {
996 Box::new(self.downgrade())
997 }
998
999 fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext) {
1000 self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx))
1001 }
1002
1003 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
1004 self.read(cx).to_state_proto(cx)
1005 }
1006
1007 fn add_event_to_update_proto(
1008 &self,
1009 event: &dyn Any,
1010 update: &mut Option<proto::update_view::Variant>,
1011 cx: &WindowContext,
1012 ) -> bool {
1013 if let Some(event) = event.downcast_ref() {
1014 self.read(cx).add_event_to_update_proto(event, update, cx)
1015 } else {
1016 false
1017 }
1018 }
1019
1020 fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent> {
1021 T::to_follow_event(event.downcast_ref()?)
1022 }
1023
1024 fn apply_update_proto(
1025 &self,
1026 project: &Model<Project>,
1027 message: proto::update_view::Variant,
1028 cx: &mut WindowContext,
1029 ) -> Task<Result<()>> {
1030 self.update(cx, |this, cx| this.apply_update_proto(project, message, cx))
1031 }
1032
1033 fn is_project_item(&self, cx: &WindowContext) -> bool {
1034 self.read(cx).is_project_item(cx)
1035 }
1036
1037 fn dedup(&self, existing: &dyn FollowableItemHandle, cx: &WindowContext) -> Option<Dedup> {
1038 let existing = existing.to_any().downcast::<T>().ok()?;
1039 self.read(cx).dedup(existing.read(cx), cx)
1040 }
1041}
1042
1043pub trait WeakFollowableItemHandle: Send + Sync {
1044 fn upgrade(&self) -> Option<Box<dyn FollowableItemHandle>>;
1045}
1046
1047impl<T: FollowableItem> WeakFollowableItemHandle for WeakView<T> {
1048 fn upgrade(&self) -> Option<Box<dyn FollowableItemHandle>> {
1049 Some(Box::new(self.upgrade()?))
1050 }
1051}
1052
1053#[cfg(any(test, feature = "test-support"))]
1054pub mod test {
1055 use super::{Item, ItemEvent, SerializableItem, TabContentParams};
1056 use crate::{ItemId, ItemNavHistory, Workspace, WorkspaceId};
1057 use gpui::{
1058 AnyElement, AppContext, Context as _, EntityId, EventEmitter, FocusableView,
1059 InteractiveElement, IntoElement, Model, Render, SharedString, Task, View, ViewContext,
1060 VisualContext, WeakView, WindowContext,
1061 };
1062 use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
1063 use std::{any::Any, cell::Cell, path::Path};
1064
1065 pub struct TestProjectItem {
1066 pub entry_id: Option<ProjectEntryId>,
1067 pub project_path: Option<ProjectPath>,
1068 pub is_dirty: bool,
1069 }
1070
1071 pub struct TestItem {
1072 pub workspace_id: Option<WorkspaceId>,
1073 pub state: String,
1074 pub label: String,
1075 pub save_count: usize,
1076 pub save_as_count: usize,
1077 pub reload_count: usize,
1078 pub is_dirty: bool,
1079 pub is_singleton: bool,
1080 pub has_conflict: bool,
1081 pub project_items: Vec<Model<TestProjectItem>>,
1082 pub nav_history: Option<ItemNavHistory>,
1083 pub tab_descriptions: Option<Vec<&'static str>>,
1084 pub tab_detail: Cell<Option<usize>>,
1085 serialize: Option<Box<dyn Fn() -> Option<Task<anyhow::Result<()>>>>>,
1086 focus_handle: gpui::FocusHandle,
1087 }
1088
1089 impl project::ProjectItem for TestProjectItem {
1090 fn try_open(
1091 _project: &Model<Project>,
1092 _path: &ProjectPath,
1093 _cx: &mut AppContext,
1094 ) -> Option<Task<gpui::Result<Model<Self>>>> {
1095 None
1096 }
1097 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
1098 self.entry_id
1099 }
1100
1101 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
1102 self.project_path.clone()
1103 }
1104
1105 fn is_dirty(&self) -> bool {
1106 self.is_dirty
1107 }
1108 }
1109
1110 pub enum TestItemEvent {
1111 Edit,
1112 }
1113
1114 impl TestProjectItem {
1115 pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Model<Self> {
1116 let entry_id = Some(ProjectEntryId::from_proto(id));
1117 let project_path = Some(ProjectPath {
1118 worktree_id: WorktreeId::from_usize(0),
1119 path: Path::new(path).into(),
1120 });
1121 cx.new_model(|_| Self {
1122 entry_id,
1123 project_path,
1124 is_dirty: false,
1125 })
1126 }
1127
1128 pub fn new_untitled(cx: &mut AppContext) -> Model<Self> {
1129 cx.new_model(|_| Self {
1130 project_path: None,
1131 entry_id: None,
1132 is_dirty: false,
1133 })
1134 }
1135 }
1136
1137 impl TestItem {
1138 pub fn new(cx: &mut ViewContext<Self>) -> Self {
1139 Self {
1140 state: String::new(),
1141 label: String::new(),
1142 save_count: 0,
1143 save_as_count: 0,
1144 reload_count: 0,
1145 is_dirty: false,
1146 has_conflict: false,
1147 project_items: Vec::new(),
1148 is_singleton: true,
1149 nav_history: None,
1150 tab_descriptions: None,
1151 tab_detail: Default::default(),
1152 workspace_id: Default::default(),
1153 focus_handle: cx.focus_handle(),
1154 serialize: None,
1155 }
1156 }
1157
1158 pub fn new_deserialized(id: WorkspaceId, cx: &mut ViewContext<Self>) -> Self {
1159 let mut this = Self::new(cx);
1160 this.workspace_id = Some(id);
1161 this
1162 }
1163
1164 pub fn with_label(mut self, state: &str) -> Self {
1165 self.label = state.to_string();
1166 self
1167 }
1168
1169 pub fn with_singleton(mut self, singleton: bool) -> Self {
1170 self.is_singleton = singleton;
1171 self
1172 }
1173
1174 pub fn with_dirty(mut self, dirty: bool) -> Self {
1175 self.is_dirty = dirty;
1176 self
1177 }
1178
1179 pub fn with_conflict(mut self, has_conflict: bool) -> Self {
1180 self.has_conflict = has_conflict;
1181 self
1182 }
1183
1184 pub fn with_project_items(mut self, items: &[Model<TestProjectItem>]) -> Self {
1185 self.project_items.clear();
1186 self.project_items.extend(items.iter().cloned());
1187 self
1188 }
1189
1190 pub fn with_serialize(
1191 mut self,
1192 serialize: impl Fn() -> Option<Task<anyhow::Result<()>>> + 'static,
1193 ) -> Self {
1194 self.serialize = Some(Box::new(serialize));
1195 self
1196 }
1197
1198 pub fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
1199 self.push_to_nav_history(cx);
1200 self.state = state;
1201 }
1202
1203 fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
1204 if let Some(history) = &mut self.nav_history {
1205 history.push(Some(Box::new(self.state.clone())), cx);
1206 }
1207 }
1208 }
1209
1210 impl Render for TestItem {
1211 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1212 gpui::div().track_focus(&self.focus_handle(cx))
1213 }
1214 }
1215
1216 impl EventEmitter<ItemEvent> for TestItem {}
1217
1218 impl FocusableView for TestItem {
1219 fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
1220 self.focus_handle.clone()
1221 }
1222 }
1223
1224 impl Item for TestItem {
1225 type Event = ItemEvent;
1226
1227 fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
1228 f(*event)
1229 }
1230
1231 fn tab_description(&self, detail: usize, _: &AppContext) -> Option<SharedString> {
1232 self.tab_descriptions.as_ref().and_then(|descriptions| {
1233 let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
1234 Some(description.into())
1235 })
1236 }
1237
1238 fn telemetry_event_text(&self) -> Option<&'static str> {
1239 None
1240 }
1241
1242 fn tab_content(&self, params: TabContentParams, _cx: &WindowContext) -> AnyElement {
1243 self.tab_detail.set(params.detail);
1244 gpui::div().into_any_element()
1245 }
1246
1247 fn for_each_project_item(
1248 &self,
1249 cx: &AppContext,
1250 f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
1251 ) {
1252 self.project_items
1253 .iter()
1254 .for_each(|item| f(item.entity_id(), item.read(cx)))
1255 }
1256
1257 fn is_singleton(&self, _: &AppContext) -> bool {
1258 self.is_singleton
1259 }
1260
1261 fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
1262 self.nav_history = Some(history);
1263 }
1264
1265 fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
1266 let state = *state.downcast::<String>().unwrap_or_default();
1267 if state != self.state {
1268 self.state = state;
1269 true
1270 } else {
1271 false
1272 }
1273 }
1274
1275 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
1276 self.push_to_nav_history(cx);
1277 }
1278
1279 fn clone_on_split(
1280 &self,
1281 _workspace_id: Option<WorkspaceId>,
1282 cx: &mut ViewContext<Self>,
1283 ) -> Option<View<Self>>
1284 where
1285 Self: Sized,
1286 {
1287 Some(cx.new_view(|cx| Self {
1288 state: self.state.clone(),
1289 label: self.label.clone(),
1290 save_count: self.save_count,
1291 save_as_count: self.save_as_count,
1292 reload_count: self.reload_count,
1293 is_dirty: self.is_dirty,
1294 is_singleton: self.is_singleton,
1295 has_conflict: self.has_conflict,
1296 project_items: self.project_items.clone(),
1297 nav_history: None,
1298 tab_descriptions: None,
1299 tab_detail: Default::default(),
1300 workspace_id: self.workspace_id,
1301 focus_handle: cx.focus_handle(),
1302 serialize: None,
1303 }))
1304 }
1305
1306 fn is_dirty(&self, _: &AppContext) -> bool {
1307 self.is_dirty
1308 }
1309
1310 fn has_conflict(&self, _: &AppContext) -> bool {
1311 self.has_conflict
1312 }
1313
1314 fn can_save(&self, cx: &AppContext) -> bool {
1315 !self.project_items.is_empty()
1316 && self
1317 .project_items
1318 .iter()
1319 .all(|item| item.read(cx).entry_id.is_some())
1320 }
1321
1322 fn save(
1323 &mut self,
1324 _: bool,
1325 _: Model<Project>,
1326 _: &mut ViewContext<Self>,
1327 ) -> Task<anyhow::Result<()>> {
1328 self.save_count += 1;
1329 self.is_dirty = false;
1330 Task::ready(Ok(()))
1331 }
1332
1333 fn save_as(
1334 &mut self,
1335 _: Model<Project>,
1336 _: ProjectPath,
1337 _: &mut ViewContext<Self>,
1338 ) -> Task<anyhow::Result<()>> {
1339 self.save_as_count += 1;
1340 self.is_dirty = false;
1341 Task::ready(Ok(()))
1342 }
1343
1344 fn reload(
1345 &mut self,
1346 _: Model<Project>,
1347 _: &mut ViewContext<Self>,
1348 ) -> Task<anyhow::Result<()>> {
1349 self.reload_count += 1;
1350 self.is_dirty = false;
1351 Task::ready(Ok(()))
1352 }
1353 }
1354
1355 impl SerializableItem for TestItem {
1356 fn serialized_item_kind() -> &'static str {
1357 "TestItem"
1358 }
1359
1360 fn deserialize(
1361 _project: Model<Project>,
1362 _workspace: WeakView<Workspace>,
1363 workspace_id: WorkspaceId,
1364 _item_id: ItemId,
1365 cx: &mut WindowContext,
1366 ) -> Task<anyhow::Result<View<Self>>> {
1367 let view = cx.new_view(|cx| Self::new_deserialized(workspace_id, cx));
1368 Task::ready(Ok(view))
1369 }
1370
1371 fn cleanup(
1372 _workspace_id: WorkspaceId,
1373 _alive_items: Vec<ItemId>,
1374 _cx: &mut WindowContext,
1375 ) -> Task<anyhow::Result<()>> {
1376 Task::ready(Ok(()))
1377 }
1378
1379 fn serialize(
1380 &mut self,
1381 _workspace: &mut Workspace,
1382 _item_id: ItemId,
1383 _closing: bool,
1384 _cx: &mut ViewContext<Self>,
1385 ) -> Option<Task<anyhow::Result<()>>> {
1386 if let Some(serialize) = self.serialize.take() {
1387 let result = serialize();
1388 self.serialize = Some(serialize);
1389 result
1390 } else {
1391 None
1392 }
1393 }
1394
1395 fn should_serialize(&self, _event: &Self::Event) -> bool {
1396 false
1397 }
1398 }
1399}