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