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>) -> AnyElement<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 MouseEventHandler::<TitleBar, _>::new(0, cx, |_, cx| {
2092 Stack::new()
2093 .with_children(
2094 self.titlebar_item
2095 .as_ref()
2096 .map(|item| ChildView::new(item, cx)),
2097 )
2098 .contained()
2099 .with_style(container_theme)
2100 })
2101 .on_click(MouseButton::Left, |event, _, cx| {
2102 if event.click_count == 2 {
2103 cx.zoom_window();
2104 }
2105 })
2106 .constrained()
2107 .with_height(theme.workspace.titlebar.height)
2108 .into_any_named("titlebar")
2109 }
2110
2111 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2112 let active_entry = self.active_project_path(cx);
2113 self.project
2114 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2115 self.update_window_title(cx);
2116 }
2117
2118 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2119 let project = self.project().read(cx);
2120 let mut title = String::new();
2121
2122 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2123 let filename = path
2124 .path
2125 .file_name()
2126 .map(|s| s.to_string_lossy())
2127 .or_else(|| {
2128 Some(Cow::Borrowed(
2129 project
2130 .worktree_for_id(path.worktree_id, cx)?
2131 .read(cx)
2132 .root_name(),
2133 ))
2134 });
2135
2136 if let Some(filename) = filename {
2137 title.push_str(filename.as_ref());
2138 title.push_str(" — ");
2139 }
2140 }
2141
2142 for (i, name) in project.worktree_root_names(cx).enumerate() {
2143 if i > 0 {
2144 title.push_str(", ");
2145 }
2146 title.push_str(name);
2147 }
2148
2149 if title.is_empty() {
2150 title = "empty project".to_string();
2151 }
2152
2153 if project.is_remote() {
2154 title.push_str(" ↙");
2155 } else if project.is_shared() {
2156 title.push_str(" ↗");
2157 }
2158
2159 cx.set_window_title(&title);
2160 }
2161
2162 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2163 let is_edited = !self.project.read(cx).is_read_only()
2164 && self
2165 .items(cx)
2166 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2167 if is_edited != self.window_edited {
2168 self.window_edited = is_edited;
2169 cx.set_window_edited(self.window_edited)
2170 }
2171 }
2172
2173 fn render_disconnected_overlay(
2174 &self,
2175 cx: &mut ViewContext<Workspace>,
2176 ) -> Option<AnyElement<Workspace>> {
2177 if self.project.read(cx).is_read_only() {
2178 enum DisconnectedOverlay {}
2179 Some(
2180 MouseEventHandler::<DisconnectedOverlay, _>::new(0, cx, |_, cx| {
2181 let theme = &cx.global::<Settings>().theme;
2182 Label::new(
2183 "Your connection to the remote project has been lost.",
2184 theme.workspace.disconnected_overlay.text.clone(),
2185 )
2186 .aligned()
2187 .contained()
2188 .with_style(theme.workspace.disconnected_overlay.container)
2189 })
2190 .with_cursor_style(CursorStyle::Arrow)
2191 .capture_all()
2192 .into_any_named("disconnected overlay"),
2193 )
2194 } else {
2195 None
2196 }
2197 }
2198
2199 fn render_notifications(
2200 &self,
2201 theme: &theme::Workspace,
2202 cx: &AppContext,
2203 ) -> Option<AnyElement<Workspace>> {
2204 if self.notifications.is_empty() {
2205 None
2206 } else {
2207 Some(
2208 Flex::column()
2209 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2210 ChildView::new(notification.as_any(), cx)
2211 .contained()
2212 .with_style(theme.notification)
2213 }))
2214 .constrained()
2215 .with_width(theme.notifications.width)
2216 .contained()
2217 .with_style(theme.notifications.container)
2218 .aligned()
2219 .bottom()
2220 .right()
2221 .into_any(),
2222 )
2223 }
2224 }
2225
2226 // RPC handlers
2227
2228 async fn handle_follow(
2229 this: ViewHandle<Self>,
2230 envelope: TypedEnvelope<proto::Follow>,
2231 _: Arc<Client>,
2232 mut cx: AsyncAppContext,
2233 ) -> Result<proto::FollowResponse> {
2234 this.update(&mut cx, |this, cx| {
2235 let client = &this.client;
2236 this.leader_state
2237 .followers
2238 .insert(envelope.original_sender_id()?);
2239
2240 let active_view_id = this.active_item(cx).and_then(|i| {
2241 Some(
2242 i.to_followable_item_handle(cx)?
2243 .remote_id(client, cx)?
2244 .to_proto(),
2245 )
2246 });
2247
2248 cx.notify();
2249
2250 Ok(proto::FollowResponse {
2251 active_view_id,
2252 views: this
2253 .panes()
2254 .iter()
2255 .flat_map(|pane| {
2256 let leader_id = this.leader_for_pane(pane);
2257 pane.read(cx).items().filter_map({
2258 let cx = &cx;
2259 move |item| {
2260 let item = item.to_followable_item_handle(cx)?;
2261 let id = item.remote_id(client, cx)?.to_proto();
2262 let variant = item.to_state_proto(cx)?;
2263 Some(proto::View {
2264 id: Some(id),
2265 leader_id,
2266 variant: Some(variant),
2267 })
2268 }
2269 })
2270 })
2271 .collect(),
2272 })
2273 })?
2274 }
2275
2276 async fn handle_unfollow(
2277 this: ViewHandle<Self>,
2278 envelope: TypedEnvelope<proto::Unfollow>,
2279 _: Arc<Client>,
2280 mut cx: AsyncAppContext,
2281 ) -> Result<()> {
2282 this.update(&mut cx, |this, cx| {
2283 this.leader_state
2284 .followers
2285 .remove(&envelope.original_sender_id()?);
2286 cx.notify();
2287 Ok(())
2288 })?
2289 }
2290
2291 async fn handle_update_followers(
2292 this: ViewHandle<Self>,
2293 envelope: TypedEnvelope<proto::UpdateFollowers>,
2294 _: Arc<Client>,
2295 cx: AsyncAppContext,
2296 ) -> Result<()> {
2297 let leader_id = envelope.original_sender_id()?;
2298 this.read_with(&cx, |this, _| {
2299 this.leader_updates_tx
2300 .unbounded_send((leader_id, envelope.payload))
2301 })?;
2302 Ok(())
2303 }
2304
2305 async fn process_leader_update(
2306 this: ViewHandle<Self>,
2307 leader_id: PeerId,
2308 update: proto::UpdateFollowers,
2309 cx: &mut AsyncAppContext,
2310 ) -> Result<()> {
2311 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2312 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2313 this.update(cx, |this, _| {
2314 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2315 for state in state.values_mut() {
2316 state.active_view_id =
2317 if let Some(active_view_id) = update_active_view.id.clone() {
2318 Some(ViewId::from_proto(active_view_id)?)
2319 } else {
2320 None
2321 };
2322 }
2323 }
2324 anyhow::Ok(())
2325 })??;
2326 }
2327 proto::update_followers::Variant::UpdateView(update_view) => {
2328 let variant = update_view
2329 .variant
2330 .ok_or_else(|| anyhow!("missing update view variant"))?;
2331 let id = update_view
2332 .id
2333 .ok_or_else(|| anyhow!("missing update view id"))?;
2334 let mut tasks = Vec::new();
2335 this.update(cx, |this, cx| {
2336 let project = this.project.clone();
2337 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2338 for state in state.values_mut() {
2339 let view_id = ViewId::from_proto(id.clone())?;
2340 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2341 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2342 }
2343 }
2344 }
2345 anyhow::Ok(())
2346 })??;
2347 try_join_all(tasks).await.log_err();
2348 }
2349 proto::update_followers::Variant::CreateView(view) => {
2350 let panes = this.read_with(cx, |this, _| {
2351 this.follower_states_by_leader
2352 .get(&leader_id)
2353 .into_iter()
2354 .flat_map(|states_by_pane| states_by_pane.keys())
2355 .cloned()
2356 .collect()
2357 });
2358 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2359 }
2360 }
2361 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2362 Ok(())
2363 }
2364
2365 async fn add_views_from_leader(
2366 this: ViewHandle<Self>,
2367 leader_id: PeerId,
2368 panes: Vec<ViewHandle<Pane>>,
2369 views: Vec<proto::View>,
2370 cx: &mut AsyncAppContext,
2371 ) -> Result<()> {
2372 let project = this.read_with(cx, |this, _| this.project.clone());
2373 let replica_id = project
2374 .read_with(cx, |project, _| {
2375 project
2376 .collaborators()
2377 .get(&leader_id)
2378 .map(|c| c.replica_id)
2379 })
2380 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2381
2382 let item_builders = cx.update(|cx| {
2383 cx.default_global::<FollowableItemBuilders>()
2384 .values()
2385 .map(|b| b.0)
2386 .collect::<Vec<_>>()
2387 });
2388
2389 let mut item_tasks_by_pane = HashMap::default();
2390 for pane in panes {
2391 let mut item_tasks = Vec::new();
2392 let mut leader_view_ids = Vec::new();
2393 for view in &views {
2394 let Some(id) = &view.id else { continue };
2395 let id = ViewId::from_proto(id.clone())?;
2396 let mut variant = view.variant.clone();
2397 if variant.is_none() {
2398 Err(anyhow!("missing variant"))?;
2399 }
2400 for build_item in &item_builders {
2401 let task = cx.update(|cx| {
2402 build_item(pane.clone(), project.clone(), id, &mut variant, cx)
2403 });
2404 if let Some(task) = task {
2405 item_tasks.push(task);
2406 leader_view_ids.push(id);
2407 break;
2408 } else {
2409 assert!(variant.is_some());
2410 }
2411 }
2412 }
2413
2414 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2415 }
2416
2417 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2418 let items = futures::future::try_join_all(item_tasks).await?;
2419 this.update(cx, |this, cx| {
2420 let state = this
2421 .follower_states_by_leader
2422 .get_mut(&leader_id)?
2423 .get_mut(&pane)?;
2424
2425 for (id, item) in leader_view_ids.into_iter().zip(items) {
2426 item.set_leader_replica_id(Some(replica_id), cx);
2427 state.items_by_leader_view_id.insert(id, item);
2428 }
2429
2430 Some(())
2431 })?;
2432 }
2433 Ok(())
2434 }
2435
2436 fn update_followers(
2437 &self,
2438 update: proto::update_followers::Variant,
2439 cx: &AppContext,
2440 ) -> Option<()> {
2441 let project_id = self.project.read(cx).remote_id()?;
2442 if !self.leader_state.followers.is_empty() {
2443 self.client
2444 .send(proto::UpdateFollowers {
2445 project_id,
2446 follower_ids: self.leader_state.followers.iter().copied().collect(),
2447 variant: Some(update),
2448 })
2449 .log_err();
2450 }
2451 None
2452 }
2453
2454 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2455 self.follower_states_by_leader
2456 .iter()
2457 .find_map(|(leader_id, state)| {
2458 if state.contains_key(pane) {
2459 Some(*leader_id)
2460 } else {
2461 None
2462 }
2463 })
2464 }
2465
2466 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2467 cx.notify();
2468
2469 let call = self.active_call()?;
2470 let room = call.read(cx).room()?.read(cx);
2471 let participant = room.remote_participant_for_peer_id(leader_id)?;
2472 let mut items_to_activate = Vec::new();
2473 match participant.location {
2474 call::ParticipantLocation::SharedProject { project_id } => {
2475 if Some(project_id) == self.project.read(cx).remote_id() {
2476 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2477 if let Some(item) = state
2478 .active_view_id
2479 .and_then(|id| state.items_by_leader_view_id.get(&id))
2480 {
2481 items_to_activate.push((pane.clone(), item.boxed_clone()));
2482 } else {
2483 if let Some(shared_screen) =
2484 self.shared_screen_for_peer(leader_id, pane, cx)
2485 {
2486 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2487 }
2488 }
2489 }
2490 }
2491 }
2492 call::ParticipantLocation::UnsharedProject => {}
2493 call::ParticipantLocation::External => {
2494 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2495 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2496 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2497 }
2498 }
2499 }
2500 }
2501
2502 for (pane, item) in items_to_activate {
2503 let active_item_was_focused = pane
2504 .read(cx)
2505 .active_item()
2506 .map(|active_item| cx.is_child_focused(active_item.as_any()))
2507 .unwrap_or_default();
2508
2509 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2510 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2511 } else {
2512 Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
2513 }
2514
2515 if active_item_was_focused {
2516 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2517 }
2518 }
2519
2520 None
2521 }
2522
2523 fn shared_screen_for_peer(
2524 &self,
2525 peer_id: PeerId,
2526 pane: &ViewHandle<Pane>,
2527 cx: &mut ViewContext<Self>,
2528 ) -> Option<ViewHandle<SharedScreen>> {
2529 let call = self.active_call()?;
2530 let room = call.read(cx).room()?.read(cx);
2531 let participant = room.remote_participant_for_peer_id(peer_id)?;
2532 let track = participant.tracks.values().next()?.clone();
2533 let user = participant.user.clone();
2534
2535 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2536 if item.read(cx).peer_id == peer_id {
2537 return Some(item);
2538 }
2539 }
2540
2541 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2542 }
2543
2544 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2545 if active {
2546 cx.background()
2547 .spawn(persistence::DB.update_timestamp(self.database_id()))
2548 .detach();
2549 } else {
2550 for pane in &self.panes {
2551 pane.update(cx, |pane, cx| {
2552 if let Some(item) = pane.active_item() {
2553 item.workspace_deactivated(cx);
2554 }
2555 if matches!(
2556 cx.global::<Settings>().autosave,
2557 Autosave::OnWindowChange | Autosave::OnFocusChange
2558 ) {
2559 for item in pane.items() {
2560 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2561 .detach_and_log_err(cx);
2562 }
2563 }
2564 });
2565 }
2566 }
2567 }
2568
2569 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2570 self.active_call.as_ref().map(|(call, _)| call)
2571 }
2572
2573 fn on_active_call_event(
2574 &mut self,
2575 _: ModelHandle<ActiveCall>,
2576 event: &call::room::Event,
2577 cx: &mut ViewContext<Self>,
2578 ) {
2579 match event {
2580 call::room::Event::ParticipantLocationChanged { participant_id }
2581 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2582 self.leader_updated(*participant_id, cx);
2583 }
2584 _ => {}
2585 }
2586 }
2587
2588 pub fn database_id(&self) -> WorkspaceId {
2589 self.database_id
2590 }
2591
2592 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2593 let project = self.project().read(cx);
2594
2595 if project.is_local() {
2596 Some(
2597 project
2598 .visible_worktrees(cx)
2599 .map(|worktree| worktree.read(cx).abs_path())
2600 .collect::<Vec<_>>()
2601 .into(),
2602 )
2603 } else {
2604 None
2605 }
2606 }
2607
2608 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2609 match member {
2610 Member::Axis(PaneAxis { members, .. }) => {
2611 for child in members.iter() {
2612 self.remove_panes(child.clone(), cx)
2613 }
2614 }
2615 Member::Pane(pane) => self.remove_pane(pane.clone(), cx),
2616 }
2617 }
2618
2619 fn serialize_workspace(&self, cx: &AppContext) {
2620 fn serialize_pane_handle(
2621 pane_handle: &ViewHandle<Pane>,
2622 cx: &AppContext,
2623 ) -> SerializedPane {
2624 let (items, active) = {
2625 let pane = pane_handle.read(cx);
2626 let active_item_id = pane.active_item().map(|item| item.id());
2627 (
2628 pane.items()
2629 .filter_map(|item_handle| {
2630 Some(SerializedItem {
2631 kind: Arc::from(item_handle.serialized_item_kind()?),
2632 item_id: item_handle.id(),
2633 active: Some(item_handle.id()) == active_item_id,
2634 })
2635 })
2636 .collect::<Vec<_>>(),
2637 pane.is_active(),
2638 )
2639 };
2640
2641 SerializedPane::new(items, active)
2642 }
2643
2644 fn build_serialized_pane_group(
2645 pane_group: &Member,
2646 cx: &AppContext,
2647 ) -> SerializedPaneGroup {
2648 match pane_group {
2649 Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
2650 axis: *axis,
2651 children: members
2652 .iter()
2653 .map(|member| build_serialized_pane_group(member, cx))
2654 .collect::<Vec<_>>(),
2655 },
2656 Member::Pane(pane_handle) => {
2657 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2658 }
2659 }
2660 }
2661
2662 if let Some(location) = self.location(cx) {
2663 // Load bearing special case:
2664 // - with_local_workspace() relies on this to not have other stuff open
2665 // when you open your log
2666 if !location.paths().is_empty() {
2667 let dock_pane = serialize_pane_handle(self.dock.pane(), cx);
2668 let center_group = build_serialized_pane_group(&self.center.root, cx);
2669
2670 let serialized_workspace = SerializedWorkspace {
2671 id: self.database_id,
2672 location,
2673 dock_position: self.dock.position(),
2674 dock_pane,
2675 center_group,
2676 left_sidebar_open: self.left_sidebar.read(cx).is_open(),
2677 bounds: Default::default(),
2678 display: Default::default(),
2679 };
2680
2681 cx.background()
2682 .spawn(persistence::DB.save_workspace(serialized_workspace))
2683 .detach();
2684 }
2685 }
2686 }
2687
2688 fn load_from_serialized_workspace(
2689 workspace: WeakViewHandle<Workspace>,
2690 serialized_workspace: SerializedWorkspace,
2691 cx: &mut AppContext,
2692 ) {
2693 cx.spawn(|mut cx| async move {
2694 if let Some(workspace) = workspace.upgrade(&cx) {
2695 let (project, dock_pane_handle, old_center_pane) =
2696 workspace.read_with(&cx, |workspace, _| {
2697 (
2698 workspace.project().clone(),
2699 workspace.dock_pane().clone(),
2700 workspace.last_active_center_pane.clone(),
2701 )
2702 });
2703
2704 serialized_workspace
2705 .dock_pane
2706 .deserialize_to(
2707 &project,
2708 &dock_pane_handle,
2709 serialized_workspace.id,
2710 &workspace,
2711 &mut cx,
2712 )
2713 .await?;
2714
2715 // Traverse the splits tree and add to things
2716 let center_group = serialized_workspace
2717 .center_group
2718 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
2719 .await;
2720
2721 // Remove old panes from workspace panes list
2722 workspace.update(&mut cx, |workspace, cx| {
2723 if let Some((center_group, active_pane)) = center_group {
2724 workspace.remove_panes(workspace.center.root.clone(), cx);
2725
2726 // Swap workspace center group
2727 workspace.center = PaneGroup::with_root(center_group);
2728
2729 // Change the focus to the workspace first so that we retrigger focus in on the pane.
2730 cx.focus_self();
2731
2732 if let Some(active_pane) = active_pane {
2733 cx.focus(&active_pane);
2734 } else {
2735 cx.focus(workspace.panes.last().unwrap());
2736 }
2737 } else {
2738 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
2739 if let Some(old_center_handle) = old_center_handle {
2740 cx.focus(&old_center_handle)
2741 } else {
2742 cx.focus_self()
2743 }
2744 }
2745
2746 if workspace.left_sidebar().read(cx).is_open()
2747 != serialized_workspace.left_sidebar_open
2748 {
2749 workspace.toggle_sidebar(SidebarSide::Left, cx);
2750 }
2751
2752 // Note that without after_window, the focus_self() and
2753 // the focus the dock generates start generating alternating
2754 // focus due to the deferred execution each triggering each other
2755 cx.after_window_update(move |workspace, cx| {
2756 Dock::set_dock_position(
2757 workspace,
2758 serialized_workspace.dock_position,
2759 true,
2760 cx,
2761 );
2762 });
2763
2764 cx.notify();
2765 })?;
2766
2767 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
2768 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))
2769 }
2770 anyhow::Ok(())
2771 })
2772 .detach_and_log_err(cx);
2773 }
2774
2775 #[cfg(any(test, feature = "test-support"))]
2776 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
2777 Self::new(None, 0, project, |_, _| None, || &[], cx)
2778 }
2779}
2780
2781fn notify_if_database_failed(workspace: &ViewHandle<Workspace>, cx: &mut AsyncAppContext) {
2782 workspace.update(cx, |workspace, cx| {
2783 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
2784 workspace.show_notification_once(0, cx, |cx| {
2785 cx.add_view(|_| {
2786 MessageNotification::new(
2787 "Failed to load any database file.",
2788 OsOpen::new("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()),
2789 "Click to let us know about this error"
2790 )
2791 })
2792 });
2793 } else {
2794 let backup_path = (*db::BACKUP_DB_PATH).read();
2795 if let Some(backup_path) = &*backup_path {
2796 workspace.show_notification_once(0, cx, |cx| {
2797 cx.add_view(|_| {
2798 let backup_path = backup_path.to_string_lossy();
2799 MessageNotification::new(
2800 format!(
2801 "Database file was corrupted. Old database backed up to {}",
2802 backup_path
2803 ),
2804 OsOpen::new(backup_path.to_string()),
2805 "Click to show old database in finder",
2806 )
2807 })
2808 });
2809 }
2810 }
2811 }).log_err();
2812}
2813
2814impl Entity for Workspace {
2815 type Event = Event;
2816}
2817
2818impl View for Workspace {
2819 fn ui_name() -> &'static str {
2820 "Workspace"
2821 }
2822
2823 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2824 let theme = cx.global::<Settings>().theme.clone();
2825 Stack::new()
2826 .with_child(
2827 Flex::column()
2828 .with_child(self.render_titlebar(&theme, cx))
2829 .with_child(
2830 Stack::new()
2831 .with_child({
2832 let project = self.project.clone();
2833 Flex::row()
2834 .with_children(
2835 if self.left_sidebar.read(cx).active_item().is_some() {
2836 Some(
2837 ChildView::new(&self.left_sidebar, cx)
2838 .constrained()
2839 .dynamically(|constraint, _, cx| {
2840 SizeConstraint::new(
2841 Vector2F::new(20., constraint.min.y()),
2842 Vector2F::new(
2843 cx.window_size().x() * 0.8,
2844 constraint.max.y(),
2845 ),
2846 )
2847 }),
2848 )
2849 } else {
2850 None
2851 },
2852 )
2853 .with_child(
2854 FlexItem::new(
2855 Flex::column()
2856 .with_child(
2857 FlexItem::new(self.center.render(
2858 &project,
2859 &theme,
2860 &self.follower_states_by_leader,
2861 self.active_call(),
2862 self.active_pane(),
2863 cx,
2864 ))
2865 .flex(1., true),
2866 )
2867 .with_children(self.dock.render(
2868 &theme,
2869 DockAnchor::Bottom,
2870 cx,
2871 )),
2872 )
2873 .flex(1., true),
2874 )
2875 .with_children(self.dock.render(&theme, DockAnchor::Right, cx))
2876 .with_children(
2877 if self.right_sidebar.read(cx).active_item().is_some() {
2878 Some(
2879 ChildView::new(&self.right_sidebar, cx)
2880 .constrained()
2881 .dynamically(|constraint, _, cx| {
2882 SizeConstraint::new(
2883 Vector2F::new(20., constraint.min.y()),
2884 Vector2F::new(
2885 cx.window_size().x() * 0.8,
2886 constraint.max.y(),
2887 ),
2888 )
2889 }),
2890 )
2891 } else {
2892 None
2893 },
2894 )
2895 })
2896 .with_child(Overlay::new(
2897 Stack::new()
2898 .with_children(self.dock.render(
2899 &theme,
2900 DockAnchor::Expanded,
2901 cx,
2902 ))
2903 .with_children(self.modal.as_ref().map(|modal| {
2904 ChildView::new(modal, cx)
2905 .contained()
2906 .with_style(theme.workspace.modal)
2907 .aligned()
2908 .top()
2909 }))
2910 .with_children(self.render_notifications(&theme.workspace, cx)),
2911 ))
2912 .flex(1.0, true),
2913 )
2914 .with_child(ChildView::new(&self.status_bar, cx))
2915 .contained()
2916 .with_background_color(theme.workspace.background),
2917 )
2918 .with_children(DragAndDrop::render(cx))
2919 .with_children(self.render_disconnected_overlay(cx))
2920 .into_any_named("workspace")
2921 }
2922
2923 fn focus_in(&mut self, view: AnyViewHandle, cx: &mut ViewContext<Self>) {
2924 if cx.is_self_focused() {
2925 cx.focus(&self.active_pane);
2926 } else {
2927 for pane in self.panes() {
2928 let view = view.clone();
2929 if pane.update(cx, |_, cx| view.id() == cx.view_id() || cx.is_child(view)) {
2930 self.handle_pane_focused(pane.clone(), cx);
2931 break;
2932 }
2933 }
2934 }
2935 }
2936
2937 fn keymap_context(&self, _: &AppContext) -> KeymapContext {
2938 Self::default_keymap_context()
2939 }
2940}
2941
2942impl ViewId {
2943 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
2944 Ok(Self {
2945 creator: message
2946 .creator
2947 .ok_or_else(|| anyhow!("creator is missing"))?,
2948 id: message.id,
2949 })
2950 }
2951
2952 pub(crate) fn to_proto(&self) -> proto::ViewId {
2953 proto::ViewId {
2954 creator: Some(self.creator),
2955 id: self.id,
2956 }
2957 }
2958}
2959
2960pub trait WorkspaceHandle {
2961 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2962}
2963
2964impl WorkspaceHandle for ViewHandle<Workspace> {
2965 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2966 self.read(cx)
2967 .worktrees(cx)
2968 .flat_map(|worktree| {
2969 let worktree_id = worktree.read(cx).id();
2970 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2971 worktree_id,
2972 path: f.path.clone(),
2973 })
2974 })
2975 .collect::<Vec<_>>()
2976 }
2977}
2978
2979impl std::fmt::Debug for OpenPaths {
2980 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2981 f.debug_struct("OpenPaths")
2982 .field("paths", &self.paths)
2983 .finish()
2984 }
2985}
2986
2987pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2988
2989pub fn activate_workspace_for_project(
2990 cx: &mut AppContext,
2991 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
2992) -> Option<ViewHandle<Workspace>> {
2993 for window_id in cx.window_ids().collect::<Vec<_>>() {
2994 let handle = cx
2995 .update_window(window_id, |cx| {
2996 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
2997 let project = workspace_handle.read(cx).project.clone();
2998 if project.update(cx, &predicate) {
2999 cx.activate_window();
3000 return Some(workspace_handle.clone());
3001 }
3002 }
3003 None
3004 })
3005 .flatten();
3006
3007 if handle.is_some() {
3008 return handle;
3009 }
3010 }
3011 None
3012}
3013
3014pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3015 DB.last_workspace().await.log_err().flatten()
3016}
3017
3018#[allow(clippy::type_complexity)]
3019pub fn open_paths(
3020 abs_paths: &[PathBuf],
3021 app_state: &Arc<AppState>,
3022 requesting_window_id: Option<usize>,
3023 cx: &mut AppContext,
3024) -> Task<
3025 Result<(
3026 ViewHandle<Workspace>,
3027 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
3028 )>,
3029> {
3030 log::info!("open paths {:?}", abs_paths);
3031
3032 // Open paths in existing workspace if possible
3033 let existing =
3034 activate_workspace_for_project(cx, |project, cx| project.contains_paths(abs_paths, cx));
3035
3036 let app_state = app_state.clone();
3037 let abs_paths = abs_paths.to_vec();
3038 cx.spawn(|mut cx| async move {
3039 if let Some(existing) = existing {
3040 Ok((
3041 existing.clone(),
3042 existing
3043 .update(&mut cx, |workspace, cx| {
3044 workspace.open_paths(abs_paths, true, cx)
3045 })?
3046 .await,
3047 ))
3048 } else {
3049 let contains_directory =
3050 futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
3051 .await
3052 .contains(&false);
3053
3054 cx.update(|cx| {
3055 let task =
3056 Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx);
3057
3058 cx.spawn(|mut cx| async move {
3059 let (workspace, items) = task.await;
3060
3061 workspace.update(&mut cx, |workspace, cx| {
3062 if contains_directory {
3063 workspace.toggle_sidebar(SidebarSide::Left, cx);
3064 }
3065 })?;
3066
3067 anyhow::Ok((workspace, items))
3068 })
3069 })
3070 .await
3071 }
3072 })
3073}
3074
3075pub fn open_new(
3076 app_state: &Arc<AppState>,
3077 cx: &mut AppContext,
3078 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
3079) -> Task<()> {
3080 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
3081 cx.spawn(|mut cx| async move {
3082 let (workspace, opened_paths) = task.await;
3083
3084 workspace
3085 .update(&mut cx, |workspace, cx| {
3086 if opened_paths.is_empty() {
3087 init(workspace, cx)
3088 }
3089 })
3090 .log_err();
3091 })
3092}
3093
3094fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
3095 let mut parts = value.split(',');
3096 let width: usize = parts.next()?.parse().ok()?;
3097 let height: usize = parts.next()?.parse().ok()?;
3098 Some(vec2f(width as f32, height as f32))
3099}
3100
3101#[cfg(test)]
3102mod tests {
3103 use std::{cell::RefCell, rc::Rc};
3104
3105 use crate::item::test::{TestItem, TestItemEvent, TestProjectItem};
3106
3107 use super::*;
3108 use fs::FakeFs;
3109 use gpui::{executor::Deterministic, TestAppContext};
3110 use project::{Project, ProjectEntryId};
3111 use serde_json::json;
3112
3113 #[gpui::test]
3114 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
3115 cx.foreground().forbid_parking();
3116 Settings::test_async(cx);
3117
3118 let fs = FakeFs::new(cx.background());
3119 let project = Project::test(fs, [], cx).await;
3120 let (_, workspace) = cx.add_window(|cx| {
3121 Workspace::new(
3122 Default::default(),
3123 0,
3124 project.clone(),
3125 |_, _| None,
3126 || &[],
3127 cx,
3128 )
3129 });
3130
3131 // Adding an item with no ambiguity renders the tab without detail.
3132 let item1 = cx.add_view(&workspace, |_| {
3133 let mut item = TestItem::new();
3134 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
3135 item
3136 });
3137 workspace.update(cx, |workspace, cx| {
3138 workspace.add_item(Box::new(item1.clone()), cx);
3139 });
3140 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
3141
3142 // Adding an item that creates ambiguity increases the level of detail on
3143 // both tabs.
3144 let item2 = cx.add_view(&workspace, |_| {
3145 let mut item = TestItem::new();
3146 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3147 item
3148 });
3149 workspace.update(cx, |workspace, cx| {
3150 workspace.add_item(Box::new(item2.clone()), cx);
3151 });
3152 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3153 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3154
3155 // Adding an item that creates ambiguity increases the level of detail only
3156 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
3157 // we stop at the highest detail available.
3158 let item3 = cx.add_view(&workspace, |_| {
3159 let mut item = TestItem::new();
3160 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3161 item
3162 });
3163 workspace.update(cx, |workspace, cx| {
3164 workspace.add_item(Box::new(item3.clone()), cx);
3165 });
3166 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3167 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3168 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3169 }
3170
3171 #[gpui::test]
3172 async fn test_tracking_active_path(cx: &mut TestAppContext) {
3173 cx.foreground().forbid_parking();
3174 Settings::test_async(cx);
3175 let fs = FakeFs::new(cx.background());
3176 fs.insert_tree(
3177 "/root1",
3178 json!({
3179 "one.txt": "",
3180 "two.txt": "",
3181 }),
3182 )
3183 .await;
3184 fs.insert_tree(
3185 "/root2",
3186 json!({
3187 "three.txt": "",
3188 }),
3189 )
3190 .await;
3191
3192 let project = Project::test(fs, ["root1".as_ref()], cx).await;
3193 let (window_id, workspace) = cx.add_window(|cx| {
3194 Workspace::new(
3195 Default::default(),
3196 0,
3197 project.clone(),
3198 |_, _| None,
3199 || &[],
3200 cx,
3201 )
3202 });
3203 let worktree_id = project.read_with(cx, |project, cx| {
3204 project.worktrees(cx).next().unwrap().read(cx).id()
3205 });
3206
3207 let item1 = cx.add_view(&workspace, |cx| {
3208 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3209 });
3210 let item2 = cx.add_view(&workspace, |cx| {
3211 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3212 });
3213
3214 // Add an item to an empty pane
3215 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3216 project.read_with(cx, |project, cx| {
3217 assert_eq!(
3218 project.active_entry(),
3219 project
3220 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3221 .map(|e| e.id)
3222 );
3223 });
3224 assert_eq!(
3225 cx.current_window_title(window_id).as_deref(),
3226 Some("one.txt — root1")
3227 );
3228
3229 // Add a second item to a non-empty pane
3230 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3231 assert_eq!(
3232 cx.current_window_title(window_id).as_deref(),
3233 Some("two.txt — root1")
3234 );
3235 project.read_with(cx, |project, cx| {
3236 assert_eq!(
3237 project.active_entry(),
3238 project
3239 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3240 .map(|e| e.id)
3241 );
3242 });
3243
3244 // Close the active item
3245 workspace
3246 .update(cx, |workspace, cx| {
3247 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
3248 })
3249 .await
3250 .unwrap();
3251 assert_eq!(
3252 cx.current_window_title(window_id).as_deref(),
3253 Some("one.txt — root1")
3254 );
3255 project.read_with(cx, |project, cx| {
3256 assert_eq!(
3257 project.active_entry(),
3258 project
3259 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3260 .map(|e| e.id)
3261 );
3262 });
3263
3264 // Add a project folder
3265 project
3266 .update(cx, |project, cx| {
3267 project.find_or_create_local_worktree("/root2", true, cx)
3268 })
3269 .await
3270 .unwrap();
3271 assert_eq!(
3272 cx.current_window_title(window_id).as_deref(),
3273 Some("one.txt — root1, root2")
3274 );
3275
3276 // Remove a project folder
3277 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3278 assert_eq!(
3279 cx.current_window_title(window_id).as_deref(),
3280 Some("one.txt — root2")
3281 );
3282 }
3283
3284 #[gpui::test]
3285 async fn test_close_window(cx: &mut TestAppContext) {
3286 cx.foreground().forbid_parking();
3287 Settings::test_async(cx);
3288 let fs = FakeFs::new(cx.background());
3289 fs.insert_tree("/root", json!({ "one": "" })).await;
3290
3291 let project = Project::test(fs, ["root".as_ref()], cx).await;
3292 let (window_id, workspace) = cx.add_window(|cx| {
3293 Workspace::new(
3294 Default::default(),
3295 0,
3296 project.clone(),
3297 |_, _| None,
3298 || &[],
3299 cx,
3300 )
3301 });
3302
3303 // When there are no dirty items, there's nothing to do.
3304 let item1 = cx.add_view(&workspace, |_| TestItem::new());
3305 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3306 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3307 assert!(task.await.unwrap());
3308
3309 // When there are dirty untitled items, prompt to save each one. If the user
3310 // cancels any prompt, then abort.
3311 let item2 = cx.add_view(&workspace, |_| TestItem::new().with_dirty(true));
3312 let item3 = cx.add_view(&workspace, |cx| {
3313 TestItem::new()
3314 .with_dirty(true)
3315 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3316 });
3317 workspace.update(cx, |w, cx| {
3318 w.add_item(Box::new(item2.clone()), cx);
3319 w.add_item(Box::new(item3.clone()), cx);
3320 });
3321 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3322 cx.foreground().run_until_parked();
3323 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
3324 cx.foreground().run_until_parked();
3325 assert!(!cx.has_pending_prompt(window_id));
3326 assert!(!task.await.unwrap());
3327 }
3328
3329 #[gpui::test]
3330 async fn test_close_pane_items(cx: &mut TestAppContext) {
3331 cx.foreground().forbid_parking();
3332 Settings::test_async(cx);
3333 let fs = FakeFs::new(cx.background());
3334
3335 let project = Project::test(fs, None, cx).await;
3336 let (window_id, workspace) = cx.add_window(|cx| {
3337 Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
3338 });
3339
3340 let item1 = cx.add_view(&workspace, |cx| {
3341 TestItem::new()
3342 .with_dirty(true)
3343 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3344 });
3345 let item2 = cx.add_view(&workspace, |cx| {
3346 TestItem::new()
3347 .with_dirty(true)
3348 .with_conflict(true)
3349 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3350 });
3351 let item3 = cx.add_view(&workspace, |cx| {
3352 TestItem::new()
3353 .with_dirty(true)
3354 .with_conflict(true)
3355 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
3356 });
3357 let item4 = cx.add_view(&workspace, |cx| {
3358 TestItem::new()
3359 .with_dirty(true)
3360 .with_project_items(&[TestProjectItem::new_untitled(cx)])
3361 });
3362 let pane = workspace.update(cx, |workspace, cx| {
3363 workspace.add_item(Box::new(item1.clone()), cx);
3364 workspace.add_item(Box::new(item2.clone()), cx);
3365 workspace.add_item(Box::new(item3.clone()), cx);
3366 workspace.add_item(Box::new(item4.clone()), cx);
3367 workspace.active_pane().clone()
3368 });
3369
3370 let close_items = workspace.update(cx, |workspace, cx| {
3371 pane.update(cx, |pane, cx| {
3372 pane.activate_item(1, true, true, cx);
3373 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3374 });
3375
3376 let item1_id = item1.id();
3377 let item3_id = item3.id();
3378 let item4_id = item4.id();
3379 Pane::close_items(workspace, pane.clone(), cx, move |id| {
3380 [item1_id, item3_id, item4_id].contains(&id)
3381 })
3382 });
3383 cx.foreground().run_until_parked();
3384
3385 // There's a prompt to save item 1.
3386 pane.read_with(cx, |pane, _| {
3387 assert_eq!(pane.items_len(), 4);
3388 assert_eq!(pane.active_item().unwrap().id(), item1.id());
3389 });
3390 assert!(cx.has_pending_prompt(window_id));
3391
3392 // Confirm saving item 1.
3393 cx.simulate_prompt_answer(window_id, 0);
3394 cx.foreground().run_until_parked();
3395
3396 // Item 1 is saved. There's a prompt to save item 3.
3397 pane.read_with(cx, |pane, cx| {
3398 assert_eq!(item1.read(cx).save_count, 1);
3399 assert_eq!(item1.read(cx).save_as_count, 0);
3400 assert_eq!(item1.read(cx).reload_count, 0);
3401 assert_eq!(pane.items_len(), 3);
3402 assert_eq!(pane.active_item().unwrap().id(), item3.id());
3403 });
3404 assert!(cx.has_pending_prompt(window_id));
3405
3406 // Cancel saving item 3.
3407 cx.simulate_prompt_answer(window_id, 1);
3408 cx.foreground().run_until_parked();
3409
3410 // Item 3 is reloaded. There's a prompt to save item 4.
3411 pane.read_with(cx, |pane, cx| {
3412 assert_eq!(item3.read(cx).save_count, 0);
3413 assert_eq!(item3.read(cx).save_as_count, 0);
3414 assert_eq!(item3.read(cx).reload_count, 1);
3415 assert_eq!(pane.items_len(), 2);
3416 assert_eq!(pane.active_item().unwrap().id(), item4.id());
3417 });
3418 assert!(cx.has_pending_prompt(window_id));
3419
3420 // Confirm saving item 4.
3421 cx.simulate_prompt_answer(window_id, 0);
3422 cx.foreground().run_until_parked();
3423
3424 // There's a prompt for a path for item 4.
3425 cx.simulate_new_path_selection(|_| Some(Default::default()));
3426 close_items.await.unwrap();
3427
3428 // The requested items are closed.
3429 pane.read_with(cx, |pane, cx| {
3430 assert_eq!(item4.read(cx).save_count, 0);
3431 assert_eq!(item4.read(cx).save_as_count, 1);
3432 assert_eq!(item4.read(cx).reload_count, 0);
3433 assert_eq!(pane.items_len(), 1);
3434 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3435 });
3436 }
3437
3438 #[gpui::test]
3439 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
3440 cx.foreground().forbid_parking();
3441 Settings::test_async(cx);
3442 let fs = FakeFs::new(cx.background());
3443
3444 let project = Project::test(fs, [], cx).await;
3445 let (window_id, workspace) = cx.add_window(|cx| {
3446 Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
3447 });
3448
3449 // Create several workspace items with single project entries, and two
3450 // workspace items with multiple project entries.
3451 let single_entry_items = (0..=4)
3452 .map(|project_entry_id| {
3453 cx.add_view(&workspace, |cx| {
3454 TestItem::new()
3455 .with_dirty(true)
3456 .with_project_items(&[TestProjectItem::new(
3457 project_entry_id,
3458 &format!("{project_entry_id}.txt"),
3459 cx,
3460 )])
3461 })
3462 })
3463 .collect::<Vec<_>>();
3464 let item_2_3 = cx.add_view(&workspace, |cx| {
3465 TestItem::new()
3466 .with_dirty(true)
3467 .with_singleton(false)
3468 .with_project_items(&[
3469 single_entry_items[2].read(cx).project_items[0].clone(),
3470 single_entry_items[3].read(cx).project_items[0].clone(),
3471 ])
3472 });
3473 let item_3_4 = cx.add_view(&workspace, |cx| {
3474 TestItem::new()
3475 .with_dirty(true)
3476 .with_singleton(false)
3477 .with_project_items(&[
3478 single_entry_items[3].read(cx).project_items[0].clone(),
3479 single_entry_items[4].read(cx).project_items[0].clone(),
3480 ])
3481 });
3482
3483 // Create two panes that contain the following project entries:
3484 // left pane:
3485 // multi-entry items: (2, 3)
3486 // single-entry items: 0, 1, 2, 3, 4
3487 // right pane:
3488 // single-entry items: 1
3489 // multi-entry items: (3, 4)
3490 let left_pane = workspace.update(cx, |workspace, cx| {
3491 let left_pane = workspace.active_pane().clone();
3492 workspace.add_item(Box::new(item_2_3.clone()), cx);
3493 for item in single_entry_items {
3494 workspace.add_item(Box::new(item), cx);
3495 }
3496 left_pane.update(cx, |pane, cx| {
3497 pane.activate_item(2, true, true, cx);
3498 });
3499
3500 workspace
3501 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3502 .unwrap();
3503
3504 left_pane
3505 });
3506
3507 //Need to cause an effect flush in order to respect new focus
3508 workspace.update(cx, |workspace, cx| {
3509 workspace.add_item(Box::new(item_3_4.clone()), cx);
3510 cx.focus(&left_pane);
3511 });
3512
3513 // When closing all of the items in the left pane, we should be prompted twice:
3514 // once for project entry 0, and once for project entry 2. After those two
3515 // prompts, the task should complete.
3516
3517 let close = workspace.update(cx, |workspace, cx| {
3518 Pane::close_items(workspace, left_pane.clone(), cx, |_| true)
3519 });
3520
3521 cx.foreground().run_until_parked();
3522 left_pane.read_with(cx, |pane, cx| {
3523 assert_eq!(
3524 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3525 &[ProjectEntryId::from_proto(0)]
3526 );
3527 });
3528 cx.simulate_prompt_answer(window_id, 0);
3529
3530 cx.foreground().run_until_parked();
3531 left_pane.read_with(cx, |pane, cx| {
3532 assert_eq!(
3533 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3534 &[ProjectEntryId::from_proto(2)]
3535 );
3536 });
3537 cx.simulate_prompt_answer(window_id, 0);
3538
3539 cx.foreground().run_until_parked();
3540 close.await.unwrap();
3541 left_pane.read_with(cx, |pane, _| {
3542 assert_eq!(pane.items_len(), 0);
3543 });
3544 }
3545
3546 #[gpui::test]
3547 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3548 deterministic.forbid_parking();
3549
3550 Settings::test_async(cx);
3551 let fs = FakeFs::new(cx.background());
3552
3553 let project = Project::test(fs, [], cx).await;
3554 let (window_id, workspace) = cx.add_window(|cx| {
3555 Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
3556 });
3557
3558 let item = cx.add_view(&workspace, |cx| {
3559 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3560 });
3561 let item_id = item.id();
3562 workspace.update(cx, |workspace, cx| {
3563 workspace.add_item(Box::new(item.clone()), cx);
3564 });
3565
3566 // Autosave on window change.
3567 item.update(cx, |item, cx| {
3568 cx.update_global(|settings: &mut Settings, _| {
3569 settings.autosave = Autosave::OnWindowChange;
3570 });
3571 item.is_dirty = true;
3572 });
3573
3574 // Deactivating the window saves the file.
3575 cx.simulate_window_activation(None);
3576 deterministic.run_until_parked();
3577 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3578
3579 // Autosave on focus change.
3580 item.update(cx, |item, cx| {
3581 cx.focus_self();
3582 cx.update_global(|settings: &mut Settings, _| {
3583 settings.autosave = Autosave::OnFocusChange;
3584 });
3585 item.is_dirty = true;
3586 });
3587
3588 // Blurring the item saves the file.
3589 item.update(cx, |_, cx| cx.blur());
3590 deterministic.run_until_parked();
3591 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3592
3593 // Deactivating the window still saves the file.
3594 cx.simulate_window_activation(Some(window_id));
3595 item.update(cx, |item, cx| {
3596 cx.focus_self();
3597 item.is_dirty = true;
3598 });
3599 cx.simulate_window_activation(None);
3600
3601 deterministic.run_until_parked();
3602 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3603
3604 // Autosave after delay.
3605 item.update(cx, |item, cx| {
3606 cx.update_global(|settings: &mut Settings, _| {
3607 settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
3608 });
3609 item.is_dirty = true;
3610 cx.emit(TestItemEvent::Edit);
3611 });
3612
3613 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3614 deterministic.advance_clock(Duration::from_millis(250));
3615 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3616
3617 // After delay expires, the file is saved.
3618 deterministic.advance_clock(Duration::from_millis(250));
3619 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3620
3621 // Autosave on focus change, ensuring closing the tab counts as such.
3622 item.update(cx, |item, cx| {
3623 cx.update_global(|settings: &mut Settings, _| {
3624 settings.autosave = Autosave::OnFocusChange;
3625 });
3626 item.is_dirty = true;
3627 });
3628
3629 workspace
3630 .update(cx, |workspace, cx| {
3631 let pane = workspace.active_pane().clone();
3632 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3633 })
3634 .await
3635 .unwrap();
3636 assert!(!cx.has_pending_prompt(window_id));
3637 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3638
3639 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3640 workspace.update(cx, |workspace, cx| {
3641 workspace.add_item(Box::new(item.clone()), cx);
3642 });
3643 item.update(cx, |item, cx| {
3644 item.project_items[0].update(cx, |item, _| {
3645 item.entry_id = None;
3646 });
3647 item.is_dirty = true;
3648 cx.blur();
3649 });
3650 deterministic.run_until_parked();
3651 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3652
3653 // Ensure autosave is prevented for deleted files also when closing the buffer.
3654 let _close_items = workspace.update(cx, |workspace, cx| {
3655 let pane = workspace.active_pane().clone();
3656 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3657 });
3658 deterministic.run_until_parked();
3659 assert!(cx.has_pending_prompt(window_id));
3660 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3661 }
3662
3663 #[gpui::test]
3664 async fn test_pane_navigation(
3665 deterministic: Arc<Deterministic>,
3666 cx: &mut gpui::TestAppContext,
3667 ) {
3668 deterministic.forbid_parking();
3669 Settings::test_async(cx);
3670 let fs = FakeFs::new(cx.background());
3671
3672 let project = Project::test(fs, [], cx).await;
3673 let (_, workspace) = cx.add_window(|cx| {
3674 Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
3675 });
3676
3677 let item = cx.add_view(&workspace, |cx| {
3678 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3679 });
3680 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3681 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3682 let toolbar_notify_count = Rc::new(RefCell::new(0));
3683
3684 workspace.update(cx, |workspace, cx| {
3685 workspace.add_item(Box::new(item.clone()), cx);
3686 let toolbar_notification_count = toolbar_notify_count.clone();
3687 cx.observe(&toolbar, move |_, _, _| {
3688 *toolbar_notification_count.borrow_mut() += 1
3689 })
3690 .detach();
3691 });
3692
3693 pane.read_with(cx, |pane, _| {
3694 assert!(!pane.can_navigate_backward());
3695 assert!(!pane.can_navigate_forward());
3696 });
3697
3698 item.update(cx, |item, cx| {
3699 item.set_state("one".to_string(), cx);
3700 });
3701
3702 // Toolbar must be notified to re-render the navigation buttons
3703 assert_eq!(*toolbar_notify_count.borrow(), 1);
3704
3705 pane.read_with(cx, |pane, _| {
3706 assert!(pane.can_navigate_backward());
3707 assert!(!pane.can_navigate_forward());
3708 });
3709
3710 workspace
3711 .update(cx, |workspace, cx| {
3712 Pane::go_back(workspace, Some(pane.clone()), cx)
3713 })
3714 .await
3715 .unwrap();
3716
3717 assert_eq!(*toolbar_notify_count.borrow(), 3);
3718 pane.read_with(cx, |pane, _| {
3719 assert!(!pane.can_navigate_backward());
3720 assert!(pane.can_navigate_forward());
3721 });
3722 }
3723}