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