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