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