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