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