1/// NOTE: Focus only 'takes' after an update has flushed_effects.
2///
3/// This may cause issues when you're trying to write tests that use workspace focus to add items at
4/// specific locations.
5pub mod dock;
6pub mod pane;
7pub mod pane_group;
8pub mod searchable;
9pub mod shared_screen;
10pub mod sidebar;
11mod status_bar;
12mod toolbar;
13
14use anyhow::{anyhow, Context, Result};
15use call::ActiveCall;
16use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
17use collections::{hash_map, HashMap, HashSet};
18use dock::{DefaultItemFactory, Dock, ToggleDockButton};
19use drag_and_drop::DragAndDrop;
20use fs::{self, Fs};
21use futures::{channel::oneshot, FutureExt, StreamExt};
22use gpui::{
23 actions,
24 elements::*,
25 impl_actions, impl_internal_actions,
26 platform::{CursorStyle, WindowOptions},
27 AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
28 MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
29 ViewContext, ViewHandle, WeakViewHandle,
30};
31use language::LanguageRegistry;
32use log::{error, warn};
33pub use pane::*;
34pub use pane_group::*;
35use postage::prelude::Stream;
36use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
37use searchable::SearchableItemHandle;
38use serde::Deserialize;
39use settings::{Autosave, DockAnchor, Settings};
40use shared_screen::SharedScreen;
41use sidebar::{Sidebar, SidebarButtons, SidebarSide, ToggleSidebarItem};
42use smallvec::SmallVec;
43use status_bar::StatusBar;
44pub use status_bar::StatusItemView;
45use std::{
46 any::{Any, TypeId},
47 borrow::Cow,
48 cell::RefCell,
49 fmt,
50 future::Future,
51 path::{Path, PathBuf},
52 rc::Rc,
53 sync::{
54 atomic::{AtomicBool, Ordering::SeqCst},
55 Arc,
56 },
57 time::Duration,
58};
59use theme::{Theme, ThemeRegistry};
60pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
61use util::ResultExt;
62
63type ProjectItemBuilders = HashMap<
64 TypeId,
65 fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
66>;
67
68type FollowableItemBuilder = fn(
69 ViewHandle<Pane>,
70 ModelHandle<Project>,
71 &mut Option<proto::view::Variant>,
72 &mut MutableAppContext,
73) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
74type FollowableItemBuilders = HashMap<
75 TypeId,
76 (
77 FollowableItemBuilder,
78 fn(AnyViewHandle) -> Box<dyn FollowableItemHandle>,
79 ),
80>;
81
82#[derive(Clone, PartialEq)]
83pub struct RemoveWorktreeFromProject(pub WorktreeId);
84
85actions!(
86 workspace,
87 [
88 Open,
89 NewFile,
90 NewWindow,
91 CloseWindow,
92 AddFolderToProject,
93 Unfollow,
94 Save,
95 SaveAs,
96 SaveAll,
97 ActivatePreviousPane,
98 ActivateNextPane,
99 FollowNextCollaborator,
100 ToggleLeftSidebar,
101 ToggleRightSidebar,
102 NewTerminal,
103 NewSearch,
104 ]
105);
106
107#[derive(Clone, PartialEq)]
108pub struct OpenPaths {
109 pub paths: Vec<PathBuf>,
110}
111
112#[derive(Clone, Deserialize, PartialEq)]
113pub struct ActivatePane(pub usize);
114
115#[derive(Clone, PartialEq)]
116pub struct ToggleFollow(pub PeerId);
117
118#[derive(Clone, PartialEq)]
119pub struct JoinProject {
120 pub project_id: u64,
121 pub follow_user_id: u64,
122}
123
124#[derive(Clone, PartialEq)]
125pub struct OpenSharedScreen {
126 pub peer_id: PeerId,
127}
128
129#[derive(Clone, PartialEq)]
130pub struct SplitWithItem {
131 pane_to_split: WeakViewHandle<Pane>,
132 split_direction: SplitDirection,
133 from: WeakViewHandle<Pane>,
134 item_id_to_move: usize,
135}
136
137#[derive(Clone, PartialEq)]
138pub struct SplitWithProjectEntry {
139 pane_to_split: WeakViewHandle<Pane>,
140 split_direction: SplitDirection,
141 project_entry: ProjectEntryId,
142}
143
144#[derive(Clone, PartialEq)]
145pub struct OpenProjectEntryInPane {
146 pane: WeakViewHandle<Pane>,
147 project_entry: ProjectEntryId,
148}
149
150impl_internal_actions!(
151 workspace,
152 [
153 OpenPaths,
154 ToggleFollow,
155 JoinProject,
156 OpenSharedScreen,
157 RemoveWorktreeFromProject,
158 SplitWithItem,
159 SplitWithProjectEntry,
160 OpenProjectEntryInPane,
161 ]
162);
163impl_actions!(workspace, [ActivatePane]);
164
165pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
166 pane::init(cx);
167 dock::init(cx);
168
169 cx.add_global_action(open);
170 cx.add_global_action({
171 let app_state = Arc::downgrade(&app_state);
172 move |action: &OpenPaths, cx: &mut MutableAppContext| {
173 if let Some(app_state) = app_state.upgrade() {
174 open_paths(&action.paths, &app_state, cx).detach();
175 }
176 }
177 });
178 cx.add_global_action({
179 let app_state = Arc::downgrade(&app_state);
180 move |_: &NewFile, cx: &mut MutableAppContext| {
181 if let Some(app_state) = app_state.upgrade() {
182 open_new(&app_state, cx)
183 }
184 }
185 });
186 cx.add_global_action({
187 let app_state = Arc::downgrade(&app_state);
188 move |_: &NewWindow, cx: &mut MutableAppContext| {
189 if let Some(app_state) = app_state.upgrade() {
190 open_new(&app_state, cx)
191 }
192 }
193 });
194
195 cx.add_async_action(Workspace::toggle_follow);
196 cx.add_async_action(Workspace::follow_next_collaborator);
197 cx.add_async_action(Workspace::close);
198 cx.add_async_action(Workspace::save_all);
199 cx.add_action(Workspace::open_shared_screen);
200 cx.add_action(Workspace::add_folder_to_project);
201 cx.add_action(Workspace::remove_folder_from_project);
202 cx.add_action(
203 |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
204 let pane = workspace.active_pane().clone();
205 workspace.unfollow(&pane, cx);
206 },
207 );
208 cx.add_action(
209 |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
210 workspace.save_active_item(false, cx).detach_and_log_err(cx);
211 },
212 );
213 cx.add_action(
214 |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
215 workspace.save_active_item(true, cx).detach_and_log_err(cx);
216 },
217 );
218 cx.add_action(Workspace::toggle_sidebar_item);
219 cx.add_action(Workspace::focus_center);
220 cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
221 workspace.activate_previous_pane(cx)
222 });
223 cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
224 workspace.activate_next_pane(cx)
225 });
226 cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| {
227 workspace.toggle_sidebar(SidebarSide::Left, cx);
228 });
229 cx.add_action(|workspace: &mut Workspace, _: &ToggleRightSidebar, cx| {
230 workspace.toggle_sidebar(SidebarSide::Right, cx);
231 });
232 cx.add_action(Workspace::activate_pane_at_index);
233 cx.add_action(
234 |workspace: &mut Workspace,
235 SplitWithItem {
236 from,
237 pane_to_split,
238 item_id_to_move,
239 split_direction,
240 }: &_,
241 cx| {
242 workspace.split_pane_with_item(
243 from.clone(),
244 pane_to_split.clone(),
245 *item_id_to_move,
246 *split_direction,
247 cx,
248 )
249 },
250 );
251
252 cx.add_async_action(
253 |workspace: &mut Workspace,
254 SplitWithProjectEntry {
255 pane_to_split,
256 split_direction,
257 project_entry,
258 }: &_,
259 cx| {
260 pane_to_split.upgrade(cx).and_then(|pane_to_split| {
261 let new_pane = workspace.add_pane(cx);
262 workspace
263 .center
264 .split(&pane_to_split, &new_pane, *split_direction)
265 .unwrap();
266
267 workspace
268 .project
269 .read(cx)
270 .path_for_entry(*project_entry, cx)
271 .map(|path| {
272 let task = workspace.open_path(path, Some(new_pane.downgrade()), true, cx);
273 cx.foreground().spawn(async move {
274 task.await?;
275 Ok(())
276 })
277 })
278 })
279 },
280 );
281
282 cx.add_async_action(
283 |workspace: &mut Workspace,
284 OpenProjectEntryInPane {
285 pane,
286 project_entry,
287 }: &_,
288 cx| {
289 workspace
290 .project
291 .read(cx)
292 .path_for_entry(*project_entry, cx)
293 .map(|path| {
294 let task = workspace.open_path(path, Some(pane.clone()), true, cx);
295 cx.foreground().spawn(async move {
296 task.await?;
297 Ok(())
298 })
299 })
300 },
301 );
302
303 let client = &app_state.client;
304 client.add_view_request_handler(Workspace::handle_follow);
305 client.add_view_message_handler(Workspace::handle_unfollow);
306 client.add_view_message_handler(Workspace::handle_update_followers);
307}
308
309pub fn register_project_item<I: ProjectItem>(cx: &mut MutableAppContext) {
310 cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
311 builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
312 let item = model.downcast::<I::Item>().unwrap();
313 Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
314 });
315 });
316}
317
318pub fn register_followable_item<I: FollowableItem>(cx: &mut MutableAppContext) {
319 cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
320 builders.insert(
321 TypeId::of::<I>(),
322 (
323 |pane, project, state, cx| {
324 I::from_state_proto(pane, project, state, cx).map(|task| {
325 cx.foreground()
326 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
327 })
328 },
329 |this| Box::new(this.downcast::<I>().unwrap()),
330 ),
331 );
332 });
333}
334
335pub struct AppState {
336 pub languages: Arc<LanguageRegistry>,
337 pub themes: Arc<ThemeRegistry>,
338 pub client: Arc<client::Client>,
339 pub user_store: ModelHandle<client::UserStore>,
340 pub fs: Arc<dyn fs::Fs>,
341 pub build_window_options: fn() -> WindowOptions<'static>,
342 pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
343 pub default_item_factory: DefaultItemFactory,
344}
345
346#[derive(Eq, PartialEq, Hash)]
347pub enum ItemEvent {
348 CloseItem,
349 UpdateTab,
350 UpdateBreadcrumbs,
351 Edit,
352}
353
354pub trait Item: View {
355 fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
356 fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
357 fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
358 false
359 }
360 fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
361 None
362 }
363 fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
364 -> ElementBox;
365 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
366 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
367 fn is_singleton(&self, cx: &AppContext) -> bool;
368 fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>);
369 fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
370 where
371 Self: Sized,
372 {
373 None
374 }
375 fn is_dirty(&self, _: &AppContext) -> bool {
376 false
377 }
378 fn has_conflict(&self, _: &AppContext) -> bool {
379 false
380 }
381 fn can_save(&self, cx: &AppContext) -> bool;
382 fn save(
383 &mut self,
384 project: ModelHandle<Project>,
385 cx: &mut ViewContext<Self>,
386 ) -> Task<Result<()>>;
387 fn save_as(
388 &mut self,
389 project: ModelHandle<Project>,
390 abs_path: PathBuf,
391 cx: &mut ViewContext<Self>,
392 ) -> Task<Result<()>>;
393 fn reload(
394 &mut self,
395 project: ModelHandle<Project>,
396 cx: &mut ViewContext<Self>,
397 ) -> Task<Result<()>>;
398 fn git_diff_recalc(
399 &mut self,
400 _project: ModelHandle<Project>,
401 _cx: &mut ViewContext<Self>,
402 ) -> Task<Result<()>> {
403 Task::ready(Ok(()))
404 }
405 fn to_item_events(event: &Self::Event) -> Vec<ItemEvent>;
406 fn should_close_item_on_event(_: &Self::Event) -> bool {
407 false
408 }
409 fn should_update_tab_on_event(_: &Self::Event) -> bool {
410 false
411 }
412 fn is_edit_event(_: &Self::Event) -> bool {
413 false
414 }
415 fn act_as_type(
416 &self,
417 type_id: TypeId,
418 self_handle: &ViewHandle<Self>,
419 _: &AppContext,
420 ) -> Option<AnyViewHandle> {
421 if TypeId::of::<Self>() == type_id {
422 Some(self_handle.into())
423 } else {
424 None
425 }
426 }
427 fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
428 None
429 }
430
431 fn breadcrumb_location(&self) -> ToolbarItemLocation {
432 ToolbarItemLocation::Hidden
433 }
434 fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option<Vec<ElementBox>> {
435 None
436 }
437}
438
439pub trait ProjectItem: Item {
440 type Item: project::Item;
441
442 fn for_project_item(
443 project: ModelHandle<Project>,
444 item: ModelHandle<Self::Item>,
445 cx: &mut ViewContext<Self>,
446 ) -> Self;
447}
448
449pub trait FollowableItem: Item {
450 fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
451 fn from_state_proto(
452 pane: ViewHandle<Pane>,
453 project: ModelHandle<Project>,
454 state: &mut Option<proto::view::Variant>,
455 cx: &mut MutableAppContext,
456 ) -> Option<Task<Result<ViewHandle<Self>>>>;
457 fn add_event_to_update_proto(
458 &self,
459 event: &Self::Event,
460 update: &mut Option<proto::update_view::Variant>,
461 cx: &AppContext,
462 ) -> bool;
463 fn apply_update_proto(
464 &mut self,
465 message: proto::update_view::Variant,
466 cx: &mut ViewContext<Self>,
467 ) -> Result<()>;
468
469 fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
470 fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
471}
472
473pub trait FollowableItemHandle: ItemHandle {
474 fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext);
475 fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
476 fn add_event_to_update_proto(
477 &self,
478 event: &dyn Any,
479 update: &mut Option<proto::update_view::Variant>,
480 cx: &AppContext,
481 ) -> bool;
482 fn apply_update_proto(
483 &self,
484 message: proto::update_view::Variant,
485 cx: &mut MutableAppContext,
486 ) -> Result<()>;
487 fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
488}
489
490impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
491 fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext) {
492 self.update(cx, |this, cx| {
493 this.set_leader_replica_id(leader_replica_id, cx)
494 })
495 }
496
497 fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
498 self.read(cx).to_state_proto(cx)
499 }
500
501 fn add_event_to_update_proto(
502 &self,
503 event: &dyn Any,
504 update: &mut Option<proto::update_view::Variant>,
505 cx: &AppContext,
506 ) -> bool {
507 if let Some(event) = event.downcast_ref() {
508 self.read(cx).add_event_to_update_proto(event, update, cx)
509 } else {
510 false
511 }
512 }
513
514 fn apply_update_proto(
515 &self,
516 message: proto::update_view::Variant,
517 cx: &mut MutableAppContext,
518 ) -> Result<()> {
519 self.update(cx, |this, cx| this.apply_update_proto(message, cx))
520 }
521
522 fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool {
523 if let Some(event) = event.downcast_ref() {
524 T::should_unfollow_on_event(event, cx)
525 } else {
526 false
527 }
528 }
529}
530
531struct DelayedDebouncedEditAction {
532 task: Option<Task<()>>,
533 cancel_channel: Option<oneshot::Sender<()>>,
534}
535
536impl DelayedDebouncedEditAction {
537 fn new() -> DelayedDebouncedEditAction {
538 DelayedDebouncedEditAction {
539 task: None,
540 cancel_channel: None,
541 }
542 }
543
544 fn fire_new<F, Fut>(
545 &mut self,
546 delay: Duration,
547 workspace: &Workspace,
548 cx: &mut ViewContext<Workspace>,
549 f: F,
550 ) where
551 F: FnOnce(ModelHandle<Project>, AsyncAppContext) -> Fut + 'static,
552 Fut: 'static + Future<Output = ()>,
553 {
554 if let Some(channel) = self.cancel_channel.take() {
555 _ = channel.send(());
556 }
557
558 let project = workspace.project().downgrade();
559
560 let (sender, mut receiver) = oneshot::channel::<()>();
561 self.cancel_channel = Some(sender);
562
563 let previous_task = self.task.take();
564 self.task = Some(cx.spawn_weak(|_, cx| async move {
565 let mut timer = cx.background().timer(delay).fuse();
566 if let Some(previous_task) = previous_task {
567 previous_task.await;
568 }
569
570 futures::select_biased! {
571 _ = receiver => return,
572 _ = timer => {}
573 }
574
575 if let Some(project) = project.upgrade(&cx) {
576 (f)(project, cx).await;
577 }
578 }));
579 }
580}
581
582pub trait ItemHandle: 'static + fmt::Debug {
583 fn subscribe_to_item_events(
584 &self,
585 cx: &mut MutableAppContext,
586 handler: Box<dyn Fn(ItemEvent, &mut MutableAppContext)>,
587 ) -> gpui::Subscription;
588 fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
589 fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
590 -> ElementBox;
591 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
592 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
593 fn is_singleton(&self, cx: &AppContext) -> bool;
594 fn boxed_clone(&self) -> Box<dyn ItemHandle>;
595 fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>>;
596 fn added_to_pane(
597 &self,
598 workspace: &mut Workspace,
599 pane: ViewHandle<Pane>,
600 cx: &mut ViewContext<Workspace>,
601 );
602 fn deactivated(&self, cx: &mut MutableAppContext);
603 fn workspace_deactivated(&self, cx: &mut MutableAppContext);
604 fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool;
605 fn id(&self) -> usize;
606 fn window_id(&self) -> usize;
607 fn to_any(&self) -> AnyViewHandle;
608 fn is_dirty(&self, cx: &AppContext) -> bool;
609 fn has_conflict(&self, cx: &AppContext) -> bool;
610 fn can_save(&self, cx: &AppContext) -> bool;
611 fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>>;
612 fn save_as(
613 &self,
614 project: ModelHandle<Project>,
615 abs_path: PathBuf,
616 cx: &mut MutableAppContext,
617 ) -> Task<Result<()>>;
618 fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext)
619 -> Task<Result<()>>;
620 fn git_diff_recalc(
621 &self,
622 project: ModelHandle<Project>,
623 cx: &mut MutableAppContext,
624 ) -> Task<Result<()>>;
625 fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
626 fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
627 fn on_release(
628 &self,
629 cx: &mut MutableAppContext,
630 callback: Box<dyn FnOnce(&mut MutableAppContext)>,
631 ) -> gpui::Subscription;
632 fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
633 fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
634 fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<ElementBox>>;
635}
636
637pub trait WeakItemHandle {
638 fn id(&self) -> usize;
639 fn window_id(&self) -> usize;
640 fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
641}
642
643impl dyn ItemHandle {
644 pub fn downcast<T: View>(&self) -> Option<ViewHandle<T>> {
645 self.to_any().downcast()
646 }
647
648 pub fn act_as<T: View>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
649 self.act_as_type(TypeId::of::<T>(), cx)
650 .and_then(|t| t.downcast())
651 }
652}
653
654impl<T: Item> ItemHandle for ViewHandle<T> {
655 fn subscribe_to_item_events(
656 &self,
657 cx: &mut MutableAppContext,
658 handler: Box<dyn Fn(ItemEvent, &mut MutableAppContext)>,
659 ) -> gpui::Subscription {
660 cx.subscribe(self, move |_, event, cx| {
661 for item_event in T::to_item_events(event) {
662 handler(item_event, cx)
663 }
664 })
665 }
666
667 fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
668 self.read(cx).tab_description(detail, cx)
669 }
670
671 fn tab_content(
672 &self,
673 detail: Option<usize>,
674 style: &theme::Tab,
675 cx: &AppContext,
676 ) -> ElementBox {
677 self.read(cx).tab_content(detail, style, cx)
678 }
679
680 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
681 self.read(cx).project_path(cx)
682 }
683
684 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
685 self.read(cx).project_entry_ids(cx)
686 }
687
688 fn is_singleton(&self, cx: &AppContext) -> bool {
689 self.read(cx).is_singleton(cx)
690 }
691
692 fn boxed_clone(&self) -> Box<dyn ItemHandle> {
693 Box::new(self.clone())
694 }
695
696 fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
697 self.update(cx, |item, cx| {
698 cx.add_option_view(|cx| item.clone_on_split(cx))
699 })
700 .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
701 }
702
703 fn added_to_pane(
704 &self,
705 workspace: &mut Workspace,
706 pane: ViewHandle<Pane>,
707 cx: &mut ViewContext<Workspace>,
708 ) {
709 let history = pane.read(cx).nav_history_for_item(self);
710 self.update(cx, |this, cx| this.set_nav_history(history, cx));
711
712 if let Some(followed_item) = self.to_followable_item_handle(cx) {
713 if let Some(message) = followed_item.to_state_proto(cx) {
714 workspace.update_followers(
715 proto::update_followers::Variant::CreateView(proto::View {
716 id: followed_item.id() as u64,
717 variant: Some(message),
718 leader_id: workspace.leader_for_pane(&pane).map(|id| id.0),
719 }),
720 cx,
721 );
722 }
723 }
724
725 if workspace
726 .panes_by_item
727 .insert(self.id(), pane.downgrade())
728 .is_none()
729 {
730 let mut pending_autosave = DelayedDebouncedEditAction::new();
731 let mut pending_git_update = DelayedDebouncedEditAction::new();
732 let pending_update = Rc::new(RefCell::new(None));
733 let pending_update_scheduled = Rc::new(AtomicBool::new(false));
734
735 let mut event_subscription =
736 Some(cx.subscribe(self, move |workspace, item, event, cx| {
737 let pane = if let Some(pane) = workspace
738 .panes_by_item
739 .get(&item.id())
740 .and_then(|pane| pane.upgrade(cx))
741 {
742 pane
743 } else {
744 log::error!("unexpected item event after pane was dropped");
745 return;
746 };
747
748 if let Some(item) = item.to_followable_item_handle(cx) {
749 let leader_id = workspace.leader_for_pane(&pane);
750
751 if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
752 workspace.unfollow(&pane, cx);
753 }
754
755 if item.add_event_to_update_proto(
756 event,
757 &mut *pending_update.borrow_mut(),
758 cx,
759 ) && !pending_update_scheduled.load(SeqCst)
760 {
761 pending_update_scheduled.store(true, SeqCst);
762 cx.after_window_update({
763 let pending_update = pending_update.clone();
764 let pending_update_scheduled = pending_update_scheduled.clone();
765 move |this, cx| {
766 pending_update_scheduled.store(false, SeqCst);
767 this.update_followers(
768 proto::update_followers::Variant::UpdateView(
769 proto::UpdateView {
770 id: item.id() as u64,
771 variant: pending_update.borrow_mut().take(),
772 leader_id: leader_id.map(|id| id.0),
773 },
774 ),
775 cx,
776 );
777 }
778 });
779 }
780 }
781
782 for item_event in T::to_item_events(event).into_iter() {
783 match item_event {
784 ItemEvent::CloseItem => {
785 Pane::close_item(workspace, pane, item.id(), cx)
786 .detach_and_log_err(cx);
787 return;
788 }
789
790 ItemEvent::UpdateTab => {
791 pane.update(cx, |_, cx| {
792 cx.emit(pane::Event::ChangeItemTitle);
793 cx.notify();
794 });
795 }
796
797 ItemEvent::Edit => {
798 if let Autosave::AfterDelay { milliseconds } =
799 cx.global::<Settings>().autosave
800 {
801 let delay = Duration::from_millis(milliseconds);
802 let item = item.clone();
803 pending_autosave.fire_new(
804 delay,
805 workspace,
806 cx,
807 |project, mut cx| async move {
808 cx.update(|cx| Pane::autosave_item(&item, project, cx))
809 .await
810 .log_err();
811 },
812 );
813 }
814
815 let settings = cx.global::<Settings>();
816 let debounce_delay = settings.git_overrides.gutter_debounce;
817
818 let item = item.clone();
819
820 if let Some(delay) = debounce_delay {
821 const MIN_GIT_DELAY: u64 = 50;
822
823 let delay = delay.max(MIN_GIT_DELAY);
824 let duration = Duration::from_millis(delay);
825
826 pending_git_update.fire_new(
827 duration,
828 workspace,
829 cx,
830 |project, mut cx| async move {
831 cx.update(|cx| item.git_diff_recalc(project, cx))
832 .await
833 .log_err();
834 },
835 );
836 } else {
837 let project = workspace.project().downgrade();
838 cx.spawn_weak(|_, mut cx| async move {
839 if let Some(project) = project.upgrade(&cx) {
840 cx.update(|cx| item.git_diff_recalc(project, cx))
841 .await
842 .log_err();
843 }
844 })
845 .detach();
846 }
847 }
848
849 _ => {}
850 }
851 }
852 }));
853
854 cx.observe_focus(self, move |workspace, item, focused, cx| {
855 if !focused && cx.global::<Settings>().autosave == Autosave::OnFocusChange {
856 Pane::autosave_item(&item, workspace.project.clone(), cx)
857 .detach_and_log_err(cx);
858 }
859 })
860 .detach();
861
862 let item_id = self.id();
863 cx.observe_release(self, move |workspace, _, _| {
864 workspace.panes_by_item.remove(&item_id);
865 event_subscription.take();
866 })
867 .detach();
868 }
869 }
870
871 fn deactivated(&self, cx: &mut MutableAppContext) {
872 self.update(cx, |this, cx| this.deactivated(cx));
873 }
874
875 fn workspace_deactivated(&self, cx: &mut MutableAppContext) {
876 self.update(cx, |this, cx| this.workspace_deactivated(cx));
877 }
878
879 fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool {
880 self.update(cx, |this, cx| this.navigate(data, cx))
881 }
882
883 fn id(&self) -> usize {
884 self.id()
885 }
886
887 fn window_id(&self) -> usize {
888 self.window_id()
889 }
890
891 fn to_any(&self) -> AnyViewHandle {
892 self.into()
893 }
894
895 fn is_dirty(&self, cx: &AppContext) -> bool {
896 self.read(cx).is_dirty(cx)
897 }
898
899 fn has_conflict(&self, cx: &AppContext) -> bool {
900 self.read(cx).has_conflict(cx)
901 }
902
903 fn can_save(&self, cx: &AppContext) -> bool {
904 self.read(cx).can_save(cx)
905 }
906
907 fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>> {
908 self.update(cx, |item, cx| item.save(project, cx))
909 }
910
911 fn save_as(
912 &self,
913 project: ModelHandle<Project>,
914 abs_path: PathBuf,
915 cx: &mut MutableAppContext,
916 ) -> Task<anyhow::Result<()>> {
917 self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
918 }
919
920 fn reload(
921 &self,
922 project: ModelHandle<Project>,
923 cx: &mut MutableAppContext,
924 ) -> Task<Result<()>> {
925 self.update(cx, |item, cx| item.reload(project, cx))
926 }
927
928 fn git_diff_recalc(
929 &self,
930 project: ModelHandle<Project>,
931 cx: &mut MutableAppContext,
932 ) -> Task<Result<()>> {
933 self.update(cx, |item, cx| item.git_diff_recalc(project, cx))
934 }
935
936 fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
937 self.read(cx).act_as_type(type_id, self, cx)
938 }
939
940 fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
941 if cx.has_global::<FollowableItemBuilders>() {
942 let builders = cx.global::<FollowableItemBuilders>();
943 let item = self.to_any();
944 Some(builders.get(&item.view_type())?.1(item))
945 } else {
946 None
947 }
948 }
949
950 fn on_release(
951 &self,
952 cx: &mut MutableAppContext,
953 callback: Box<dyn FnOnce(&mut MutableAppContext)>,
954 ) -> gpui::Subscription {
955 cx.observe_release(self, move |_, cx| callback(cx))
956 }
957
958 fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
959 self.read(cx).as_searchable(self)
960 }
961
962 fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation {
963 self.read(cx).breadcrumb_location()
964 }
965
966 fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
967 self.read(cx).breadcrumbs(theme, cx)
968 }
969}
970
971impl From<Box<dyn ItemHandle>> for AnyViewHandle {
972 fn from(val: Box<dyn ItemHandle>) -> Self {
973 val.to_any()
974 }
975}
976
977impl From<&Box<dyn ItemHandle>> for AnyViewHandle {
978 fn from(val: &Box<dyn ItemHandle>) -> Self {
979 val.to_any()
980 }
981}
982
983impl Clone for Box<dyn ItemHandle> {
984 fn clone(&self) -> Box<dyn ItemHandle> {
985 self.boxed_clone()
986 }
987}
988
989impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
990 fn id(&self) -> usize {
991 self.id()
992 }
993
994 fn window_id(&self) -> usize {
995 self.window_id()
996 }
997
998 fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
999 self.upgrade(cx).map(|v| Box::new(v) as Box<dyn ItemHandle>)
1000 }
1001}
1002
1003pub trait Notification: View {
1004 fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool;
1005}
1006
1007pub trait NotificationHandle {
1008 fn id(&self) -> usize;
1009 fn to_any(&self) -> AnyViewHandle;
1010}
1011
1012impl<T: Notification> NotificationHandle for ViewHandle<T> {
1013 fn id(&self) -> usize {
1014 self.id()
1015 }
1016
1017 fn to_any(&self) -> AnyViewHandle {
1018 self.into()
1019 }
1020}
1021
1022impl From<&dyn NotificationHandle> for AnyViewHandle {
1023 fn from(val: &dyn NotificationHandle) -> Self {
1024 val.to_any()
1025 }
1026}
1027
1028impl AppState {
1029 #[cfg(any(test, feature = "test-support"))]
1030 pub fn test(cx: &mut MutableAppContext) -> Arc<Self> {
1031 use fs::HomeDir;
1032
1033 cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf()));
1034 let settings = Settings::test(cx);
1035 cx.set_global(settings);
1036
1037 let fs = fs::FakeFs::new(cx.background().clone());
1038 let languages = Arc::new(LanguageRegistry::test());
1039 let http_client = client::test::FakeHttpClient::with_404_response();
1040 let client = Client::new(http_client.clone(), cx);
1041 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
1042 let themes = ThemeRegistry::new((), cx.font_cache().clone());
1043 Arc::new(Self {
1044 client,
1045 themes,
1046 fs,
1047 languages,
1048 user_store,
1049 initialize_workspace: |_, _, _| {},
1050 build_window_options: Default::default,
1051 default_item_factory: |_, _| unimplemented!(),
1052 })
1053 }
1054}
1055
1056pub enum Event {
1057 DockAnchorChanged,
1058 PaneAdded(ViewHandle<Pane>),
1059 ContactRequestedJoin(u64),
1060}
1061
1062pub struct Workspace {
1063 weak_self: WeakViewHandle<Self>,
1064 client: Arc<Client>,
1065 user_store: ModelHandle<client::UserStore>,
1066 remote_entity_subscription: Option<client::Subscription>,
1067 fs: Arc<dyn Fs>,
1068 modal: Option<AnyViewHandle>,
1069 center: PaneGroup,
1070 left_sidebar: ViewHandle<Sidebar>,
1071 right_sidebar: ViewHandle<Sidebar>,
1072 panes: Vec<ViewHandle<Pane>>,
1073 panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
1074 active_pane: ViewHandle<Pane>,
1075 last_active_center_pane: Option<WeakViewHandle<Pane>>,
1076 status_bar: ViewHandle<StatusBar>,
1077 titlebar_item: Option<AnyViewHandle>,
1078 dock: Dock,
1079 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
1080 project: ModelHandle<Project>,
1081 leader_state: LeaderState,
1082 follower_states_by_leader: FollowerStatesByLeader,
1083 last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
1084 window_edited: bool,
1085 active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
1086 _observe_current_user: Task<()>,
1087}
1088
1089#[derive(Default)]
1090struct LeaderState {
1091 followers: HashSet<PeerId>,
1092}
1093
1094type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
1095
1096#[derive(Default)]
1097struct FollowerState {
1098 active_view_id: Option<u64>,
1099 items_by_leader_view_id: HashMap<u64, FollowerItem>,
1100}
1101
1102#[derive(Debug)]
1103enum FollowerItem {
1104 Loading(Vec<proto::update_view::Variant>),
1105 Loaded(Box<dyn FollowableItemHandle>),
1106}
1107
1108impl Workspace {
1109 pub fn new(
1110 project: ModelHandle<Project>,
1111 dock_default_factory: DefaultItemFactory,
1112 cx: &mut ViewContext<Self>,
1113 ) -> Self {
1114 cx.observe_fullscreen(|_, _, cx| cx.notify()).detach();
1115
1116 cx.observe_window_activation(Self::on_window_activation_changed)
1117 .detach();
1118 cx.observe(&project, |_, _, cx| cx.notify()).detach();
1119 cx.subscribe(&project, move |this, _, event, cx| {
1120 match event {
1121 project::Event::RemoteIdChanged(remote_id) => {
1122 this.project_remote_id_changed(*remote_id, cx);
1123 }
1124 project::Event::CollaboratorLeft(peer_id) => {
1125 this.collaborator_left(*peer_id, cx);
1126 }
1127 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
1128 this.update_window_title(cx);
1129 }
1130 project::Event::DisconnectedFromHost => {
1131 this.update_window_edited(cx);
1132 cx.blur();
1133 }
1134 _ => {}
1135 }
1136 cx.notify()
1137 })
1138 .detach();
1139
1140 let center_pane = cx.add_view(|cx| Pane::new(None, cx));
1141 let pane_id = center_pane.id();
1142 cx.subscribe(¢er_pane, move |this, _, event, cx| {
1143 this.handle_pane_event(pane_id, event, cx)
1144 })
1145 .detach();
1146 cx.focus(¢er_pane);
1147 cx.emit(Event::PaneAdded(center_pane.clone()));
1148
1149 let fs = project.read(cx).fs().clone();
1150 let user_store = project.read(cx).user_store();
1151 let client = project.read(cx).client();
1152 let mut current_user = user_store.read(cx).watch_current_user();
1153 let mut connection_status = client.status();
1154 let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
1155 current_user.recv().await;
1156 connection_status.recv().await;
1157 let mut stream =
1158 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
1159
1160 while stream.recv().await.is_some() {
1161 cx.update(|cx| {
1162 if let Some(this) = this.upgrade(cx) {
1163 this.update(cx, |_, cx| cx.notify());
1164 }
1165 })
1166 }
1167 });
1168
1169 let handle = cx.handle();
1170 let weak_handle = cx.weak_handle();
1171
1172 cx.emit_global(WorkspaceCreated(weak_handle.clone()));
1173
1174 let dock = Dock::new(cx, dock_default_factory);
1175 let dock_pane = dock.pane().clone();
1176
1177 let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left));
1178 let right_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Right));
1179 let left_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), cx));
1180 let toggle_dock = cx.add_view(|cx| ToggleDockButton::new(handle, cx));
1181 let right_sidebar_buttons =
1182 cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx));
1183 let status_bar = cx.add_view(|cx| {
1184 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
1185 status_bar.add_left_item(left_sidebar_buttons, cx);
1186 status_bar.add_right_item(right_sidebar_buttons, cx);
1187 status_bar.add_right_item(toggle_dock, cx);
1188 status_bar
1189 });
1190
1191 cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
1192 drag_and_drop.register_container(weak_handle.clone());
1193 });
1194
1195 let mut active_call = None;
1196 if cx.has_global::<ModelHandle<ActiveCall>>() {
1197 let call = cx.global::<ModelHandle<ActiveCall>>().clone();
1198 let mut subscriptions = Vec::new();
1199 subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
1200 active_call = Some((call, subscriptions));
1201 }
1202
1203 let mut this = Workspace {
1204 modal: None,
1205 weak_self: weak_handle,
1206 center: PaneGroup::new(center_pane.clone()),
1207 dock,
1208 // When removing an item, the last element remaining in this array
1209 // is used to find where focus should fallback to. As such, the order
1210 // of these two variables is important.
1211 panes: vec![dock_pane, center_pane.clone()],
1212 panes_by_item: Default::default(),
1213 active_pane: center_pane.clone(),
1214 last_active_center_pane: Some(center_pane.downgrade()),
1215 status_bar,
1216 titlebar_item: None,
1217 notifications: Default::default(),
1218 client,
1219 remote_entity_subscription: None,
1220 user_store,
1221 fs,
1222 left_sidebar,
1223 right_sidebar,
1224 project,
1225 leader_state: Default::default(),
1226 follower_states_by_leader: Default::default(),
1227 last_leaders_by_pane: Default::default(),
1228 window_edited: false,
1229 active_call,
1230 _observe_current_user,
1231 };
1232 this.project_remote_id_changed(this.project.read(cx).remote_id(), cx);
1233 cx.defer(|this, cx| this.update_window_title(cx));
1234
1235 this
1236 }
1237
1238 pub fn weak_handle(&self) -> WeakViewHandle<Self> {
1239 self.weak_self.clone()
1240 }
1241
1242 pub fn left_sidebar(&self) -> &ViewHandle<Sidebar> {
1243 &self.left_sidebar
1244 }
1245
1246 pub fn right_sidebar(&self) -> &ViewHandle<Sidebar> {
1247 &self.right_sidebar
1248 }
1249
1250 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
1251 &self.status_bar
1252 }
1253
1254 pub fn user_store(&self) -> &ModelHandle<UserStore> {
1255 &self.user_store
1256 }
1257
1258 pub fn project(&self) -> &ModelHandle<Project> {
1259 &self.project
1260 }
1261
1262 pub fn client(&self) -> &Arc<Client> {
1263 &self.client
1264 }
1265
1266 pub fn set_titlebar_item(
1267 &mut self,
1268 item: impl Into<AnyViewHandle>,
1269 cx: &mut ViewContext<Self>,
1270 ) {
1271 self.titlebar_item = Some(item.into());
1272 cx.notify();
1273 }
1274
1275 pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
1276 self.titlebar_item.clone()
1277 }
1278
1279 /// Call the given callback with a workspace whose project is local.
1280 ///
1281 /// If the given workspace has a local project, then it will be passed
1282 /// to the callback. Otherwise, a new empty window will be created.
1283 pub fn with_local_workspace<T, F>(
1284 &mut self,
1285 cx: &mut ViewContext<Self>,
1286 app_state: Arc<AppState>,
1287 callback: F,
1288 ) -> T
1289 where
1290 T: 'static,
1291 F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
1292 {
1293 if self.project.read(cx).is_local() {
1294 callback(self, cx)
1295 } else {
1296 let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
1297 let mut workspace = Workspace::new(
1298 Project::local(
1299 app_state.client.clone(),
1300 app_state.user_store.clone(),
1301 app_state.languages.clone(),
1302 app_state.fs.clone(),
1303 cx,
1304 ),
1305 app_state.default_item_factory,
1306 cx,
1307 );
1308 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
1309 workspace
1310 });
1311 workspace.update(cx, callback)
1312 }
1313 }
1314
1315 pub fn worktrees<'a>(
1316 &self,
1317 cx: &'a AppContext,
1318 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
1319 self.project.read(cx).worktrees(cx)
1320 }
1321
1322 pub fn visible_worktrees<'a>(
1323 &self,
1324 cx: &'a AppContext,
1325 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
1326 self.project.read(cx).visible_worktrees(cx)
1327 }
1328
1329 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
1330 let futures = self
1331 .worktrees(cx)
1332 .filter_map(|worktree| worktree.read(cx).as_local())
1333 .map(|worktree| worktree.scan_complete())
1334 .collect::<Vec<_>>();
1335 async move {
1336 for future in futures {
1337 future.await;
1338 }
1339 }
1340 }
1341
1342 pub fn close(
1343 &mut self,
1344 _: &CloseWindow,
1345 cx: &mut ViewContext<Self>,
1346 ) -> Option<Task<Result<()>>> {
1347 let prepare = self.prepare_to_close(false, cx);
1348 Some(cx.spawn(|this, mut cx| async move {
1349 if prepare.await? {
1350 this.update(&mut cx, |_, cx| {
1351 let window_id = cx.window_id();
1352 cx.remove_window(window_id);
1353 });
1354 }
1355 Ok(())
1356 }))
1357 }
1358
1359 pub fn prepare_to_close(
1360 &mut self,
1361 quitting: bool,
1362 cx: &mut ViewContext<Self>,
1363 ) -> Task<Result<bool>> {
1364 let active_call = self.active_call().cloned();
1365 let window_id = cx.window_id();
1366 let workspace_count = cx
1367 .window_ids()
1368 .flat_map(|window_id| cx.root_view::<Workspace>(window_id))
1369 .count();
1370 cx.spawn(|this, mut cx| async move {
1371 if let Some(active_call) = active_call {
1372 if !quitting
1373 && workspace_count == 1
1374 && active_call.read_with(&cx, |call, _| call.room().is_some())
1375 {
1376 let answer = cx
1377 .prompt(
1378 window_id,
1379 PromptLevel::Warning,
1380 "Do you want to leave the current call?",
1381 &["Close window and hang up", "Cancel"],
1382 )
1383 .next()
1384 .await;
1385 if answer == Some(1) {
1386 return anyhow::Ok(false);
1387 } else {
1388 active_call.update(&mut cx, |call, cx| call.hang_up(cx))?;
1389 }
1390 }
1391 }
1392
1393 Ok(this
1394 .update(&mut cx, |this, cx| this.save_all_internal(true, cx))
1395 .await?)
1396 })
1397 }
1398
1399 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1400 let save_all = self.save_all_internal(false, cx);
1401 Some(cx.foreground().spawn(async move {
1402 save_all.await?;
1403 Ok(())
1404 }))
1405 }
1406
1407 fn save_all_internal(
1408 &mut self,
1409 should_prompt_to_save: bool,
1410 cx: &mut ViewContext<Self>,
1411 ) -> Task<Result<bool>> {
1412 if self.project.read(cx).is_read_only() {
1413 return Task::ready(Ok(true));
1414 }
1415
1416 let dirty_items = self
1417 .panes
1418 .iter()
1419 .flat_map(|pane| {
1420 pane.read(cx).items().filter_map(|item| {
1421 if item.is_dirty(cx) {
1422 Some((pane.clone(), item.boxed_clone()))
1423 } else {
1424 None
1425 }
1426 })
1427 })
1428 .collect::<Vec<_>>();
1429
1430 let project = self.project.clone();
1431 cx.spawn_weak(|_, mut cx| async move {
1432 for (pane, item) in dirty_items {
1433 let (singleton, project_entry_ids) =
1434 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
1435 if singleton || !project_entry_ids.is_empty() {
1436 if let Some(ix) =
1437 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))
1438 {
1439 if !Pane::save_item(
1440 project.clone(),
1441 &pane,
1442 ix,
1443 &*item,
1444 should_prompt_to_save,
1445 &mut cx,
1446 )
1447 .await?
1448 {
1449 return Ok(false);
1450 }
1451 }
1452 }
1453 }
1454 Ok(true)
1455 })
1456 }
1457
1458 #[allow(clippy::type_complexity)]
1459 pub fn open_paths(
1460 &mut self,
1461 mut abs_paths: Vec<PathBuf>,
1462 visible: bool,
1463 cx: &mut ViewContext<Self>,
1464 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1465 let fs = self.fs.clone();
1466
1467 // Sort the paths to ensure we add worktrees for parents before their children.
1468 abs_paths.sort_unstable();
1469 cx.spawn(|this, mut cx| async move {
1470 let mut project_paths = Vec::new();
1471 for path in &abs_paths {
1472 project_paths.push(
1473 this.update(&mut cx, |this, cx| {
1474 this.project_path_for_path(path, visible, cx)
1475 })
1476 .await
1477 .log_err(),
1478 );
1479 }
1480
1481 let tasks = abs_paths
1482 .iter()
1483 .cloned()
1484 .zip(project_paths.into_iter())
1485 .map(|(abs_path, project_path)| {
1486 let this = this.clone();
1487 cx.spawn(|mut cx| {
1488 let fs = fs.clone();
1489 async move {
1490 let (_worktree, project_path) = project_path?;
1491 if fs.is_file(&abs_path).await {
1492 Some(
1493 this.update(&mut cx, |this, cx| {
1494 this.open_path(project_path, None, true, cx)
1495 })
1496 .await,
1497 )
1498 } else {
1499 None
1500 }
1501 }
1502 })
1503 })
1504 .collect::<Vec<_>>();
1505
1506 futures::future::join_all(tasks).await
1507 })
1508 }
1509
1510 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1511 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1512 files: false,
1513 directories: true,
1514 multiple: true,
1515 });
1516 cx.spawn(|this, mut cx| async move {
1517 if let Some(paths) = paths.recv().await.flatten() {
1518 let results = this
1519 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))
1520 .await;
1521 for result in results.into_iter().flatten() {
1522 result.log_err();
1523 }
1524 }
1525 })
1526 .detach();
1527 }
1528
1529 fn remove_folder_from_project(
1530 &mut self,
1531 RemoveWorktreeFromProject(worktree_id): &RemoveWorktreeFromProject,
1532 cx: &mut ViewContext<Self>,
1533 ) {
1534 self.project
1535 .update(cx, |project, cx| project.remove_worktree(*worktree_id, cx));
1536 }
1537
1538 fn project_path_for_path(
1539 &self,
1540 abs_path: &Path,
1541 visible: bool,
1542 cx: &mut ViewContext<Self>,
1543 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1544 let entry = self.project().update(cx, |project, cx| {
1545 project.find_or_create_local_worktree(abs_path, visible, cx)
1546 });
1547 cx.spawn(|_, cx| async move {
1548 let (worktree, path) = entry.await?;
1549 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1550 Ok((
1551 worktree,
1552 ProjectPath {
1553 worktree_id,
1554 path: path.into(),
1555 },
1556 ))
1557 })
1558 }
1559
1560 /// Returns the modal that was toggled closed if it was open.
1561 pub fn toggle_modal<V, F>(
1562 &mut self,
1563 cx: &mut ViewContext<Self>,
1564 add_view: F,
1565 ) -> Option<ViewHandle<V>>
1566 where
1567 V: 'static + View,
1568 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1569 {
1570 cx.notify();
1571 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1572 // it. Otherwise, create a new modal and set it as active.
1573 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
1574 if let Some(already_open_modal) = already_open_modal {
1575 cx.focus_self();
1576 Some(already_open_modal)
1577 } else {
1578 let modal = add_view(self, cx);
1579 cx.focus(&modal);
1580 self.modal = Some(modal.into());
1581 None
1582 }
1583 }
1584
1585 pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
1586 self.modal
1587 .as_ref()
1588 .and_then(|modal| modal.clone().downcast::<V>())
1589 }
1590
1591 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
1592 if self.modal.take().is_some() {
1593 cx.focus(&self.active_pane);
1594 cx.notify();
1595 }
1596 }
1597
1598 pub fn show_notification<V: Notification>(
1599 &mut self,
1600 id: usize,
1601 cx: &mut ViewContext<Self>,
1602 build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
1603 ) {
1604 let type_id = TypeId::of::<V>();
1605 if self
1606 .notifications
1607 .iter()
1608 .all(|(existing_type_id, existing_id, _)| {
1609 (*existing_type_id, *existing_id) != (type_id, id)
1610 })
1611 {
1612 let notification = build_notification(cx);
1613 cx.subscribe(¬ification, move |this, handle, event, cx| {
1614 if handle.read(cx).should_dismiss_notification_on_event(event) {
1615 this.dismiss_notification(type_id, id, cx);
1616 }
1617 })
1618 .detach();
1619 self.notifications
1620 .push((type_id, id, Box::new(notification)));
1621 cx.notify();
1622 }
1623 }
1624
1625 fn dismiss_notification(&mut self, type_id: TypeId, id: usize, cx: &mut ViewContext<Self>) {
1626 self.notifications
1627 .retain(|(existing_type_id, existing_id, _)| {
1628 if (*existing_type_id, *existing_id) == (type_id, id) {
1629 cx.notify();
1630 false
1631 } else {
1632 true
1633 }
1634 });
1635 }
1636
1637 pub fn items<'a>(
1638 &'a self,
1639 cx: &'a AppContext,
1640 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1641 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1642 }
1643
1644 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1645 self.items_of_type(cx).max_by_key(|item| item.id())
1646 }
1647
1648 pub fn items_of_type<'a, T: Item>(
1649 &'a self,
1650 cx: &'a AppContext,
1651 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1652 self.panes
1653 .iter()
1654 .flat_map(|pane| pane.read(cx).items_of_type())
1655 }
1656
1657 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1658 self.active_pane().read(cx).active_item()
1659 }
1660
1661 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1662 self.active_item(cx).and_then(|item| item.project_path(cx))
1663 }
1664
1665 pub fn save_active_item(
1666 &mut self,
1667 force_name_change: bool,
1668 cx: &mut ViewContext<Self>,
1669 ) -> Task<Result<()>> {
1670 let project = self.project.clone();
1671 if let Some(item) = self.active_item(cx) {
1672 if !force_name_change && item.can_save(cx) {
1673 if item.has_conflict(cx.as_ref()) {
1674 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1675
1676 let mut answer = cx.prompt(
1677 PromptLevel::Warning,
1678 CONFLICT_MESSAGE,
1679 &["Overwrite", "Cancel"],
1680 );
1681 cx.spawn(|_, mut cx| async move {
1682 let answer = answer.recv().await;
1683 if answer == Some(0) {
1684 cx.update(|cx| item.save(project, cx)).await?;
1685 }
1686 Ok(())
1687 })
1688 } else {
1689 item.save(project, cx)
1690 }
1691 } else if item.is_singleton(cx) {
1692 let worktree = self.worktrees(cx).next();
1693 let start_abs_path = worktree
1694 .and_then(|w| w.read(cx).as_local())
1695 .map_or(Path::new(""), |w| w.abs_path())
1696 .to_path_buf();
1697 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1698 cx.spawn(|_, mut cx| async move {
1699 if let Some(abs_path) = abs_path.recv().await.flatten() {
1700 cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
1701 }
1702 Ok(())
1703 })
1704 } else {
1705 Task::ready(Ok(()))
1706 }
1707 } else {
1708 Task::ready(Ok(()))
1709 }
1710 }
1711
1712 pub fn toggle_sidebar(&mut self, sidebar_side: SidebarSide, cx: &mut ViewContext<Self>) {
1713 let sidebar = match sidebar_side {
1714 SidebarSide::Left => &mut self.left_sidebar,
1715 SidebarSide::Right => &mut self.right_sidebar,
1716 };
1717 let open = sidebar.update(cx, |sidebar, cx| {
1718 let open = !sidebar.is_open();
1719 sidebar.set_open(open, cx);
1720 open
1721 });
1722
1723 if open {
1724 Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
1725 }
1726
1727 cx.focus_self();
1728 cx.notify();
1729 }
1730
1731 pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
1732 let sidebar = match action.sidebar_side {
1733 SidebarSide::Left => &mut self.left_sidebar,
1734 SidebarSide::Right => &mut self.right_sidebar,
1735 };
1736 let active_item = sidebar.update(cx, move |sidebar, cx| {
1737 if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
1738 sidebar.set_open(false, cx);
1739 None
1740 } else {
1741 sidebar.set_open(true, cx);
1742 sidebar.activate_item(action.item_index, cx);
1743 sidebar.active_item().cloned()
1744 }
1745 });
1746
1747 if let Some(active_item) = active_item {
1748 Dock::hide_on_sidebar_shown(self, action.sidebar_side, cx);
1749
1750 if active_item.is_focused(cx) {
1751 cx.focus_self();
1752 } else {
1753 cx.focus(active_item.to_any());
1754 }
1755 } else {
1756 cx.focus_self();
1757 }
1758 cx.notify();
1759 }
1760
1761 pub fn toggle_sidebar_item_focus(
1762 &mut self,
1763 sidebar_side: SidebarSide,
1764 item_index: usize,
1765 cx: &mut ViewContext<Self>,
1766 ) {
1767 let sidebar = match sidebar_side {
1768 SidebarSide::Left => &mut self.left_sidebar,
1769 SidebarSide::Right => &mut self.right_sidebar,
1770 };
1771 let active_item = sidebar.update(cx, |sidebar, cx| {
1772 sidebar.set_open(true, cx);
1773 sidebar.activate_item(item_index, cx);
1774 sidebar.active_item().cloned()
1775 });
1776 if let Some(active_item) = active_item {
1777 Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
1778
1779 if active_item.is_focused(cx) {
1780 cx.focus_self();
1781 } else {
1782 cx.focus(active_item.to_any());
1783 }
1784 }
1785 cx.notify();
1786 }
1787
1788 pub fn focus_center(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
1789 cx.focus_self();
1790 cx.notify();
1791 }
1792
1793 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1794 let pane = cx.add_view(|cx| Pane::new(None, cx));
1795 let pane_id = pane.id();
1796 cx.subscribe(&pane, move |this, _, event, cx| {
1797 this.handle_pane_event(pane_id, event, cx)
1798 })
1799 .detach();
1800 self.panes.push(pane.clone());
1801 cx.focus(pane.clone());
1802 cx.emit(Event::PaneAdded(pane.clone()));
1803 pane
1804 }
1805
1806 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1807 let active_pane = self.active_pane().clone();
1808 Pane::add_item(self, &active_pane, item, true, true, None, cx);
1809 }
1810
1811 pub fn open_path(
1812 &mut self,
1813 path: impl Into<ProjectPath>,
1814 pane: Option<WeakViewHandle<Pane>>,
1815 focus_item: bool,
1816 cx: &mut ViewContext<Self>,
1817 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1818 let pane = pane.unwrap_or_else(|| self.active_pane().downgrade());
1819 let task = self.load_path(path.into(), cx);
1820 cx.spawn(|this, mut cx| async move {
1821 let (project_entry_id, build_item) = task.await?;
1822 let pane = pane
1823 .upgrade(&cx)
1824 .ok_or_else(|| anyhow!("pane was closed"))?;
1825 this.update(&mut cx, |this, cx| {
1826 Ok(Pane::open_item(
1827 this,
1828 pane,
1829 project_entry_id,
1830 focus_item,
1831 cx,
1832 build_item,
1833 ))
1834 })
1835 })
1836 }
1837
1838 pub(crate) fn load_path(
1839 &mut self,
1840 path: ProjectPath,
1841 cx: &mut ViewContext<Self>,
1842 ) -> Task<
1843 Result<(
1844 ProjectEntryId,
1845 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
1846 )>,
1847 > {
1848 let project = self.project().clone();
1849 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1850 cx.as_mut().spawn(|mut cx| async move {
1851 let (project_entry_id, project_item) = project_item.await?;
1852 let build_item = cx.update(|cx| {
1853 cx.default_global::<ProjectItemBuilders>()
1854 .get(&project_item.model_type())
1855 .ok_or_else(|| anyhow!("no item builder for project item"))
1856 .cloned()
1857 })?;
1858 let build_item =
1859 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
1860 Ok((project_entry_id, build_item))
1861 })
1862 }
1863
1864 pub fn open_project_item<T>(
1865 &mut self,
1866 project_item: ModelHandle<T::Item>,
1867 cx: &mut ViewContext<Self>,
1868 ) -> ViewHandle<T>
1869 where
1870 T: ProjectItem,
1871 {
1872 use project::Item as _;
1873
1874 let entry_id = project_item.read(cx).entry_id(cx);
1875 if let Some(item) = entry_id
1876 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1877 .and_then(|item| item.downcast())
1878 {
1879 self.activate_item(&item, cx);
1880 return item;
1881 }
1882
1883 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1884 self.add_item(Box::new(item.clone()), cx);
1885 item
1886 }
1887
1888 pub fn open_shared_screen(&mut self, action: &OpenSharedScreen, cx: &mut ViewContext<Self>) {
1889 if let Some(shared_screen) =
1890 self.shared_screen_for_peer(action.peer_id, &self.active_pane, cx)
1891 {
1892 let pane = self.active_pane.clone();
1893 Pane::add_item(self, &pane, Box::new(shared_screen), false, true, None, cx);
1894 }
1895 }
1896
1897 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1898 let result = self.panes.iter().find_map(|pane| {
1899 pane.read(cx)
1900 .index_for_item(item)
1901 .map(|ix| (pane.clone(), ix))
1902 });
1903 if let Some((pane, ix)) = result {
1904 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1905 true
1906 } else {
1907 false
1908 }
1909 }
1910
1911 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1912 let panes = self.center.panes();
1913 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1914 cx.focus(pane);
1915 } else {
1916 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1917 }
1918 }
1919
1920 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1921 let next_pane = {
1922 let panes = self.center.panes();
1923 let ix = panes
1924 .iter()
1925 .position(|pane| **pane == self.active_pane)
1926 .unwrap();
1927 let next_ix = (ix + 1) % panes.len();
1928 panes[next_ix].clone()
1929 };
1930 cx.focus(next_pane);
1931 }
1932
1933 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1934 let prev_pane = {
1935 let panes = self.center.panes();
1936 let ix = panes
1937 .iter()
1938 .position(|pane| **pane == self.active_pane)
1939 .unwrap();
1940 let prev_ix = if ix == 0 { panes.len() - 1 } else { ix - 1 };
1941 panes[prev_ix].clone()
1942 };
1943 cx.focus(prev_pane);
1944 }
1945
1946 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1947 if self.active_pane != pane {
1948 self.active_pane
1949 .update(cx, |pane, cx| pane.set_active(false, cx));
1950 self.active_pane = pane.clone();
1951 self.active_pane
1952 .update(cx, |pane, cx| pane.set_active(true, cx));
1953 self.status_bar.update(cx, |status_bar, cx| {
1954 status_bar.set_active_pane(&self.active_pane, cx);
1955 });
1956 self.active_item_path_changed(cx);
1957
1958 if &pane == self.dock_pane() {
1959 Dock::show(self, cx);
1960 } else {
1961 self.last_active_center_pane = Some(pane.downgrade());
1962 if self.dock.is_anchored_at(DockAnchor::Expanded) {
1963 Dock::hide(self, cx);
1964 }
1965 }
1966 cx.notify();
1967 }
1968
1969 self.update_followers(
1970 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1971 id: self.active_item(cx).map(|item| item.id() as u64),
1972 leader_id: self.leader_for_pane(&pane).map(|id| id.0),
1973 }),
1974 cx,
1975 );
1976 }
1977
1978 fn handle_pane_event(
1979 &mut self,
1980 pane_id: usize,
1981 event: &pane::Event,
1982 cx: &mut ViewContext<Self>,
1983 ) {
1984 if let Some(pane) = self.pane(pane_id) {
1985 let is_dock = &pane == self.dock.pane();
1986 match event {
1987 pane::Event::Split(direction) if !is_dock => {
1988 self.split_pane(pane, *direction, cx);
1989 }
1990 pane::Event::Remove if !is_dock => self.remove_pane(pane, cx),
1991 pane::Event::Remove if is_dock => Dock::hide(self, cx),
1992 pane::Event::ActivateItem { local } => {
1993 if *local {
1994 self.unfollow(&pane, cx);
1995 }
1996 if &pane == self.active_pane() {
1997 self.active_item_path_changed(cx);
1998 }
1999 }
2000 pane::Event::ChangeItemTitle => {
2001 if pane == self.active_pane {
2002 self.active_item_path_changed(cx);
2003 }
2004 self.update_window_edited(cx);
2005 }
2006 pane::Event::RemoveItem { item_id } => {
2007 self.update_window_edited(cx);
2008 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
2009 if entry.get().id() == pane.id() {
2010 entry.remove();
2011 }
2012 }
2013 }
2014 _ => {}
2015 }
2016 } else if self.dock.visible_pane().is_none() {
2017 error!("pane {} not found", pane_id);
2018 }
2019 }
2020
2021 pub fn split_pane(
2022 &mut self,
2023 pane: ViewHandle<Pane>,
2024 direction: SplitDirection,
2025 cx: &mut ViewContext<Self>,
2026 ) -> Option<ViewHandle<Pane>> {
2027 if &pane == self.dock_pane() {
2028 warn!("Can't split dock pane.");
2029 return None;
2030 }
2031
2032 pane.read(cx).active_item().map(|item| {
2033 let new_pane = self.add_pane(cx);
2034 if let Some(clone) = item.clone_on_split(cx.as_mut()) {
2035 Pane::add_item(self, &new_pane, clone, true, true, None, cx);
2036 }
2037 self.center.split(&pane, &new_pane, direction).unwrap();
2038 cx.notify();
2039 new_pane
2040 })
2041 }
2042
2043 pub fn split_pane_with_item(
2044 &mut self,
2045 from: WeakViewHandle<Pane>,
2046 pane_to_split: WeakViewHandle<Pane>,
2047 item_id_to_move: usize,
2048 split_direction: SplitDirection,
2049 cx: &mut ViewContext<Self>,
2050 ) {
2051 if let Some((pane_to_split, from)) = pane_to_split.upgrade(cx).zip(from.upgrade(cx)) {
2052 if &pane_to_split == self.dock_pane() {
2053 warn!("Can't split dock pane.");
2054 return;
2055 }
2056
2057 let new_pane = self.add_pane(cx);
2058 Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
2059 self.center
2060 .split(&pane_to_split, &new_pane, split_direction)
2061 .unwrap();
2062 cx.notify();
2063 }
2064 }
2065
2066 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
2067 if self.center.remove(&pane).unwrap() {
2068 self.panes.retain(|p| p != &pane);
2069 cx.focus(self.panes.last().unwrap().clone());
2070 self.unfollow(&pane, cx);
2071 self.last_leaders_by_pane.remove(&pane.downgrade());
2072 for removed_item in pane.read(cx).items() {
2073 self.panes_by_item.remove(&removed_item.id());
2074 }
2075 if self.last_active_center_pane == Some(pane.downgrade()) {
2076 self.last_active_center_pane = None;
2077 }
2078
2079 cx.notify();
2080 } else {
2081 self.active_item_path_changed(cx);
2082 }
2083 }
2084
2085 pub fn panes(&self) -> &[ViewHandle<Pane>] {
2086 &self.panes
2087 }
2088
2089 fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
2090 self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
2091 }
2092
2093 pub fn active_pane(&self) -> &ViewHandle<Pane> {
2094 &self.active_pane
2095 }
2096
2097 pub fn dock_pane(&self) -> &ViewHandle<Pane> {
2098 self.dock.pane()
2099 }
2100
2101 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
2102 if let Some(remote_id) = remote_id {
2103 self.remote_entity_subscription =
2104 Some(self.client.add_view_for_remote_entity(remote_id, cx));
2105 } else {
2106 self.remote_entity_subscription.take();
2107 }
2108 }
2109
2110 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2111 self.leader_state.followers.remove(&peer_id);
2112 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
2113 for state in states_by_pane.into_values() {
2114 for item in state.items_by_leader_view_id.into_values() {
2115 if let FollowerItem::Loaded(item) = item {
2116 item.set_leader_replica_id(None, cx);
2117 }
2118 }
2119 }
2120 }
2121 cx.notify();
2122 }
2123
2124 pub fn toggle_follow(
2125 &mut self,
2126 ToggleFollow(leader_id): &ToggleFollow,
2127 cx: &mut ViewContext<Self>,
2128 ) -> Option<Task<Result<()>>> {
2129 let leader_id = *leader_id;
2130 let pane = self.active_pane().clone();
2131
2132 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
2133 if leader_id == prev_leader_id {
2134 return None;
2135 }
2136 }
2137
2138 self.last_leaders_by_pane
2139 .insert(pane.downgrade(), leader_id);
2140 self.follower_states_by_leader
2141 .entry(leader_id)
2142 .or_default()
2143 .insert(pane.clone(), Default::default());
2144 cx.notify();
2145
2146 let project_id = self.project.read(cx).remote_id()?;
2147 let request = self.client.request(proto::Follow {
2148 project_id,
2149 leader_id: leader_id.0,
2150 });
2151 Some(cx.spawn_weak(|this, mut cx| async move {
2152 let response = request.await?;
2153 if let Some(this) = this.upgrade(&cx) {
2154 this.update(&mut cx, |this, _| {
2155 let state = this
2156 .follower_states_by_leader
2157 .get_mut(&leader_id)
2158 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
2159 .ok_or_else(|| anyhow!("following interrupted"))?;
2160 state.active_view_id = response.active_view_id;
2161 Ok::<_, anyhow::Error>(())
2162 })?;
2163 Self::add_views_from_leader(this, leader_id, vec![pane], response.views, &mut cx)
2164 .await?;
2165 }
2166 Ok(())
2167 }))
2168 }
2169
2170 pub fn follow_next_collaborator(
2171 &mut self,
2172 _: &FollowNextCollaborator,
2173 cx: &mut ViewContext<Self>,
2174 ) -> Option<Task<Result<()>>> {
2175 let collaborators = self.project.read(cx).collaborators();
2176 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
2177 let mut collaborators = collaborators.keys().copied();
2178 for peer_id in collaborators.by_ref() {
2179 if peer_id == leader_id {
2180 break;
2181 }
2182 }
2183 collaborators.next()
2184 } else if let Some(last_leader_id) =
2185 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
2186 {
2187 if collaborators.contains_key(last_leader_id) {
2188 Some(*last_leader_id)
2189 } else {
2190 None
2191 }
2192 } else {
2193 None
2194 };
2195
2196 next_leader_id
2197 .or_else(|| collaborators.keys().copied().next())
2198 .and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx))
2199 }
2200
2201 pub fn unfollow(
2202 &mut self,
2203 pane: &ViewHandle<Pane>,
2204 cx: &mut ViewContext<Self>,
2205 ) -> Option<PeerId> {
2206 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
2207 let leader_id = *leader_id;
2208 if let Some(state) = states_by_pane.remove(pane) {
2209 for (_, item) in state.items_by_leader_view_id {
2210 if let FollowerItem::Loaded(item) = item {
2211 item.set_leader_replica_id(None, cx);
2212 }
2213 }
2214
2215 if states_by_pane.is_empty() {
2216 self.follower_states_by_leader.remove(&leader_id);
2217 if let Some(project_id) = self.project.read(cx).remote_id() {
2218 self.client
2219 .send(proto::Unfollow {
2220 project_id,
2221 leader_id: leader_id.0,
2222 })
2223 .log_err();
2224 }
2225 }
2226
2227 cx.notify();
2228 return Some(leader_id);
2229 }
2230 }
2231 None
2232 }
2233
2234 pub fn is_following(&self, peer_id: PeerId) -> bool {
2235 self.follower_states_by_leader.contains_key(&peer_id)
2236 }
2237
2238 fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
2239 let project = &self.project.read(cx);
2240 let mut worktree_root_names = String::new();
2241 for (i, name) in project.worktree_root_names(cx).enumerate() {
2242 if i > 0 {
2243 worktree_root_names.push_str(", ");
2244 }
2245 worktree_root_names.push_str(name);
2246 }
2247
2248 // TODO: There should be a better system in place for this
2249 // (https://github.com/zed-industries/zed/issues/1290)
2250 let is_fullscreen = cx.window_is_fullscreen(cx.window_id());
2251 let container_theme = if is_fullscreen {
2252 let mut container_theme = theme.workspace.titlebar.container;
2253 container_theme.padding.left = container_theme.padding.right;
2254 container_theme
2255 } else {
2256 theme.workspace.titlebar.container
2257 };
2258
2259 enum TitleBar {}
2260 ConstrainedBox::new(
2261 MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
2262 Container::new(
2263 Stack::new()
2264 .with_child(
2265 Label::new(worktree_root_names, theme.workspace.titlebar.title.clone())
2266 .aligned()
2267 .left()
2268 .boxed(),
2269 )
2270 .with_children(
2271 self.titlebar_item
2272 .as_ref()
2273 .map(|item| ChildView::new(item, cx).aligned().right().boxed()),
2274 )
2275 .boxed(),
2276 )
2277 .with_style(container_theme)
2278 .boxed()
2279 })
2280 .on_click(MouseButton::Left, |event, cx| {
2281 if event.click_count == 2 {
2282 cx.zoom_window(cx.window_id());
2283 }
2284 })
2285 .boxed(),
2286 )
2287 .with_height(theme.workspace.titlebar.height)
2288 .named("titlebar")
2289 }
2290
2291 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2292 let active_entry = self.active_project_path(cx);
2293 self.project
2294 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2295 self.update_window_title(cx);
2296 }
2297
2298 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2299 let mut title = String::new();
2300 let project = self.project().read(cx);
2301 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2302 let filename = path
2303 .path
2304 .file_name()
2305 .map(|s| s.to_string_lossy())
2306 .or_else(|| {
2307 Some(Cow::Borrowed(
2308 project
2309 .worktree_for_id(path.worktree_id, cx)?
2310 .read(cx)
2311 .root_name(),
2312 ))
2313 });
2314 if let Some(filename) = filename {
2315 title.push_str(filename.as_ref());
2316 title.push_str(" — ");
2317 }
2318 }
2319 for (i, name) in project.worktree_root_names(cx).enumerate() {
2320 if i > 0 {
2321 title.push_str(", ");
2322 }
2323 title.push_str(name);
2324 }
2325 if title.is_empty() {
2326 title = "empty project".to_string();
2327 }
2328 cx.set_window_title(&title);
2329 }
2330
2331 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2332 let is_edited = !self.project.read(cx).is_read_only()
2333 && self
2334 .items(cx)
2335 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2336 if is_edited != self.window_edited {
2337 self.window_edited = is_edited;
2338 cx.set_window_edited(self.window_edited)
2339 }
2340 }
2341
2342 fn render_disconnected_overlay(&self, cx: &mut RenderContext<Workspace>) -> Option<ElementBox> {
2343 if self.project.read(cx).is_read_only() {
2344 enum DisconnectedOverlay {}
2345 Some(
2346 MouseEventHandler::<DisconnectedOverlay>::new(0, cx, |_, cx| {
2347 let theme = &cx.global::<Settings>().theme;
2348 Label::new(
2349 "Your connection to the remote project has been lost.".to_string(),
2350 theme.workspace.disconnected_overlay.text.clone(),
2351 )
2352 .aligned()
2353 .contained()
2354 .with_style(theme.workspace.disconnected_overlay.container)
2355 .boxed()
2356 })
2357 .with_cursor_style(CursorStyle::Arrow)
2358 .capture_all()
2359 .boxed(),
2360 )
2361 } else {
2362 None
2363 }
2364 }
2365
2366 fn render_notifications(
2367 &self,
2368 theme: &theme::Workspace,
2369 cx: &AppContext,
2370 ) -> Option<ElementBox> {
2371 if self.notifications.is_empty() {
2372 None
2373 } else {
2374 Some(
2375 Flex::column()
2376 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2377 ChildView::new(notification.as_ref(), cx)
2378 .contained()
2379 .with_style(theme.notification)
2380 .boxed()
2381 }))
2382 .constrained()
2383 .with_width(theme.notifications.width)
2384 .contained()
2385 .with_style(theme.notifications.container)
2386 .aligned()
2387 .bottom()
2388 .right()
2389 .boxed(),
2390 )
2391 }
2392 }
2393
2394 // RPC handlers
2395
2396 async fn handle_follow(
2397 this: ViewHandle<Self>,
2398 envelope: TypedEnvelope<proto::Follow>,
2399 _: Arc<Client>,
2400 mut cx: AsyncAppContext,
2401 ) -> Result<proto::FollowResponse> {
2402 this.update(&mut cx, |this, cx| {
2403 this.leader_state
2404 .followers
2405 .insert(envelope.original_sender_id()?);
2406
2407 let active_view_id = this
2408 .active_item(cx)
2409 .and_then(|i| i.to_followable_item_handle(cx))
2410 .map(|i| i.id() as u64);
2411 Ok(proto::FollowResponse {
2412 active_view_id,
2413 views: this
2414 .panes()
2415 .iter()
2416 .flat_map(|pane| {
2417 let leader_id = this.leader_for_pane(pane).map(|id| id.0);
2418 pane.read(cx).items().filter_map({
2419 let cx = &cx;
2420 move |item| {
2421 let id = item.id() as u64;
2422 let item = item.to_followable_item_handle(cx)?;
2423 let variant = item.to_state_proto(cx)?;
2424 Some(proto::View {
2425 id,
2426 leader_id,
2427 variant: Some(variant),
2428 })
2429 }
2430 })
2431 })
2432 .collect(),
2433 })
2434 })
2435 }
2436
2437 async fn handle_unfollow(
2438 this: ViewHandle<Self>,
2439 envelope: TypedEnvelope<proto::Unfollow>,
2440 _: Arc<Client>,
2441 mut cx: AsyncAppContext,
2442 ) -> Result<()> {
2443 this.update(&mut cx, |this, _| {
2444 this.leader_state
2445 .followers
2446 .remove(&envelope.original_sender_id()?);
2447 Ok(())
2448 })
2449 }
2450
2451 async fn handle_update_followers(
2452 this: ViewHandle<Self>,
2453 envelope: TypedEnvelope<proto::UpdateFollowers>,
2454 _: Arc<Client>,
2455 mut cx: AsyncAppContext,
2456 ) -> Result<()> {
2457 let leader_id = envelope.original_sender_id()?;
2458 match envelope
2459 .payload
2460 .variant
2461 .ok_or_else(|| anyhow!("invalid update"))?
2462 {
2463 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2464 this.update(&mut cx, |this, cx| {
2465 this.update_leader_state(leader_id, cx, |state, _| {
2466 state.active_view_id = update_active_view.id;
2467 });
2468 Ok::<_, anyhow::Error>(())
2469 })
2470 }
2471 proto::update_followers::Variant::UpdateView(update_view) => {
2472 this.update(&mut cx, |this, cx| {
2473 let variant = update_view
2474 .variant
2475 .ok_or_else(|| anyhow!("missing update view variant"))?;
2476 this.update_leader_state(leader_id, cx, |state, cx| {
2477 let variant = variant.clone();
2478 match state
2479 .items_by_leader_view_id
2480 .entry(update_view.id)
2481 .or_insert(FollowerItem::Loading(Vec::new()))
2482 {
2483 FollowerItem::Loaded(item) => {
2484 item.apply_update_proto(variant, cx).log_err();
2485 }
2486 FollowerItem::Loading(updates) => updates.push(variant),
2487 }
2488 });
2489 Ok(())
2490 })
2491 }
2492 proto::update_followers::Variant::CreateView(view) => {
2493 let panes = this.read_with(&cx, |this, _| {
2494 this.follower_states_by_leader
2495 .get(&leader_id)
2496 .into_iter()
2497 .flat_map(|states_by_pane| states_by_pane.keys())
2498 .cloned()
2499 .collect()
2500 });
2501 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], &mut cx)
2502 .await?;
2503 Ok(())
2504 }
2505 }
2506 .log_err();
2507
2508 Ok(())
2509 }
2510
2511 async fn add_views_from_leader(
2512 this: ViewHandle<Self>,
2513 leader_id: PeerId,
2514 panes: Vec<ViewHandle<Pane>>,
2515 views: Vec<proto::View>,
2516 cx: &mut AsyncAppContext,
2517 ) -> Result<()> {
2518 let project = this.read_with(cx, |this, _| this.project.clone());
2519 let replica_id = project
2520 .read_with(cx, |project, _| {
2521 project
2522 .collaborators()
2523 .get(&leader_id)
2524 .map(|c| c.replica_id)
2525 })
2526 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2527
2528 let item_builders = cx.update(|cx| {
2529 cx.default_global::<FollowableItemBuilders>()
2530 .values()
2531 .map(|b| b.0)
2532 .collect::<Vec<_>>()
2533 });
2534
2535 let mut item_tasks_by_pane = HashMap::default();
2536 for pane in panes {
2537 let mut item_tasks = Vec::new();
2538 let mut leader_view_ids = Vec::new();
2539 for view in &views {
2540 let mut variant = view.variant.clone();
2541 if variant.is_none() {
2542 Err(anyhow!("missing variant"))?;
2543 }
2544 for build_item in &item_builders {
2545 let task =
2546 cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx));
2547 if let Some(task) = task {
2548 item_tasks.push(task);
2549 leader_view_ids.push(view.id);
2550 break;
2551 } else {
2552 assert!(variant.is_some());
2553 }
2554 }
2555 }
2556
2557 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2558 }
2559
2560 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2561 let items = futures::future::try_join_all(item_tasks).await?;
2562 this.update(cx, |this, cx| {
2563 let state = this
2564 .follower_states_by_leader
2565 .get_mut(&leader_id)?
2566 .get_mut(&pane)?;
2567
2568 for (id, item) in leader_view_ids.into_iter().zip(items) {
2569 item.set_leader_replica_id(Some(replica_id), cx);
2570 match state.items_by_leader_view_id.entry(id) {
2571 hash_map::Entry::Occupied(e) => {
2572 let e = e.into_mut();
2573 if let FollowerItem::Loading(updates) = e {
2574 for update in updates.drain(..) {
2575 item.apply_update_proto(update, cx)
2576 .context("failed to apply view update")
2577 .log_err();
2578 }
2579 }
2580 *e = FollowerItem::Loaded(item);
2581 }
2582 hash_map::Entry::Vacant(e) => {
2583 e.insert(FollowerItem::Loaded(item));
2584 }
2585 }
2586 }
2587
2588 Some(())
2589 });
2590 }
2591 this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
2592
2593 Ok(())
2594 }
2595
2596 fn update_followers(
2597 &self,
2598 update: proto::update_followers::Variant,
2599 cx: &AppContext,
2600 ) -> Option<()> {
2601 let project_id = self.project.read(cx).remote_id()?;
2602 if !self.leader_state.followers.is_empty() {
2603 self.client
2604 .send(proto::UpdateFollowers {
2605 project_id,
2606 follower_ids: self.leader_state.followers.iter().map(|f| f.0).collect(),
2607 variant: Some(update),
2608 })
2609 .log_err();
2610 }
2611 None
2612 }
2613
2614 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2615 self.follower_states_by_leader
2616 .iter()
2617 .find_map(|(leader_id, state)| {
2618 if state.contains_key(pane) {
2619 Some(*leader_id)
2620 } else {
2621 None
2622 }
2623 })
2624 }
2625
2626 fn update_leader_state(
2627 &mut self,
2628 leader_id: PeerId,
2629 cx: &mut ViewContext<Self>,
2630 mut update_fn: impl FnMut(&mut FollowerState, &mut ViewContext<Self>),
2631 ) {
2632 for (_, state) in self
2633 .follower_states_by_leader
2634 .get_mut(&leader_id)
2635 .into_iter()
2636 .flatten()
2637 {
2638 update_fn(state, cx);
2639 }
2640 self.leader_updated(leader_id, cx);
2641 }
2642
2643 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2644 cx.notify();
2645
2646 let call = self.active_call()?;
2647 let room = call.read(cx).room()?.read(cx);
2648 let participant = room.remote_participants().get(&leader_id)?;
2649
2650 let mut items_to_add = Vec::new();
2651 match participant.location {
2652 call::ParticipantLocation::SharedProject { project_id } => {
2653 if Some(project_id) == self.project.read(cx).remote_id() {
2654 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2655 if let Some(FollowerItem::Loaded(item)) = state
2656 .active_view_id
2657 .and_then(|id| state.items_by_leader_view_id.get(&id))
2658 {
2659 items_to_add.push((pane.clone(), item.boxed_clone()));
2660 }
2661 }
2662 }
2663 }
2664 call::ParticipantLocation::UnsharedProject => {}
2665 call::ParticipantLocation::External => {
2666 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2667 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2668 items_to_add.push((pane.clone(), Box::new(shared_screen)));
2669 }
2670 }
2671 }
2672 }
2673
2674 for (pane, item) in items_to_add {
2675 Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
2676 if pane == self.active_pane {
2677 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2678 }
2679 }
2680
2681 None
2682 }
2683
2684 fn shared_screen_for_peer(
2685 &self,
2686 peer_id: PeerId,
2687 pane: &ViewHandle<Pane>,
2688 cx: &mut ViewContext<Self>,
2689 ) -> Option<ViewHandle<SharedScreen>> {
2690 let call = self.active_call()?;
2691 let room = call.read(cx).room()?.read(cx);
2692 let participant = room.remote_participants().get(&peer_id)?;
2693 let track = participant.tracks.values().next()?.clone();
2694 let user = participant.user.clone();
2695
2696 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2697 if item.read(cx).peer_id == peer_id {
2698 return Some(item);
2699 }
2700 }
2701
2702 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2703 }
2704
2705 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2706 if !active {
2707 for pane in &self.panes {
2708 pane.update(cx, |pane, cx| {
2709 if let Some(item) = pane.active_item() {
2710 item.workspace_deactivated(cx);
2711 }
2712 if matches!(
2713 cx.global::<Settings>().autosave,
2714 Autosave::OnWindowChange | Autosave::OnFocusChange
2715 ) {
2716 for item in pane.items() {
2717 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2718 .detach_and_log_err(cx);
2719 }
2720 }
2721 });
2722 }
2723 }
2724 }
2725
2726 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2727 self.active_call.as_ref().map(|(call, _)| call)
2728 }
2729
2730 fn on_active_call_event(
2731 &mut self,
2732 _: ModelHandle<ActiveCall>,
2733 event: &call::room::Event,
2734 cx: &mut ViewContext<Self>,
2735 ) {
2736 match event {
2737 call::room::Event::ParticipantLocationChanged { participant_id }
2738 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2739 self.leader_updated(*participant_id, cx);
2740 }
2741 _ => {}
2742 }
2743 }
2744}
2745
2746impl Entity for Workspace {
2747 type Event = Event;
2748}
2749
2750impl View for Workspace {
2751 fn ui_name() -> &'static str {
2752 "Workspace"
2753 }
2754
2755 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
2756 let theme = cx.global::<Settings>().theme.clone();
2757 Stack::new()
2758 .with_child(
2759 Flex::column()
2760 .with_child(self.render_titlebar(&theme, cx))
2761 .with_child(
2762 Stack::new()
2763 .with_child({
2764 let project = self.project.clone();
2765 Flex::row()
2766 .with_children(
2767 if self.left_sidebar.read(cx).active_item().is_some() {
2768 Some(
2769 ChildView::new(&self.left_sidebar, cx)
2770 .flex(0.8, false)
2771 .boxed(),
2772 )
2773 } else {
2774 None
2775 },
2776 )
2777 .with_child(
2778 FlexItem::new(
2779 Flex::column()
2780 .with_child(
2781 FlexItem::new(self.center.render(
2782 &project,
2783 &theme,
2784 &self.follower_states_by_leader,
2785 self.active_call(),
2786 cx,
2787 ))
2788 .flex(1., true)
2789 .boxed(),
2790 )
2791 .with_children(self.dock.render(
2792 &theme,
2793 DockAnchor::Bottom,
2794 cx,
2795 ))
2796 .boxed(),
2797 )
2798 .flex(1., true)
2799 .boxed(),
2800 )
2801 .with_children(self.dock.render(&theme, DockAnchor::Right, cx))
2802 .with_children(
2803 if self.right_sidebar.read(cx).active_item().is_some() {
2804 Some(
2805 ChildView::new(&self.right_sidebar, cx)
2806 .flex(0.8, false)
2807 .boxed(),
2808 )
2809 } else {
2810 None
2811 },
2812 )
2813 .boxed()
2814 })
2815 .with_child(
2816 Overlay::new(
2817 Stack::new()
2818 .with_children(self.dock.render(
2819 &theme,
2820 DockAnchor::Expanded,
2821 cx,
2822 ))
2823 .with_children(self.modal.as_ref().map(|modal| {
2824 ChildView::new(modal, cx)
2825 .contained()
2826 .with_style(theme.workspace.modal)
2827 .aligned()
2828 .top()
2829 .boxed()
2830 }))
2831 .with_children(
2832 self.render_notifications(&theme.workspace, cx),
2833 )
2834 .boxed(),
2835 )
2836 .boxed(),
2837 )
2838 .flex(1.0, true)
2839 .boxed(),
2840 )
2841 .with_child(ChildView::new(&self.status_bar, cx).boxed())
2842 .contained()
2843 .with_background_color(theme.workspace.background)
2844 .boxed(),
2845 )
2846 .with_children(DragAndDrop::render(cx))
2847 .with_children(self.render_disconnected_overlay(cx))
2848 .named("workspace")
2849 }
2850
2851 fn focus_in(&mut self, view: AnyViewHandle, cx: &mut ViewContext<Self>) {
2852 if cx.is_self_focused() {
2853 cx.focus(&self.active_pane);
2854 } else {
2855 for pane in self.panes() {
2856 let view = view.clone();
2857 if pane.update(cx, |_, cx| cx.is_child(view)) {
2858 self.handle_pane_focused(pane.clone(), cx);
2859 break;
2860 }
2861 }
2862 }
2863 }
2864
2865 fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
2866 let mut keymap = Self::default_keymap_context();
2867 if self.active_pane() == self.dock_pane() {
2868 keymap.set.insert("Dock".into());
2869 }
2870 keymap
2871 }
2872}
2873
2874pub trait WorkspaceHandle {
2875 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2876}
2877
2878impl WorkspaceHandle for ViewHandle<Workspace> {
2879 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2880 self.read(cx)
2881 .worktrees(cx)
2882 .flat_map(|worktree| {
2883 let worktree_id = worktree.read(cx).id();
2884 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2885 worktree_id,
2886 path: f.path.clone(),
2887 })
2888 })
2889 .collect::<Vec<_>>()
2890 }
2891}
2892
2893impl std::fmt::Debug for OpenPaths {
2894 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2895 f.debug_struct("OpenPaths")
2896 .field("paths", &self.paths)
2897 .finish()
2898 }
2899}
2900
2901fn open(_: &Open, cx: &mut MutableAppContext) {
2902 let mut paths = cx.prompt_for_paths(PathPromptOptions {
2903 files: true,
2904 directories: true,
2905 multiple: true,
2906 });
2907 cx.spawn(|mut cx| async move {
2908 if let Some(paths) = paths.recv().await.flatten() {
2909 cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
2910 }
2911 })
2912 .detach();
2913}
2914
2915pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2916
2917pub fn activate_workspace_for_project(
2918 cx: &mut MutableAppContext,
2919 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
2920) -> Option<ViewHandle<Workspace>> {
2921 for window_id in cx.window_ids().collect::<Vec<_>>() {
2922 if let Some(workspace_handle) = cx.root_view::<Workspace>(window_id) {
2923 let project = workspace_handle.read(cx).project.clone();
2924 if project.update(cx, &predicate) {
2925 cx.activate_window(window_id);
2926 return Some(workspace_handle);
2927 }
2928 }
2929 }
2930 None
2931}
2932
2933#[allow(clippy::type_complexity)]
2934pub fn open_paths(
2935 abs_paths: &[PathBuf],
2936 app_state: &Arc<AppState>,
2937 cx: &mut MutableAppContext,
2938) -> Task<(
2939 ViewHandle<Workspace>,
2940 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
2941)> {
2942 log::info!("open paths {:?}", abs_paths);
2943
2944 // Open paths in existing workspace if possible
2945 let existing =
2946 activate_workspace_for_project(cx, |project, cx| project.contains_paths(abs_paths, cx));
2947
2948 let app_state = app_state.clone();
2949 let abs_paths = abs_paths.to_vec();
2950 cx.spawn(|mut cx| async move {
2951 let mut new_project = None;
2952 let workspace = if let Some(existing) = existing {
2953 existing
2954 } else {
2955 let contains_directory =
2956 futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
2957 .await
2958 .contains(&false);
2959
2960 cx.add_window((app_state.build_window_options)(), |cx| {
2961 let project = Project::local(
2962 app_state.client.clone(),
2963 app_state.user_store.clone(),
2964 app_state.languages.clone(),
2965 app_state.fs.clone(),
2966 cx,
2967 );
2968 new_project = Some(project.clone());
2969 let mut workspace = Workspace::new(project, app_state.default_item_factory, cx);
2970 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
2971 if contains_directory {
2972 workspace.toggle_sidebar(SidebarSide::Left, cx);
2973 }
2974 workspace
2975 })
2976 .1
2977 };
2978
2979 let items = workspace
2980 .update(&mut cx, |workspace, cx| {
2981 workspace.open_paths(abs_paths, true, cx)
2982 })
2983 .await;
2984
2985 (workspace, items)
2986 })
2987}
2988
2989fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
2990 let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
2991 let mut workspace = Workspace::new(
2992 Project::local(
2993 app_state.client.clone(),
2994 app_state.user_store.clone(),
2995 app_state.languages.clone(),
2996 app_state.fs.clone(),
2997 cx,
2998 ),
2999 app_state.default_item_factory,
3000 cx,
3001 );
3002 (app_state.initialize_workspace)(&mut workspace, app_state, cx);
3003 workspace
3004 });
3005 cx.dispatch_action_at(window_id, workspace.id(), NewFile);
3006}
3007
3008#[cfg(test)]
3009mod tests {
3010 use std::cell::Cell;
3011
3012 use crate::sidebar::SidebarItem;
3013
3014 use super::*;
3015 use fs::FakeFs;
3016 use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
3017 use project::{Project, ProjectEntryId};
3018 use serde_json::json;
3019
3020 pub fn default_item_factory(
3021 _workspace: &mut Workspace,
3022 _cx: &mut ViewContext<Workspace>,
3023 ) -> Box<dyn ItemHandle> {
3024 unimplemented!();
3025 }
3026
3027 #[gpui::test]
3028 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
3029 cx.foreground().forbid_parking();
3030 Settings::test_async(cx);
3031
3032 let fs = FakeFs::new(cx.background());
3033 let project = Project::test(fs, [], cx).await;
3034 let (_, workspace) =
3035 cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
3036
3037 // Adding an item with no ambiguity renders the tab without detail.
3038 let item1 = cx.add_view(&workspace, |_| {
3039 let mut item = TestItem::new();
3040 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
3041 item
3042 });
3043 workspace.update(cx, |workspace, cx| {
3044 workspace.add_item(Box::new(item1.clone()), cx);
3045 });
3046 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
3047
3048 // Adding an item that creates ambiguity increases the level of detail on
3049 // both tabs.
3050 let item2 = cx.add_view(&workspace, |_| {
3051 let mut item = TestItem::new();
3052 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3053 item
3054 });
3055 workspace.update(cx, |workspace, cx| {
3056 workspace.add_item(Box::new(item2.clone()), cx);
3057 });
3058 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3059 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3060
3061 // Adding an item that creates ambiguity increases the level of detail only
3062 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
3063 // we stop at the highest detail available.
3064 let item3 = cx.add_view(&workspace, |_| {
3065 let mut item = TestItem::new();
3066 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3067 item
3068 });
3069 workspace.update(cx, |workspace, cx| {
3070 workspace.add_item(Box::new(item3.clone()), cx);
3071 });
3072 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3073 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3074 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3075 }
3076
3077 #[gpui::test]
3078 async fn test_tracking_active_path(cx: &mut TestAppContext) {
3079 cx.foreground().forbid_parking();
3080 Settings::test_async(cx);
3081 let fs = FakeFs::new(cx.background());
3082 fs.insert_tree(
3083 "/root1",
3084 json!({
3085 "one.txt": "",
3086 "two.txt": "",
3087 }),
3088 )
3089 .await;
3090 fs.insert_tree(
3091 "/root2",
3092 json!({
3093 "three.txt": "",
3094 }),
3095 )
3096 .await;
3097
3098 let project = Project::test(fs, ["root1".as_ref()], cx).await;
3099 let (window_id, workspace) =
3100 cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
3101 let worktree_id = project.read_with(cx, |project, cx| {
3102 project.worktrees(cx).next().unwrap().read(cx).id()
3103 });
3104
3105 let item1 = cx.add_view(&workspace, |_| {
3106 let mut item = TestItem::new();
3107 item.project_path = Some((worktree_id, "one.txt").into());
3108 item
3109 });
3110 let item2 = cx.add_view(&workspace, |_| {
3111 let mut item = TestItem::new();
3112 item.project_path = Some((worktree_id, "two.txt").into());
3113 item
3114 });
3115
3116 // Add an item to an empty pane
3117 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3118 project.read_with(cx, |project, cx| {
3119 assert_eq!(
3120 project.active_entry(),
3121 project
3122 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3123 .map(|e| e.id)
3124 );
3125 });
3126 assert_eq!(
3127 cx.current_window_title(window_id).as_deref(),
3128 Some("one.txt — root1")
3129 );
3130
3131 // Add a second item to a non-empty pane
3132 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3133 assert_eq!(
3134 cx.current_window_title(window_id).as_deref(),
3135 Some("two.txt — root1")
3136 );
3137 project.read_with(cx, |project, cx| {
3138 assert_eq!(
3139 project.active_entry(),
3140 project
3141 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3142 .map(|e| e.id)
3143 );
3144 });
3145
3146 // Close the active item
3147 workspace
3148 .update(cx, |workspace, cx| {
3149 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
3150 })
3151 .await
3152 .unwrap();
3153 assert_eq!(
3154 cx.current_window_title(window_id).as_deref(),
3155 Some("one.txt — root1")
3156 );
3157 project.read_with(cx, |project, cx| {
3158 assert_eq!(
3159 project.active_entry(),
3160 project
3161 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3162 .map(|e| e.id)
3163 );
3164 });
3165
3166 // Add a project folder
3167 project
3168 .update(cx, |project, cx| {
3169 project.find_or_create_local_worktree("/root2", true, cx)
3170 })
3171 .await
3172 .unwrap();
3173 assert_eq!(
3174 cx.current_window_title(window_id).as_deref(),
3175 Some("one.txt — root1, root2")
3176 );
3177
3178 // Remove a project folder
3179 project.update(cx, |project, cx| {
3180 project.remove_worktree(worktree_id, cx);
3181 });
3182 assert_eq!(
3183 cx.current_window_title(window_id).as_deref(),
3184 Some("one.txt — root2")
3185 );
3186 }
3187
3188 #[gpui::test]
3189 async fn test_close_window(cx: &mut TestAppContext) {
3190 cx.foreground().forbid_parking();
3191 Settings::test_async(cx);
3192 let fs = FakeFs::new(cx.background());
3193 fs.insert_tree("/root", json!({ "one": "" })).await;
3194
3195 let project = Project::test(fs, ["root".as_ref()], cx).await;
3196 let (window_id, workspace) =
3197 cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
3198
3199 // When there are no dirty items, there's nothing to do.
3200 let item1 = cx.add_view(&workspace, |_| TestItem::new());
3201 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3202 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3203 assert!(task.await.unwrap());
3204
3205 // When there are dirty untitled items, prompt to save each one. If the user
3206 // cancels any prompt, then abort.
3207 let item2 = cx.add_view(&workspace, |_| {
3208 let mut item = TestItem::new();
3209 item.is_dirty = true;
3210 item
3211 });
3212 let item3 = cx.add_view(&workspace, |_| {
3213 let mut item = TestItem::new();
3214 item.is_dirty = true;
3215 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
3216 item
3217 });
3218 workspace.update(cx, |w, cx| {
3219 w.add_item(Box::new(item2.clone()), cx);
3220 w.add_item(Box::new(item3.clone()), cx);
3221 });
3222 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3223 cx.foreground().run_until_parked();
3224 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
3225 cx.foreground().run_until_parked();
3226 assert!(!cx.has_pending_prompt(window_id));
3227 assert!(!task.await.unwrap());
3228 }
3229
3230 #[gpui::test]
3231 async fn test_close_pane_items(cx: &mut TestAppContext) {
3232 cx.foreground().forbid_parking();
3233 Settings::test_async(cx);
3234 let fs = FakeFs::new(cx.background());
3235
3236 let project = Project::test(fs, None, cx).await;
3237 let (window_id, workspace) =
3238 cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
3239
3240 let item1 = cx.add_view(&workspace, |_| {
3241 let mut item = TestItem::new();
3242 item.is_dirty = true;
3243 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
3244 item
3245 });
3246 let item2 = cx.add_view(&workspace, |_| {
3247 let mut item = TestItem::new();
3248 item.is_dirty = true;
3249 item.has_conflict = true;
3250 item.project_entry_ids = vec![ProjectEntryId::from_proto(2)];
3251 item
3252 });
3253 let item3 = cx.add_view(&workspace, |_| {
3254 let mut item = TestItem::new();
3255 item.is_dirty = true;
3256 item.has_conflict = true;
3257 item.project_entry_ids = vec![ProjectEntryId::from_proto(3)];
3258 item
3259 });
3260 let item4 = cx.add_view(&workspace, |_| {
3261 let mut item = TestItem::new();
3262 item.is_dirty = true;
3263 item
3264 });
3265 let pane = workspace.update(cx, |workspace, cx| {
3266 workspace.add_item(Box::new(item1.clone()), cx);
3267 workspace.add_item(Box::new(item2.clone()), cx);
3268 workspace.add_item(Box::new(item3.clone()), cx);
3269 workspace.add_item(Box::new(item4.clone()), cx);
3270 workspace.active_pane().clone()
3271 });
3272
3273 let close_items = workspace.update(cx, |workspace, cx| {
3274 pane.update(cx, |pane, cx| {
3275 pane.activate_item(1, true, true, cx);
3276 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3277 });
3278
3279 let item1_id = item1.id();
3280 let item3_id = item3.id();
3281 let item4_id = item4.id();
3282 Pane::close_items(workspace, pane.clone(), cx, move |id| {
3283 [item1_id, item3_id, item4_id].contains(&id)
3284 })
3285 });
3286
3287 cx.foreground().run_until_parked();
3288 pane.read_with(cx, |pane, _| {
3289 assert_eq!(pane.items_len(), 4);
3290 assert_eq!(pane.active_item().unwrap().id(), item1.id());
3291 });
3292
3293 cx.simulate_prompt_answer(window_id, 0);
3294 cx.foreground().run_until_parked();
3295 pane.read_with(cx, |pane, cx| {
3296 assert_eq!(item1.read(cx).save_count, 1);
3297 assert_eq!(item1.read(cx).save_as_count, 0);
3298 assert_eq!(item1.read(cx).reload_count, 0);
3299 assert_eq!(pane.items_len(), 3);
3300 assert_eq!(pane.active_item().unwrap().id(), item3.id());
3301 });
3302
3303 cx.simulate_prompt_answer(window_id, 1);
3304 cx.foreground().run_until_parked();
3305 pane.read_with(cx, |pane, cx| {
3306 assert_eq!(item3.read(cx).save_count, 0);
3307 assert_eq!(item3.read(cx).save_as_count, 0);
3308 assert_eq!(item3.read(cx).reload_count, 1);
3309 assert_eq!(pane.items_len(), 2);
3310 assert_eq!(pane.active_item().unwrap().id(), item4.id());
3311 });
3312
3313 cx.simulate_prompt_answer(window_id, 0);
3314 cx.foreground().run_until_parked();
3315 cx.simulate_new_path_selection(|_| Some(Default::default()));
3316 close_items.await.unwrap();
3317 pane.read_with(cx, |pane, cx| {
3318 assert_eq!(item4.read(cx).save_count, 0);
3319 assert_eq!(item4.read(cx).save_as_count, 1);
3320 assert_eq!(item4.read(cx).reload_count, 0);
3321 assert_eq!(pane.items_len(), 1);
3322 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3323 });
3324 }
3325
3326 #[gpui::test]
3327 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
3328 cx.foreground().forbid_parking();
3329 Settings::test_async(cx);
3330 let fs = FakeFs::new(cx.background());
3331
3332 let project = Project::test(fs, [], cx).await;
3333 let (window_id, workspace) =
3334 cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
3335
3336 // Create several workspace items with single project entries, and two
3337 // workspace items with multiple project entries.
3338 let single_entry_items = (0..=4)
3339 .map(|project_entry_id| {
3340 let mut item = TestItem::new();
3341 item.is_dirty = true;
3342 item.project_entry_ids = vec![ProjectEntryId::from_proto(project_entry_id)];
3343 item.is_singleton = true;
3344 item
3345 })
3346 .collect::<Vec<_>>();
3347 let item_2_3 = {
3348 let mut item = TestItem::new();
3349 item.is_dirty = true;
3350 item.is_singleton = false;
3351 item.project_entry_ids =
3352 vec![ProjectEntryId::from_proto(2), ProjectEntryId::from_proto(3)];
3353 item
3354 };
3355 let item_3_4 = {
3356 let mut item = TestItem::new();
3357 item.is_dirty = true;
3358 item.is_singleton = false;
3359 item.project_entry_ids =
3360 vec![ProjectEntryId::from_proto(3), ProjectEntryId::from_proto(4)];
3361 item
3362 };
3363
3364 // Create two panes that contain the following project entries:
3365 // left pane:
3366 // multi-entry items: (2, 3)
3367 // single-entry items: 0, 1, 2, 3, 4
3368 // right pane:
3369 // single-entry items: 1
3370 // multi-entry items: (3, 4)
3371 let left_pane = workspace.update(cx, |workspace, cx| {
3372 let left_pane = workspace.active_pane().clone();
3373 workspace.add_item(Box::new(cx.add_view(|_| item_2_3.clone())), cx);
3374 for item in &single_entry_items {
3375 workspace.add_item(Box::new(cx.add_view(|_| item.clone())), cx);
3376 }
3377 left_pane.update(cx, |pane, cx| {
3378 pane.activate_item(2, true, true, cx);
3379 });
3380
3381 workspace
3382 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3383 .unwrap();
3384
3385 left_pane
3386 });
3387
3388 //Need to cause an effect flush in order to respect new focus
3389 workspace.update(cx, |workspace, cx| {
3390 workspace.add_item(Box::new(cx.add_view(|_| item_3_4.clone())), cx);
3391 cx.focus(left_pane.clone());
3392 });
3393
3394 // When closing all of the items in the left pane, we should be prompted twice:
3395 // once for project entry 0, and once for project entry 2. After those two
3396 // prompts, the task should complete.
3397
3398 let close = workspace.update(cx, |workspace, cx| {
3399 Pane::close_items(workspace, left_pane.clone(), cx, |_| true)
3400 });
3401
3402 cx.foreground().run_until_parked();
3403 left_pane.read_with(cx, |pane, cx| {
3404 assert_eq!(
3405 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3406 &[ProjectEntryId::from_proto(0)]
3407 );
3408 });
3409 cx.simulate_prompt_answer(window_id, 0);
3410
3411 cx.foreground().run_until_parked();
3412 left_pane.read_with(cx, |pane, cx| {
3413 assert_eq!(
3414 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3415 &[ProjectEntryId::from_proto(2)]
3416 );
3417 });
3418 cx.simulate_prompt_answer(window_id, 0);
3419
3420 cx.foreground().run_until_parked();
3421 close.await.unwrap();
3422 left_pane.read_with(cx, |pane, _| {
3423 assert_eq!(pane.items_len(), 0);
3424 });
3425 }
3426
3427 #[gpui::test]
3428 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3429 deterministic.forbid_parking();
3430
3431 Settings::test_async(cx);
3432 let fs = FakeFs::new(cx.background());
3433
3434 let project = Project::test(fs, [], cx).await;
3435 let (window_id, workspace) =
3436 cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
3437
3438 let item = cx.add_view(&workspace, |_| {
3439 let mut item = TestItem::new();
3440 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
3441 item
3442 });
3443 let item_id = item.id();
3444 workspace.update(cx, |workspace, cx| {
3445 workspace.add_item(Box::new(item.clone()), cx);
3446 });
3447
3448 // Autosave on window change.
3449 item.update(cx, |item, cx| {
3450 cx.update_global(|settings: &mut Settings, _| {
3451 settings.autosave = Autosave::OnWindowChange;
3452 });
3453 item.is_dirty = true;
3454 });
3455
3456 // Deactivating the window saves the file.
3457 cx.simulate_window_activation(None);
3458 deterministic.run_until_parked();
3459 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3460
3461 // Autosave on focus change.
3462 item.update(cx, |item, cx| {
3463 cx.focus_self();
3464 cx.update_global(|settings: &mut Settings, _| {
3465 settings.autosave = Autosave::OnFocusChange;
3466 });
3467 item.is_dirty = true;
3468 });
3469
3470 // Blurring the item saves the file.
3471 item.update(cx, |_, cx| cx.blur());
3472 deterministic.run_until_parked();
3473 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3474
3475 // Deactivating the window still saves the file.
3476 cx.simulate_window_activation(Some(window_id));
3477 item.update(cx, |item, cx| {
3478 cx.focus_self();
3479 item.is_dirty = true;
3480 });
3481 cx.simulate_window_activation(None);
3482
3483 deterministic.run_until_parked();
3484 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3485
3486 // Autosave after delay.
3487 item.update(cx, |item, cx| {
3488 cx.update_global(|settings: &mut Settings, _| {
3489 settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
3490 });
3491 item.is_dirty = true;
3492 cx.emit(TestItemEvent::Edit);
3493 });
3494
3495 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3496 deterministic.advance_clock(Duration::from_millis(250));
3497 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3498
3499 // After delay expires, the file is saved.
3500 deterministic.advance_clock(Duration::from_millis(250));
3501 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3502
3503 // Autosave on focus change, ensuring closing the tab counts as such.
3504 item.update(cx, |item, cx| {
3505 cx.update_global(|settings: &mut Settings, _| {
3506 settings.autosave = Autosave::OnFocusChange;
3507 });
3508 item.is_dirty = true;
3509 });
3510
3511 workspace
3512 .update(cx, |workspace, cx| {
3513 let pane = workspace.active_pane().clone();
3514 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3515 })
3516 .await
3517 .unwrap();
3518 assert!(!cx.has_pending_prompt(window_id));
3519 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3520
3521 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3522 workspace.update(cx, |workspace, cx| {
3523 workspace.add_item(Box::new(item.clone()), cx);
3524 });
3525 item.update(cx, |item, cx| {
3526 item.project_entry_ids = Default::default();
3527 item.is_dirty = true;
3528 cx.blur();
3529 });
3530 deterministic.run_until_parked();
3531 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3532
3533 // Ensure autosave is prevented for deleted files also when closing the buffer.
3534 let _close_items = workspace.update(cx, |workspace, cx| {
3535 let pane = workspace.active_pane().clone();
3536 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3537 });
3538 deterministic.run_until_parked();
3539 assert!(cx.has_pending_prompt(window_id));
3540 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3541 }
3542
3543 #[gpui::test]
3544 async fn test_pane_navigation(
3545 deterministic: Arc<Deterministic>,
3546 cx: &mut gpui::TestAppContext,
3547 ) {
3548 deterministic.forbid_parking();
3549 Settings::test_async(cx);
3550 let fs = FakeFs::new(cx.background());
3551
3552 let project = Project::test(fs, [], cx).await;
3553 let (_, workspace) = cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
3554
3555 let item = cx.add_view(&workspace, |_| {
3556 let mut item = TestItem::new();
3557 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
3558 item
3559 });
3560 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3561 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3562 let toolbar_notify_count = Rc::new(RefCell::new(0));
3563
3564 workspace.update(cx, |workspace, cx| {
3565 workspace.add_item(Box::new(item.clone()), cx);
3566 let toolbar_notification_count = toolbar_notify_count.clone();
3567 cx.observe(&toolbar, move |_, _, _| {
3568 *toolbar_notification_count.borrow_mut() += 1
3569 })
3570 .detach();
3571 });
3572
3573 pane.read_with(cx, |pane, _| {
3574 assert!(!pane.can_navigate_backward());
3575 assert!(!pane.can_navigate_forward());
3576 });
3577
3578 item.update(cx, |item, cx| {
3579 item.set_state("one".to_string(), cx);
3580 });
3581
3582 // Toolbar must be notified to re-render the navigation buttons
3583 assert_eq!(*toolbar_notify_count.borrow(), 1);
3584
3585 pane.read_with(cx, |pane, _| {
3586 assert!(pane.can_navigate_backward());
3587 assert!(!pane.can_navigate_forward());
3588 });
3589
3590 workspace
3591 .update(cx, |workspace, cx| {
3592 Pane::go_back(workspace, Some(pane.clone()), cx)
3593 })
3594 .await;
3595
3596 assert_eq!(*toolbar_notify_count.borrow(), 3);
3597 pane.read_with(cx, |pane, _| {
3598 assert!(!pane.can_navigate_backward());
3599 assert!(pane.can_navigate_forward());
3600 });
3601 }
3602
3603 pub struct TestItem {
3604 state: String,
3605 pub label: String,
3606 save_count: usize,
3607 save_as_count: usize,
3608 reload_count: usize,
3609 is_dirty: bool,
3610 is_singleton: bool,
3611 has_conflict: bool,
3612 project_entry_ids: Vec<ProjectEntryId>,
3613 project_path: Option<ProjectPath>,
3614 nav_history: Option<ItemNavHistory>,
3615 tab_descriptions: Option<Vec<&'static str>>,
3616 tab_detail: Cell<Option<usize>>,
3617 }
3618
3619 pub enum TestItemEvent {
3620 Edit,
3621 }
3622
3623 impl Clone for TestItem {
3624 fn clone(&self) -> Self {
3625 Self {
3626 state: self.state.clone(),
3627 label: self.label.clone(),
3628 save_count: self.save_count,
3629 save_as_count: self.save_as_count,
3630 reload_count: self.reload_count,
3631 is_dirty: self.is_dirty,
3632 is_singleton: self.is_singleton,
3633 has_conflict: self.has_conflict,
3634 project_entry_ids: self.project_entry_ids.clone(),
3635 project_path: self.project_path.clone(),
3636 nav_history: None,
3637 tab_descriptions: None,
3638 tab_detail: Default::default(),
3639 }
3640 }
3641 }
3642
3643 impl TestItem {
3644 pub fn new() -> Self {
3645 Self {
3646 state: String::new(),
3647 label: String::new(),
3648 save_count: 0,
3649 save_as_count: 0,
3650 reload_count: 0,
3651 is_dirty: false,
3652 has_conflict: false,
3653 project_entry_ids: Vec::new(),
3654 project_path: None,
3655 is_singleton: true,
3656 nav_history: None,
3657 tab_descriptions: None,
3658 tab_detail: Default::default(),
3659 }
3660 }
3661
3662 pub fn with_label(mut self, state: &str) -> Self {
3663 self.label = state.to_string();
3664 self
3665 }
3666
3667 pub fn with_singleton(mut self, singleton: bool) -> Self {
3668 self.is_singleton = singleton;
3669 self
3670 }
3671
3672 pub fn with_project_entry_ids(mut self, project_entry_ids: &[u64]) -> Self {
3673 self.project_entry_ids.extend(
3674 project_entry_ids
3675 .iter()
3676 .copied()
3677 .map(ProjectEntryId::from_proto),
3678 );
3679 self
3680 }
3681
3682 fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
3683 self.push_to_nav_history(cx);
3684 self.state = state;
3685 }
3686
3687 fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
3688 if let Some(history) = &mut self.nav_history {
3689 history.push(Some(Box::new(self.state.clone())), cx);
3690 }
3691 }
3692 }
3693
3694 impl Entity for TestItem {
3695 type Event = TestItemEvent;
3696 }
3697
3698 impl View for TestItem {
3699 fn ui_name() -> &'static str {
3700 "TestItem"
3701 }
3702
3703 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
3704 Empty::new().boxed()
3705 }
3706 }
3707
3708 impl Item for TestItem {
3709 fn tab_description<'a>(&'a self, detail: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
3710 self.tab_descriptions.as_ref().and_then(|descriptions| {
3711 let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
3712 Some(description.into())
3713 })
3714 }
3715
3716 fn tab_content(&self, detail: Option<usize>, _: &theme::Tab, _: &AppContext) -> ElementBox {
3717 self.tab_detail.set(detail);
3718 Empty::new().boxed()
3719 }
3720
3721 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
3722 self.project_path.clone()
3723 }
3724
3725 fn project_entry_ids(&self, _: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
3726 self.project_entry_ids.iter().copied().collect()
3727 }
3728
3729 fn is_singleton(&self, _: &AppContext) -> bool {
3730 self.is_singleton
3731 }
3732
3733 fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
3734 self.nav_history = Some(history);
3735 }
3736
3737 fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
3738 let state = *state.downcast::<String>().unwrap_or_default();
3739 if state != self.state {
3740 self.state = state;
3741 true
3742 } else {
3743 false
3744 }
3745 }
3746
3747 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
3748 self.push_to_nav_history(cx);
3749 }
3750
3751 fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
3752 where
3753 Self: Sized,
3754 {
3755 Some(self.clone())
3756 }
3757
3758 fn is_dirty(&self, _: &AppContext) -> bool {
3759 self.is_dirty
3760 }
3761
3762 fn has_conflict(&self, _: &AppContext) -> bool {
3763 self.has_conflict
3764 }
3765
3766 fn can_save(&self, _: &AppContext) -> bool {
3767 !self.project_entry_ids.is_empty()
3768 }
3769
3770 fn save(
3771 &mut self,
3772 _: ModelHandle<Project>,
3773 _: &mut ViewContext<Self>,
3774 ) -> Task<anyhow::Result<()>> {
3775 self.save_count += 1;
3776 self.is_dirty = false;
3777 Task::ready(Ok(()))
3778 }
3779
3780 fn save_as(
3781 &mut self,
3782 _: ModelHandle<Project>,
3783 _: std::path::PathBuf,
3784 _: &mut ViewContext<Self>,
3785 ) -> Task<anyhow::Result<()>> {
3786 self.save_as_count += 1;
3787 self.is_dirty = false;
3788 Task::ready(Ok(()))
3789 }
3790
3791 fn reload(
3792 &mut self,
3793 _: ModelHandle<Project>,
3794 _: &mut ViewContext<Self>,
3795 ) -> Task<anyhow::Result<()>> {
3796 self.reload_count += 1;
3797 self.is_dirty = false;
3798 Task::ready(Ok(()))
3799 }
3800
3801 fn to_item_events(_: &Self::Event) -> Vec<ItemEvent> {
3802 vec![ItemEvent::UpdateTab, ItemEvent::Edit]
3803 }
3804 }
3805
3806 impl SidebarItem for TestItem {}
3807}