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