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