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