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