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