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