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