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