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