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 discarded(&self, project: Entity<Project>, window: &mut Window, cx: &mut App);
545 fn on_removed(&self, cx: &App);
546 fn workspace_deactivated(&self, window: &mut Window, cx: &mut App);
547 fn navigate(&self, data: Box<dyn Any>, window: &mut Window, cx: &mut App) -> bool;
548 fn item_id(&self) -> EntityId;
549 fn to_any(&self) -> AnyView;
550 fn is_dirty(&self, cx: &App) -> bool;
551 fn has_deleted_file(&self, cx: &App) -> bool;
552 fn has_conflict(&self, cx: &App) -> bool;
553 fn can_save(&self, cx: &App) -> bool;
554 fn can_save_as(&self, cx: &App) -> bool;
555 fn save(
556 &self,
557 options: SaveOptions,
558 project: Entity<Project>,
559 window: &mut Window,
560 cx: &mut App,
561 ) -> Task<Result<()>>;
562 fn save_as(
563 &self,
564 project: Entity<Project>,
565 path: ProjectPath,
566 window: &mut Window,
567 cx: &mut App,
568 ) -> Task<Result<()>>;
569 fn reload(
570 &self,
571 project: Entity<Project>,
572 window: &mut Window,
573 cx: &mut App,
574 ) -> Task<Result<()>>;
575 fn act_as_type(&self, type_id: TypeId, cx: &App) -> Option<AnyView>;
576 fn to_followable_item_handle(&self, cx: &App) -> Option<Box<dyn FollowableItemHandle>>;
577 fn to_serializable_item_handle(&self, cx: &App) -> Option<Box<dyn SerializableItemHandle>>;
578 fn on_release(
579 &self,
580 cx: &mut App,
581 callback: Box<dyn FnOnce(&mut App) + Send>,
582 ) -> gpui::Subscription;
583 fn to_searchable_item_handle(&self, cx: &App) -> Option<Box<dyn SearchableItemHandle>>;
584 fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation;
585 fn breadcrumbs(&self, theme: &Theme, cx: &App) -> Option<Vec<BreadcrumbText>>;
586 fn show_toolbar(&self, cx: &App) -> bool;
587 fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>>;
588 fn downgrade_item(&self) -> Box<dyn WeakItemHandle>;
589 fn workspace_settings<'a>(&self, cx: &'a App) -> &'a WorkspaceSettings;
590 fn preserve_preview(&self, cx: &App) -> bool;
591 fn include_in_nav_history(&self) -> bool;
592 fn relay_action(&self, action: Box<dyn Action>, window: &mut Window, cx: &mut App);
593 fn can_autosave(&self, cx: &App) -> bool {
594 let is_deleted = self.project_entry_ids(cx).is_empty();
595 self.is_dirty(cx) && !self.has_conflict(cx) && self.can_save(cx) && !is_deleted
596 }
597}
598
599pub trait WeakItemHandle: Send + Sync {
600 fn id(&self) -> EntityId;
601 fn boxed_clone(&self) -> Box<dyn WeakItemHandle>;
602 fn upgrade(&self) -> Option<Box<dyn ItemHandle>>;
603}
604
605impl dyn ItemHandle {
606 pub fn downcast<V: 'static>(&self) -> Option<Entity<V>> {
607 self.to_any().downcast().ok()
608 }
609
610 pub fn act_as<V: 'static>(&self, cx: &App) -> Option<Entity<V>> {
611 self.act_as_type(TypeId::of::<V>(), cx)
612 .and_then(|t| t.downcast().ok())
613 }
614}
615
616impl<T: Item> ItemHandle for Entity<T> {
617 fn subscribe_to_item_events(
618 &self,
619 window: &mut Window,
620 cx: &mut App,
621 handler: Box<dyn Fn(ItemEvent, &mut Window, &mut App)>,
622 ) -> gpui::Subscription {
623 window.subscribe(self, cx, move |_, event, window, cx| {
624 T::to_item_events(event, |item_event| handler(item_event, window, cx));
625 })
626 }
627
628 fn item_focus_handle(&self, cx: &App) -> FocusHandle {
629 self.read(cx).focus_handle(cx)
630 }
631
632 fn telemetry_event_text(&self, cx: &App) -> Option<&'static str> {
633 self.read(cx).telemetry_event_text()
634 }
635
636 fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement {
637 self.read(cx).tab_content(params, window, cx)
638 }
639 fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString {
640 self.read(cx).tab_content_text(detail, cx)
641 }
642
643 fn suggested_filename(&self, cx: &App) -> SharedString {
644 self.read(cx).suggested_filename(cx)
645 }
646
647 fn tab_icon(&self, window: &Window, cx: &App) -> Option<Icon> {
648 self.read(cx).tab_icon(window, cx)
649 }
650
651 fn tab_tooltip_content(&self, cx: &App) -> Option<TabTooltipContent> {
652 self.read(cx).tab_tooltip_content(cx)
653 }
654
655 fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
656 self.read(cx).tab_tooltip_text(cx)
657 }
658
659 fn dragged_tab_content(
660 &self,
661 params: TabContentParams,
662 window: &Window,
663 cx: &App,
664 ) -> AnyElement {
665 self.read(cx).tab_content(
666 TabContentParams {
667 selected: true,
668 ..params
669 },
670 window,
671 cx,
672 )
673 }
674
675 fn project_path(&self, cx: &App) -> Option<ProjectPath> {
676 let this = self.read(cx);
677 let mut result = None;
678 if this.is_singleton(cx) {
679 this.for_each_project_item(cx, &mut |_, item| {
680 result = item.project_path(cx);
681 });
682 }
683 result
684 }
685
686 fn workspace_settings<'a>(&self, cx: &'a App) -> &'a WorkspaceSettings {
687 if let Some(project_path) = self.project_path(cx) {
688 WorkspaceSettings::get(
689 Some(SettingsLocation {
690 worktree_id: project_path.worktree_id,
691 path: &project_path.path,
692 }),
693 cx,
694 )
695 } else {
696 WorkspaceSettings::get_global(cx)
697 }
698 }
699
700 fn project_entry_ids(&self, cx: &App) -> SmallVec<[ProjectEntryId; 3]> {
701 let mut result = SmallVec::new();
702 self.read(cx).for_each_project_item(cx, &mut |_, item| {
703 if let Some(id) = item.entry_id(cx) {
704 result.push(id);
705 }
706 });
707 result
708 }
709
710 fn project_paths(&self, cx: &App) -> SmallVec<[ProjectPath; 3]> {
711 let mut result = SmallVec::new();
712 self.read(cx).for_each_project_item(cx, &mut |_, item| {
713 if let Some(id) = item.project_path(cx) {
714 result.push(id);
715 }
716 });
717 result
718 }
719
720 fn project_item_model_ids(&self, cx: &App) -> SmallVec<[EntityId; 3]> {
721 let mut result = SmallVec::new();
722 self.read(cx).for_each_project_item(cx, &mut |id, _| {
723 result.push(id);
724 });
725 result
726 }
727
728 fn for_each_project_item(
729 &self,
730 cx: &App,
731 f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
732 ) {
733 self.read(cx).for_each_project_item(cx, f)
734 }
735
736 fn is_singleton(&self, cx: &App) -> bool {
737 self.read(cx).is_singleton(cx)
738 }
739
740 fn boxed_clone(&self) -> Box<dyn ItemHandle> {
741 Box::new(self.clone())
742 }
743
744 fn clone_on_split(
745 &self,
746 workspace_id: Option<WorkspaceId>,
747 window: &mut Window,
748 cx: &mut App,
749 ) -> Option<Box<dyn ItemHandle>> {
750 self.update(cx, |item, cx| item.clone_on_split(workspace_id, window, cx))
751 .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
752 }
753
754 fn added_to_pane(
755 &self,
756 workspace: &mut Workspace,
757 pane: Entity<Pane>,
758 window: &mut Window,
759 cx: &mut Context<Workspace>,
760 ) {
761 let weak_item = self.downgrade();
762 let history = pane.read(cx).nav_history_for_item(self);
763 self.update(cx, |this, cx| {
764 this.set_nav_history(history, window, cx);
765 this.added_to_workspace(workspace, window, cx);
766 });
767
768 if let Some(serializable_item) = self.to_serializable_item_handle(cx) {
769 workspace
770 .enqueue_item_serialization(serializable_item)
771 .log_err();
772 }
773
774 if workspace
775 .panes_by_item
776 .insert(self.item_id(), pane.downgrade())
777 .is_none()
778 {
779 let mut pending_autosave = DelayedDebouncedEditAction::new();
780 let (pending_update_tx, mut pending_update_rx) = mpsc::unbounded();
781 let pending_update = Rc::new(RefCell::new(None));
782
783 let mut send_follower_updates = None;
784 if let Some(item) = self.to_followable_item_handle(cx) {
785 let is_project_item = item.is_project_item(window, cx);
786 let item = item.downgrade();
787
788 send_follower_updates = Some(cx.spawn_in(window, {
789 let pending_update = pending_update.clone();
790 async move |workspace, cx| {
791 while let Some(mut leader_id) = pending_update_rx.next().await {
792 while let Ok(Some(id)) = pending_update_rx.try_next() {
793 leader_id = id;
794 }
795
796 workspace.update_in(cx, |workspace, window, cx| {
797 let Some(item) = item.upgrade() else { return };
798 workspace.update_followers(
799 is_project_item,
800 proto::update_followers::Variant::UpdateView(
801 proto::UpdateView {
802 id: item
803 .remote_id(workspace.client(), window, cx)
804 .and_then(|id| id.to_proto()),
805 variant: pending_update.borrow_mut().take(),
806 leader_id,
807 },
808 ),
809 window,
810 cx,
811 );
812 })?;
813 cx.background_executor().timer(LEADER_UPDATE_THROTTLE).await;
814 }
815 anyhow::Ok(())
816 }
817 }));
818 }
819
820 let mut event_subscription = Some(cx.subscribe_in(
821 self,
822 window,
823 move |workspace, item: &Entity<T>, event, window, cx| {
824 let pane = if let Some(pane) = workspace
825 .panes_by_item
826 .get(&item.item_id())
827 .and_then(|pane| pane.upgrade())
828 {
829 pane
830 } else {
831 return;
832 };
833
834 if let Some(item) = item.to_followable_item_handle(cx) {
835 let leader_id = workspace.leader_for_pane(&pane);
836
837 if let Some(leader_id) = leader_id
838 && let Some(FollowEvent::Unfollow) = item.to_follow_event(event)
839 {
840 workspace.unfollow(leader_id, window, cx);
841 }
842
843 if item.item_focus_handle(cx).contains_focused(window, cx) {
844 match leader_id {
845 Some(CollaboratorId::Agent) => {}
846 Some(CollaboratorId::PeerId(leader_peer_id)) => {
847 item.add_event_to_update_proto(
848 event,
849 &mut pending_update.borrow_mut(),
850 window,
851 cx,
852 );
853 pending_update_tx.unbounded_send(Some(leader_peer_id)).ok();
854 }
855 None => {
856 item.add_event_to_update_proto(
857 event,
858 &mut pending_update.borrow_mut(),
859 window,
860 cx,
861 );
862 pending_update_tx.unbounded_send(None).ok();
863 }
864 }
865 }
866 }
867
868 if let Some(item) = item.to_serializable_item_handle(cx)
869 && item.should_serialize(event, cx)
870 {
871 workspace.enqueue_item_serialization(item).ok();
872 }
873
874 T::to_item_events(event, |event| match event {
875 ItemEvent::CloseItem => {
876 pane.update(cx, |pane, cx| {
877 pane.close_item_by_id(
878 item.item_id(),
879 crate::SaveIntent::Close,
880 window,
881 cx,
882 )
883 })
884 .detach_and_log_err(cx);
885 }
886
887 ItemEvent::UpdateTab => {
888 workspace.update_item_dirty_state(item, window, cx);
889
890 if item.has_deleted_file(cx)
891 && !item.is_dirty(cx)
892 && item.workspace_settings(cx).close_on_file_delete
893 {
894 let item_id = item.item_id();
895 let close_item_task = pane.update(cx, |pane, cx| {
896 pane.close_item_by_id(
897 item_id,
898 crate::SaveIntent::Close,
899 window,
900 cx,
901 )
902 });
903 cx.spawn_in(window, {
904 let pane = pane.clone();
905 async move |_workspace, cx| {
906 close_item_task.await?;
907 pane.update(cx, |pane, _cx| {
908 pane.nav_history_mut().remove_item(item_id);
909 })
910 }
911 })
912 .detach_and_log_err(cx);
913 } else {
914 pane.update(cx, |_, cx| {
915 cx.emit(pane::Event::ChangeItemTitle);
916 cx.notify();
917 });
918 }
919 }
920
921 ItemEvent::Edit => {
922 let autosave = item.workspace_settings(cx).autosave;
923
924 if let AutosaveSetting::AfterDelay { milliseconds } = autosave {
925 let delay = Duration::from_millis(milliseconds);
926 let item = item.clone();
927 pending_autosave.fire_new(
928 delay,
929 window,
930 cx,
931 move |workspace, window, cx| {
932 Pane::autosave_item(
933 &item,
934 workspace.project().clone(),
935 window,
936 cx,
937 )
938 },
939 );
940 }
941 pane.update(cx, |pane, cx| pane.handle_item_edit(item.item_id(), cx));
942 }
943
944 _ => {}
945 });
946 },
947 ));
948
949 cx.on_blur(
950 &self.read(cx).focus_handle(cx),
951 window,
952 move |workspace, window, cx| {
953 if let Some(item) = weak_item.upgrade()
954 && item.workspace_settings(cx).autosave == AutosaveSetting::OnFocusChange
955 {
956 Pane::autosave_item(&item, workspace.project.clone(), window, cx)
957 .detach_and_log_err(cx);
958 }
959 },
960 )
961 .detach();
962
963 let item_id = self.item_id();
964 workspace.update_item_dirty_state(self, window, cx);
965 cx.observe_release_in(self, window, move |workspace, _, _, _| {
966 workspace.panes_by_item.remove(&item_id);
967 event_subscription.take();
968 send_follower_updates.take();
969 })
970 .detach();
971 }
972
973 cx.defer_in(window, |workspace, window, cx| {
974 workspace.serialize_workspace(window, cx);
975 });
976 }
977
978 fn discarded(&self, project: Entity<Project>, window: &mut Window, cx: &mut App) {
979 self.update(cx, |this, cx| this.discarded(project, window, cx));
980 }
981
982 fn deactivated(&self, window: &mut Window, cx: &mut App) {
983 self.update(cx, |this, cx| this.deactivated(window, cx));
984 }
985
986 fn on_removed(&self, cx: &App) {
987 self.read(cx).on_removed(cx);
988 }
989
990 fn workspace_deactivated(&self, window: &mut Window, cx: &mut App) {
991 self.update(cx, |this, cx| this.workspace_deactivated(window, cx));
992 }
993
994 fn navigate(&self, data: Box<dyn Any>, window: &mut Window, cx: &mut App) -> bool {
995 self.update(cx, |this, cx| this.navigate(data, window, cx))
996 }
997
998 fn item_id(&self) -> EntityId {
999 self.entity_id()
1000 }
1001
1002 fn to_any(&self) -> AnyView {
1003 self.clone().into()
1004 }
1005
1006 fn is_dirty(&self, cx: &App) -> bool {
1007 self.read(cx).is_dirty(cx)
1008 }
1009
1010 fn has_deleted_file(&self, cx: &App) -> bool {
1011 self.read(cx).has_deleted_file(cx)
1012 }
1013
1014 fn has_conflict(&self, cx: &App) -> bool {
1015 self.read(cx).has_conflict(cx)
1016 }
1017
1018 fn can_save(&self, cx: &App) -> bool {
1019 self.read(cx).can_save(cx)
1020 }
1021
1022 fn can_save_as(&self, cx: &App) -> bool {
1023 self.read(cx).can_save_as(cx)
1024 }
1025
1026 fn save(
1027 &self,
1028 options: SaveOptions,
1029 project: Entity<Project>,
1030 window: &mut Window,
1031 cx: &mut App,
1032 ) -> Task<Result<()>> {
1033 self.update(cx, |item, cx| item.save(options, project, window, cx))
1034 }
1035
1036 fn save_as(
1037 &self,
1038 project: Entity<Project>,
1039 path: ProjectPath,
1040 window: &mut Window,
1041 cx: &mut App,
1042 ) -> Task<anyhow::Result<()>> {
1043 self.update(cx, |item, cx| item.save_as(project, path, window, cx))
1044 }
1045
1046 fn reload(
1047 &self,
1048 project: Entity<Project>,
1049 window: &mut Window,
1050 cx: &mut App,
1051 ) -> Task<Result<()>> {
1052 self.update(cx, |item, cx| item.reload(project, window, cx))
1053 }
1054
1055 fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a App) -> Option<AnyView> {
1056 self.read(cx).act_as_type(type_id, self, cx)
1057 }
1058
1059 fn to_followable_item_handle(&self, cx: &App) -> Option<Box<dyn FollowableItemHandle>> {
1060 FollowableViewRegistry::to_followable_view(self.clone(), cx)
1061 }
1062
1063 fn on_release(
1064 &self,
1065 cx: &mut App,
1066 callback: Box<dyn FnOnce(&mut App) + Send>,
1067 ) -> gpui::Subscription {
1068 cx.observe_release(self, move |_, cx| callback(cx))
1069 }
1070
1071 fn to_searchable_item_handle(&self, cx: &App) -> Option<Box<dyn SearchableItemHandle>> {
1072 self.read(cx).as_searchable(self)
1073 }
1074
1075 fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
1076 self.read(cx).breadcrumb_location(cx)
1077 }
1078
1079 fn breadcrumbs(&self, theme: &Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
1080 self.read(cx).breadcrumbs(theme, cx)
1081 }
1082
1083 fn show_toolbar(&self, cx: &App) -> bool {
1084 self.read(cx).show_toolbar()
1085 }
1086
1087 fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>> {
1088 self.read(cx).pixel_position_of_cursor(cx)
1089 }
1090
1091 fn downgrade_item(&self) -> Box<dyn WeakItemHandle> {
1092 Box::new(self.downgrade())
1093 }
1094
1095 fn to_serializable_item_handle(&self, cx: &App) -> Option<Box<dyn SerializableItemHandle>> {
1096 SerializableItemRegistry::view_to_serializable_item_handle(self.to_any(), cx)
1097 }
1098
1099 fn preserve_preview(&self, cx: &App) -> bool {
1100 self.read(cx).preserve_preview(cx)
1101 }
1102
1103 fn include_in_nav_history(&self) -> bool {
1104 T::include_in_nav_history()
1105 }
1106
1107 fn relay_action(&self, action: Box<dyn Action>, window: &mut Window, cx: &mut App) {
1108 self.update(cx, |this, cx| {
1109 this.focus_handle(cx).focus(window);
1110 window.dispatch_action(action, cx);
1111 })
1112 }
1113}
1114
1115impl From<Box<dyn ItemHandle>> for AnyView {
1116 fn from(val: Box<dyn ItemHandle>) -> Self {
1117 val.to_any()
1118 }
1119}
1120
1121impl From<&Box<dyn ItemHandle>> for AnyView {
1122 fn from(val: &Box<dyn ItemHandle>) -> Self {
1123 val.to_any()
1124 }
1125}
1126
1127impl Clone for Box<dyn ItemHandle> {
1128 fn clone(&self) -> Box<dyn ItemHandle> {
1129 self.boxed_clone()
1130 }
1131}
1132
1133impl<T: Item> WeakItemHandle for WeakEntity<T> {
1134 fn id(&self) -> EntityId {
1135 self.entity_id()
1136 }
1137
1138 fn boxed_clone(&self) -> Box<dyn WeakItemHandle> {
1139 Box::new(self.clone())
1140 }
1141
1142 fn upgrade(&self) -> Option<Box<dyn ItemHandle>> {
1143 self.upgrade().map(|v| Box::new(v) as Box<dyn ItemHandle>)
1144 }
1145}
1146
1147#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1148pub struct ProjectItemKind(pub &'static str);
1149
1150pub trait ProjectItem: Item {
1151 type Item: project::ProjectItem;
1152
1153 fn project_item_kind() -> Option<ProjectItemKind> {
1154 None
1155 }
1156
1157 fn for_project_item(
1158 project: Entity<Project>,
1159 pane: Option<&Pane>,
1160 item: Entity<Self::Item>,
1161 window: &mut Window,
1162 cx: &mut Context<Self>,
1163 ) -> Self
1164 where
1165 Self: Sized;
1166
1167 /// A fallback handler, which will be called after [`project::ProjectItem::try_open`] fails,
1168 /// with the error from that failure as an argument.
1169 /// Allows to open an item that can gracefully display and handle errors.
1170 fn for_broken_project_item(
1171 _abs_path: &Path,
1172 _is_local: bool,
1173 _e: &anyhow::Error,
1174 _window: &mut Window,
1175 _cx: &mut App,
1176 ) -> Option<InvalidBufferView>
1177 where
1178 Self: Sized,
1179 {
1180 None
1181 }
1182}
1183
1184#[derive(Debug)]
1185pub enum FollowEvent {
1186 Unfollow,
1187}
1188
1189pub enum Dedup {
1190 KeepExisting,
1191 ReplaceExisting,
1192}
1193
1194pub trait FollowableItem: Item {
1195 fn remote_id(&self) -> Option<ViewId>;
1196 fn to_state_proto(&self, window: &Window, cx: &App) -> Option<proto::view::Variant>;
1197 fn from_state_proto(
1198 project: Entity<Workspace>,
1199 id: ViewId,
1200 state: &mut Option<proto::view::Variant>,
1201 window: &mut Window,
1202 cx: &mut App,
1203 ) -> Option<Task<Result<Entity<Self>>>>;
1204 fn to_follow_event(event: &Self::Event) -> Option<FollowEvent>;
1205 fn add_event_to_update_proto(
1206 &self,
1207 event: &Self::Event,
1208 update: &mut Option<proto::update_view::Variant>,
1209 window: &Window,
1210 cx: &App,
1211 ) -> bool;
1212 fn apply_update_proto(
1213 &mut self,
1214 project: &Entity<Project>,
1215 message: proto::update_view::Variant,
1216 window: &mut Window,
1217 cx: &mut Context<Self>,
1218 ) -> Task<Result<()>>;
1219 fn is_project_item(&self, window: &Window, cx: &App) -> bool;
1220 fn set_leader_id(
1221 &mut self,
1222 leader_peer_id: Option<CollaboratorId>,
1223 window: &mut Window,
1224 cx: &mut Context<Self>,
1225 );
1226 fn dedup(&self, existing: &Self, window: &Window, cx: &App) -> Option<Dedup>;
1227 fn update_agent_location(
1228 &mut self,
1229 _location: language::Anchor,
1230 _window: &mut Window,
1231 _cx: &mut Context<Self>,
1232 ) {
1233 }
1234}
1235
1236pub trait FollowableItemHandle: ItemHandle {
1237 fn remote_id(&self, client: &Arc<Client>, window: &mut Window, cx: &mut App) -> Option<ViewId>;
1238 fn downgrade(&self) -> Box<dyn WeakFollowableItemHandle>;
1239 fn set_leader_id(
1240 &self,
1241 leader_peer_id: Option<CollaboratorId>,
1242 window: &mut Window,
1243 cx: &mut App,
1244 );
1245 fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant>;
1246 fn add_event_to_update_proto(
1247 &self,
1248 event: &dyn Any,
1249 update: &mut Option<proto::update_view::Variant>,
1250 window: &mut Window,
1251 cx: &mut App,
1252 ) -> bool;
1253 fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent>;
1254 fn apply_update_proto(
1255 &self,
1256 project: &Entity<Project>,
1257 message: proto::update_view::Variant,
1258 window: &mut Window,
1259 cx: &mut App,
1260 ) -> Task<Result<()>>;
1261 fn is_project_item(&self, window: &mut Window, cx: &mut App) -> bool;
1262 fn dedup(
1263 &self,
1264 existing: &dyn FollowableItemHandle,
1265 window: &mut Window,
1266 cx: &mut App,
1267 ) -> Option<Dedup>;
1268 fn update_agent_location(&self, location: language::Anchor, window: &mut Window, cx: &mut App);
1269}
1270
1271impl<T: FollowableItem> FollowableItemHandle for Entity<T> {
1272 fn remote_id(&self, client: &Arc<Client>, _: &mut Window, cx: &mut App) -> Option<ViewId> {
1273 self.read(cx).remote_id().or_else(|| {
1274 client.peer_id().map(|creator| ViewId {
1275 creator: CollaboratorId::PeerId(creator),
1276 id: self.item_id().as_u64(),
1277 })
1278 })
1279 }
1280
1281 fn downgrade(&self) -> Box<dyn WeakFollowableItemHandle> {
1282 Box::new(self.downgrade())
1283 }
1284
1285 fn set_leader_id(&self, leader_id: Option<CollaboratorId>, window: &mut Window, cx: &mut App) {
1286 self.update(cx, |this, cx| this.set_leader_id(leader_id, window, cx))
1287 }
1288
1289 fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant> {
1290 self.read(cx).to_state_proto(window, cx)
1291 }
1292
1293 fn add_event_to_update_proto(
1294 &self,
1295 event: &dyn Any,
1296 update: &mut Option<proto::update_view::Variant>,
1297 window: &mut Window,
1298 cx: &mut App,
1299 ) -> bool {
1300 if let Some(event) = event.downcast_ref() {
1301 self.read(cx)
1302 .add_event_to_update_proto(event, update, window, cx)
1303 } else {
1304 false
1305 }
1306 }
1307
1308 fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent> {
1309 T::to_follow_event(event.downcast_ref()?)
1310 }
1311
1312 fn apply_update_proto(
1313 &self,
1314 project: &Entity<Project>,
1315 message: proto::update_view::Variant,
1316 window: &mut Window,
1317 cx: &mut App,
1318 ) -> Task<Result<()>> {
1319 self.update(cx, |this, cx| {
1320 this.apply_update_proto(project, message, window, cx)
1321 })
1322 }
1323
1324 fn is_project_item(&self, window: &mut Window, cx: &mut App) -> bool {
1325 self.read(cx).is_project_item(window, cx)
1326 }
1327
1328 fn dedup(
1329 &self,
1330 existing: &dyn FollowableItemHandle,
1331 window: &mut Window,
1332 cx: &mut App,
1333 ) -> Option<Dedup> {
1334 let existing = existing.to_any().downcast::<T>().ok()?;
1335 self.read(cx).dedup(existing.read(cx), window, cx)
1336 }
1337
1338 fn update_agent_location(&self, location: language::Anchor, window: &mut Window, cx: &mut App) {
1339 self.update(cx, |this, cx| {
1340 this.update_agent_location(location, window, cx)
1341 })
1342 }
1343}
1344
1345pub trait WeakFollowableItemHandle: Send + Sync {
1346 fn upgrade(&self) -> Option<Box<dyn FollowableItemHandle>>;
1347}
1348
1349impl<T: FollowableItem> WeakFollowableItemHandle for WeakEntity<T> {
1350 fn upgrade(&self) -> Option<Box<dyn FollowableItemHandle>> {
1351 Some(Box::new(self.upgrade()?))
1352 }
1353}
1354
1355#[cfg(any(test, feature = "test-support"))]
1356pub mod test {
1357 use super::{Item, ItemEvent, SerializableItem, TabContentParams};
1358 use crate::{ItemId, ItemNavHistory, Workspace, WorkspaceId, item::SaveOptions};
1359 use gpui::{
1360 AnyElement, App, AppContext as _, Context, Entity, EntityId, EventEmitter, Focusable,
1361 InteractiveElement, IntoElement, Render, SharedString, Task, WeakEntity, Window,
1362 };
1363 use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
1364 use std::{any::Any, cell::Cell, path::Path};
1365
1366 pub struct TestProjectItem {
1367 pub entry_id: Option<ProjectEntryId>,
1368 pub project_path: Option<ProjectPath>,
1369 pub is_dirty: bool,
1370 }
1371
1372 pub struct TestItem {
1373 pub workspace_id: Option<WorkspaceId>,
1374 pub state: String,
1375 pub label: String,
1376 pub save_count: usize,
1377 pub save_as_count: usize,
1378 pub reload_count: usize,
1379 pub is_dirty: bool,
1380 pub is_singleton: bool,
1381 pub has_conflict: bool,
1382 pub has_deleted_file: bool,
1383 pub project_items: Vec<Entity<TestProjectItem>>,
1384 pub nav_history: Option<ItemNavHistory>,
1385 pub tab_descriptions: Option<Vec<&'static str>>,
1386 pub tab_detail: Cell<Option<usize>>,
1387 serialize: Option<Box<dyn Fn() -> Option<Task<anyhow::Result<()>>>>>,
1388 focus_handle: gpui::FocusHandle,
1389 }
1390
1391 impl project::ProjectItem for TestProjectItem {
1392 fn try_open(
1393 _project: &Entity<Project>,
1394 _path: &ProjectPath,
1395 _cx: &mut App,
1396 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
1397 None
1398 }
1399 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
1400 self.entry_id
1401 }
1402
1403 fn project_path(&self, _: &App) -> Option<ProjectPath> {
1404 self.project_path.clone()
1405 }
1406
1407 fn is_dirty(&self) -> bool {
1408 self.is_dirty
1409 }
1410 }
1411
1412 pub enum TestItemEvent {
1413 Edit,
1414 }
1415
1416 impl TestProjectItem {
1417 pub fn new(id: u64, path: &str, cx: &mut App) -> Entity<Self> {
1418 let entry_id = Some(ProjectEntryId::from_proto(id));
1419 let project_path = Some(ProjectPath {
1420 worktree_id: WorktreeId::from_usize(0),
1421 path: Path::new(path).into(),
1422 });
1423 cx.new(|_| Self {
1424 entry_id,
1425 project_path,
1426 is_dirty: false,
1427 })
1428 }
1429
1430 pub fn new_untitled(cx: &mut App) -> Entity<Self> {
1431 cx.new(|_| Self {
1432 project_path: None,
1433 entry_id: None,
1434 is_dirty: false,
1435 })
1436 }
1437
1438 pub fn new_dirty(id: u64, path: &str, cx: &mut App) -> Entity<Self> {
1439 let entry_id = Some(ProjectEntryId::from_proto(id));
1440 let project_path = Some(ProjectPath {
1441 worktree_id: WorktreeId::from_usize(0),
1442 path: Path::new(path).into(),
1443 });
1444 cx.new(|_| Self {
1445 entry_id,
1446 project_path,
1447 is_dirty: true,
1448 })
1449 }
1450 }
1451
1452 impl TestItem {
1453 pub fn new(cx: &mut Context<Self>) -> Self {
1454 Self {
1455 state: String::new(),
1456 label: String::new(),
1457 save_count: 0,
1458 save_as_count: 0,
1459 reload_count: 0,
1460 is_dirty: false,
1461 has_conflict: false,
1462 has_deleted_file: false,
1463 project_items: Vec::new(),
1464 is_singleton: true,
1465 nav_history: None,
1466 tab_descriptions: None,
1467 tab_detail: Default::default(),
1468 workspace_id: Default::default(),
1469 focus_handle: cx.focus_handle(),
1470 serialize: None,
1471 }
1472 }
1473
1474 pub fn new_deserialized(id: WorkspaceId, cx: &mut Context<Self>) -> Self {
1475 let mut this = Self::new(cx);
1476 this.workspace_id = Some(id);
1477 this
1478 }
1479
1480 pub fn with_label(mut self, state: &str) -> Self {
1481 self.label = state.to_string();
1482 self
1483 }
1484
1485 pub fn with_singleton(mut self, singleton: bool) -> Self {
1486 self.is_singleton = singleton;
1487 self
1488 }
1489
1490 pub fn set_has_deleted_file(&mut self, deleted: bool) {
1491 self.has_deleted_file = deleted;
1492 }
1493
1494 pub fn with_dirty(mut self, dirty: bool) -> Self {
1495 self.is_dirty = dirty;
1496 self
1497 }
1498
1499 pub fn with_conflict(mut self, has_conflict: bool) -> Self {
1500 self.has_conflict = has_conflict;
1501 self
1502 }
1503
1504 pub fn with_project_items(mut self, items: &[Entity<TestProjectItem>]) -> Self {
1505 self.project_items.clear();
1506 self.project_items.extend(items.iter().cloned());
1507 self
1508 }
1509
1510 pub fn with_serialize(
1511 mut self,
1512 serialize: impl Fn() -> Option<Task<anyhow::Result<()>>> + 'static,
1513 ) -> Self {
1514 self.serialize = Some(Box::new(serialize));
1515 self
1516 }
1517
1518 pub fn set_state(&mut self, state: String, cx: &mut Context<Self>) {
1519 self.push_to_nav_history(cx);
1520 self.state = state;
1521 }
1522
1523 fn push_to_nav_history(&mut self, cx: &mut Context<Self>) {
1524 if let Some(history) = &mut self.nav_history {
1525 history.push(Some(Box::new(self.state.clone())), cx);
1526 }
1527 }
1528 }
1529
1530 impl Render for TestItem {
1531 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1532 gpui::div().track_focus(&self.focus_handle(cx))
1533 }
1534 }
1535
1536 impl EventEmitter<ItemEvent> for TestItem {}
1537
1538 impl Focusable for TestItem {
1539 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
1540 self.focus_handle.clone()
1541 }
1542 }
1543
1544 impl Item for TestItem {
1545 type Event = ItemEvent;
1546
1547 fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
1548 f(*event)
1549 }
1550
1551 fn tab_content_text(&self, detail: usize, _cx: &App) -> SharedString {
1552 self.tab_descriptions
1553 .as_ref()
1554 .and_then(|descriptions| {
1555 let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
1556 description.into()
1557 })
1558 .unwrap_or_default()
1559 .into()
1560 }
1561
1562 fn telemetry_event_text(&self) -> Option<&'static str> {
1563 None
1564 }
1565
1566 fn tab_content(&self, params: TabContentParams, _window: &Window, _cx: &App) -> AnyElement {
1567 self.tab_detail.set(params.detail);
1568 gpui::div().into_any_element()
1569 }
1570
1571 fn for_each_project_item(
1572 &self,
1573 cx: &App,
1574 f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
1575 ) {
1576 self.project_items
1577 .iter()
1578 .for_each(|item| f(item.entity_id(), item.read(cx)))
1579 }
1580
1581 fn is_singleton(&self, _: &App) -> bool {
1582 self.is_singleton
1583 }
1584
1585 fn set_nav_history(
1586 &mut self,
1587 history: ItemNavHistory,
1588 _window: &mut Window,
1589 _: &mut Context<Self>,
1590 ) {
1591 self.nav_history = Some(history);
1592 }
1593
1594 fn navigate(
1595 &mut self,
1596 state: Box<dyn Any>,
1597 _window: &mut Window,
1598 _: &mut Context<Self>,
1599 ) -> bool {
1600 let state = *state.downcast::<String>().unwrap_or_default();
1601 if state != self.state {
1602 self.state = state;
1603 true
1604 } else {
1605 false
1606 }
1607 }
1608
1609 fn deactivated(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1610 self.push_to_nav_history(cx);
1611 }
1612
1613 fn clone_on_split(
1614 &self,
1615 _workspace_id: Option<WorkspaceId>,
1616 _: &mut Window,
1617 cx: &mut Context<Self>,
1618 ) -> Option<Entity<Self>>
1619 where
1620 Self: Sized,
1621 {
1622 Some(cx.new(|cx| Self {
1623 state: self.state.clone(),
1624 label: self.label.clone(),
1625 save_count: self.save_count,
1626 save_as_count: self.save_as_count,
1627 reload_count: self.reload_count,
1628 is_dirty: self.is_dirty,
1629 is_singleton: self.is_singleton,
1630 has_conflict: self.has_conflict,
1631 has_deleted_file: self.has_deleted_file,
1632 project_items: self.project_items.clone(),
1633 nav_history: None,
1634 tab_descriptions: None,
1635 tab_detail: Default::default(),
1636 workspace_id: self.workspace_id,
1637 focus_handle: cx.focus_handle(),
1638 serialize: None,
1639 }))
1640 }
1641
1642 fn is_dirty(&self, _: &App) -> bool {
1643 self.is_dirty
1644 }
1645
1646 fn has_conflict(&self, _: &App) -> bool {
1647 self.has_conflict
1648 }
1649
1650 fn has_deleted_file(&self, _: &App) -> bool {
1651 self.has_deleted_file
1652 }
1653
1654 fn can_save(&self, cx: &App) -> bool {
1655 !self.project_items.is_empty()
1656 && self
1657 .project_items
1658 .iter()
1659 .all(|item| item.read(cx).entry_id.is_some())
1660 }
1661
1662 fn can_save_as(&self, _cx: &App) -> bool {
1663 self.is_singleton
1664 }
1665
1666 fn save(
1667 &mut self,
1668 _: SaveOptions,
1669 _: Entity<Project>,
1670 _window: &mut Window,
1671 cx: &mut Context<Self>,
1672 ) -> Task<anyhow::Result<()>> {
1673 self.save_count += 1;
1674 self.is_dirty = false;
1675 for item in &self.project_items {
1676 item.update(cx, |item, _| {
1677 if item.is_dirty {
1678 item.is_dirty = false;
1679 }
1680 })
1681 }
1682 Task::ready(Ok(()))
1683 }
1684
1685 fn save_as(
1686 &mut self,
1687 _: Entity<Project>,
1688 _: ProjectPath,
1689 _window: &mut Window,
1690 _: &mut Context<Self>,
1691 ) -> Task<anyhow::Result<()>> {
1692 self.save_as_count += 1;
1693 self.is_dirty = false;
1694 Task::ready(Ok(()))
1695 }
1696
1697 fn reload(
1698 &mut self,
1699 _: Entity<Project>,
1700 _window: &mut Window,
1701 _: &mut Context<Self>,
1702 ) -> Task<anyhow::Result<()>> {
1703 self.reload_count += 1;
1704 self.is_dirty = false;
1705 Task::ready(Ok(()))
1706 }
1707 }
1708
1709 impl SerializableItem for TestItem {
1710 fn serialized_item_kind() -> &'static str {
1711 "TestItem"
1712 }
1713
1714 fn deserialize(
1715 _project: Entity<Project>,
1716 _workspace: WeakEntity<Workspace>,
1717 workspace_id: WorkspaceId,
1718 _item_id: ItemId,
1719 _window: &mut Window,
1720 cx: &mut App,
1721 ) -> Task<anyhow::Result<Entity<Self>>> {
1722 let entity = cx.new(|cx| Self::new_deserialized(workspace_id, cx));
1723 Task::ready(Ok(entity))
1724 }
1725
1726 fn cleanup(
1727 _workspace_id: WorkspaceId,
1728 _alive_items: Vec<ItemId>,
1729 _window: &mut Window,
1730 _cx: &mut App,
1731 ) -> Task<anyhow::Result<()>> {
1732 Task::ready(Ok(()))
1733 }
1734
1735 fn serialize(
1736 &mut self,
1737 _workspace: &mut Workspace,
1738 _item_id: ItemId,
1739 _closing: bool,
1740 _window: &mut Window,
1741 _cx: &mut Context<Self>,
1742 ) -> Option<Task<anyhow::Result<()>>> {
1743 if let Some(serialize) = self.serialize.take() {
1744 let result = serialize();
1745 self.serialize = Some(serialize);
1746 result
1747 } else {
1748 None
1749 }
1750 }
1751
1752 fn should_serialize(&self, _event: &Self::Event) -> bool {
1753 false
1754 }
1755 }
1756}