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