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