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