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