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