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