1use crate::{
2 CollaboratorId, DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory,
3 SerializableItemRegistry, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
4 invalid_buffer_view::InvalidBufferView,
5 pane::{self, Pane},
6 persistence::model::ItemId,
7 searchable::SearchableItemHandle,
8 workspace_settings::{AutosaveSetting, WorkspaceSettings},
9};
10use anyhow::Result;
11use client::{Client, proto};
12use futures::{StreamExt, channel::mpsc};
13use gpui::{
14 Action, AnyElement, AnyView, App, Context, Entity, EntityId, EventEmitter, FocusHandle,
15 Focusable, Font, HighlightStyle, Pixels, Point, Render, SharedString, Task, WeakEntity, Window,
16};
17use project::{Project, ProjectEntryId, ProjectPath};
18use schemars::JsonSchema;
19use serde::{Deserialize, Serialize};
20use settings::{Settings, SettingsLocation, SettingsSources, SettingsUi};
21use smallvec::SmallVec;
22use std::{
23 any::{Any, TypeId},
24 cell::RefCell,
25 ops::Range,
26 path::Path,
27 rc::Rc,
28 sync::Arc,
29 time::Duration,
30};
31use theme::Theme;
32use ui::{Color, Icon, IntoElement, Label, LabelCommon};
33use util::ResultExt;
34
35pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200);
36
37#[derive(Clone, Copy, Debug)]
38pub struct SaveOptions {
39 pub format: bool,
40 pub autosave: bool,
41}
42
43impl Default for SaveOptions {
44 fn default() -> Self {
45 Self {
46 format: true,
47 autosave: false,
48 }
49 }
50}
51
52#[derive(Deserialize)]
53pub struct ItemSettings {
54 pub git_status: bool,
55 pub close_position: ClosePosition,
56 pub activate_on_close: ActivateOnClose,
57 pub file_icons: bool,
58 pub show_diagnostics: ShowDiagnostics,
59 pub show_close_button: ShowCloseButton,
60}
61
62#[derive(Deserialize)]
63pub struct PreviewTabsSettings {
64 pub enabled: bool,
65 pub enable_preview_from_file_finder: bool,
66 pub enable_preview_from_code_navigation: bool,
67}
68
69#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
70#[serde(rename_all = "lowercase")]
71pub enum ClosePosition {
72 Left,
73 #[default]
74 Right,
75}
76
77#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
78#[serde(rename_all = "lowercase")]
79pub enum ShowCloseButton {
80 Always,
81 #[default]
82 Hover,
83 Hidden,
84}
85
86#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
87#[serde(rename_all = "snake_case")]
88pub enum ShowDiagnostics {
89 #[default]
90 Off,
91 Errors,
92 All,
93}
94
95#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
96#[serde(rename_all = "snake_case")]
97pub enum ActivateOnClose {
98 #[default]
99 History,
100 Neighbour,
101 LeftNeighbour,
102}
103
104#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
105pub struct ItemSettingsContent {
106 /// Whether to show the Git file status on a tab item.
107 ///
108 /// Default: false
109 git_status: Option<bool>,
110 /// Position of the close button in a tab.
111 ///
112 /// Default: right
113 close_position: Option<ClosePosition>,
114 /// Whether to show the file icon for a tab.
115 ///
116 /// Default: false
117 file_icons: Option<bool>,
118 /// What to do after closing the current tab.
119 ///
120 /// Default: history
121 pub activate_on_close: Option<ActivateOnClose>,
122 /// Which files containing diagnostic errors/warnings to mark in the tabs.
123 /// This setting can take the following three values:
124 ///
125 /// Default: off
126 show_diagnostics: Option<ShowDiagnostics>,
127 /// Whether to always show the close button on tabs.
128 ///
129 /// Default: false
130 show_close_button: Option<ShowCloseButton>,
131}
132
133#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
134pub struct PreviewTabsSettingsContent {
135 /// Whether to show opened editors as preview tabs.
136 /// 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.
137 ///
138 /// Default: true
139 enabled: Option<bool>,
140 /// Whether to open tabs in preview mode when selected from the file finder.
141 ///
142 /// Default: false
143 enable_preview_from_file_finder: Option<bool>,
144 /// Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.
145 ///
146 /// Default: false
147 enable_preview_from_code_navigation: Option<bool>,
148}
149
150impl Settings for ItemSettings {
151 const KEY: Option<&'static str> = Some("tabs");
152
153 type FileContent = ItemSettingsContent;
154
155 fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
156 sources.json_merge()
157 }
158
159 fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
160 if let Some(b) = vscode.read_bool("workbench.editor.tabActionCloseVisibility") {
161 current.show_close_button = Some(if b {
162 ShowCloseButton::Always
163 } else {
164 ShowCloseButton::Hidden
165 })
166 }
167 vscode.enum_setting(
168 "workbench.editor.tabActionLocation",
169 &mut current.close_position,
170 |s| match s {
171 "right" => Some(ClosePosition::Right),
172 "left" => Some(ClosePosition::Left),
173 _ => None,
174 },
175 );
176 if let Some(b) = vscode.read_bool("workbench.editor.focusRecentEditorAfterClose") {
177 current.activate_on_close = Some(if b {
178 ActivateOnClose::History
179 } else {
180 ActivateOnClose::LeftNeighbour
181 })
182 }
183
184 vscode.bool_setting("workbench.editor.showIcons", &mut current.file_icons);
185 vscode.bool_setting("git.decorations.enabled", &mut current.git_status);
186 }
187}
188
189impl Settings for PreviewTabsSettings {
190 const KEY: Option<&'static str> = Some("preview_tabs");
191
192 type FileContent = PreviewTabsSettingsContent;
193
194 fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
195 sources.json_merge()
196 }
197
198 fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
199 vscode.bool_setting("workbench.editor.enablePreview", &mut current.enabled);
200 vscode.bool_setting(
201 "workbench.editor.enablePreviewFromCodeNavigation",
202 &mut current.enable_preview_from_code_navigation,
203 );
204 vscode.bool_setting(
205 "workbench.editor.enablePreviewFromQuickOpen",
206 &mut current.enable_preview_from_file_finder,
207 );
208 }
209}
210
211#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
212pub enum ItemEvent {
213 CloseItem,
214 UpdateTab,
215 UpdateBreadcrumbs,
216 Edit,
217}
218
219// TODO: Combine this with existing HighlightedText struct?
220pub struct BreadcrumbText {
221 pub text: String,
222 pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
223 pub font: Option<Font>,
224}
225
226#[derive(Clone, Copy, Default, Debug)]
227pub struct TabContentParams {
228 pub detail: Option<usize>,
229 pub selected: bool,
230 pub preview: bool,
231 /// Tab content should be deemphasized when active pane does not have focus.
232 pub deemphasized: bool,
233}
234
235impl TabContentParams {
236 /// Returns the text color to be used for the tab content.
237 pub fn text_color(&self) -> Color {
238 if self.deemphasized {
239 if self.selected {
240 Color::Muted
241 } else {
242 Color::Hidden
243 }
244 } else if self.selected {
245 Color::Default
246 } else {
247 Color::Muted
248 }
249 }
250}
251
252pub enum TabTooltipContent {
253 Text(SharedString),
254 Custom(Box<dyn Fn(&mut Window, &mut App) -> AnyView>),
255}
256
257pub trait Item: Focusable + EventEmitter<Self::Event> + Render + Sized {
258 type Event;
259
260 /// Returns the tab contents.
261 ///
262 /// By default this returns a [`Label`] that displays that text from
263 /// `tab_content_text`.
264 fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
265 let text = self.tab_content_text(params.detail.unwrap_or_default(), cx);
266
267 Label::new(text)
268 .color(params.text_color())
269 .into_any_element()
270 }
271
272 /// Returns the textual contents of the tab.
273 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString;
274
275 /// Returns the suggested filename for saving this item.
276 /// By default, returns the tab content text.
277 fn suggested_filename(&self, cx: &App) -> SharedString {
278 self.tab_content_text(0, cx)
279 }
280
281 fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
282 None
283 }
284
285 /// Returns the tab tooltip text.
286 ///
287 /// Use this if you don't need to customize the tab tooltip content.
288 fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
289 None
290 }
291
292 /// Returns the tab tooltip content.
293 ///
294 /// By default this returns a Tooltip text from
295 /// `tab_tooltip_text`.
296 fn tab_tooltip_content(&self, cx: &App) -> Option<TabTooltipContent> {
297 self.tab_tooltip_text(cx).map(TabTooltipContent::Text)
298 }
299
300 fn to_item_events(_event: &Self::Event, _f: impl FnMut(ItemEvent)) {}
301
302 fn deactivated(&mut self, _window: &mut Window, _: &mut Context<Self>) {}
303 fn discarded(&self, _project: Entity<Project>, _window: &mut Window, _cx: &mut Context<Self>) {}
304 fn on_removed(&self, _cx: &App) {}
305 fn workspace_deactivated(&mut self, _window: &mut Window, _: &mut Context<Self>) {}
306 fn navigate(&mut self, _: Box<dyn Any>, _window: &mut Window, _: &mut Context<Self>) -> bool {
307 false
308 }
309
310 fn telemetry_event_text(&self) -> Option<&'static str> {
311 None
312 }
313
314 /// (model id, Item)
315 fn for_each_project_item(
316 &self,
317 _: &App,
318 _: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
319 ) {
320 }
321 fn is_singleton(&self, _cx: &App) -> bool {
322 false
323 }
324 fn set_nav_history(&mut self, _: ItemNavHistory, _window: &mut Window, _: &mut Context<Self>) {}
325 fn clone_on_split(
326 &self,
327 _workspace_id: Option<WorkspaceId>,
328 _window: &mut Window,
329 _: &mut Context<Self>,
330 ) -> Option<Entity<Self>>
331 where
332 Self: Sized,
333 {
334 None
335 }
336 fn is_dirty(&self, _: &App) -> bool {
337 false
338 }
339 fn has_deleted_file(&self, _: &App) -> bool {
340 false
341 }
342 fn has_conflict(&self, _: &App) -> bool {
343 false
344 }
345 fn can_save(&self, _cx: &App) -> bool {
346 false
347 }
348 fn can_save_as(&self, _: &App) -> bool {
349 false
350 }
351 fn save(
352 &mut self,
353 _options: SaveOptions,
354 _project: Entity<Project>,
355 _window: &mut Window,
356 _cx: &mut Context<Self>,
357 ) -> Task<Result<()>> {
358 unimplemented!("save() must be implemented if can_save() returns true")
359 }
360 fn save_as(
361 &mut self,
362 _project: Entity<Project>,
363 _path: ProjectPath,
364 _window: &mut Window,
365 _cx: &mut Context<Self>,
366 ) -> Task<Result<()>> {
367 unimplemented!("save_as() must be implemented if can_save() returns true")
368 }
369 fn reload(
370 &mut self,
371 _project: Entity<Project>,
372 _window: &mut Window,
373 _cx: &mut Context<Self>,
374 ) -> Task<Result<()>> {
375 unimplemented!("reload() must be implemented if can_save() returns true")
376 }
377
378 fn act_as_type<'a>(
379 &'a self,
380 type_id: TypeId,
381 self_handle: &'a Entity<Self>,
382 _: &'a App,
383 ) -> Option<AnyView> {
384 if TypeId::of::<Self>() == type_id {
385 Some(self_handle.clone().into())
386 } else {
387 None
388 }
389 }
390
391 fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
392 None
393 }
394
395 fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
396 ToolbarItemLocation::Hidden
397 }
398
399 fn breadcrumbs(&self, _theme: &Theme, _cx: &App) -> Option<Vec<BreadcrumbText>> {
400 None
401 }
402
403 fn added_to_workspace(
404 &mut self,
405 _workspace: &mut Workspace,
406 _window: &mut Window,
407 _cx: &mut Context<Self>,
408 ) {
409 }
410
411 fn show_toolbar(&self) -> bool {
412 true
413 }
414
415 fn pixel_position_of_cursor(&self, _: &App) -> Option<Point<Pixels>> {
416 None
417 }
418
419 fn preserve_preview(&self, _cx: &App) -> bool {
420 false
421 }
422
423 fn include_in_nav_history() -> bool {
424 true
425 }
426}
427
428pub trait SerializableItem: Item {
429 fn serialized_item_kind() -> &'static str;
430
431 fn cleanup(
432 workspace_id: WorkspaceId,
433 alive_items: Vec<ItemId>,
434 window: &mut Window,
435 cx: &mut App,
436 ) -> Task<Result<()>>;
437
438 fn deserialize(
439 _project: Entity<Project>,
440 _workspace: WeakEntity<Workspace>,
441 _workspace_id: WorkspaceId,
442 _item_id: ItemId,
443 _window: &mut Window,
444 _cx: &mut App,
445 ) -> Task<Result<Entity<Self>>>;
446
447 fn serialize(
448 &mut self,
449 workspace: &mut Workspace,
450 item_id: ItemId,
451 closing: bool,
452 window: &mut Window,
453 cx: &mut Context<Self>,
454 ) -> Option<Task<Result<()>>>;
455
456 fn should_serialize(&self, event: &Self::Event) -> bool;
457}
458
459pub trait SerializableItemHandle: ItemHandle {
460 fn serialized_item_kind(&self) -> &'static str;
461 fn serialize(
462 &self,
463 workspace: &mut Workspace,
464 closing: bool,
465 window: &mut Window,
466 cx: &mut App,
467 ) -> Option<Task<Result<()>>>;
468 fn should_serialize(&self, event: &dyn Any, cx: &App) -> bool;
469}
470
471impl<T> SerializableItemHandle for Entity<T>
472where
473 T: SerializableItem,
474{
475 fn serialized_item_kind(&self) -> &'static str {
476 T::serialized_item_kind()
477 }
478
479 fn serialize(
480 &self,
481 workspace: &mut Workspace,
482 closing: bool,
483 window: &mut Window,
484 cx: &mut App,
485 ) -> Option<Task<Result<()>>> {
486 self.update(cx, |this, cx| {
487 this.serialize(workspace, cx.entity_id().as_u64(), closing, window, cx)
488 })
489 }
490
491 fn should_serialize(&self, event: &dyn Any, cx: &App) -> bool {
492 event
493 .downcast_ref::<T::Event>()
494 .is_some_and(|event| self.read(cx).should_serialize(event))
495 }
496}
497
498pub trait ItemHandle: 'static + Send {
499 fn item_focus_handle(&self, cx: &App) -> FocusHandle;
500 fn subscribe_to_item_events(
501 &self,
502 window: &mut Window,
503 cx: &mut App,
504 handler: Box<dyn Fn(ItemEvent, &mut Window, &mut App)>,
505 ) -> gpui::Subscription;
506 fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement;
507 fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString;
508 fn suggested_filename(&self, cx: &App) -> SharedString;
509 fn tab_icon(&self, window: &Window, cx: &App) -> Option<Icon>;
510 fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString>;
511 fn tab_tooltip_content(&self, cx: &App) -> Option<TabTooltipContent>;
512 fn telemetry_event_text(&self, cx: &App) -> Option<&'static str>;
513 fn dragged_tab_content(
514 &self,
515 params: TabContentParams,
516 window: &Window,
517 cx: &App,
518 ) -> AnyElement;
519 fn project_path(&self, cx: &App) -> Option<ProjectPath>;
520 fn project_entry_ids(&self, cx: &App) -> SmallVec<[ProjectEntryId; 3]>;
521 fn project_paths(&self, cx: &App) -> SmallVec<[ProjectPath; 3]>;
522 fn project_item_model_ids(&self, cx: &App) -> SmallVec<[EntityId; 3]>;
523 fn for_each_project_item(
524 &self,
525 _: &App,
526 _: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
527 );
528 fn is_singleton(&self, cx: &App) -> bool;
529 fn boxed_clone(&self) -> Box<dyn ItemHandle>;
530 fn clone_on_split(
531 &self,
532 workspace_id: Option<WorkspaceId>,
533 window: &mut Window,
534 cx: &mut App,
535 ) -> Option<Box<dyn ItemHandle>>;
536 fn added_to_pane(
537 &self,
538 workspace: &mut Workspace,
539 pane: Entity<Pane>,
540 window: &mut Window,
541 cx: &mut Context<Workspace>,
542 );
543 fn deactivated(&self, window: &mut Window, cx: &mut App);
544 fn on_removed(&self, cx: &App);
545 fn workspace_deactivated(&self, window: &mut Window, cx: &mut App);
546 fn navigate(&self, data: Box<dyn Any>, window: &mut Window, cx: &mut App) -> bool;
547 fn item_id(&self) -> EntityId;
548 fn to_any(&self) -> AnyView;
549 fn is_dirty(&self, cx: &App) -> bool;
550 fn has_deleted_file(&self, cx: &App) -> bool;
551 fn has_conflict(&self, cx: &App) -> bool;
552 fn can_save(&self, cx: &App) -> bool;
553 fn can_save_as(&self, cx: &App) -> bool;
554 fn save(
555 &self,
556 options: SaveOptions,
557 project: Entity<Project>,
558 window: &mut Window,
559 cx: &mut App,
560 ) -> Task<Result<()>>;
561 fn save_as(
562 &self,
563 project: Entity<Project>,
564 path: ProjectPath,
565 window: &mut Window,
566 cx: &mut App,
567 ) -> Task<Result<()>>;
568 fn reload(
569 &self,
570 project: Entity<Project>,
571 window: &mut Window,
572 cx: &mut App,
573 ) -> Task<Result<()>>;
574 fn act_as_type(&self, type_id: TypeId, cx: &App) -> Option<AnyView>;
575 fn to_followable_item_handle(&self, cx: &App) -> Option<Box<dyn FollowableItemHandle>>;
576 fn to_serializable_item_handle(&self, cx: &App) -> Option<Box<dyn SerializableItemHandle>>;
577 fn on_release(
578 &self,
579 cx: &mut App,
580 callback: Box<dyn FnOnce(&mut App) + Send>,
581 ) -> gpui::Subscription;
582 fn to_searchable_item_handle(&self, cx: &App) -> Option<Box<dyn SearchableItemHandle>>;
583 fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation;
584 fn breadcrumbs(&self, theme: &Theme, cx: &App) -> Option<Vec<BreadcrumbText>>;
585 fn show_toolbar(&self, cx: &App) -> bool;
586 fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>>;
587 fn downgrade_item(&self) -> Box<dyn WeakItemHandle>;
588 fn workspace_settings<'a>(&self, cx: &'a App) -> &'a WorkspaceSettings;
589 fn preserve_preview(&self, cx: &App) -> bool;
590 fn include_in_nav_history(&self) -> bool;
591 fn relay_action(&self, action: Box<dyn Action>, window: &mut Window, cx: &mut App);
592 fn can_autosave(&self, cx: &App) -> bool {
593 let is_deleted = self.project_entry_ids(cx).is_empty();
594 self.is_dirty(cx) && !self.has_conflict(cx) && self.can_save(cx) && !is_deleted
595 }
596}
597
598pub trait WeakItemHandle: Send + Sync {
599 fn id(&self) -> EntityId;
600 fn boxed_clone(&self) -> Box<dyn WeakItemHandle>;
601 fn upgrade(&self) -> Option<Box<dyn ItemHandle>>;
602}
603
604impl dyn ItemHandle {
605 pub fn downcast<V: 'static>(&self) -> Option<Entity<V>> {
606 self.to_any().downcast().ok()
607 }
608
609 pub fn act_as<V: 'static>(&self, cx: &App) -> Option<Entity<V>> {
610 self.act_as_type(TypeId::of::<V>(), cx)
611 .and_then(|t| t.downcast().ok())
612 }
613}
614
615impl<T: Item> ItemHandle for Entity<T> {
616 fn subscribe_to_item_events(
617 &self,
618 window: &mut Window,
619 cx: &mut App,
620 handler: Box<dyn Fn(ItemEvent, &mut Window, &mut App)>,
621 ) -> gpui::Subscription {
622 window.subscribe(self, cx, move |_, event, window, cx| {
623 T::to_item_events(event, |item_event| handler(item_event, window, cx));
624 })
625 }
626
627 fn item_focus_handle(&self, cx: &App) -> FocusHandle {
628 self.read(cx).focus_handle(cx)
629 }
630
631 fn telemetry_event_text(&self, cx: &App) -> Option<&'static str> {
632 self.read(cx).telemetry_event_text()
633 }
634
635 fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement {
636 self.read(cx).tab_content(params, window, cx)
637 }
638 fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString {
639 self.read(cx).tab_content_text(detail, cx)
640 }
641
642 fn suggested_filename(&self, cx: &App) -> SharedString {
643 self.read(cx).suggested_filename(cx)
644 }
645
646 fn tab_icon(&self, window: &Window, cx: &App) -> Option<Icon> {
647 self.read(cx).tab_icon(window, cx)
648 }
649
650 fn tab_tooltip_content(&self, cx: &App) -> Option<TabTooltipContent> {
651 self.read(cx).tab_tooltip_content(cx)
652 }
653
654 fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
655 self.read(cx).tab_tooltip_text(cx)
656 }
657
658 fn dragged_tab_content(
659 &self,
660 params: TabContentParams,
661 window: &Window,
662 cx: &App,
663 ) -> AnyElement {
664 self.read(cx).tab_content(
665 TabContentParams {
666 selected: true,
667 ..params
668 },
669 window,
670 cx,
671 )
672 }
673
674 fn project_path(&self, cx: &App) -> Option<ProjectPath> {
675 let this = self.read(cx);
676 let mut result = None;
677 if this.is_singleton(cx) {
678 this.for_each_project_item(cx, &mut |_, item| {
679 result = item.project_path(cx);
680 });
681 }
682 result
683 }
684
685 fn workspace_settings<'a>(&self, cx: &'a App) -> &'a WorkspaceSettings {
686 if let Some(project_path) = self.project_path(cx) {
687 WorkspaceSettings::get(
688 Some(SettingsLocation {
689 worktree_id: project_path.worktree_id,
690 path: &project_path.path,
691 }),
692 cx,
693 )
694 } else {
695 WorkspaceSettings::get_global(cx)
696 }
697 }
698
699 fn project_entry_ids(&self, cx: &App) -> SmallVec<[ProjectEntryId; 3]> {
700 let mut result = SmallVec::new();
701 self.read(cx).for_each_project_item(cx, &mut |_, item| {
702 if let Some(id) = item.entry_id(cx) {
703 result.push(id);
704 }
705 });
706 result
707 }
708
709 fn project_paths(&self, cx: &App) -> SmallVec<[ProjectPath; 3]> {
710 let mut result = SmallVec::new();
711 self.read(cx).for_each_project_item(cx, &mut |_, item| {
712 if let Some(id) = item.project_path(cx) {
713 result.push(id);
714 }
715 });
716 result
717 }
718
719 fn project_item_model_ids(&self, cx: &App) -> SmallVec<[EntityId; 3]> {
720 let mut result = SmallVec::new();
721 self.read(cx).for_each_project_item(cx, &mut |id, _| {
722 result.push(id);
723 });
724 result
725 }
726
727 fn for_each_project_item(
728 &self,
729 cx: &App,
730 f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
731 ) {
732 self.read(cx).for_each_project_item(cx, f)
733 }
734
735 fn is_singleton(&self, cx: &App) -> bool {
736 self.read(cx).is_singleton(cx)
737 }
738
739 fn boxed_clone(&self) -> Box<dyn ItemHandle> {
740 Box::new(self.clone())
741 }
742
743 fn clone_on_split(
744 &self,
745 workspace_id: Option<WorkspaceId>,
746 window: &mut Window,
747 cx: &mut App,
748 ) -> Option<Box<dyn ItemHandle>> {
749 self.update(cx, |item, cx| item.clone_on_split(workspace_id, window, cx))
750 .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
751 }
752
753 fn added_to_pane(
754 &self,
755 workspace: &mut Workspace,
756 pane: Entity<Pane>,
757 window: &mut Window,
758 cx: &mut Context<Workspace>,
759 ) {
760 let weak_item = self.downgrade();
761 let history = pane.read(cx).nav_history_for_item(self);
762 self.update(cx, |this, cx| {
763 this.set_nav_history(history, window, cx);
764 this.added_to_workspace(workspace, window, cx);
765 });
766
767 if let Some(serializable_item) = self.to_serializable_item_handle(cx) {
768 workspace
769 .enqueue_item_serialization(serializable_item)
770 .log_err();
771 }
772
773 if workspace
774 .panes_by_item
775 .insert(self.item_id(), pane.downgrade())
776 .is_none()
777 {
778 let mut pending_autosave = DelayedDebouncedEditAction::new();
779 let (pending_update_tx, mut pending_update_rx) = mpsc::unbounded();
780 let pending_update = Rc::new(RefCell::new(None));
781
782 let mut send_follower_updates = None;
783 if let Some(item) = self.to_followable_item_handle(cx) {
784 let is_project_item = item.is_project_item(window, cx);
785 let item = item.downgrade();
786
787 send_follower_updates = Some(cx.spawn_in(window, {
788 let pending_update = pending_update.clone();
789 async move |workspace, cx| {
790 while let Some(mut leader_id) = pending_update_rx.next().await {
791 while let Ok(Some(id)) = pending_update_rx.try_next() {
792 leader_id = id;
793 }
794
795 workspace.update_in(cx, |workspace, window, cx| {
796 let Some(item) = item.upgrade() else { return };
797 workspace.update_followers(
798 is_project_item,
799 proto::update_followers::Variant::UpdateView(
800 proto::UpdateView {
801 id: item
802 .remote_id(workspace.client(), window, cx)
803 .and_then(|id| id.to_proto()),
804 variant: pending_update.borrow_mut().take(),
805 leader_id,
806 },
807 ),
808 window,
809 cx,
810 );
811 })?;
812 cx.background_executor().timer(LEADER_UPDATE_THROTTLE).await;
813 }
814 anyhow::Ok(())
815 }
816 }));
817 }
818
819 let mut event_subscription = Some(cx.subscribe_in(
820 self,
821 window,
822 move |workspace, item: &Entity<T>, event, window, cx| {
823 let pane = if let Some(pane) = workspace
824 .panes_by_item
825 .get(&item.item_id())
826 .and_then(|pane| pane.upgrade())
827 {
828 pane
829 } else {
830 return;
831 };
832
833 if let Some(item) = item.to_followable_item_handle(cx) {
834 let leader_id = workspace.leader_for_pane(&pane);
835
836 if let Some(leader_id) = leader_id
837 && let Some(FollowEvent::Unfollow) = item.to_follow_event(event)
838 {
839 workspace.unfollow(leader_id, window, cx);
840 }
841
842 if item.item_focus_handle(cx).contains_focused(window, cx) {
843 match leader_id {
844 Some(CollaboratorId::Agent) => {}
845 Some(CollaboratorId::PeerId(leader_peer_id)) => {
846 item.add_event_to_update_proto(
847 event,
848 &mut pending_update.borrow_mut(),
849 window,
850 cx,
851 );
852 pending_update_tx.unbounded_send(Some(leader_peer_id)).ok();
853 }
854 None => {
855 item.add_event_to_update_proto(
856 event,
857 &mut pending_update.borrow_mut(),
858 window,
859 cx,
860 );
861 pending_update_tx.unbounded_send(None).ok();
862 }
863 }
864 }
865 }
866
867 if let Some(item) = item.to_serializable_item_handle(cx)
868 && item.should_serialize(event, cx)
869 {
870 workspace.enqueue_item_serialization(item).ok();
871 }
872
873 T::to_item_events(event, |event| match event {
874 ItemEvent::CloseItem => {
875 pane.update(cx, |pane, cx| {
876 pane.close_item_by_id(
877 item.item_id(),
878 crate::SaveIntent::Close,
879 window,
880 cx,
881 )
882 })
883 .detach_and_log_err(cx);
884 }
885
886 ItemEvent::UpdateTab => {
887 workspace.update_item_dirty_state(item, window, cx);
888
889 if item.has_deleted_file(cx)
890 && !item.is_dirty(cx)
891 && item.workspace_settings(cx).close_on_file_delete
892 {
893 let item_id = item.item_id();
894 let close_item_task = pane.update(cx, |pane, cx| {
895 pane.close_item_by_id(
896 item_id,
897 crate::SaveIntent::Close,
898 window,
899 cx,
900 )
901 });
902 cx.spawn_in(window, {
903 let pane = pane.clone();
904 async move |_workspace, cx| {
905 close_item_task.await?;
906 pane.update(cx, |pane, _cx| {
907 pane.nav_history_mut().remove_item(item_id);
908 })
909 }
910 })
911 .detach_and_log_err(cx);
912 } else {
913 pane.update(cx, |_, cx| {
914 cx.emit(pane::Event::ChangeItemTitle);
915 cx.notify();
916 });
917 }
918 }
919
920 ItemEvent::Edit => {
921 let autosave = item.workspace_settings(cx).autosave;
922
923 if let AutosaveSetting::AfterDelay { milliseconds } = autosave {
924 let delay = Duration::from_millis(milliseconds);
925 let item = item.clone();
926 pending_autosave.fire_new(
927 delay,
928 window,
929 cx,
930 move |workspace, window, cx| {
931 Pane::autosave_item(
932 &item,
933 workspace.project().clone(),
934 window,
935 cx,
936 )
937 },
938 );
939 }
940 pane.update(cx, |pane, cx| pane.handle_item_edit(item.item_id(), cx));
941 }
942
943 _ => {}
944 });
945 },
946 ));
947
948 cx.on_blur(
949 &self.read(cx).focus_handle(cx),
950 window,
951 move |workspace, window, cx| {
952 if let Some(item) = weak_item.upgrade()
953 && item.workspace_settings(cx).autosave == AutosaveSetting::OnFocusChange
954 {
955 Pane::autosave_item(&item, workspace.project.clone(), window, cx)
956 .detach_and_log_err(cx);
957 }
958 },
959 )
960 .detach();
961
962 let item_id = self.item_id();
963 workspace.update_item_dirty_state(self, window, cx);
964 cx.observe_release_in(self, window, move |workspace, _, _, _| {
965 workspace.panes_by_item.remove(&item_id);
966 event_subscription.take();
967 send_follower_updates.take();
968 })
969 .detach();
970 }
971
972 cx.defer_in(window, |workspace, window, cx| {
973 workspace.serialize_workspace(window, cx);
974 });
975 }
976
977 fn deactivated(&self, window: &mut Window, cx: &mut App) {
978 self.update(cx, |this, cx| this.deactivated(window, cx));
979 }
980
981 fn on_removed(&self, cx: &App) {
982 self.read(cx).on_removed(cx);
983 }
984
985 fn workspace_deactivated(&self, window: &mut Window, cx: &mut App) {
986 self.update(cx, |this, cx| this.workspace_deactivated(window, cx));
987 }
988
989 fn navigate(&self, data: Box<dyn Any>, window: &mut Window, cx: &mut App) -> bool {
990 self.update(cx, |this, cx| this.navigate(data, window, cx))
991 }
992
993 fn item_id(&self) -> EntityId {
994 self.entity_id()
995 }
996
997 fn to_any(&self) -> AnyView {
998 self.clone().into()
999 }
1000
1001 fn is_dirty(&self, cx: &App) -> bool {
1002 self.read(cx).is_dirty(cx)
1003 }
1004
1005 fn has_deleted_file(&self, cx: &App) -> bool {
1006 self.read(cx).has_deleted_file(cx)
1007 }
1008
1009 fn has_conflict(&self, cx: &App) -> bool {
1010 self.read(cx).has_conflict(cx)
1011 }
1012
1013 fn can_save(&self, cx: &App) -> bool {
1014 self.read(cx).can_save(cx)
1015 }
1016
1017 fn can_save_as(&self, cx: &App) -> bool {
1018 self.read(cx).can_save_as(cx)
1019 }
1020
1021 fn save(
1022 &self,
1023 options: SaveOptions,
1024 project: Entity<Project>,
1025 window: &mut Window,
1026 cx: &mut App,
1027 ) -> Task<Result<()>> {
1028 self.update(cx, |item, cx| item.save(options, project, window, cx))
1029 }
1030
1031 fn save_as(
1032 &self,
1033 project: Entity<Project>,
1034 path: ProjectPath,
1035 window: &mut Window,
1036 cx: &mut App,
1037 ) -> Task<anyhow::Result<()>> {
1038 self.update(cx, |item, cx| item.save_as(project, path, window, cx))
1039 }
1040
1041 fn reload(
1042 &self,
1043 project: Entity<Project>,
1044 window: &mut Window,
1045 cx: &mut App,
1046 ) -> Task<Result<()>> {
1047 self.update(cx, |item, cx| item.reload(project, window, cx))
1048 }
1049
1050 fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a App) -> Option<AnyView> {
1051 self.read(cx).act_as_type(type_id, self, cx)
1052 }
1053
1054 fn to_followable_item_handle(&self, cx: &App) -> Option<Box<dyn FollowableItemHandle>> {
1055 FollowableViewRegistry::to_followable_view(self.clone(), cx)
1056 }
1057
1058 fn on_release(
1059 &self,
1060 cx: &mut App,
1061 callback: Box<dyn FnOnce(&mut App) + Send>,
1062 ) -> gpui::Subscription {
1063 cx.observe_release(self, move |_, cx| callback(cx))
1064 }
1065
1066 fn to_searchable_item_handle(&self, cx: &App) -> Option<Box<dyn SearchableItemHandle>> {
1067 self.read(cx).as_searchable(self)
1068 }
1069
1070 fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
1071 self.read(cx).breadcrumb_location(cx)
1072 }
1073
1074 fn breadcrumbs(&self, theme: &Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
1075 self.read(cx).breadcrumbs(theme, cx)
1076 }
1077
1078 fn show_toolbar(&self, cx: &App) -> bool {
1079 self.read(cx).show_toolbar()
1080 }
1081
1082 fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>> {
1083 self.read(cx).pixel_position_of_cursor(cx)
1084 }
1085
1086 fn downgrade_item(&self) -> Box<dyn WeakItemHandle> {
1087 Box::new(self.downgrade())
1088 }
1089
1090 fn to_serializable_item_handle(&self, cx: &App) -> Option<Box<dyn SerializableItemHandle>> {
1091 SerializableItemRegistry::view_to_serializable_item_handle(self.to_any(), cx)
1092 }
1093
1094 fn preserve_preview(&self, cx: &App) -> bool {
1095 self.read(cx).preserve_preview(cx)
1096 }
1097
1098 fn include_in_nav_history(&self) -> bool {
1099 T::include_in_nav_history()
1100 }
1101
1102 fn relay_action(&self, action: Box<dyn Action>, window: &mut Window, cx: &mut App) {
1103 self.update(cx, |this, cx| {
1104 this.focus_handle(cx).focus(window);
1105 window.dispatch_action(action, cx);
1106 })
1107 }
1108}
1109
1110impl From<Box<dyn ItemHandle>> for AnyView {
1111 fn from(val: Box<dyn ItemHandle>) -> Self {
1112 val.to_any()
1113 }
1114}
1115
1116impl From<&Box<dyn ItemHandle>> for AnyView {
1117 fn from(val: &Box<dyn ItemHandle>) -> Self {
1118 val.to_any()
1119 }
1120}
1121
1122impl Clone for Box<dyn ItemHandle> {
1123 fn clone(&self) -> Box<dyn ItemHandle> {
1124 self.boxed_clone()
1125 }
1126}
1127
1128impl<T: Item> WeakItemHandle for WeakEntity<T> {
1129 fn id(&self) -> EntityId {
1130 self.entity_id()
1131 }
1132
1133 fn boxed_clone(&self) -> Box<dyn WeakItemHandle> {
1134 Box::new(self.clone())
1135 }
1136
1137 fn upgrade(&self) -> Option<Box<dyn ItemHandle>> {
1138 self.upgrade().map(|v| Box::new(v) as Box<dyn ItemHandle>)
1139 }
1140}
1141
1142#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1143pub struct ProjectItemKind(pub &'static str);
1144
1145pub trait ProjectItem: Item {
1146 type Item: project::ProjectItem;
1147
1148 fn project_item_kind() -> Option<ProjectItemKind> {
1149 None
1150 }
1151
1152 fn for_project_item(
1153 project: Entity<Project>,
1154 pane: Option<&Pane>,
1155 item: Entity<Self::Item>,
1156 window: &mut Window,
1157 cx: &mut Context<Self>,
1158 ) -> Self
1159 where
1160 Self: Sized;
1161
1162 /// A fallback handler, which will be called after [`project::ProjectItem::try_open`] fails,
1163 /// with the error from that failure as an argument.
1164 /// Allows to open an item that can gracefully display and handle errors.
1165 fn for_broken_project_item(
1166 _abs_path: &Path,
1167 _is_local: bool,
1168 _e: &anyhow::Error,
1169 _window: &mut Window,
1170 _cx: &mut App,
1171 ) -> Option<InvalidBufferView>
1172 where
1173 Self: Sized,
1174 {
1175 None
1176 }
1177}
1178
1179#[derive(Debug)]
1180pub enum FollowEvent {
1181 Unfollow,
1182}
1183
1184pub enum Dedup {
1185 KeepExisting,
1186 ReplaceExisting,
1187}
1188
1189pub trait FollowableItem: Item {
1190 fn remote_id(&self) -> Option<ViewId>;
1191 fn to_state_proto(&self, window: &Window, cx: &App) -> Option<proto::view::Variant>;
1192 fn from_state_proto(
1193 project: Entity<Workspace>,
1194 id: ViewId,
1195 state: &mut Option<proto::view::Variant>,
1196 window: &mut Window,
1197 cx: &mut App,
1198 ) -> Option<Task<Result<Entity<Self>>>>;
1199 fn to_follow_event(event: &Self::Event) -> Option<FollowEvent>;
1200 fn add_event_to_update_proto(
1201 &self,
1202 event: &Self::Event,
1203 update: &mut Option<proto::update_view::Variant>,
1204 window: &Window,
1205 cx: &App,
1206 ) -> bool;
1207 fn apply_update_proto(
1208 &mut self,
1209 project: &Entity<Project>,
1210 message: proto::update_view::Variant,
1211 window: &mut Window,
1212 cx: &mut Context<Self>,
1213 ) -> Task<Result<()>>;
1214 fn is_project_item(&self, window: &Window, cx: &App) -> bool;
1215 fn set_leader_id(
1216 &mut self,
1217 leader_peer_id: Option<CollaboratorId>,
1218 window: &mut Window,
1219 cx: &mut Context<Self>,
1220 );
1221 fn dedup(&self, existing: &Self, window: &Window, cx: &App) -> Option<Dedup>;
1222 fn update_agent_location(
1223 &mut self,
1224 _location: language::Anchor,
1225 _window: &mut Window,
1226 _cx: &mut Context<Self>,
1227 ) {
1228 }
1229}
1230
1231pub trait FollowableItemHandle: ItemHandle {
1232 fn remote_id(&self, client: &Arc<Client>, window: &mut Window, cx: &mut App) -> Option<ViewId>;
1233 fn downgrade(&self) -> Box<dyn WeakFollowableItemHandle>;
1234 fn set_leader_id(
1235 &self,
1236 leader_peer_id: Option<CollaboratorId>,
1237 window: &mut Window,
1238 cx: &mut App,
1239 );
1240 fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant>;
1241 fn add_event_to_update_proto(
1242 &self,
1243 event: &dyn Any,
1244 update: &mut Option<proto::update_view::Variant>,
1245 window: &mut Window,
1246 cx: &mut App,
1247 ) -> bool;
1248 fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent>;
1249 fn apply_update_proto(
1250 &self,
1251 project: &Entity<Project>,
1252 message: proto::update_view::Variant,
1253 window: &mut Window,
1254 cx: &mut App,
1255 ) -> Task<Result<()>>;
1256 fn is_project_item(&self, window: &mut Window, cx: &mut App) -> bool;
1257 fn dedup(
1258 &self,
1259 existing: &dyn FollowableItemHandle,
1260 window: &mut Window,
1261 cx: &mut App,
1262 ) -> Option<Dedup>;
1263 fn update_agent_location(&self, location: language::Anchor, window: &mut Window, cx: &mut App);
1264}
1265
1266impl<T: FollowableItem> FollowableItemHandle for Entity<T> {
1267 fn remote_id(&self, client: &Arc<Client>, _: &mut Window, cx: &mut App) -> Option<ViewId> {
1268 self.read(cx).remote_id().or_else(|| {
1269 client.peer_id().map(|creator| ViewId {
1270 creator: CollaboratorId::PeerId(creator),
1271 id: self.item_id().as_u64(),
1272 })
1273 })
1274 }
1275
1276 fn downgrade(&self) -> Box<dyn WeakFollowableItemHandle> {
1277 Box::new(self.downgrade())
1278 }
1279
1280 fn set_leader_id(&self, leader_id: Option<CollaboratorId>, window: &mut Window, cx: &mut App) {
1281 self.update(cx, |this, cx| this.set_leader_id(leader_id, window, cx))
1282 }
1283
1284 fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant> {
1285 self.read(cx).to_state_proto(window, cx)
1286 }
1287
1288 fn add_event_to_update_proto(
1289 &self,
1290 event: &dyn Any,
1291 update: &mut Option<proto::update_view::Variant>,
1292 window: &mut Window,
1293 cx: &mut App,
1294 ) -> bool {
1295 if let Some(event) = event.downcast_ref() {
1296 self.read(cx)
1297 .add_event_to_update_proto(event, update, window, cx)
1298 } else {
1299 false
1300 }
1301 }
1302
1303 fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent> {
1304 T::to_follow_event(event.downcast_ref()?)
1305 }
1306
1307 fn apply_update_proto(
1308 &self,
1309 project: &Entity<Project>,
1310 message: proto::update_view::Variant,
1311 window: &mut Window,
1312 cx: &mut App,
1313 ) -> Task<Result<()>> {
1314 self.update(cx, |this, cx| {
1315 this.apply_update_proto(project, message, window, cx)
1316 })
1317 }
1318
1319 fn is_project_item(&self, window: &mut Window, cx: &mut App) -> bool {
1320 self.read(cx).is_project_item(window, cx)
1321 }
1322
1323 fn dedup(
1324 &self,
1325 existing: &dyn FollowableItemHandle,
1326 window: &mut Window,
1327 cx: &mut App,
1328 ) -> Option<Dedup> {
1329 let existing = existing.to_any().downcast::<T>().ok()?;
1330 self.read(cx).dedup(existing.read(cx), window, cx)
1331 }
1332
1333 fn update_agent_location(&self, location: language::Anchor, window: &mut Window, cx: &mut App) {
1334 self.update(cx, |this, cx| {
1335 this.update_agent_location(location, window, cx)
1336 })
1337 }
1338}
1339
1340pub trait WeakFollowableItemHandle: Send + Sync {
1341 fn upgrade(&self) -> Option<Box<dyn FollowableItemHandle>>;
1342}
1343
1344impl<T: FollowableItem> WeakFollowableItemHandle for WeakEntity<T> {
1345 fn upgrade(&self) -> Option<Box<dyn FollowableItemHandle>> {
1346 Some(Box::new(self.upgrade()?))
1347 }
1348}
1349
1350#[cfg(any(test, feature = "test-support"))]
1351pub mod test {
1352 use super::{Item, ItemEvent, SerializableItem, TabContentParams};
1353 use crate::{ItemId, ItemNavHistory, Workspace, WorkspaceId, item::SaveOptions};
1354 use gpui::{
1355 AnyElement, App, AppContext as _, Context, Entity, EntityId, EventEmitter, Focusable,
1356 InteractiveElement, IntoElement, Render, SharedString, Task, WeakEntity, Window,
1357 };
1358 use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
1359 use std::{any::Any, cell::Cell, path::Path};
1360
1361 pub struct TestProjectItem {
1362 pub entry_id: Option<ProjectEntryId>,
1363 pub project_path: Option<ProjectPath>,
1364 pub is_dirty: bool,
1365 }
1366
1367 pub struct TestItem {
1368 pub workspace_id: Option<WorkspaceId>,
1369 pub state: String,
1370 pub label: String,
1371 pub save_count: usize,
1372 pub save_as_count: usize,
1373 pub reload_count: usize,
1374 pub is_dirty: bool,
1375 pub is_singleton: bool,
1376 pub has_conflict: bool,
1377 pub has_deleted_file: bool,
1378 pub project_items: Vec<Entity<TestProjectItem>>,
1379 pub nav_history: Option<ItemNavHistory>,
1380 pub tab_descriptions: Option<Vec<&'static str>>,
1381 pub tab_detail: Cell<Option<usize>>,
1382 serialize: Option<Box<dyn Fn() -> Option<Task<anyhow::Result<()>>>>>,
1383 focus_handle: gpui::FocusHandle,
1384 }
1385
1386 impl project::ProjectItem for TestProjectItem {
1387 fn try_open(
1388 _project: &Entity<Project>,
1389 _path: &ProjectPath,
1390 _cx: &mut App,
1391 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
1392 None
1393 }
1394 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
1395 self.entry_id
1396 }
1397
1398 fn project_path(&self, _: &App) -> Option<ProjectPath> {
1399 self.project_path.clone()
1400 }
1401
1402 fn is_dirty(&self) -> bool {
1403 self.is_dirty
1404 }
1405 }
1406
1407 pub enum TestItemEvent {
1408 Edit,
1409 }
1410
1411 impl TestProjectItem {
1412 pub fn new(id: u64, path: &str, cx: &mut App) -> Entity<Self> {
1413 let entry_id = Some(ProjectEntryId::from_proto(id));
1414 let project_path = Some(ProjectPath {
1415 worktree_id: WorktreeId::from_usize(0),
1416 path: Path::new(path).into(),
1417 });
1418 cx.new(|_| Self {
1419 entry_id,
1420 project_path,
1421 is_dirty: false,
1422 })
1423 }
1424
1425 pub fn new_untitled(cx: &mut App) -> Entity<Self> {
1426 cx.new(|_| Self {
1427 project_path: None,
1428 entry_id: None,
1429 is_dirty: false,
1430 })
1431 }
1432
1433 pub fn new_dirty(id: u64, path: &str, cx: &mut App) -> Entity<Self> {
1434 let entry_id = Some(ProjectEntryId::from_proto(id));
1435 let project_path = Some(ProjectPath {
1436 worktree_id: WorktreeId::from_usize(0),
1437 path: Path::new(path).into(),
1438 });
1439 cx.new(|_| Self {
1440 entry_id,
1441 project_path,
1442 is_dirty: true,
1443 })
1444 }
1445 }
1446
1447 impl TestItem {
1448 pub fn new(cx: &mut Context<Self>) -> Self {
1449 Self {
1450 state: String::new(),
1451 label: String::new(),
1452 save_count: 0,
1453 save_as_count: 0,
1454 reload_count: 0,
1455 is_dirty: false,
1456 has_conflict: false,
1457 has_deleted_file: false,
1458 project_items: Vec::new(),
1459 is_singleton: true,
1460 nav_history: None,
1461 tab_descriptions: None,
1462 tab_detail: Default::default(),
1463 workspace_id: Default::default(),
1464 focus_handle: cx.focus_handle(),
1465 serialize: None,
1466 }
1467 }
1468
1469 pub fn new_deserialized(id: WorkspaceId, cx: &mut Context<Self>) -> Self {
1470 let mut this = Self::new(cx);
1471 this.workspace_id = Some(id);
1472 this
1473 }
1474
1475 pub fn with_label(mut self, state: &str) -> Self {
1476 self.label = state.to_string();
1477 self
1478 }
1479
1480 pub fn with_singleton(mut self, singleton: bool) -> Self {
1481 self.is_singleton = singleton;
1482 self
1483 }
1484
1485 pub fn set_has_deleted_file(&mut self, deleted: bool) {
1486 self.has_deleted_file = deleted;
1487 }
1488
1489 pub fn with_dirty(mut self, dirty: bool) -> Self {
1490 self.is_dirty = dirty;
1491 self
1492 }
1493
1494 pub fn with_conflict(mut self, has_conflict: bool) -> Self {
1495 self.has_conflict = has_conflict;
1496 self
1497 }
1498
1499 pub fn with_project_items(mut self, items: &[Entity<TestProjectItem>]) -> Self {
1500 self.project_items.clear();
1501 self.project_items.extend(items.iter().cloned());
1502 self
1503 }
1504
1505 pub fn with_serialize(
1506 mut self,
1507 serialize: impl Fn() -> Option<Task<anyhow::Result<()>>> + 'static,
1508 ) -> Self {
1509 self.serialize = Some(Box::new(serialize));
1510 self
1511 }
1512
1513 pub fn set_state(&mut self, state: String, cx: &mut Context<Self>) {
1514 self.push_to_nav_history(cx);
1515 self.state = state;
1516 }
1517
1518 fn push_to_nav_history(&mut self, cx: &mut Context<Self>) {
1519 if let Some(history) = &mut self.nav_history {
1520 history.push(Some(Box::new(self.state.clone())), cx);
1521 }
1522 }
1523 }
1524
1525 impl Render for TestItem {
1526 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1527 gpui::div().track_focus(&self.focus_handle(cx))
1528 }
1529 }
1530
1531 impl EventEmitter<ItemEvent> for TestItem {}
1532
1533 impl Focusable for TestItem {
1534 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
1535 self.focus_handle.clone()
1536 }
1537 }
1538
1539 impl Item for TestItem {
1540 type Event = ItemEvent;
1541
1542 fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
1543 f(*event)
1544 }
1545
1546 fn tab_content_text(&self, detail: usize, _cx: &App) -> SharedString {
1547 self.tab_descriptions
1548 .as_ref()
1549 .and_then(|descriptions| {
1550 let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
1551 description.into()
1552 })
1553 .unwrap_or_default()
1554 .into()
1555 }
1556
1557 fn telemetry_event_text(&self) -> Option<&'static str> {
1558 None
1559 }
1560
1561 fn tab_content(&self, params: TabContentParams, _window: &Window, _cx: &App) -> AnyElement {
1562 self.tab_detail.set(params.detail);
1563 gpui::div().into_any_element()
1564 }
1565
1566 fn for_each_project_item(
1567 &self,
1568 cx: &App,
1569 f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
1570 ) {
1571 self.project_items
1572 .iter()
1573 .for_each(|item| f(item.entity_id(), item.read(cx)))
1574 }
1575
1576 fn is_singleton(&self, _: &App) -> bool {
1577 self.is_singleton
1578 }
1579
1580 fn set_nav_history(
1581 &mut self,
1582 history: ItemNavHistory,
1583 _window: &mut Window,
1584 _: &mut Context<Self>,
1585 ) {
1586 self.nav_history = Some(history);
1587 }
1588
1589 fn navigate(
1590 &mut self,
1591 state: Box<dyn Any>,
1592 _window: &mut Window,
1593 _: &mut Context<Self>,
1594 ) -> bool {
1595 let state = *state.downcast::<String>().unwrap_or_default();
1596 if state != self.state {
1597 self.state = state;
1598 true
1599 } else {
1600 false
1601 }
1602 }
1603
1604 fn deactivated(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1605 self.push_to_nav_history(cx);
1606 }
1607
1608 fn clone_on_split(
1609 &self,
1610 _workspace_id: Option<WorkspaceId>,
1611 _: &mut Window,
1612 cx: &mut Context<Self>,
1613 ) -> Option<Entity<Self>>
1614 where
1615 Self: Sized,
1616 {
1617 Some(cx.new(|cx| Self {
1618 state: self.state.clone(),
1619 label: self.label.clone(),
1620 save_count: self.save_count,
1621 save_as_count: self.save_as_count,
1622 reload_count: self.reload_count,
1623 is_dirty: self.is_dirty,
1624 is_singleton: self.is_singleton,
1625 has_conflict: self.has_conflict,
1626 has_deleted_file: self.has_deleted_file,
1627 project_items: self.project_items.clone(),
1628 nav_history: None,
1629 tab_descriptions: None,
1630 tab_detail: Default::default(),
1631 workspace_id: self.workspace_id,
1632 focus_handle: cx.focus_handle(),
1633 serialize: None,
1634 }))
1635 }
1636
1637 fn is_dirty(&self, _: &App) -> bool {
1638 self.is_dirty
1639 }
1640
1641 fn has_conflict(&self, _: &App) -> bool {
1642 self.has_conflict
1643 }
1644
1645 fn has_deleted_file(&self, _: &App) -> bool {
1646 self.has_deleted_file
1647 }
1648
1649 fn can_save(&self, cx: &App) -> bool {
1650 !self.project_items.is_empty()
1651 && self
1652 .project_items
1653 .iter()
1654 .all(|item| item.read(cx).entry_id.is_some())
1655 }
1656
1657 fn can_save_as(&self, _cx: &App) -> bool {
1658 self.is_singleton
1659 }
1660
1661 fn save(
1662 &mut self,
1663 _: SaveOptions,
1664 _: Entity<Project>,
1665 _window: &mut Window,
1666 cx: &mut Context<Self>,
1667 ) -> Task<anyhow::Result<()>> {
1668 self.save_count += 1;
1669 self.is_dirty = false;
1670 for item in &self.project_items {
1671 item.update(cx, |item, _| {
1672 if item.is_dirty {
1673 item.is_dirty = false;
1674 }
1675 })
1676 }
1677 Task::ready(Ok(()))
1678 }
1679
1680 fn save_as(
1681 &mut self,
1682 _: Entity<Project>,
1683 _: ProjectPath,
1684 _window: &mut Window,
1685 _: &mut Context<Self>,
1686 ) -> Task<anyhow::Result<()>> {
1687 self.save_as_count += 1;
1688 self.is_dirty = false;
1689 Task::ready(Ok(()))
1690 }
1691
1692 fn reload(
1693 &mut self,
1694 _: Entity<Project>,
1695 _window: &mut Window,
1696 _: &mut Context<Self>,
1697 ) -> Task<anyhow::Result<()>> {
1698 self.reload_count += 1;
1699 self.is_dirty = false;
1700 Task::ready(Ok(()))
1701 }
1702 }
1703
1704 impl SerializableItem for TestItem {
1705 fn serialized_item_kind() -> &'static str {
1706 "TestItem"
1707 }
1708
1709 fn deserialize(
1710 _project: Entity<Project>,
1711 _workspace: WeakEntity<Workspace>,
1712 workspace_id: WorkspaceId,
1713 _item_id: ItemId,
1714 _window: &mut Window,
1715 cx: &mut App,
1716 ) -> Task<anyhow::Result<Entity<Self>>> {
1717 let entity = cx.new(|cx| Self::new_deserialized(workspace_id, cx));
1718 Task::ready(Ok(entity))
1719 }
1720
1721 fn cleanup(
1722 _workspace_id: WorkspaceId,
1723 _alive_items: Vec<ItemId>,
1724 _window: &mut Window,
1725 _cx: &mut App,
1726 ) -> Task<anyhow::Result<()>> {
1727 Task::ready(Ok(()))
1728 }
1729
1730 fn serialize(
1731 &mut self,
1732 _workspace: &mut Workspace,
1733 _item_id: ItemId,
1734 _closing: bool,
1735 _window: &mut Window,
1736 _cx: &mut Context<Self>,
1737 ) -> Option<Task<anyhow::Result<()>>> {
1738 if let Some(serialize) = self.serialize.take() {
1739 let result = serialize();
1740 self.serialize = Some(serialize);
1741 result
1742 } else {
1743 None
1744 }
1745 }
1746
1747 fn should_serialize(&self, _event: &Self::Event) -> bool {
1748 false
1749 }
1750 }
1751}