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