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