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