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