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