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