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 indoc::indoc! {"
2788 Failed to load any database file :(
2789 "},
2790 OsOpen::new("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()),
2791 "Click to let us know about this error"
2792 )
2793 })
2794 });
2795 } else {
2796 let backup_path = (*db::BACKUP_DB_PATH).read();
2797 if let Some(backup_path) = &*backup_path {
2798 workspace.show_notification_once(0, cx, |cx| {
2799 cx.add_view(|_| {
2800 let backup_path = backup_path.to_string_lossy();
2801 MessageNotification::new(
2802 format!(
2803 indoc::indoc! {"
2804 Database file was corrupted :(
2805 Old database backed up to:
2806 {}
2807 "},
2808 backup_path
2809 ),
2810 OsOpen::new(backup_path.to_string()),
2811 "Click to show old database in finder",
2812 )
2813 })
2814 });
2815 }
2816 }
2817 }).log_err();
2818}
2819
2820impl Entity for Workspace {
2821 type Event = Event;
2822}
2823
2824impl View for Workspace {
2825 fn ui_name() -> &'static str {
2826 "Workspace"
2827 }
2828
2829 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2830 let theme = cx.global::<Settings>().theme.clone();
2831 Stack::new()
2832 .with_child(
2833 Flex::column()
2834 .with_child(self.render_titlebar(&theme, cx))
2835 .with_child(
2836 Stack::new()
2837 .with_child({
2838 let project = self.project.clone();
2839 Flex::row()
2840 .with_children(
2841 if self.left_sidebar.read(cx).active_item().is_some() {
2842 Some(
2843 ChildView::new(&self.left_sidebar, cx)
2844 .constrained()
2845 .dynamically(|constraint, _, cx| {
2846 SizeConstraint::new(
2847 Vector2F::new(20., constraint.min.y()),
2848 Vector2F::new(
2849 cx.window_size().x() * 0.8,
2850 constraint.max.y(),
2851 ),
2852 )
2853 }),
2854 )
2855 } else {
2856 None
2857 },
2858 )
2859 .with_child(
2860 FlexItem::new(
2861 Flex::column()
2862 .with_child(
2863 FlexItem::new(self.center.render(
2864 &project,
2865 &theme,
2866 &self.follower_states_by_leader,
2867 self.active_call(),
2868 self.active_pane(),
2869 cx,
2870 ))
2871 .flex(1., true),
2872 )
2873 .with_children(self.dock.render(
2874 &theme,
2875 DockAnchor::Bottom,
2876 cx,
2877 )),
2878 )
2879 .flex(1., true),
2880 )
2881 .with_children(self.dock.render(&theme, DockAnchor::Right, cx))
2882 .with_children(
2883 if self.right_sidebar.read(cx).active_item().is_some() {
2884 Some(
2885 ChildView::new(&self.right_sidebar, cx)
2886 .constrained()
2887 .dynamically(|constraint, _, cx| {
2888 SizeConstraint::new(
2889 Vector2F::new(20., constraint.min.y()),
2890 Vector2F::new(
2891 cx.window_size().x() * 0.8,
2892 constraint.max.y(),
2893 ),
2894 )
2895 }),
2896 )
2897 } else {
2898 None
2899 },
2900 )
2901 })
2902 .with_child(Overlay::new(
2903 Stack::new()
2904 .with_children(self.dock.render(
2905 &theme,
2906 DockAnchor::Expanded,
2907 cx,
2908 ))
2909 .with_children(self.modal.as_ref().map(|modal| {
2910 ChildView::new(modal, cx)
2911 .contained()
2912 .with_style(theme.workspace.modal)
2913 .aligned()
2914 .top()
2915 }))
2916 .with_children(self.render_notifications(&theme.workspace, cx)),
2917 ))
2918 .flex(1.0, true),
2919 )
2920 .with_child(ChildView::new(&self.status_bar, cx))
2921 .contained()
2922 .with_background_color(theme.workspace.background),
2923 )
2924 .with_children(DragAndDrop::render(cx))
2925 .with_children(self.render_disconnected_overlay(cx))
2926 .into_any_named("workspace")
2927 }
2928
2929 fn focus_in(&mut self, view: AnyViewHandle, cx: &mut ViewContext<Self>) {
2930 if cx.is_self_focused() {
2931 cx.focus(&self.active_pane);
2932 } else {
2933 for pane in self.panes() {
2934 let view = view.clone();
2935 if pane.update(cx, |_, cx| view.id() == cx.view_id() || cx.is_child(view)) {
2936 self.handle_pane_focused(pane.clone(), cx);
2937 break;
2938 }
2939 }
2940 }
2941 }
2942
2943 fn keymap_context(&self, _: &AppContext) -> KeymapContext {
2944 Self::default_keymap_context()
2945 }
2946}
2947
2948impl ViewId {
2949 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
2950 Ok(Self {
2951 creator: message
2952 .creator
2953 .ok_or_else(|| anyhow!("creator is missing"))?,
2954 id: message.id,
2955 })
2956 }
2957
2958 pub(crate) fn to_proto(&self) -> proto::ViewId {
2959 proto::ViewId {
2960 creator: Some(self.creator),
2961 id: self.id,
2962 }
2963 }
2964}
2965
2966pub trait WorkspaceHandle {
2967 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2968}
2969
2970impl WorkspaceHandle for ViewHandle<Workspace> {
2971 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2972 self.read(cx)
2973 .worktrees(cx)
2974 .flat_map(|worktree| {
2975 let worktree_id = worktree.read(cx).id();
2976 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2977 worktree_id,
2978 path: f.path.clone(),
2979 })
2980 })
2981 .collect::<Vec<_>>()
2982 }
2983}
2984
2985impl std::fmt::Debug for OpenPaths {
2986 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2987 f.debug_struct("OpenPaths")
2988 .field("paths", &self.paths)
2989 .finish()
2990 }
2991}
2992
2993pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2994
2995pub fn activate_workspace_for_project(
2996 cx: &mut AppContext,
2997 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
2998) -> Option<ViewHandle<Workspace>> {
2999 for window_id in cx.window_ids().collect::<Vec<_>>() {
3000 let handle = cx
3001 .update_window(window_id, |cx| {
3002 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
3003 let project = workspace_handle.read(cx).project.clone();
3004 if project.update(cx, &predicate) {
3005 cx.activate_window();
3006 return Some(workspace_handle.clone());
3007 }
3008 }
3009 None
3010 })
3011 .flatten();
3012
3013 if handle.is_some() {
3014 return handle;
3015 }
3016 }
3017 None
3018}
3019
3020pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3021 DB.last_workspace().await.log_err().flatten()
3022}
3023
3024#[allow(clippy::type_complexity)]
3025pub fn open_paths(
3026 abs_paths: &[PathBuf],
3027 app_state: &Arc<AppState>,
3028 requesting_window_id: Option<usize>,
3029 cx: &mut AppContext,
3030) -> Task<
3031 Result<(
3032 ViewHandle<Workspace>,
3033 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
3034 )>,
3035> {
3036 log::info!("open paths {:?}", abs_paths);
3037
3038 // Open paths in existing workspace if possible
3039 let existing =
3040 activate_workspace_for_project(cx, |project, cx| project.contains_paths(abs_paths, cx));
3041
3042 let app_state = app_state.clone();
3043 let abs_paths = abs_paths.to_vec();
3044 cx.spawn(|mut cx| async move {
3045 if let Some(existing) = existing {
3046 Ok((
3047 existing.clone(),
3048 existing
3049 .update(&mut cx, |workspace, cx| {
3050 workspace.open_paths(abs_paths, true, cx)
3051 })?
3052 .await,
3053 ))
3054 } else {
3055 let contains_directory =
3056 futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
3057 .await
3058 .contains(&false);
3059
3060 cx.update(|cx| {
3061 let task =
3062 Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx);
3063
3064 cx.spawn(|mut cx| async move {
3065 let (workspace, items) = task.await;
3066
3067 workspace.update(&mut cx, |workspace, cx| {
3068 if contains_directory {
3069 workspace.toggle_sidebar(SidebarSide::Left, cx);
3070 }
3071 })?;
3072
3073 anyhow::Ok((workspace, items))
3074 })
3075 })
3076 .await
3077 }
3078 })
3079}
3080
3081pub fn open_new(
3082 app_state: &Arc<AppState>,
3083 cx: &mut AppContext,
3084 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
3085) -> Task<()> {
3086 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
3087 cx.spawn(|mut cx| async move {
3088 let (workspace, opened_paths) = task.await;
3089
3090 workspace
3091 .update(&mut cx, |workspace, cx| {
3092 if opened_paths.is_empty() {
3093 init(workspace, cx)
3094 }
3095 })
3096 .log_err();
3097 })
3098}
3099
3100fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
3101 let mut parts = value.split(',');
3102 let width: usize = parts.next()?.parse().ok()?;
3103 let height: usize = parts.next()?.parse().ok()?;
3104 Some(vec2f(width as f32, height as f32))
3105}
3106
3107#[cfg(test)]
3108mod tests {
3109 use std::{cell::RefCell, rc::Rc};
3110
3111 use crate::item::test::{TestItem, TestItemEvent, TestProjectItem};
3112
3113 use super::*;
3114 use fs::FakeFs;
3115 use gpui::{executor::Deterministic, TestAppContext};
3116 use project::{Project, ProjectEntryId};
3117 use serde_json::json;
3118
3119 #[gpui::test]
3120 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
3121 cx.foreground().forbid_parking();
3122 Settings::test_async(cx);
3123
3124 let fs = FakeFs::new(cx.background());
3125 let project = Project::test(fs, [], cx).await;
3126 let (_, workspace) = cx.add_window(|cx| {
3127 Workspace::new(
3128 Default::default(),
3129 0,
3130 project.clone(),
3131 |_, _| None,
3132 || &[],
3133 cx,
3134 )
3135 });
3136
3137 // Adding an item with no ambiguity renders the tab without detail.
3138 let item1 = cx.add_view(&workspace, |_| {
3139 let mut item = TestItem::new();
3140 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
3141 item
3142 });
3143 workspace.update(cx, |workspace, cx| {
3144 workspace.add_item(Box::new(item1.clone()), cx);
3145 });
3146 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
3147
3148 // Adding an item that creates ambiguity increases the level of detail on
3149 // both tabs.
3150 let item2 = cx.add_view(&workspace, |_| {
3151 let mut item = TestItem::new();
3152 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3153 item
3154 });
3155 workspace.update(cx, |workspace, cx| {
3156 workspace.add_item(Box::new(item2.clone()), cx);
3157 });
3158 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3159 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3160
3161 // Adding an item that creates ambiguity increases the level of detail only
3162 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
3163 // we stop at the highest detail available.
3164 let item3 = cx.add_view(&workspace, |_| {
3165 let mut item = TestItem::new();
3166 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3167 item
3168 });
3169 workspace.update(cx, |workspace, cx| {
3170 workspace.add_item(Box::new(item3.clone()), cx);
3171 });
3172 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3173 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3174 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3175 }
3176
3177 #[gpui::test]
3178 async fn test_tracking_active_path(cx: &mut TestAppContext) {
3179 cx.foreground().forbid_parking();
3180 Settings::test_async(cx);
3181 let fs = FakeFs::new(cx.background());
3182 fs.insert_tree(
3183 "/root1",
3184 json!({
3185 "one.txt": "",
3186 "two.txt": "",
3187 }),
3188 )
3189 .await;
3190 fs.insert_tree(
3191 "/root2",
3192 json!({
3193 "three.txt": "",
3194 }),
3195 )
3196 .await;
3197
3198 let project = Project::test(fs, ["root1".as_ref()], cx).await;
3199 let (window_id, workspace) = cx.add_window(|cx| {
3200 Workspace::new(
3201 Default::default(),
3202 0,
3203 project.clone(),
3204 |_, _| None,
3205 || &[],
3206 cx,
3207 )
3208 });
3209 let worktree_id = project.read_with(cx, |project, cx| {
3210 project.worktrees(cx).next().unwrap().read(cx).id()
3211 });
3212
3213 let item1 = cx.add_view(&workspace, |cx| {
3214 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3215 });
3216 let item2 = cx.add_view(&workspace, |cx| {
3217 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3218 });
3219
3220 // Add an item to an empty pane
3221 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3222 project.read_with(cx, |project, cx| {
3223 assert_eq!(
3224 project.active_entry(),
3225 project
3226 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3227 .map(|e| e.id)
3228 );
3229 });
3230 assert_eq!(
3231 cx.current_window_title(window_id).as_deref(),
3232 Some("one.txt — root1")
3233 );
3234
3235 // Add a second item to a non-empty pane
3236 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3237 assert_eq!(
3238 cx.current_window_title(window_id).as_deref(),
3239 Some("two.txt — root1")
3240 );
3241 project.read_with(cx, |project, cx| {
3242 assert_eq!(
3243 project.active_entry(),
3244 project
3245 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3246 .map(|e| e.id)
3247 );
3248 });
3249
3250 // Close the active item
3251 workspace
3252 .update(cx, |workspace, cx| {
3253 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
3254 })
3255 .await
3256 .unwrap();
3257 assert_eq!(
3258 cx.current_window_title(window_id).as_deref(),
3259 Some("one.txt — root1")
3260 );
3261 project.read_with(cx, |project, cx| {
3262 assert_eq!(
3263 project.active_entry(),
3264 project
3265 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3266 .map(|e| e.id)
3267 );
3268 });
3269
3270 // Add a project folder
3271 project
3272 .update(cx, |project, cx| {
3273 project.find_or_create_local_worktree("/root2", true, cx)
3274 })
3275 .await
3276 .unwrap();
3277 assert_eq!(
3278 cx.current_window_title(window_id).as_deref(),
3279 Some("one.txt — root1, root2")
3280 );
3281
3282 // Remove a project folder
3283 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3284 assert_eq!(
3285 cx.current_window_title(window_id).as_deref(),
3286 Some("one.txt — root2")
3287 );
3288 }
3289
3290 #[gpui::test]
3291 async fn test_close_window(cx: &mut TestAppContext) {
3292 cx.foreground().forbid_parking();
3293 Settings::test_async(cx);
3294 let fs = FakeFs::new(cx.background());
3295 fs.insert_tree("/root", json!({ "one": "" })).await;
3296
3297 let project = Project::test(fs, ["root".as_ref()], cx).await;
3298 let (window_id, workspace) = cx.add_window(|cx| {
3299 Workspace::new(
3300 Default::default(),
3301 0,
3302 project.clone(),
3303 |_, _| None,
3304 || &[],
3305 cx,
3306 )
3307 });
3308
3309 // When there are no dirty items, there's nothing to do.
3310 let item1 = cx.add_view(&workspace, |_| TestItem::new());
3311 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3312 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3313 assert!(task.await.unwrap());
3314
3315 // When there are dirty untitled items, prompt to save each one. If the user
3316 // cancels any prompt, then abort.
3317 let item2 = cx.add_view(&workspace, |_| TestItem::new().with_dirty(true));
3318 let item3 = cx.add_view(&workspace, |cx| {
3319 TestItem::new()
3320 .with_dirty(true)
3321 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3322 });
3323 workspace.update(cx, |w, cx| {
3324 w.add_item(Box::new(item2.clone()), cx);
3325 w.add_item(Box::new(item3.clone()), cx);
3326 });
3327 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3328 cx.foreground().run_until_parked();
3329 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
3330 cx.foreground().run_until_parked();
3331 assert!(!cx.has_pending_prompt(window_id));
3332 assert!(!task.await.unwrap());
3333 }
3334
3335 #[gpui::test]
3336 async fn test_close_pane_items(cx: &mut TestAppContext) {
3337 cx.foreground().forbid_parking();
3338 Settings::test_async(cx);
3339 let fs = FakeFs::new(cx.background());
3340
3341 let project = Project::test(fs, None, cx).await;
3342 let (window_id, workspace) = cx.add_window(|cx| {
3343 Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
3344 });
3345
3346 let item1 = cx.add_view(&workspace, |cx| {
3347 TestItem::new()
3348 .with_dirty(true)
3349 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3350 });
3351 let item2 = cx.add_view(&workspace, |cx| {
3352 TestItem::new()
3353 .with_dirty(true)
3354 .with_conflict(true)
3355 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3356 });
3357 let item3 = cx.add_view(&workspace, |cx| {
3358 TestItem::new()
3359 .with_dirty(true)
3360 .with_conflict(true)
3361 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
3362 });
3363 let item4 = cx.add_view(&workspace, |cx| {
3364 TestItem::new()
3365 .with_dirty(true)
3366 .with_project_items(&[TestProjectItem::new_untitled(cx)])
3367 });
3368 let pane = workspace.update(cx, |workspace, cx| {
3369 workspace.add_item(Box::new(item1.clone()), cx);
3370 workspace.add_item(Box::new(item2.clone()), cx);
3371 workspace.add_item(Box::new(item3.clone()), cx);
3372 workspace.add_item(Box::new(item4.clone()), cx);
3373 workspace.active_pane().clone()
3374 });
3375
3376 let close_items = workspace.update(cx, |workspace, cx| {
3377 pane.update(cx, |pane, cx| {
3378 pane.activate_item(1, true, true, cx);
3379 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3380 });
3381
3382 let item1_id = item1.id();
3383 let item3_id = item3.id();
3384 let item4_id = item4.id();
3385 Pane::close_items(workspace, pane.clone(), cx, move |id| {
3386 [item1_id, item3_id, item4_id].contains(&id)
3387 })
3388 });
3389 cx.foreground().run_until_parked();
3390
3391 // There's a prompt to save item 1.
3392 pane.read_with(cx, |pane, _| {
3393 assert_eq!(pane.items_len(), 4);
3394 assert_eq!(pane.active_item().unwrap().id(), item1.id());
3395 });
3396 assert!(cx.has_pending_prompt(window_id));
3397
3398 // Confirm saving item 1.
3399 cx.simulate_prompt_answer(window_id, 0);
3400 cx.foreground().run_until_parked();
3401
3402 // Item 1 is saved. There's a prompt to save item 3.
3403 pane.read_with(cx, |pane, cx| {
3404 assert_eq!(item1.read(cx).save_count, 1);
3405 assert_eq!(item1.read(cx).save_as_count, 0);
3406 assert_eq!(item1.read(cx).reload_count, 0);
3407 assert_eq!(pane.items_len(), 3);
3408 assert_eq!(pane.active_item().unwrap().id(), item3.id());
3409 });
3410 assert!(cx.has_pending_prompt(window_id));
3411
3412 // Cancel saving item 3.
3413 cx.simulate_prompt_answer(window_id, 1);
3414 cx.foreground().run_until_parked();
3415
3416 // Item 3 is reloaded. There's a prompt to save item 4.
3417 pane.read_with(cx, |pane, cx| {
3418 assert_eq!(item3.read(cx).save_count, 0);
3419 assert_eq!(item3.read(cx).save_as_count, 0);
3420 assert_eq!(item3.read(cx).reload_count, 1);
3421 assert_eq!(pane.items_len(), 2);
3422 assert_eq!(pane.active_item().unwrap().id(), item4.id());
3423 });
3424 assert!(cx.has_pending_prompt(window_id));
3425
3426 // Confirm saving item 4.
3427 cx.simulate_prompt_answer(window_id, 0);
3428 cx.foreground().run_until_parked();
3429
3430 // There's a prompt for a path for item 4.
3431 cx.simulate_new_path_selection(|_| Some(Default::default()));
3432 close_items.await.unwrap();
3433
3434 // The requested items are closed.
3435 pane.read_with(cx, |pane, cx| {
3436 assert_eq!(item4.read(cx).save_count, 0);
3437 assert_eq!(item4.read(cx).save_as_count, 1);
3438 assert_eq!(item4.read(cx).reload_count, 0);
3439 assert_eq!(pane.items_len(), 1);
3440 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3441 });
3442 }
3443
3444 #[gpui::test]
3445 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
3446 cx.foreground().forbid_parking();
3447 Settings::test_async(cx);
3448 let fs = FakeFs::new(cx.background());
3449
3450 let project = Project::test(fs, [], cx).await;
3451 let (window_id, workspace) = cx.add_window(|cx| {
3452 Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
3453 });
3454
3455 // Create several workspace items with single project entries, and two
3456 // workspace items with multiple project entries.
3457 let single_entry_items = (0..=4)
3458 .map(|project_entry_id| {
3459 cx.add_view(&workspace, |cx| {
3460 TestItem::new()
3461 .with_dirty(true)
3462 .with_project_items(&[TestProjectItem::new(
3463 project_entry_id,
3464 &format!("{project_entry_id}.txt"),
3465 cx,
3466 )])
3467 })
3468 })
3469 .collect::<Vec<_>>();
3470 let item_2_3 = cx.add_view(&workspace, |cx| {
3471 TestItem::new()
3472 .with_dirty(true)
3473 .with_singleton(false)
3474 .with_project_items(&[
3475 single_entry_items[2].read(cx).project_items[0].clone(),
3476 single_entry_items[3].read(cx).project_items[0].clone(),
3477 ])
3478 });
3479 let item_3_4 = cx.add_view(&workspace, |cx| {
3480 TestItem::new()
3481 .with_dirty(true)
3482 .with_singleton(false)
3483 .with_project_items(&[
3484 single_entry_items[3].read(cx).project_items[0].clone(),
3485 single_entry_items[4].read(cx).project_items[0].clone(),
3486 ])
3487 });
3488
3489 // Create two panes that contain the following project entries:
3490 // left pane:
3491 // multi-entry items: (2, 3)
3492 // single-entry items: 0, 1, 2, 3, 4
3493 // right pane:
3494 // single-entry items: 1
3495 // multi-entry items: (3, 4)
3496 let left_pane = workspace.update(cx, |workspace, cx| {
3497 let left_pane = workspace.active_pane().clone();
3498 workspace.add_item(Box::new(item_2_3.clone()), cx);
3499 for item in single_entry_items {
3500 workspace.add_item(Box::new(item), cx);
3501 }
3502 left_pane.update(cx, |pane, cx| {
3503 pane.activate_item(2, true, true, cx);
3504 });
3505
3506 workspace
3507 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3508 .unwrap();
3509
3510 left_pane
3511 });
3512
3513 //Need to cause an effect flush in order to respect new focus
3514 workspace.update(cx, |workspace, cx| {
3515 workspace.add_item(Box::new(item_3_4.clone()), cx);
3516 cx.focus(&left_pane);
3517 });
3518
3519 // When closing all of the items in the left pane, we should be prompted twice:
3520 // once for project entry 0, and once for project entry 2. After those two
3521 // prompts, the task should complete.
3522
3523 let close = workspace.update(cx, |workspace, cx| {
3524 Pane::close_items(workspace, left_pane.clone(), cx, |_| true)
3525 });
3526
3527 cx.foreground().run_until_parked();
3528 left_pane.read_with(cx, |pane, cx| {
3529 assert_eq!(
3530 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3531 &[ProjectEntryId::from_proto(0)]
3532 );
3533 });
3534 cx.simulate_prompt_answer(window_id, 0);
3535
3536 cx.foreground().run_until_parked();
3537 left_pane.read_with(cx, |pane, cx| {
3538 assert_eq!(
3539 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3540 &[ProjectEntryId::from_proto(2)]
3541 );
3542 });
3543 cx.simulate_prompt_answer(window_id, 0);
3544
3545 cx.foreground().run_until_parked();
3546 close.await.unwrap();
3547 left_pane.read_with(cx, |pane, _| {
3548 assert_eq!(pane.items_len(), 0);
3549 });
3550 }
3551
3552 #[gpui::test]
3553 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3554 deterministic.forbid_parking();
3555
3556 Settings::test_async(cx);
3557 let fs = FakeFs::new(cx.background());
3558
3559 let project = Project::test(fs, [], cx).await;
3560 let (window_id, workspace) = cx.add_window(|cx| {
3561 Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
3562 });
3563
3564 let item = cx.add_view(&workspace, |cx| {
3565 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3566 });
3567 let item_id = item.id();
3568 workspace.update(cx, |workspace, cx| {
3569 workspace.add_item(Box::new(item.clone()), cx);
3570 });
3571
3572 // Autosave on window change.
3573 item.update(cx, |item, cx| {
3574 cx.update_global(|settings: &mut Settings, _| {
3575 settings.autosave = Autosave::OnWindowChange;
3576 });
3577 item.is_dirty = true;
3578 });
3579
3580 // Deactivating the window saves the file.
3581 cx.simulate_window_activation(None);
3582 deterministic.run_until_parked();
3583 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3584
3585 // Autosave on focus change.
3586 item.update(cx, |item, cx| {
3587 cx.focus_self();
3588 cx.update_global(|settings: &mut Settings, _| {
3589 settings.autosave = Autosave::OnFocusChange;
3590 });
3591 item.is_dirty = true;
3592 });
3593
3594 // Blurring the item saves the file.
3595 item.update(cx, |_, cx| cx.blur());
3596 deterministic.run_until_parked();
3597 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3598
3599 // Deactivating the window still saves the file.
3600 cx.simulate_window_activation(Some(window_id));
3601 item.update(cx, |item, cx| {
3602 cx.focus_self();
3603 item.is_dirty = true;
3604 });
3605 cx.simulate_window_activation(None);
3606
3607 deterministic.run_until_parked();
3608 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3609
3610 // Autosave after delay.
3611 item.update(cx, |item, cx| {
3612 cx.update_global(|settings: &mut Settings, _| {
3613 settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
3614 });
3615 item.is_dirty = true;
3616 cx.emit(TestItemEvent::Edit);
3617 });
3618
3619 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3620 deterministic.advance_clock(Duration::from_millis(250));
3621 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3622
3623 // After delay expires, the file is saved.
3624 deterministic.advance_clock(Duration::from_millis(250));
3625 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3626
3627 // Autosave on focus change, ensuring closing the tab counts as such.
3628 item.update(cx, |item, cx| {
3629 cx.update_global(|settings: &mut Settings, _| {
3630 settings.autosave = Autosave::OnFocusChange;
3631 });
3632 item.is_dirty = true;
3633 });
3634
3635 workspace
3636 .update(cx, |workspace, cx| {
3637 let pane = workspace.active_pane().clone();
3638 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3639 })
3640 .await
3641 .unwrap();
3642 assert!(!cx.has_pending_prompt(window_id));
3643 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3644
3645 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3646 workspace.update(cx, |workspace, cx| {
3647 workspace.add_item(Box::new(item.clone()), cx);
3648 });
3649 item.update(cx, |item, cx| {
3650 item.project_items[0].update(cx, |item, _| {
3651 item.entry_id = None;
3652 });
3653 item.is_dirty = true;
3654 cx.blur();
3655 });
3656 deterministic.run_until_parked();
3657 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3658
3659 // Ensure autosave is prevented for deleted files also when closing the buffer.
3660 let _close_items = workspace.update(cx, |workspace, cx| {
3661 let pane = workspace.active_pane().clone();
3662 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3663 });
3664 deterministic.run_until_parked();
3665 assert!(cx.has_pending_prompt(window_id));
3666 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3667 }
3668
3669 #[gpui::test]
3670 async fn test_pane_navigation(
3671 deterministic: Arc<Deterministic>,
3672 cx: &mut gpui::TestAppContext,
3673 ) {
3674 deterministic.forbid_parking();
3675 Settings::test_async(cx);
3676 let fs = FakeFs::new(cx.background());
3677
3678 let project = Project::test(fs, [], cx).await;
3679 let (_, workspace) = cx.add_window(|cx| {
3680 Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
3681 });
3682
3683 let item = cx.add_view(&workspace, |cx| {
3684 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3685 });
3686 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3687 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3688 let toolbar_notify_count = Rc::new(RefCell::new(0));
3689
3690 workspace.update(cx, |workspace, cx| {
3691 workspace.add_item(Box::new(item.clone()), cx);
3692 let toolbar_notification_count = toolbar_notify_count.clone();
3693 cx.observe(&toolbar, move |_, _, _| {
3694 *toolbar_notification_count.borrow_mut() += 1
3695 })
3696 .detach();
3697 });
3698
3699 pane.read_with(cx, |pane, _| {
3700 assert!(!pane.can_navigate_backward());
3701 assert!(!pane.can_navigate_forward());
3702 });
3703
3704 item.update(cx, |item, cx| {
3705 item.set_state("one".to_string(), cx);
3706 });
3707
3708 // Toolbar must be notified to re-render the navigation buttons
3709 assert_eq!(*toolbar_notify_count.borrow(), 1);
3710
3711 pane.read_with(cx, |pane, _| {
3712 assert!(pane.can_navigate_backward());
3713 assert!(!pane.can_navigate_forward());
3714 });
3715
3716 workspace
3717 .update(cx, |workspace, cx| {
3718 Pane::go_back(workspace, Some(pane.clone()), cx)
3719 })
3720 .await
3721 .unwrap();
3722
3723 assert_eq!(*toolbar_notify_count.borrow(), 3);
3724 pane.read_with(cx, |pane, _| {
3725 assert!(!pane.can_navigate_backward());
3726 assert!(pane.can_navigate_forward());
3727 });
3728 }
3729}