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