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