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