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