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