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