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