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 fn include_in_nav_history() -> bool {
325 true
326 }
327}
328
329pub trait SerializableItem: Item {
330 fn serialized_item_kind() -> &'static str;
331
332 fn cleanup(
333 workspace_id: WorkspaceId,
334 alive_items: Vec<ItemId>,
335 cx: &mut WindowContext,
336 ) -> Task<Result<()>>;
337
338 fn deserialize(
339 _project: Model<Project>,
340 _workspace: WeakView<Workspace>,
341 _workspace_id: WorkspaceId,
342 _item_id: ItemId,
343 _cx: &mut WindowContext,
344 ) -> Task<Result<View<Self>>>;
345
346 fn serialize(
347 &mut self,
348 workspace: &mut Workspace,
349 item_id: ItemId,
350 closing: bool,
351 cx: &mut ViewContext<Self>,
352 ) -> Option<Task<Result<()>>>;
353
354 fn should_serialize(&self, event: &Self::Event) -> bool;
355}
356
357pub trait SerializableItemHandle: ItemHandle {
358 fn serialized_item_kind(&self) -> &'static str;
359 fn serialize(
360 &self,
361 workspace: &mut Workspace,
362 closing: bool,
363 cx: &mut WindowContext,
364 ) -> Option<Task<Result<()>>>;
365 fn should_serialize(&self, event: &dyn Any, cx: &AppContext) -> bool;
366}
367
368impl<T> SerializableItemHandle for View<T>
369where
370 T: SerializableItem,
371{
372 fn serialized_item_kind(&self) -> &'static str {
373 T::serialized_item_kind()
374 }
375
376 fn serialize(
377 &self,
378 workspace: &mut Workspace,
379 closing: bool,
380 cx: &mut WindowContext,
381 ) -> Option<Task<Result<()>>> {
382 self.update(cx, |this, cx| {
383 this.serialize(workspace, cx.entity_id().as_u64(), closing, cx)
384 })
385 }
386
387 fn should_serialize(&self, event: &dyn Any, cx: &AppContext) -> bool {
388 event
389 .downcast_ref::<T::Event>()
390 .map_or(false, |event| self.read(cx).should_serialize(event))
391 }
392}
393
394pub trait ItemHandle: 'static + Send {
395 fn subscribe_to_item_events(
396 &self,
397 cx: &mut WindowContext,
398 handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
399 ) -> gpui::Subscription;
400 fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
401 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
402 fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString>;
403 fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement;
404 fn tab_icon(&self, cx: &WindowContext) -> Option<Icon>;
405 fn telemetry_event_text(&self, cx: &WindowContext) -> Option<&'static str>;
406 fn dragged_tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement;
407 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
408 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
409 fn project_paths(&self, cx: &AppContext) -> SmallVec<[ProjectPath; 3]>;
410 fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[EntityId; 3]>;
411 fn for_each_project_item(
412 &self,
413 _: &AppContext,
414 _: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
415 );
416 fn is_singleton(&self, cx: &AppContext) -> bool;
417 fn boxed_clone(&self) -> Box<dyn ItemHandle>;
418 fn clone_on_split(
419 &self,
420 workspace_id: Option<WorkspaceId>,
421 cx: &mut WindowContext,
422 ) -> Option<Box<dyn ItemHandle>>;
423 fn added_to_pane(
424 &self,
425 workspace: &mut Workspace,
426 pane: View<Pane>,
427 cx: &mut ViewContext<Workspace>,
428 );
429 fn deactivated(&self, cx: &mut WindowContext);
430 fn discarded(&self, project: Model<Project>, cx: &mut WindowContext);
431 fn workspace_deactivated(&self, cx: &mut WindowContext);
432 fn navigate(&self, data: Box<dyn Any>, cx: &mut WindowContext) -> bool;
433 fn item_id(&self) -> EntityId;
434 fn to_any(&self) -> AnyView;
435 fn is_dirty(&self, cx: &AppContext) -> bool;
436 fn has_deleted_file(&self, cx: &AppContext) -> bool;
437 fn has_conflict(&self, cx: &AppContext) -> bool;
438 fn can_save(&self, cx: &AppContext) -> bool;
439 fn save(
440 &self,
441 format: bool,
442 project: Model<Project>,
443 cx: &mut WindowContext,
444 ) -> Task<Result<()>>;
445 fn save_as(
446 &self,
447 project: Model<Project>,
448 path: ProjectPath,
449 cx: &mut WindowContext,
450 ) -> Task<Result<()>>;
451 fn reload(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
452 fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyView>;
453 fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
454 fn to_serializable_item_handle(
455 &self,
456 cx: &AppContext,
457 ) -> Option<Box<dyn SerializableItemHandle>>;
458 fn on_release(
459 &self,
460 cx: &mut AppContext,
461 callback: Box<dyn FnOnce(&mut AppContext) + Send>,
462 ) -> gpui::Subscription;
463 fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
464 fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
465 fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>>;
466 fn show_toolbar(&self, cx: &AppContext) -> bool;
467 fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>>;
468 fn downgrade_item(&self) -> Box<dyn WeakItemHandle>;
469 fn workspace_settings<'a>(&self, cx: &'a AppContext) -> &'a WorkspaceSettings;
470 fn preserve_preview(&self, cx: &AppContext) -> bool;
471 fn include_in_nav_history(&self) -> bool;
472}
473
474pub trait WeakItemHandle: Send + Sync {
475 fn id(&self) -> EntityId;
476 fn boxed_clone(&self) -> Box<dyn WeakItemHandle>;
477 fn upgrade(&self) -> Option<Box<dyn ItemHandle>>;
478}
479
480impl dyn ItemHandle {
481 pub fn downcast<V: 'static>(&self) -> Option<View<V>> {
482 self.to_any().downcast().ok()
483 }
484
485 pub fn act_as<V: 'static>(&self, cx: &AppContext) -> Option<View<V>> {
486 self.act_as_type(TypeId::of::<V>(), cx)
487 .and_then(|t| t.downcast().ok())
488 }
489}
490
491impl<T: Item> ItemHandle for View<T> {
492 fn subscribe_to_item_events(
493 &self,
494 cx: &mut WindowContext,
495 handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
496 ) -> gpui::Subscription {
497 cx.subscribe(self, move |_, event, cx| {
498 T::to_item_events(event, |item_event| handler(item_event, cx));
499 })
500 }
501
502 fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
503 self.focus_handle(cx)
504 }
505
506 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
507 self.read(cx).tab_tooltip_text(cx)
508 }
509
510 fn telemetry_event_text(&self, cx: &WindowContext) -> Option<&'static str> {
511 self.read(cx).telemetry_event_text()
512 }
513
514 fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString> {
515 self.read(cx).tab_description(detail, cx)
516 }
517
518 fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
519 self.read(cx).tab_content(params, cx)
520 }
521
522 fn tab_icon(&self, cx: &WindowContext) -> Option<Icon> {
523 self.read(cx).tab_icon(cx)
524 }
525
526 fn dragged_tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
527 self.read(cx).tab_content(
528 TabContentParams {
529 selected: true,
530 ..params
531 },
532 cx,
533 )
534 }
535
536 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
537 let this = self.read(cx);
538 let mut result = None;
539 if this.is_singleton(cx) {
540 this.for_each_project_item(cx, &mut |_, item| {
541 result = item.project_path(cx);
542 });
543 }
544 result
545 }
546
547 fn workspace_settings<'a>(&self, cx: &'a AppContext) -> &'a WorkspaceSettings {
548 if let Some(project_path) = self.project_path(cx) {
549 WorkspaceSettings::get(
550 Some(SettingsLocation {
551 worktree_id: project_path.worktree_id,
552 path: &project_path.path,
553 }),
554 cx,
555 )
556 } else {
557 WorkspaceSettings::get_global(cx)
558 }
559 }
560
561 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
562 let mut result = SmallVec::new();
563 self.read(cx).for_each_project_item(cx, &mut |_, item| {
564 if let Some(id) = item.entry_id(cx) {
565 result.push(id);
566 }
567 });
568 result
569 }
570
571 fn project_paths(&self, cx: &AppContext) -> SmallVec<[ProjectPath; 3]> {
572 let mut result = SmallVec::new();
573 self.read(cx).for_each_project_item(cx, &mut |_, item| {
574 if let Some(id) = item.project_path(cx) {
575 result.push(id);
576 }
577 });
578 result
579 }
580
581 fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[EntityId; 3]> {
582 let mut result = SmallVec::new();
583 self.read(cx).for_each_project_item(cx, &mut |id, _| {
584 result.push(id);
585 });
586 result
587 }
588
589 fn for_each_project_item(
590 &self,
591 cx: &AppContext,
592 f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
593 ) {
594 self.read(cx).for_each_project_item(cx, f)
595 }
596
597 fn is_singleton(&self, cx: &AppContext) -> bool {
598 self.read(cx).is_singleton(cx)
599 }
600
601 fn boxed_clone(&self) -> Box<dyn ItemHandle> {
602 Box::new(self.clone())
603 }
604
605 fn clone_on_split(
606 &self,
607 workspace_id: Option<WorkspaceId>,
608 cx: &mut WindowContext,
609 ) -> Option<Box<dyn ItemHandle>> {
610 self.update(cx, |item, cx| item.clone_on_split(workspace_id, cx))
611 .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
612 }
613
614 fn added_to_pane(
615 &self,
616 workspace: &mut Workspace,
617 pane: View<Pane>,
618 cx: &mut ViewContext<Workspace>,
619 ) {
620 let weak_item = self.downgrade();
621 let history = pane.read(cx).nav_history_for_item(self);
622 self.update(cx, |this, cx| {
623 this.set_nav_history(history, cx);
624 this.added_to_workspace(workspace, cx);
625 });
626
627 if let Some(serializable_item) = self.to_serializable_item_handle(cx) {
628 workspace
629 .enqueue_item_serialization(serializable_item)
630 .log_err();
631 }
632
633 if workspace
634 .panes_by_item
635 .insert(self.item_id(), pane.downgrade())
636 .is_none()
637 {
638 let mut pending_autosave = DelayedDebouncedEditAction::new();
639 let (pending_update_tx, mut pending_update_rx) = mpsc::unbounded();
640 let pending_update = Rc::new(RefCell::new(None));
641
642 let mut send_follower_updates = None;
643 if let Some(item) = self.to_followable_item_handle(cx) {
644 let is_project_item = item.is_project_item(cx);
645 let item = item.downgrade();
646
647 send_follower_updates = Some(cx.spawn({
648 let pending_update = pending_update.clone();
649 |workspace, mut cx| async move {
650 while let Some(mut leader_id) = pending_update_rx.next().await {
651 while let Ok(Some(id)) = pending_update_rx.try_next() {
652 leader_id = id;
653 }
654
655 workspace.update(&mut cx, |workspace, cx| {
656 let Some(item) = item.upgrade() else { return };
657 workspace.update_followers(
658 is_project_item,
659 proto::update_followers::Variant::UpdateView(
660 proto::UpdateView {
661 id: item
662 .remote_id(workspace.client(), cx)
663 .map(|id| id.to_proto()),
664 variant: pending_update.borrow_mut().take(),
665 leader_id,
666 },
667 ),
668 cx,
669 );
670 })?;
671 cx.background_executor().timer(LEADER_UPDATE_THROTTLE).await;
672 }
673 anyhow::Ok(())
674 }
675 }));
676 }
677
678 let mut event_subscription = Some(cx.subscribe(
679 self,
680 move |workspace, item: View<T>, event, cx| {
681 let pane = if let Some(pane) = workspace
682 .panes_by_item
683 .get(&item.item_id())
684 .and_then(|pane| pane.upgrade())
685 {
686 pane
687 } else {
688 return;
689 };
690
691 if let Some(item) = item.to_followable_item_handle(cx) {
692 let leader_id = workspace.leader_for_pane(&pane);
693
694 if let Some(leader_id) = leader_id {
695 if let Some(FollowEvent::Unfollow) = item.to_follow_event(event) {
696 workspace.unfollow(leader_id, cx);
697 }
698 }
699
700 if item.focus_handle(cx).contains_focused(cx) {
701 item.add_event_to_update_proto(
702 event,
703 &mut pending_update.borrow_mut(),
704 cx,
705 );
706 pending_update_tx.unbounded_send(leader_id).ok();
707 }
708 }
709
710 if let Some(item) = item.to_serializable_item_handle(cx) {
711 if item.should_serialize(event, cx) {
712 workspace.enqueue_item_serialization(item).ok();
713 }
714 }
715
716 T::to_item_events(event, |event| match event {
717 ItemEvent::CloseItem => {
718 pane.update(cx, |pane, cx| {
719 pane.close_item_by_id(item.item_id(), crate::SaveIntent::Close, cx)
720 })
721 .detach_and_log_err(cx);
722 }
723
724 ItemEvent::UpdateTab => {
725 pane.update(cx, |_, cx| {
726 cx.emit(pane::Event::ChangeItemTitle);
727 cx.notify();
728 });
729 }
730
731 ItemEvent::Edit => {
732 let autosave = item.workspace_settings(cx).autosave;
733
734 if let AutosaveSetting::AfterDelay { milliseconds } = autosave {
735 let delay = Duration::from_millis(milliseconds);
736 let item = item.clone();
737 pending_autosave.fire_new(delay, cx, move |workspace, cx| {
738 Pane::autosave_item(&item, workspace.project().clone(), cx)
739 });
740 }
741 pane.update(cx, |pane, cx| pane.handle_item_edit(item.item_id(), cx));
742 }
743
744 _ => {}
745 });
746 },
747 ));
748
749 cx.on_blur(&self.focus_handle(cx), move |workspace, cx| {
750 if let Some(item) = weak_item.upgrade() {
751 if item.workspace_settings(cx).autosave == AutosaveSetting::OnFocusChange {
752 Pane::autosave_item(&item, workspace.project.clone(), cx)
753 .detach_and_log_err(cx);
754 }
755 }
756 })
757 .detach();
758
759 let item_id = self.item_id();
760 cx.observe_release(self, move |workspace, _, _| {
761 workspace.panes_by_item.remove(&item_id);
762 event_subscription.take();
763 send_follower_updates.take();
764 })
765 .detach();
766 }
767
768 cx.defer(|workspace, cx| {
769 workspace.serialize_workspace(cx);
770 });
771 }
772
773 fn discarded(&self, project: Model<Project>, cx: &mut WindowContext) {
774 self.update(cx, |this, cx| this.discarded(project, cx));
775 }
776
777 fn deactivated(&self, cx: &mut WindowContext) {
778 self.update(cx, |this, cx| this.deactivated(cx));
779 }
780
781 fn workspace_deactivated(&self, cx: &mut WindowContext) {
782 self.update(cx, |this, cx| this.workspace_deactivated(cx));
783 }
784
785 fn navigate(&self, data: Box<dyn Any>, cx: &mut WindowContext) -> bool {
786 self.update(cx, |this, cx| this.navigate(data, cx))
787 }
788
789 fn item_id(&self) -> EntityId {
790 self.entity_id()
791 }
792
793 fn to_any(&self) -> AnyView {
794 self.clone().into()
795 }
796
797 fn is_dirty(&self, cx: &AppContext) -> bool {
798 self.read(cx).is_dirty(cx)
799 }
800
801 fn has_deleted_file(&self, cx: &AppContext) -> bool {
802 self.read(cx).has_deleted_file(cx)
803 }
804
805 fn has_conflict(&self, cx: &AppContext) -> bool {
806 self.read(cx).has_conflict(cx)
807 }
808
809 fn can_save(&self, cx: &AppContext) -> bool {
810 self.read(cx).can_save(cx)
811 }
812
813 fn save(
814 &self,
815 format: bool,
816 project: Model<Project>,
817 cx: &mut WindowContext,
818 ) -> Task<Result<()>> {
819 self.update(cx, |item, cx| item.save(format, project, cx))
820 }
821
822 fn save_as(
823 &self,
824 project: Model<Project>,
825 path: ProjectPath,
826 cx: &mut WindowContext,
827 ) -> Task<anyhow::Result<()>> {
828 self.update(cx, |item, cx| item.save_as(project, path, cx))
829 }
830
831 fn reload(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>> {
832 self.update(cx, |item, cx| item.reload(project, cx))
833 }
834
835 fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<AnyView> {
836 self.read(cx).act_as_type(type_id, self, cx)
837 }
838
839 fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
840 FollowableViewRegistry::to_followable_view(self.clone(), cx)
841 }
842
843 fn on_release(
844 &self,
845 cx: &mut AppContext,
846 callback: Box<dyn FnOnce(&mut AppContext) + Send>,
847 ) -> gpui::Subscription {
848 cx.observe_release(self, move |_, cx| callback(cx))
849 }
850
851 fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
852 self.read(cx).as_searchable(self)
853 }
854
855 fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation {
856 self.read(cx).breadcrumb_location(cx)
857 }
858
859 fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
860 self.read(cx).breadcrumbs(theme, cx)
861 }
862
863 fn show_toolbar(&self, cx: &AppContext) -> bool {
864 self.read(cx).show_toolbar()
865 }
866
867 fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
868 self.read(cx).pixel_position_of_cursor(cx)
869 }
870
871 fn downgrade_item(&self) -> Box<dyn WeakItemHandle> {
872 Box::new(self.downgrade())
873 }
874
875 fn to_serializable_item_handle(
876 &self,
877 cx: &AppContext,
878 ) -> Option<Box<dyn SerializableItemHandle>> {
879 SerializableItemRegistry::view_to_serializable_item_handle(self.to_any(), cx)
880 }
881
882 fn preserve_preview(&self, cx: &AppContext) -> bool {
883 self.read(cx).preserve_preview(cx)
884 }
885
886 fn include_in_nav_history(&self) -> bool {
887 T::include_in_nav_history()
888 }
889}
890
891impl From<Box<dyn ItemHandle>> for AnyView {
892 fn from(val: Box<dyn ItemHandle>) -> Self {
893 val.to_any()
894 }
895}
896
897impl From<&Box<dyn ItemHandle>> for AnyView {
898 fn from(val: &Box<dyn ItemHandle>) -> Self {
899 val.to_any()
900 }
901}
902
903impl Clone for Box<dyn ItemHandle> {
904 fn clone(&self) -> Box<dyn ItemHandle> {
905 self.boxed_clone()
906 }
907}
908
909impl<T: Item> WeakItemHandle for WeakView<T> {
910 fn id(&self) -> EntityId {
911 self.entity_id()
912 }
913
914 fn boxed_clone(&self) -> Box<dyn WeakItemHandle> {
915 Box::new(self.clone())
916 }
917
918 fn upgrade(&self) -> Option<Box<dyn ItemHandle>> {
919 self.upgrade().map(|v| Box::new(v) as Box<dyn ItemHandle>)
920 }
921}
922
923pub trait ProjectItem: Item {
924 type Item: project::ProjectItem;
925
926 fn for_project_item(
927 project: Model<Project>,
928 item: Model<Self::Item>,
929 cx: &mut ViewContext<Self>,
930 ) -> Self
931 where
932 Self: Sized;
933}
934
935#[derive(Debug)]
936pub enum FollowEvent {
937 Unfollow,
938}
939
940pub enum Dedup {
941 KeepExisting,
942 ReplaceExisting,
943}
944
945pub trait FollowableItem: Item {
946 fn remote_id(&self) -> Option<ViewId>;
947 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
948 fn from_state_proto(
949 project: View<Workspace>,
950 id: ViewId,
951 state: &mut Option<proto::view::Variant>,
952 cx: &mut WindowContext,
953 ) -> Option<Task<Result<View<Self>>>>;
954 fn to_follow_event(event: &Self::Event) -> Option<FollowEvent>;
955 fn add_event_to_update_proto(
956 &self,
957 event: &Self::Event,
958 update: &mut Option<proto::update_view::Variant>,
959 cx: &WindowContext,
960 ) -> bool;
961 fn apply_update_proto(
962 &mut self,
963 project: &Model<Project>,
964 message: proto::update_view::Variant,
965 cx: &mut ViewContext<Self>,
966 ) -> Task<Result<()>>;
967 fn is_project_item(&self, cx: &WindowContext) -> bool;
968 fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
969 fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup>;
970}
971
972pub trait FollowableItemHandle: ItemHandle {
973 fn remote_id(&self, client: &Arc<Client>, cx: &WindowContext) -> Option<ViewId>;
974 fn downgrade(&self) -> Box<dyn WeakFollowableItemHandle>;
975 fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext);
976 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
977 fn add_event_to_update_proto(
978 &self,
979 event: &dyn Any,
980 update: &mut Option<proto::update_view::Variant>,
981 cx: &WindowContext,
982 ) -> bool;
983 fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent>;
984 fn apply_update_proto(
985 &self,
986 project: &Model<Project>,
987 message: proto::update_view::Variant,
988 cx: &mut WindowContext,
989 ) -> Task<Result<()>>;
990 fn is_project_item(&self, cx: &WindowContext) -> bool;
991 fn dedup(&self, existing: &dyn FollowableItemHandle, cx: &WindowContext) -> Option<Dedup>;
992}
993
994impl<T: FollowableItem> FollowableItemHandle for View<T> {
995 fn remote_id(&self, client: &Arc<Client>, cx: &WindowContext) -> Option<ViewId> {
996 self.read(cx).remote_id().or_else(|| {
997 client.peer_id().map(|creator| ViewId {
998 creator,
999 id: self.item_id().as_u64(),
1000 })
1001 })
1002 }
1003
1004 fn downgrade(&self) -> Box<dyn WeakFollowableItemHandle> {
1005 Box::new(self.downgrade())
1006 }
1007
1008 fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext) {
1009 self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx))
1010 }
1011
1012 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
1013 self.read(cx).to_state_proto(cx)
1014 }
1015
1016 fn add_event_to_update_proto(
1017 &self,
1018 event: &dyn Any,
1019 update: &mut Option<proto::update_view::Variant>,
1020 cx: &WindowContext,
1021 ) -> bool {
1022 if let Some(event) = event.downcast_ref() {
1023 self.read(cx).add_event_to_update_proto(event, update, cx)
1024 } else {
1025 false
1026 }
1027 }
1028
1029 fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent> {
1030 T::to_follow_event(event.downcast_ref()?)
1031 }
1032
1033 fn apply_update_proto(
1034 &self,
1035 project: &Model<Project>,
1036 message: proto::update_view::Variant,
1037 cx: &mut WindowContext,
1038 ) -> Task<Result<()>> {
1039 self.update(cx, |this, cx| this.apply_update_proto(project, message, cx))
1040 }
1041
1042 fn is_project_item(&self, cx: &WindowContext) -> bool {
1043 self.read(cx).is_project_item(cx)
1044 }
1045
1046 fn dedup(&self, existing: &dyn FollowableItemHandle, cx: &WindowContext) -> Option<Dedup> {
1047 let existing = existing.to_any().downcast::<T>().ok()?;
1048 self.read(cx).dedup(existing.read(cx), cx)
1049 }
1050}
1051
1052pub trait WeakFollowableItemHandle: Send + Sync {
1053 fn upgrade(&self) -> Option<Box<dyn FollowableItemHandle>>;
1054}
1055
1056impl<T: FollowableItem> WeakFollowableItemHandle for WeakView<T> {
1057 fn upgrade(&self) -> Option<Box<dyn FollowableItemHandle>> {
1058 Some(Box::new(self.upgrade()?))
1059 }
1060}
1061
1062#[cfg(any(test, feature = "test-support"))]
1063pub mod test {
1064 use super::{Item, ItemEvent, SerializableItem, TabContentParams};
1065 use crate::{ItemId, ItemNavHistory, Workspace, WorkspaceId};
1066 use gpui::{
1067 AnyElement, AppContext, Context as _, EntityId, EventEmitter, FocusableView,
1068 InteractiveElement, IntoElement, Model, Render, SharedString, Task, View, ViewContext,
1069 VisualContext, WeakView, WindowContext,
1070 };
1071 use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
1072 use std::{any::Any, cell::Cell, path::Path};
1073
1074 pub struct TestProjectItem {
1075 pub entry_id: Option<ProjectEntryId>,
1076 pub project_path: Option<ProjectPath>,
1077 pub is_dirty: bool,
1078 }
1079
1080 pub struct TestItem {
1081 pub workspace_id: Option<WorkspaceId>,
1082 pub state: String,
1083 pub label: String,
1084 pub save_count: usize,
1085 pub save_as_count: usize,
1086 pub reload_count: usize,
1087 pub is_dirty: bool,
1088 pub is_singleton: bool,
1089 pub has_conflict: bool,
1090 pub project_items: Vec<Model<TestProjectItem>>,
1091 pub nav_history: Option<ItemNavHistory>,
1092 pub tab_descriptions: Option<Vec<&'static str>>,
1093 pub tab_detail: Cell<Option<usize>>,
1094 serialize: Option<Box<dyn Fn() -> Option<Task<anyhow::Result<()>>>>>,
1095 focus_handle: gpui::FocusHandle,
1096 }
1097
1098 impl project::ProjectItem for TestProjectItem {
1099 fn try_open(
1100 _project: &Model<Project>,
1101 _path: &ProjectPath,
1102 _cx: &mut AppContext,
1103 ) -> Option<Task<gpui::Result<Model<Self>>>> {
1104 None
1105 }
1106 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
1107 self.entry_id
1108 }
1109
1110 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
1111 self.project_path.clone()
1112 }
1113
1114 fn is_dirty(&self) -> bool {
1115 self.is_dirty
1116 }
1117 }
1118
1119 pub enum TestItemEvent {
1120 Edit,
1121 }
1122
1123 impl TestProjectItem {
1124 pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Model<Self> {
1125 let entry_id = Some(ProjectEntryId::from_proto(id));
1126 let project_path = Some(ProjectPath {
1127 worktree_id: WorktreeId::from_usize(0),
1128 path: Path::new(path).into(),
1129 });
1130 cx.new_model(|_| Self {
1131 entry_id,
1132 project_path,
1133 is_dirty: false,
1134 })
1135 }
1136
1137 pub fn new_untitled(cx: &mut AppContext) -> Model<Self> {
1138 cx.new_model(|_| Self {
1139 project_path: None,
1140 entry_id: None,
1141 is_dirty: false,
1142 })
1143 }
1144 }
1145
1146 impl TestItem {
1147 pub fn new(cx: &mut ViewContext<Self>) -> Self {
1148 Self {
1149 state: String::new(),
1150 label: String::new(),
1151 save_count: 0,
1152 save_as_count: 0,
1153 reload_count: 0,
1154 is_dirty: false,
1155 has_conflict: false,
1156 project_items: Vec::new(),
1157 is_singleton: true,
1158 nav_history: None,
1159 tab_descriptions: None,
1160 tab_detail: Default::default(),
1161 workspace_id: Default::default(),
1162 focus_handle: cx.focus_handle(),
1163 serialize: None,
1164 }
1165 }
1166
1167 pub fn new_deserialized(id: WorkspaceId, cx: &mut ViewContext<Self>) -> Self {
1168 let mut this = Self::new(cx);
1169 this.workspace_id = Some(id);
1170 this
1171 }
1172
1173 pub fn with_label(mut self, state: &str) -> Self {
1174 self.label = state.to_string();
1175 self
1176 }
1177
1178 pub fn with_singleton(mut self, singleton: bool) -> Self {
1179 self.is_singleton = singleton;
1180 self
1181 }
1182
1183 pub fn with_dirty(mut self, dirty: bool) -> Self {
1184 self.is_dirty = dirty;
1185 self
1186 }
1187
1188 pub fn with_conflict(mut self, has_conflict: bool) -> Self {
1189 self.has_conflict = has_conflict;
1190 self
1191 }
1192
1193 pub fn with_project_items(mut self, items: &[Model<TestProjectItem>]) -> Self {
1194 self.project_items.clear();
1195 self.project_items.extend(items.iter().cloned());
1196 self
1197 }
1198
1199 pub fn with_serialize(
1200 mut self,
1201 serialize: impl Fn() -> Option<Task<anyhow::Result<()>>> + 'static,
1202 ) -> Self {
1203 self.serialize = Some(Box::new(serialize));
1204 self
1205 }
1206
1207 pub fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
1208 self.push_to_nav_history(cx);
1209 self.state = state;
1210 }
1211
1212 fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
1213 if let Some(history) = &mut self.nav_history {
1214 history.push(Some(Box::new(self.state.clone())), cx);
1215 }
1216 }
1217 }
1218
1219 impl Render for TestItem {
1220 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1221 gpui::div().track_focus(&self.focus_handle(cx))
1222 }
1223 }
1224
1225 impl EventEmitter<ItemEvent> for TestItem {}
1226
1227 impl FocusableView for TestItem {
1228 fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
1229 self.focus_handle.clone()
1230 }
1231 }
1232
1233 impl Item for TestItem {
1234 type Event = ItemEvent;
1235
1236 fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
1237 f(*event)
1238 }
1239
1240 fn tab_description(&self, detail: usize, _: &AppContext) -> Option<SharedString> {
1241 self.tab_descriptions.as_ref().and_then(|descriptions| {
1242 let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
1243 Some(description.into())
1244 })
1245 }
1246
1247 fn telemetry_event_text(&self) -> Option<&'static str> {
1248 None
1249 }
1250
1251 fn tab_content(&self, params: TabContentParams, _cx: &WindowContext) -> AnyElement {
1252 self.tab_detail.set(params.detail);
1253 gpui::div().into_any_element()
1254 }
1255
1256 fn for_each_project_item(
1257 &self,
1258 cx: &AppContext,
1259 f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
1260 ) {
1261 self.project_items
1262 .iter()
1263 .for_each(|item| f(item.entity_id(), item.read(cx)))
1264 }
1265
1266 fn is_singleton(&self, _: &AppContext) -> bool {
1267 self.is_singleton
1268 }
1269
1270 fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
1271 self.nav_history = Some(history);
1272 }
1273
1274 fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
1275 let state = *state.downcast::<String>().unwrap_or_default();
1276 if state != self.state {
1277 self.state = state;
1278 true
1279 } else {
1280 false
1281 }
1282 }
1283
1284 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
1285 self.push_to_nav_history(cx);
1286 }
1287
1288 fn clone_on_split(
1289 &self,
1290 _workspace_id: Option<WorkspaceId>,
1291 cx: &mut ViewContext<Self>,
1292 ) -> Option<View<Self>>
1293 where
1294 Self: Sized,
1295 {
1296 Some(cx.new_view(|cx| Self {
1297 state: self.state.clone(),
1298 label: self.label.clone(),
1299 save_count: self.save_count,
1300 save_as_count: self.save_as_count,
1301 reload_count: self.reload_count,
1302 is_dirty: self.is_dirty,
1303 is_singleton: self.is_singleton,
1304 has_conflict: self.has_conflict,
1305 project_items: self.project_items.clone(),
1306 nav_history: None,
1307 tab_descriptions: None,
1308 tab_detail: Default::default(),
1309 workspace_id: self.workspace_id,
1310 focus_handle: cx.focus_handle(),
1311 serialize: None,
1312 }))
1313 }
1314
1315 fn is_dirty(&self, _: &AppContext) -> bool {
1316 self.is_dirty
1317 }
1318
1319 fn has_conflict(&self, _: &AppContext) -> bool {
1320 self.has_conflict
1321 }
1322
1323 fn can_save(&self, cx: &AppContext) -> bool {
1324 !self.project_items.is_empty()
1325 && self
1326 .project_items
1327 .iter()
1328 .all(|item| item.read(cx).entry_id.is_some())
1329 }
1330
1331 fn save(
1332 &mut self,
1333 _: bool,
1334 _: Model<Project>,
1335 _: &mut ViewContext<Self>,
1336 ) -> Task<anyhow::Result<()>> {
1337 self.save_count += 1;
1338 self.is_dirty = false;
1339 Task::ready(Ok(()))
1340 }
1341
1342 fn save_as(
1343 &mut self,
1344 _: Model<Project>,
1345 _: ProjectPath,
1346 _: &mut ViewContext<Self>,
1347 ) -> Task<anyhow::Result<()>> {
1348 self.save_as_count += 1;
1349 self.is_dirty = false;
1350 Task::ready(Ok(()))
1351 }
1352
1353 fn reload(
1354 &mut self,
1355 _: Model<Project>,
1356 _: &mut ViewContext<Self>,
1357 ) -> Task<anyhow::Result<()>> {
1358 self.reload_count += 1;
1359 self.is_dirty = false;
1360 Task::ready(Ok(()))
1361 }
1362 }
1363
1364 impl SerializableItem for TestItem {
1365 fn serialized_item_kind() -> &'static str {
1366 "TestItem"
1367 }
1368
1369 fn deserialize(
1370 _project: Model<Project>,
1371 _workspace: WeakView<Workspace>,
1372 workspace_id: WorkspaceId,
1373 _item_id: ItemId,
1374 cx: &mut WindowContext,
1375 ) -> Task<anyhow::Result<View<Self>>> {
1376 let view = cx.new_view(|cx| Self::new_deserialized(workspace_id, cx));
1377 Task::ready(Ok(view))
1378 }
1379
1380 fn cleanup(
1381 _workspace_id: WorkspaceId,
1382 _alive_items: Vec<ItemId>,
1383 _cx: &mut WindowContext,
1384 ) -> Task<anyhow::Result<()>> {
1385 Task::ready(Ok(()))
1386 }
1387
1388 fn serialize(
1389 &mut self,
1390 _workspace: &mut Workspace,
1391 _item_id: ItemId,
1392 _closing: bool,
1393 _cx: &mut ViewContext<Self>,
1394 ) -> Option<Task<anyhow::Result<()>>> {
1395 if let Some(serialize) = self.serialize.take() {
1396 let result = serialize();
1397 self.serialize = Some(serialize);
1398 result
1399 } else {
1400 None
1401 }
1402 }
1403
1404 fn should_serialize(&self, _event: &Self::Event) -> bool {
1405 false
1406 }
1407 }
1408}