1pub mod dock;
2pub mod item;
3pub mod notifications;
4pub mod pane;
5pub mod pane_group;
6mod persistence;
7pub mod searchable;
8pub mod shared_screen;
9mod status_bar;
10mod toolbar;
11mod workspace_settings;
12
13use anyhow::{anyhow, Context, Result};
14use call::ActiveCall;
15use client::{
16 proto::{self, PeerId},
17 Client, TypedEnvelope, UserStore,
18};
19use collections::{hash_map, HashMap, HashSet};
20use drag_and_drop::DragAndDrop;
21use futures::{
22 channel::{mpsc, oneshot},
23 future::try_join_all,
24 FutureExt, StreamExt,
25};
26use gpui::{
27 actions,
28 elements::*,
29 geometry::{
30 rect::RectF,
31 vector::{vec2f, Vector2F},
32 },
33 impl_actions,
34 platform::{
35 CursorStyle, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds,
36 WindowOptions,
37 },
38 AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
39 ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
40 WeakViewHandle, WindowContext,
41};
42use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
43use itertools::Itertools;
44use language::{LanguageRegistry, Rope};
45use std::{
46 any::TypeId,
47 borrow::Cow,
48 cmp, env,
49 future::Future,
50 path::{Path, PathBuf},
51 rc::Rc,
52 str,
53 sync::{atomic::AtomicUsize, Arc},
54 time::Duration,
55};
56
57use crate::{
58 notifications::{simple_message_notification::MessageNotification, NotificationTracker},
59 persistence::model::{
60 DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
61 },
62};
63use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
64use lazy_static::lazy_static;
65use notifications::{NotificationHandle, NotifyResultExt};
66pub use pane::*;
67pub use pane_group::*;
68use persistence::{model::SerializedItem, DB};
69pub use persistence::{
70 model::{ItemId, WorkspaceLocation},
71 WorkspaceDb, DB as WORKSPACE_DB,
72};
73use postage::prelude::Stream;
74use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
75use serde::Deserialize;
76use shared_screen::SharedScreen;
77use status_bar::StatusBar;
78pub use status_bar::StatusItemView;
79use theme::{Theme, ThemeSettings};
80pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
81use util::{async_iife, ResultExt};
82pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
83
84lazy_static! {
85 static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
86 .ok()
87 .as_deref()
88 .and_then(parse_pixel_position_env_var);
89 static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
90 .ok()
91 .as_deref()
92 .and_then(parse_pixel_position_env_var);
93}
94
95pub trait Modal: View {
96 fn has_focus(&self) -> bool;
97 fn dismiss_on_event(event: &Self::Event) -> bool;
98}
99
100trait ModalHandle {
101 fn as_any(&self) -> &AnyViewHandle;
102 fn has_focus(&self, cx: &WindowContext) -> bool;
103}
104
105impl<T: Modal> ModalHandle for ViewHandle<T> {
106 fn as_any(&self) -> &AnyViewHandle {
107 self
108 }
109
110 fn has_focus(&self, cx: &WindowContext) -> bool {
111 self.read(cx).has_focus()
112 }
113}
114
115#[derive(Clone, PartialEq)]
116pub struct RemoveWorktreeFromProject(pub WorktreeId);
117
118actions!(
119 workspace,
120 [
121 Open,
122 NewFile,
123 NewWindow,
124 CloseWindow,
125 AddFolderToProject,
126 Unfollow,
127 Save,
128 SaveAs,
129 SaveAll,
130 ActivatePreviousPane,
131 ActivateNextPane,
132 FollowNextCollaborator,
133 NewTerminal,
134 NewCenterTerminal,
135 ToggleTerminalFocus,
136 NewSearch,
137 Feedback,
138 Restart,
139 Welcome,
140 ToggleZoom,
141 ToggleLeftDock,
142 ToggleRightDock,
143 ToggleBottomDock,
144 ]
145);
146
147#[derive(Clone, PartialEq)]
148pub struct OpenPaths {
149 pub paths: Vec<PathBuf>,
150}
151
152#[derive(Clone, Deserialize, PartialEq)]
153pub struct ActivatePane(pub usize);
154
155#[derive(Deserialize)]
156pub struct Toast {
157 id: usize,
158 msg: Cow<'static, str>,
159 #[serde(skip)]
160 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
161}
162
163impl Toast {
164 pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
165 Toast {
166 id,
167 msg: msg.into(),
168 on_click: None,
169 }
170 }
171
172 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
173 where
174 M: Into<Cow<'static, str>>,
175 F: Fn(&mut WindowContext) + 'static,
176 {
177 self.on_click = Some((message.into(), Arc::new(on_click)));
178 self
179 }
180}
181
182impl PartialEq for Toast {
183 fn eq(&self, other: &Self) -> bool {
184 self.id == other.id
185 && self.msg == other.msg
186 && self.on_click.is_some() == other.on_click.is_some()
187 }
188}
189
190impl Clone for Toast {
191 fn clone(&self) -> Self {
192 Toast {
193 id: self.id,
194 msg: self.msg.to_owned(),
195 on_click: self.on_click.clone(),
196 }
197 }
198}
199
200impl_actions!(workspace, [ActivatePane, Toast]);
201
202pub type WorkspaceId = i64;
203
204pub fn init_settings(cx: &mut AppContext) {
205 settings::register::<WorkspaceSettings>(cx);
206}
207
208pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
209 init_settings(cx);
210 pane::init(cx);
211 notifications::init(cx);
212
213 cx.add_global_action({
214 let app_state = Arc::downgrade(&app_state);
215 move |_: &Open, cx: &mut AppContext| {
216 let mut paths = cx.prompt_for_paths(PathPromptOptions {
217 files: true,
218 directories: true,
219 multiple: true,
220 });
221
222 if let Some(app_state) = app_state.upgrade() {
223 cx.spawn(move |mut cx| async move {
224 if let Some(paths) = paths.recv().await.flatten() {
225 cx.update(|cx| {
226 open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
227 });
228 }
229 })
230 .detach();
231 }
232 }
233 });
234 cx.add_async_action(Workspace::open);
235
236 cx.add_async_action(Workspace::follow_next_collaborator);
237 cx.add_async_action(Workspace::close);
238 cx.add_global_action(Workspace::close_global);
239 cx.add_global_action(restart);
240 cx.add_async_action(Workspace::save_all);
241 cx.add_action(Workspace::add_folder_to_project);
242 cx.add_action(
243 |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
244 let pane = workspace.active_pane().clone();
245 workspace.unfollow(&pane, cx);
246 },
247 );
248 cx.add_action(
249 |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
250 workspace.save_active_item(false, cx).detach_and_log_err(cx);
251 },
252 );
253 cx.add_action(
254 |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
255 workspace.save_active_item(true, cx).detach_and_log_err(cx);
256 },
257 );
258 cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
259 workspace.activate_previous_pane(cx)
260 });
261 cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
262 workspace.activate_next_pane(cx)
263 });
264 cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
265 workspace.toggle_dock(DockPosition::Left, cx);
266 });
267 cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
268 workspace.toggle_dock(DockPosition::Right, cx);
269 });
270 cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
271 workspace.toggle_dock(DockPosition::Bottom, cx);
272 });
273 cx.add_action(Workspace::activate_pane_at_index);
274 cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
275 workspace.reopen_closed_item(cx).detach();
276 });
277 cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
278 workspace
279 .go_back(workspace.active_pane().downgrade(), cx)
280 .detach();
281 });
282 cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
283 workspace
284 .go_forward(workspace.active_pane().downgrade(), cx)
285 .detach();
286 });
287
288 cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
289 cx.spawn(|workspace, mut cx| async move {
290 let err = install_cli::install_cli(&cx)
291 .await
292 .context("Failed to create CLI symlink");
293
294 workspace.update(&mut cx, |workspace, cx| {
295 if matches!(err, Err(_)) {
296 err.notify_err(workspace, cx);
297 } else {
298 workspace.show_notification(1, cx, |cx| {
299 cx.add_view(|_| {
300 MessageNotification::new("Successfully installed the `zed` binary")
301 })
302 });
303 }
304 })
305 })
306 .detach();
307 });
308
309 let client = &app_state.client;
310 client.add_view_request_handler(Workspace::handle_follow);
311 client.add_view_message_handler(Workspace::handle_unfollow);
312 client.add_view_message_handler(Workspace::handle_update_followers);
313}
314
315type ProjectItemBuilders = HashMap<
316 TypeId,
317 fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
318>;
319pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
320 cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
321 builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
322 let item = model.downcast::<I::Item>().unwrap();
323 Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
324 });
325 });
326}
327
328type FollowableItemBuilder = fn(
329 ViewHandle<Pane>,
330 ModelHandle<Project>,
331 ViewId,
332 &mut Option<proto::view::Variant>,
333 &mut AppContext,
334) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
335type FollowableItemBuilders = HashMap<
336 TypeId,
337 (
338 FollowableItemBuilder,
339 fn(&AnyViewHandle) -> Box<dyn FollowableItemHandle>,
340 ),
341>;
342pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
343 cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
344 builders.insert(
345 TypeId::of::<I>(),
346 (
347 |pane, project, id, state, cx| {
348 I::from_state_proto(pane, project, id, state, cx).map(|task| {
349 cx.foreground()
350 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
351 })
352 },
353 |this| Box::new(this.clone().downcast::<I>().unwrap()),
354 ),
355 );
356 });
357}
358
359type ItemDeserializers = HashMap<
360 Arc<str>,
361 fn(
362 ModelHandle<Project>,
363 WeakViewHandle<Workspace>,
364 WorkspaceId,
365 ItemId,
366 &mut ViewContext<Pane>,
367 ) -> Task<Result<Box<dyn ItemHandle>>>,
368>;
369pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
370 cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| {
371 if let Some(serialized_item_kind) = I::serialized_item_kind() {
372 deserializers.insert(
373 Arc::from(serialized_item_kind),
374 |project, workspace, workspace_id, item_id, cx| {
375 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
376 cx.foreground()
377 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
378 },
379 );
380 }
381 });
382}
383
384pub struct AppState {
385 pub languages: Arc<LanguageRegistry>,
386 pub client: Arc<client::Client>,
387 pub user_store: ModelHandle<client::UserStore>,
388 pub fs: Arc<dyn fs::Fs>,
389 pub build_window_options:
390 fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
391 pub initialize_workspace:
392 fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
393 pub background_actions: BackgroundActions,
394}
395
396impl AppState {
397 #[cfg(any(test, feature = "test-support"))]
398 pub fn test(cx: &mut AppContext) -> Arc<Self> {
399 use settings::SettingsStore;
400
401 if !cx.has_global::<SettingsStore>() {
402 cx.set_global(SettingsStore::test(cx));
403 }
404
405 let fs = fs::FakeFs::new(cx.background().clone());
406 let languages = Arc::new(LanguageRegistry::test());
407 let http_client = util::http::FakeHttpClient::with_404_response();
408 let client = Client::new(http_client.clone(), cx);
409 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
410
411 theme::init((), cx);
412 client::init(&client, cx);
413 crate::init_settings(cx);
414
415 Arc::new(Self {
416 client,
417 fs,
418 languages,
419 user_store,
420 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
421 build_window_options: |_, _, _| Default::default(),
422 background_actions: || &[],
423 })
424 }
425}
426
427struct DelayedDebouncedEditAction {
428 task: Option<Task<()>>,
429 cancel_channel: Option<oneshot::Sender<()>>,
430}
431
432impl DelayedDebouncedEditAction {
433 fn new() -> DelayedDebouncedEditAction {
434 DelayedDebouncedEditAction {
435 task: None,
436 cancel_channel: None,
437 }
438 }
439
440 fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
441 where
442 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
443 {
444 if let Some(channel) = self.cancel_channel.take() {
445 _ = channel.send(());
446 }
447
448 let (sender, mut receiver) = oneshot::channel::<()>();
449 self.cancel_channel = Some(sender);
450
451 let previous_task = self.task.take();
452 self.task = Some(cx.spawn(|workspace, mut cx| async move {
453 let mut timer = cx.background().timer(delay).fuse();
454 if let Some(previous_task) = previous_task {
455 previous_task.await;
456 }
457
458 futures::select_biased! {
459 _ = receiver => return,
460 _ = timer => {}
461 }
462
463 if let Some(result) = workspace
464 .update(&mut cx, |workspace, cx| (func)(workspace, cx))
465 .log_err()
466 {
467 result.await.log_err();
468 }
469 }));
470 }
471}
472
473pub enum Event {
474 PaneAdded(ViewHandle<Pane>),
475 ContactRequestedJoin(u64),
476}
477
478pub struct Workspace {
479 weak_self: WeakViewHandle<Self>,
480 remote_entity_subscription: Option<client::Subscription>,
481 modal: Option<ActiveModal>,
482 zoomed: Option<AnyWeakViewHandle>,
483 zoomed_position: Option<DockPosition>,
484 center: PaneGroup,
485 left_dock: ViewHandle<Dock>,
486 bottom_dock: ViewHandle<Dock>,
487 right_dock: ViewHandle<Dock>,
488 panes: Vec<ViewHandle<Pane>>,
489 panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
490 active_pane: ViewHandle<Pane>,
491 last_active_center_pane: Option<WeakViewHandle<Pane>>,
492 status_bar: ViewHandle<StatusBar>,
493 titlebar_item: Option<AnyViewHandle>,
494 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
495 project: ModelHandle<Project>,
496 leader_state: LeaderState,
497 follower_states_by_leader: FollowerStatesByLeader,
498 last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
499 window_edited: bool,
500 active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
501 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
502 database_id: WorkspaceId,
503 app_state: Arc<AppState>,
504 subscriptions: Vec<Subscription>,
505 _apply_leader_updates: Task<Result<()>>,
506 _observe_current_user: Task<Result<()>>,
507 pane_history_timestamp: Arc<AtomicUsize>,
508}
509
510struct ActiveModal {
511 view: Box<dyn ModalHandle>,
512 previously_focused_view_id: Option<usize>,
513}
514
515#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
516pub struct ViewId {
517 pub creator: PeerId,
518 pub id: u64,
519}
520
521#[derive(Default)]
522struct LeaderState {
523 followers: HashSet<PeerId>,
524}
525
526type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
527
528#[derive(Default)]
529struct FollowerState {
530 active_view_id: Option<ViewId>,
531 items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
532}
533
534impl Workspace {
535 pub fn new(
536 workspace_id: WorkspaceId,
537 project: ModelHandle<Project>,
538 app_state: Arc<AppState>,
539 cx: &mut ViewContext<Self>,
540 ) -> Self {
541 cx.observe(&project, |_, _, cx| cx.notify()).detach();
542 cx.subscribe(&project, move |this, _, event, cx| {
543 match event {
544 project::Event::RemoteIdChanged(remote_id) => {
545 this.update_window_title(cx);
546 this.project_remote_id_changed(*remote_id, cx);
547 }
548
549 project::Event::CollaboratorLeft(peer_id) => {
550 this.collaborator_left(*peer_id, cx);
551 }
552
553 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
554 this.update_window_title(cx);
555 this.serialize_workspace(cx);
556 }
557
558 project::Event::DisconnectedFromHost => {
559 this.update_window_edited(cx);
560 cx.blur();
561 }
562
563 project::Event::Closed => {
564 cx.remove_window();
565 }
566
567 project::Event::DeletedEntry(entry_id) => {
568 for pane in this.panes.iter() {
569 pane.update(cx, |pane, cx| {
570 pane.handle_deleted_project_item(*entry_id, cx)
571 });
572 }
573 }
574
575 project::Event::Notification(message) => this.show_notification(0, cx, |cx| {
576 cx.add_view(|_| MessageNotification::new(message.clone()))
577 }),
578
579 _ => {}
580 }
581 cx.notify()
582 })
583 .detach();
584
585 let weak_handle = cx.weak_handle();
586 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
587
588 let center_pane = cx.add_view(|cx| {
589 Pane::new(
590 weak_handle.clone(),
591 project.clone(),
592 app_state.background_actions,
593 pane_history_timestamp.clone(),
594 cx,
595 )
596 });
597 cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
598 cx.focus(¢er_pane);
599 cx.emit(Event::PaneAdded(center_pane.clone()));
600
601 let mut current_user = app_state.user_store.read(cx).watch_current_user();
602 let mut connection_status = app_state.client.status();
603 let _observe_current_user = cx.spawn(|this, mut cx| async move {
604 current_user.recv().await;
605 connection_status.recv().await;
606 let mut stream =
607 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
608
609 while stream.recv().await.is_some() {
610 this.update(&mut cx, |_, cx| cx.notify())?;
611 }
612 anyhow::Ok(())
613 });
614
615 // All leader updates are enqueued and then processed in a single task, so
616 // that each asynchronous operation can be run in order.
617 let (leader_updates_tx, mut leader_updates_rx) =
618 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
619 let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
620 while let Some((leader_id, update)) = leader_updates_rx.next().await {
621 Self::process_leader_update(&this, leader_id, update, &mut cx)
622 .await
623 .log_err();
624 }
625
626 Ok(())
627 });
628
629 cx.emit_global(WorkspaceCreated(weak_handle.clone()));
630
631 let left_dock = cx.add_view(|_| Dock::new(DockPosition::Left));
632 let bottom_dock = cx.add_view(|_| Dock::new(DockPosition::Bottom));
633 let right_dock = cx.add_view(|_| Dock::new(DockPosition::Right));
634 let left_dock_buttons =
635 cx.add_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx));
636 let bottom_dock_buttons =
637 cx.add_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx));
638 let right_dock_buttons =
639 cx.add_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx));
640 let status_bar = cx.add_view(|cx| {
641 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
642 status_bar.add_left_item(left_dock_buttons, cx);
643 status_bar.add_right_item(right_dock_buttons, cx);
644 status_bar.add_right_item(bottom_dock_buttons, cx);
645 status_bar
646 });
647
648 cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
649 drag_and_drop.register_container(weak_handle.clone());
650 });
651
652 let mut active_call = None;
653 if cx.has_global::<ModelHandle<ActiveCall>>() {
654 let call = cx.global::<ModelHandle<ActiveCall>>().clone();
655 let mut subscriptions = Vec::new();
656 subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
657 active_call = Some((call, subscriptions));
658 }
659
660 let subscriptions = vec![
661 cx.observe_fullscreen(|_, _, cx| cx.notify()),
662 cx.observe_window_activation(Self::on_window_activation_changed),
663 cx.observe_window_bounds(move |_, mut bounds, display, cx| {
664 // Transform fixed bounds to be stored in terms of the containing display
665 if let WindowBounds::Fixed(mut window_bounds) = bounds {
666 if let Some(screen) = cx.platform().screen_by_id(display) {
667 let screen_bounds = screen.bounds();
668 window_bounds
669 .set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x());
670 window_bounds
671 .set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y());
672 bounds = WindowBounds::Fixed(window_bounds);
673 }
674 }
675
676 cx.background()
677 .spawn(DB.set_window_bounds(workspace_id, bounds, display))
678 .detach_and_log_err(cx);
679 }),
680 cx.observe(&left_dock, |this, _, cx| {
681 this.serialize_workspace(cx);
682 cx.notify();
683 }),
684 cx.observe(&bottom_dock, |this, _, cx| {
685 this.serialize_workspace(cx);
686 cx.notify();
687 }),
688 cx.observe(&right_dock, |this, _, cx| {
689 this.serialize_workspace(cx);
690 cx.notify();
691 }),
692 ];
693
694 let mut this = Workspace {
695 weak_self: weak_handle.clone(),
696 modal: None,
697 zoomed: None,
698 zoomed_position: None,
699 center: PaneGroup::new(center_pane.clone()),
700 panes: vec![center_pane.clone()],
701 panes_by_item: Default::default(),
702 active_pane: center_pane.clone(),
703 last_active_center_pane: Some(center_pane.downgrade()),
704 status_bar,
705 titlebar_item: None,
706 notifications: Default::default(),
707 remote_entity_subscription: None,
708 left_dock,
709 bottom_dock,
710 right_dock,
711 project: project.clone(),
712 leader_state: Default::default(),
713 follower_states_by_leader: Default::default(),
714 last_leaders_by_pane: Default::default(),
715 window_edited: false,
716 active_call,
717 database_id: workspace_id,
718 app_state,
719 _observe_current_user,
720 _apply_leader_updates,
721 leader_updates_tx,
722 subscriptions,
723 pane_history_timestamp,
724 };
725 this.project_remote_id_changed(project.read(cx).remote_id(), cx);
726 cx.defer(|this, cx| this.update_window_title(cx));
727 this
728 }
729
730 fn new_local(
731 abs_paths: Vec<PathBuf>,
732 app_state: Arc<AppState>,
733 requesting_window_id: Option<usize>,
734 cx: &mut AppContext,
735 ) -> Task<(
736 WeakViewHandle<Workspace>,
737 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
738 )> {
739 let project_handle = Project::local(
740 app_state.client.clone(),
741 app_state.user_store.clone(),
742 app_state.languages.clone(),
743 app_state.fs.clone(),
744 cx,
745 );
746
747 cx.spawn(|mut cx| async move {
748 let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice());
749
750 let paths_to_open = Arc::new(abs_paths);
751
752 // Get project paths for all of the abs_paths
753 let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
754 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
755 Vec::with_capacity(paths_to_open.len());
756 for path in paths_to_open.iter().cloned() {
757 if let Some((worktree, project_entry)) = cx
758 .update(|cx| {
759 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
760 })
761 .await
762 .log_err()
763 {
764 worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path()));
765 project_paths.push((path, Some(project_entry)));
766 } else {
767 project_paths.push((path, None));
768 }
769 }
770
771 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
772 serialized_workspace.id
773 } else {
774 DB.next_id().await.unwrap_or(0)
775 };
776
777 let workspace = requesting_window_id
778 .and_then(|window_id| {
779 cx.update(|cx| {
780 cx.replace_root_view(window_id, |cx| {
781 Workspace::new(
782 workspace_id,
783 project_handle.clone(),
784 app_state.clone(),
785 cx,
786 )
787 })
788 })
789 })
790 .unwrap_or_else(|| {
791 let window_bounds_override = window_bounds_env_override(&cx);
792 let (bounds, display) = if let Some(bounds) = window_bounds_override {
793 (Some(bounds), None)
794 } else {
795 serialized_workspace
796 .as_ref()
797 .and_then(|serialized_workspace| {
798 let display = serialized_workspace.display?;
799 let mut bounds = serialized_workspace.bounds?;
800
801 // Stored bounds are relative to the containing display.
802 // So convert back to global coordinates if that screen still exists
803 if let WindowBounds::Fixed(mut window_bounds) = bounds {
804 if let Some(screen) = cx.platform().screen_by_id(display) {
805 let screen_bounds = screen.bounds();
806 window_bounds.set_origin_x(
807 window_bounds.origin_x() + screen_bounds.origin_x(),
808 );
809 window_bounds.set_origin_y(
810 window_bounds.origin_y() + screen_bounds.origin_y(),
811 );
812 bounds = WindowBounds::Fixed(window_bounds);
813 } else {
814 // Screen no longer exists. Return none here.
815 return None;
816 }
817 }
818
819 Some((bounds, display))
820 })
821 .unzip()
822 };
823
824 // Use the serialized workspace to construct the new window
825 cx.add_window(
826 (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
827 |cx| {
828 Workspace::new(
829 workspace_id,
830 project_handle.clone(),
831 app_state.clone(),
832 cx,
833 )
834 },
835 )
836 .1
837 });
838
839 (app_state.initialize_workspace)(
840 workspace.downgrade(),
841 serialized_workspace.is_some(),
842 app_state.clone(),
843 cx.clone(),
844 )
845 .await
846 .log_err();
847
848 cx.update_window(workspace.window_id(), |cx| cx.activate_window());
849
850 let workspace = workspace.downgrade();
851 notify_if_database_failed(&workspace, &mut cx);
852 let opened_items = open_items(
853 serialized_workspace,
854 &workspace,
855 project_paths,
856 app_state,
857 cx,
858 )
859 .await;
860
861 (workspace, opened_items)
862 })
863 }
864
865 pub fn weak_handle(&self) -> WeakViewHandle<Self> {
866 self.weak_self.clone()
867 }
868
869 pub fn left_dock(&self) -> &ViewHandle<Dock> {
870 &self.left_dock
871 }
872
873 pub fn bottom_dock(&self) -> &ViewHandle<Dock> {
874 &self.bottom_dock
875 }
876
877 pub fn right_dock(&self) -> &ViewHandle<Dock> {
878 &self.right_dock
879 }
880
881 pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>)
882 where
883 T::Event: std::fmt::Debug,
884 {
885 let dock = match panel.position(cx) {
886 DockPosition::Left => &self.left_dock,
887 DockPosition::Bottom => &self.bottom_dock,
888 DockPosition::Right => &self.right_dock,
889 };
890
891 self.subscriptions.push(cx.subscribe(&panel, {
892 let mut dock = dock.clone();
893 let mut prev_position = panel.position(cx);
894 move |this, panel, event, cx| {
895 if T::should_change_position_on_event(event) {
896 let new_position = panel.read(cx).position(cx);
897 let mut was_visible = false;
898 dock.update(cx, |dock, cx| {
899 prev_position = new_position;
900
901 was_visible = dock.is_open()
902 && dock
903 .visible_panel()
904 .map_or(false, |active_panel| active_panel.id() == panel.id());
905 dock.remove_panel(&panel, cx);
906 });
907
908 if panel.is_zoomed(cx) {
909 this.zoomed_position = Some(new_position);
910 }
911
912 dock = match panel.read(cx).position(cx) {
913 DockPosition::Left => &this.left_dock,
914 DockPosition::Bottom => &this.bottom_dock,
915 DockPosition::Right => &this.right_dock,
916 }
917 .clone();
918 dock.update(cx, |dock, cx| {
919 dock.add_panel(panel.clone(), cx);
920 if was_visible {
921 dock.set_open(true, cx);
922 dock.activate_panel(dock.panels_len() - 1, cx);
923 }
924 });
925 } else if T::should_zoom_in_on_event(event) {
926 dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
927 if !panel.has_focus(cx) {
928 cx.focus(&panel);
929 }
930 this.zoomed = Some(panel.downgrade().into_any());
931 this.zoomed_position = Some(panel.read(cx).position(cx));
932 } else if T::should_zoom_out_on_event(event) {
933 dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx));
934 if this.zoomed_position == Some(prev_position) {
935 this.zoomed = None;
936 this.zoomed_position = None;
937 }
938 cx.notify();
939 } else if T::is_focus_event(event) {
940 let position = panel.read(cx).position(cx);
941 this.dismiss_zoomed_items_to_reveal(Some(position), cx);
942 if panel.is_zoomed(cx) {
943 this.zoomed = Some(panel.downgrade().into_any());
944 this.zoomed_position = Some(position);
945 } else {
946 this.zoomed = None;
947 this.zoomed_position = None;
948 }
949 this.update_active_view_for_followers(cx);
950 cx.notify();
951 }
952 }
953 }));
954
955 dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
956 }
957
958 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
959 &self.status_bar
960 }
961
962 pub fn app_state(&self) -> &Arc<AppState> {
963 &self.app_state
964 }
965
966 pub fn user_store(&self) -> &ModelHandle<UserStore> {
967 &self.app_state.user_store
968 }
969
970 pub fn project(&self) -> &ModelHandle<Project> {
971 &self.project
972 }
973
974 pub fn recent_navigation_history(
975 &self,
976 limit: Option<usize>,
977 cx: &AppContext,
978 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
979 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
980 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
981 for pane in &self.panes {
982 let pane = pane.read(cx);
983 pane.nav_history()
984 .for_each_entry(cx, |entry, (project_path, fs_path)| {
985 if let Some(fs_path) = &fs_path {
986 abs_paths_opened
987 .entry(fs_path.clone())
988 .or_default()
989 .insert(project_path.clone());
990 }
991 let timestamp = entry.timestamp;
992 match history.entry(project_path) {
993 hash_map::Entry::Occupied(mut entry) => {
994 let (_, old_timestamp) = entry.get();
995 if ×tamp > old_timestamp {
996 entry.insert((fs_path, timestamp));
997 }
998 }
999 hash_map::Entry::Vacant(entry) => {
1000 entry.insert((fs_path, timestamp));
1001 }
1002 }
1003 });
1004 }
1005
1006 history
1007 .into_iter()
1008 .sorted_by_key(|(_, (_, timestamp))| *timestamp)
1009 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
1010 .rev()
1011 .filter(|(history_path, abs_path)| {
1012 let latest_project_path_opened = abs_path
1013 .as_ref()
1014 .and_then(|abs_path| abs_paths_opened.get(abs_path))
1015 .and_then(|project_paths| {
1016 project_paths
1017 .iter()
1018 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
1019 });
1020
1021 match latest_project_path_opened {
1022 Some(latest_project_path_opened) => latest_project_path_opened == history_path,
1023 None => true,
1024 }
1025 })
1026 .take(limit.unwrap_or(usize::MAX))
1027 .collect()
1028 }
1029
1030 fn navigate_history(
1031 &mut self,
1032 pane: WeakViewHandle<Pane>,
1033 mode: NavigationMode,
1034 cx: &mut ViewContext<Workspace>,
1035 ) -> Task<Result<()>> {
1036 let to_load = if let Some(pane) = pane.upgrade(cx) {
1037 cx.focus(&pane);
1038
1039 pane.update(cx, |pane, cx| {
1040 loop {
1041 // Retrieve the weak item handle from the history.
1042 let entry = pane.nav_history_mut().pop(mode, cx)?;
1043
1044 // If the item is still present in this pane, then activate it.
1045 if let Some(index) = entry
1046 .item
1047 .upgrade(cx)
1048 .and_then(|v| pane.index_for_item(v.as_ref()))
1049 {
1050 let prev_active_item_index = pane.active_item_index();
1051 pane.nav_history_mut().set_mode(mode);
1052 pane.activate_item(index, true, true, cx);
1053 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1054
1055 let mut navigated = prev_active_item_index != pane.active_item_index();
1056 if let Some(data) = entry.data {
1057 navigated |= pane.active_item()?.navigate(data, cx);
1058 }
1059
1060 if navigated {
1061 break None;
1062 }
1063 }
1064 // If the item is no longer present in this pane, then retrieve its
1065 // project path in order to reopen it.
1066 else {
1067 break pane
1068 .nav_history()
1069 .path_for_item(entry.item.id())
1070 .map(|(project_path, _)| (project_path, entry));
1071 }
1072 }
1073 })
1074 } else {
1075 None
1076 };
1077
1078 if let Some((project_path, entry)) = to_load {
1079 // If the item was no longer present, then load it again from its previous path.
1080 let task = self.load_path(project_path, cx);
1081 cx.spawn(|workspace, mut cx| async move {
1082 let task = task.await;
1083 let mut navigated = false;
1084 if let Some((project_entry_id, build_item)) = task.log_err() {
1085 let prev_active_item_id = pane.update(&mut cx, |pane, _| {
1086 pane.nav_history_mut().set_mode(mode);
1087 pane.active_item().map(|p| p.id())
1088 })?;
1089
1090 pane.update(&mut cx, |pane, cx| {
1091 let item = pane.open_item(project_entry_id, true, cx, build_item);
1092 navigated |= Some(item.id()) != prev_active_item_id;
1093 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1094 if let Some(data) = entry.data {
1095 navigated |= item.navigate(data, cx);
1096 }
1097 })?;
1098 }
1099
1100 if !navigated {
1101 workspace
1102 .update(&mut cx, |workspace, cx| {
1103 Self::navigate_history(workspace, pane, mode, cx)
1104 })?
1105 .await?;
1106 }
1107
1108 Ok(())
1109 })
1110 } else {
1111 Task::ready(Ok(()))
1112 }
1113 }
1114
1115 pub fn go_back(
1116 &mut self,
1117 pane: WeakViewHandle<Pane>,
1118 cx: &mut ViewContext<Workspace>,
1119 ) -> Task<Result<()>> {
1120 self.navigate_history(pane, NavigationMode::GoingBack, cx)
1121 }
1122
1123 pub fn go_forward(
1124 &mut self,
1125 pane: WeakViewHandle<Pane>,
1126 cx: &mut ViewContext<Workspace>,
1127 ) -> Task<Result<()>> {
1128 self.navigate_history(pane, NavigationMode::GoingForward, cx)
1129 }
1130
1131 pub fn reopen_closed_item(&mut self, cx: &mut ViewContext<Workspace>) -> Task<Result<()>> {
1132 self.navigate_history(
1133 self.active_pane().downgrade(),
1134 NavigationMode::ReopeningClosedItem,
1135 cx,
1136 )
1137 }
1138
1139 pub fn client(&self) -> &Client {
1140 &self.app_state.client
1141 }
1142
1143 pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
1144 self.titlebar_item = Some(item);
1145 cx.notify();
1146 }
1147
1148 pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
1149 self.titlebar_item.clone()
1150 }
1151
1152 /// Call the given callback with a workspace whose project is local.
1153 ///
1154 /// If the given workspace has a local project, then it will be passed
1155 /// to the callback. Otherwise, a new empty window will be created.
1156 pub fn with_local_workspace<T, F>(
1157 &mut self,
1158 cx: &mut ViewContext<Self>,
1159 callback: F,
1160 ) -> Task<Result<T>>
1161 where
1162 T: 'static,
1163 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
1164 {
1165 if self.project.read(cx).is_local() {
1166 Task::Ready(Some(Ok(callback(self, cx))))
1167 } else {
1168 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
1169 cx.spawn(|_vh, mut cx| async move {
1170 let (workspace, _) = task.await;
1171 workspace.update(&mut cx, callback)
1172 })
1173 }
1174 }
1175
1176 pub fn worktrees<'a>(
1177 &self,
1178 cx: &'a AppContext,
1179 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
1180 self.project.read(cx).worktrees(cx)
1181 }
1182
1183 pub fn visible_worktrees<'a>(
1184 &self,
1185 cx: &'a AppContext,
1186 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
1187 self.project.read(cx).visible_worktrees(cx)
1188 }
1189
1190 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
1191 let futures = self
1192 .worktrees(cx)
1193 .filter_map(|worktree| worktree.read(cx).as_local())
1194 .map(|worktree| worktree.scan_complete())
1195 .collect::<Vec<_>>();
1196 async move {
1197 for future in futures {
1198 future.await;
1199 }
1200 }
1201 }
1202
1203 pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
1204 cx.spawn(|mut cx| async move {
1205 let id = cx
1206 .window_ids()
1207 .into_iter()
1208 .find(|&id| cx.window_is_active(id));
1209 if let Some(id) = id {
1210 //This can only get called when the window's project connection has been lost
1211 //so we don't need to prompt the user for anything and instead just close the window
1212 cx.remove_window(id);
1213 }
1214 })
1215 .detach();
1216 }
1217
1218 pub fn close(
1219 &mut self,
1220 _: &CloseWindow,
1221 cx: &mut ViewContext<Self>,
1222 ) -> Option<Task<Result<()>>> {
1223 let window_id = cx.window_id();
1224 let prepare = self.prepare_to_close(false, cx);
1225 Some(cx.spawn(|_, mut cx| async move {
1226 if prepare.await? {
1227 cx.remove_window(window_id);
1228 }
1229 Ok(())
1230 }))
1231 }
1232
1233 pub fn prepare_to_close(
1234 &mut self,
1235 quitting: bool,
1236 cx: &mut ViewContext<Self>,
1237 ) -> Task<Result<bool>> {
1238 let active_call = self.active_call().cloned();
1239 let window_id = cx.window_id();
1240
1241 cx.spawn(|this, mut cx| async move {
1242 let workspace_count = cx
1243 .window_ids()
1244 .into_iter()
1245 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
1246 .count();
1247
1248 if let Some(active_call) = active_call {
1249 if !quitting
1250 && workspace_count == 1
1251 && active_call.read_with(&cx, |call, _| call.room().is_some())
1252 {
1253 let answer = cx.prompt(
1254 window_id,
1255 PromptLevel::Warning,
1256 "Do you want to leave the current call?",
1257 &["Close window and hang up", "Cancel"],
1258 );
1259
1260 if let Some(mut answer) = answer {
1261 if answer.next().await == Some(1) {
1262 return anyhow::Ok(false);
1263 } else {
1264 active_call
1265 .update(&mut cx, |call, cx| call.hang_up(cx))
1266 .await
1267 .log_err();
1268 }
1269 }
1270 }
1271 }
1272
1273 Ok(this
1274 .update(&mut cx, |this, cx| this.save_all_internal(true, cx))?
1275 .await?)
1276 })
1277 }
1278
1279 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1280 let save_all = self.save_all_internal(false, cx);
1281 Some(cx.foreground().spawn(async move {
1282 save_all.await?;
1283 Ok(())
1284 }))
1285 }
1286
1287 fn save_all_internal(
1288 &mut self,
1289 should_prompt_to_save: bool,
1290 cx: &mut ViewContext<Self>,
1291 ) -> Task<Result<bool>> {
1292 if self.project.read(cx).is_read_only() {
1293 return Task::ready(Ok(true));
1294 }
1295
1296 let dirty_items = self
1297 .panes
1298 .iter()
1299 .flat_map(|pane| {
1300 pane.read(cx).items().filter_map(|item| {
1301 if item.is_dirty(cx) {
1302 Some((pane.downgrade(), item.boxed_clone()))
1303 } else {
1304 None
1305 }
1306 })
1307 })
1308 .collect::<Vec<_>>();
1309
1310 let project = self.project.clone();
1311 cx.spawn(|_, mut cx| async move {
1312 for (pane, item) in dirty_items {
1313 let (singleton, project_entry_ids) =
1314 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
1315 if singleton || !project_entry_ids.is_empty() {
1316 if let Some(ix) =
1317 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))?
1318 {
1319 if !Pane::save_item(
1320 project.clone(),
1321 &pane,
1322 ix,
1323 &*item,
1324 should_prompt_to_save,
1325 &mut cx,
1326 )
1327 .await?
1328 {
1329 return Ok(false);
1330 }
1331 }
1332 }
1333 }
1334 Ok(true)
1335 })
1336 }
1337
1338 pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1339 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1340 files: true,
1341 directories: true,
1342 multiple: true,
1343 });
1344
1345 Some(cx.spawn(|this, mut cx| async move {
1346 if let Some(paths) = paths.recv().await.flatten() {
1347 if let Some(task) = this
1348 .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
1349 .log_err()
1350 {
1351 task.await?
1352 }
1353 }
1354 Ok(())
1355 }))
1356 }
1357
1358 pub fn open_workspace_for_paths(
1359 &mut self,
1360 paths: Vec<PathBuf>,
1361 cx: &mut ViewContext<Self>,
1362 ) -> Task<Result<()>> {
1363 let window_id = cx.window_id();
1364 let is_remote = self.project.read(cx).is_remote();
1365 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
1366 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
1367 let close_task = if is_remote || has_worktree || has_dirty_items {
1368 None
1369 } else {
1370 Some(self.prepare_to_close(false, cx))
1371 };
1372 let app_state = self.app_state.clone();
1373
1374 cx.spawn(|_, mut cx| async move {
1375 let window_id_to_replace = if let Some(close_task) = close_task {
1376 if !close_task.await? {
1377 return Ok(());
1378 }
1379 Some(window_id)
1380 } else {
1381 None
1382 };
1383 cx.update(|cx| open_paths(&paths, &app_state, window_id_to_replace, cx))
1384 .await?;
1385 Ok(())
1386 })
1387 }
1388
1389 #[allow(clippy::type_complexity)]
1390 pub fn open_paths(
1391 &mut self,
1392 mut abs_paths: Vec<PathBuf>,
1393 visible: bool,
1394 cx: &mut ViewContext<Self>,
1395 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1396 log::info!("open paths {:?}", abs_paths);
1397
1398 let fs = self.app_state.fs.clone();
1399
1400 // Sort the paths to ensure we add worktrees for parents before their children.
1401 abs_paths.sort_unstable();
1402 cx.spawn(|this, mut cx| async move {
1403 let mut project_paths = Vec::new();
1404 for path in &abs_paths {
1405 if let Some(project_path) = this
1406 .update(&mut cx, |this, cx| {
1407 Workspace::project_path_for_path(this.project.clone(), path, visible, cx)
1408 })
1409 .log_err()
1410 {
1411 project_paths.push(project_path.await.log_err());
1412 } else {
1413 project_paths.push(None);
1414 }
1415 }
1416
1417 let tasks = abs_paths
1418 .iter()
1419 .cloned()
1420 .zip(project_paths.into_iter())
1421 .map(|(abs_path, project_path)| {
1422 let this = this.clone();
1423 cx.spawn(|mut cx| {
1424 let fs = fs.clone();
1425 async move {
1426 let (_worktree, project_path) = project_path?;
1427 if fs.is_file(&abs_path).await {
1428 Some(
1429 this.update(&mut cx, |this, cx| {
1430 this.open_path(project_path, None, true, cx)
1431 })
1432 .log_err()?
1433 .await,
1434 )
1435 } else {
1436 None
1437 }
1438 }
1439 })
1440 })
1441 .collect::<Vec<_>>();
1442
1443 futures::future::join_all(tasks).await
1444 })
1445 }
1446
1447 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1448 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1449 files: false,
1450 directories: true,
1451 multiple: true,
1452 });
1453 cx.spawn(|this, mut cx| async move {
1454 if let Some(paths) = paths.recv().await.flatten() {
1455 let results = this
1456 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
1457 .await;
1458 for result in results.into_iter().flatten() {
1459 result.log_err();
1460 }
1461 }
1462 anyhow::Ok(())
1463 })
1464 .detach_and_log_err(cx);
1465 }
1466
1467 fn project_path_for_path(
1468 project: ModelHandle<Project>,
1469 abs_path: &Path,
1470 visible: bool,
1471 cx: &mut AppContext,
1472 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1473 let entry = project.update(cx, |project, cx| {
1474 project.find_or_create_local_worktree(abs_path, visible, cx)
1475 });
1476 cx.spawn(|cx| async move {
1477 let (worktree, path) = entry.await?;
1478 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1479 Ok((
1480 worktree,
1481 ProjectPath {
1482 worktree_id,
1483 path: path.into(),
1484 },
1485 ))
1486 })
1487 }
1488
1489 /// Returns the modal that was toggled closed if it was open.
1490 pub fn toggle_modal<V, F>(
1491 &mut self,
1492 cx: &mut ViewContext<Self>,
1493 add_view: F,
1494 ) -> Option<ViewHandle<V>>
1495 where
1496 V: 'static + Modal,
1497 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1498 {
1499 cx.notify();
1500 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1501 // it. Otherwise, create a new modal and set it as active.
1502 if let Some(already_open_modal) = self
1503 .dismiss_modal(cx)
1504 .and_then(|modal| modal.downcast::<V>())
1505 {
1506 cx.focus_self();
1507 Some(already_open_modal)
1508 } else {
1509 let modal = add_view(self, cx);
1510 cx.subscribe(&modal, |this, _, event, cx| {
1511 if V::dismiss_on_event(event) {
1512 this.dismiss_modal(cx);
1513 }
1514 })
1515 .detach();
1516 let previously_focused_view_id = cx.focused_view_id();
1517 cx.focus(&modal);
1518 self.modal = Some(ActiveModal {
1519 view: Box::new(modal),
1520 previously_focused_view_id,
1521 });
1522 None
1523 }
1524 }
1525
1526 pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
1527 self.modal
1528 .as_ref()
1529 .and_then(|modal| modal.view.as_any().clone().downcast::<V>())
1530 }
1531
1532 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyViewHandle> {
1533 if let Some(modal) = self.modal.take() {
1534 if let Some(previously_focused_view_id) = modal.previously_focused_view_id {
1535 if modal.view.has_focus(cx) {
1536 cx.window_context().focus(Some(previously_focused_view_id));
1537 }
1538 }
1539 cx.notify();
1540 Some(modal.view.as_any().clone())
1541 } else {
1542 None
1543 }
1544 }
1545
1546 pub fn items<'a>(
1547 &'a self,
1548 cx: &'a AppContext,
1549 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1550 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1551 }
1552
1553 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1554 self.items_of_type(cx).max_by_key(|item| item.id())
1555 }
1556
1557 pub fn items_of_type<'a, T: Item>(
1558 &'a self,
1559 cx: &'a AppContext,
1560 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1561 self.panes
1562 .iter()
1563 .flat_map(|pane| pane.read(cx).items_of_type())
1564 }
1565
1566 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1567 self.active_pane().read(cx).active_item()
1568 }
1569
1570 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1571 self.active_item(cx).and_then(|item| item.project_path(cx))
1572 }
1573
1574 pub fn save_active_item(
1575 &mut self,
1576 force_name_change: bool,
1577 cx: &mut ViewContext<Self>,
1578 ) -> Task<Result<()>> {
1579 let project = self.project.clone();
1580 if let Some(item) = self.active_item(cx) {
1581 if !force_name_change && item.can_save(cx) {
1582 if item.has_conflict(cx) {
1583 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1584
1585 let mut answer = cx.prompt(
1586 PromptLevel::Warning,
1587 CONFLICT_MESSAGE,
1588 &["Overwrite", "Cancel"],
1589 );
1590 cx.spawn(|this, mut cx| async move {
1591 let answer = answer.recv().await;
1592 if answer == Some(0) {
1593 this.update(&mut cx, |this, cx| item.save(this.project.clone(), cx))?
1594 .await?;
1595 }
1596 Ok(())
1597 })
1598 } else {
1599 item.save(self.project.clone(), cx)
1600 }
1601 } else if item.is_singleton(cx) {
1602 let worktree = self.worktrees(cx).next();
1603 let start_abs_path = worktree
1604 .and_then(|w| w.read(cx).as_local())
1605 .map_or(Path::new(""), |w| w.abs_path())
1606 .to_path_buf();
1607 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1608 cx.spawn(|this, mut cx| async move {
1609 if let Some(abs_path) = abs_path.recv().await.flatten() {
1610 this.update(&mut cx, |_, cx| item.save_as(project, abs_path, cx))?
1611 .await?;
1612 }
1613 Ok(())
1614 })
1615 } else {
1616 Task::ready(Ok(()))
1617 }
1618 } else {
1619 Task::ready(Ok(()))
1620 }
1621 }
1622
1623 pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
1624 let dock = match dock_side {
1625 DockPosition::Left => &self.left_dock,
1626 DockPosition::Bottom => &self.bottom_dock,
1627 DockPosition::Right => &self.right_dock,
1628 };
1629 let mut focus_center = false;
1630 let mut reveal_dock = false;
1631 dock.update(cx, |dock, cx| {
1632 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
1633 let was_visible = dock.is_open() && !other_is_zoomed;
1634 dock.set_open(!was_visible, cx);
1635
1636 if let Some(active_panel) = dock.active_panel() {
1637 if was_visible {
1638 if active_panel.has_focus(cx) {
1639 focus_center = true;
1640 }
1641 } else {
1642 cx.focus(active_panel.as_any());
1643 reveal_dock = true;
1644 }
1645 }
1646 });
1647
1648 if reveal_dock {
1649 self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
1650 }
1651
1652 if focus_center {
1653 cx.focus_self();
1654 }
1655
1656 cx.notify();
1657 self.serialize_workspace(cx);
1658 }
1659
1660 /// Transfer focus to the panel of the given type.
1661 pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<ViewHandle<T>> {
1662 self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?
1663 .as_any()
1664 .clone()
1665 .downcast()
1666 }
1667
1668 /// Focus the panel of the given type if it isn't already focused. If it is
1669 /// already focused, then transfer focus back to the workspace center.
1670 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1671 self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
1672 }
1673
1674 /// Focus or unfocus the given panel type, depending on the given callback.
1675 fn focus_or_unfocus_panel<T: Panel>(
1676 &mut self,
1677 cx: &mut ViewContext<Self>,
1678 should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
1679 ) -> Option<Rc<dyn PanelHandle>> {
1680 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1681 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1682 let mut focus_center = false;
1683 let mut reveal_dock = false;
1684 let panel = dock.update(cx, |dock, cx| {
1685 dock.activate_panel(panel_index, cx);
1686
1687 let panel = dock.active_panel().cloned();
1688 if let Some(panel) = panel.as_ref() {
1689 if should_focus(&**panel, cx) {
1690 dock.set_open(true, cx);
1691 cx.focus(panel.as_any());
1692 reveal_dock = true;
1693 } else {
1694 // if panel.is_zoomed(cx) {
1695 // dock.set_open(false, cx);
1696 // }
1697 focus_center = true;
1698 }
1699 }
1700 panel
1701 });
1702
1703 if focus_center {
1704 cx.focus_self();
1705 }
1706
1707 self.serialize_workspace(cx);
1708 cx.notify();
1709 return panel;
1710 }
1711 }
1712 None
1713 }
1714
1715 pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<ViewHandle<T>> {
1716 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1717 let dock = dock.read(cx);
1718 if let Some(panel) = dock.panel::<T>() {
1719 return Some(panel);
1720 }
1721 }
1722 None
1723 }
1724
1725 fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
1726 for pane in &self.panes {
1727 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1728 }
1729
1730 self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1731 self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1732 self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1733 self.zoomed = None;
1734 self.zoomed_position = None;
1735
1736 cx.notify();
1737 }
1738
1739 #[cfg(any(test, feature = "test-support"))]
1740 pub fn zoomed_view(&self, cx: &AppContext) -> Option<AnyViewHandle> {
1741 self.zoomed.and_then(|view| view.upgrade(cx))
1742 }
1743
1744 fn dismiss_zoomed_items_to_reveal(
1745 &mut self,
1746 dock_to_reveal: Option<DockPosition>,
1747 cx: &mut ViewContext<Self>,
1748 ) {
1749 // If a center pane is zoomed, unzoom it.
1750 for pane in &self.panes {
1751 if pane != &self.active_pane || dock_to_reveal.is_some() {
1752 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1753 }
1754 }
1755
1756 // If another dock is zoomed, hide it.
1757 let mut focus_center = false;
1758 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
1759 dock.update(cx, |dock, cx| {
1760 if Some(dock.position()) != dock_to_reveal {
1761 if let Some(panel) = dock.active_panel() {
1762 if panel.is_zoomed(cx) {
1763 focus_center |= panel.has_focus(cx);
1764 dock.set_open(false, cx);
1765 }
1766 }
1767 }
1768 });
1769 }
1770
1771 if focus_center {
1772 cx.focus_self();
1773 }
1774
1775 if self.zoomed_position != dock_to_reveal {
1776 self.zoomed = None;
1777 self.zoomed_position = None;
1778 }
1779
1780 cx.notify();
1781 }
1782
1783 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1784 let pane = cx.add_view(|cx| {
1785 Pane::new(
1786 self.weak_handle(),
1787 self.project.clone(),
1788 self.app_state.background_actions,
1789 self.pane_history_timestamp.clone(),
1790 cx,
1791 )
1792 });
1793 cx.subscribe(&pane, Self::handle_pane_event).detach();
1794 self.panes.push(pane.clone());
1795 cx.focus(&pane);
1796 cx.emit(Event::PaneAdded(pane.clone()));
1797 pane
1798 }
1799
1800 pub fn add_item_to_center(
1801 &mut self,
1802 item: Box<dyn ItemHandle>,
1803 cx: &mut ViewContext<Self>,
1804 ) -> bool {
1805 if let Some(center_pane) = self.last_active_center_pane.clone() {
1806 if let Some(center_pane) = center_pane.upgrade(cx) {
1807 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1808 true
1809 } else {
1810 false
1811 }
1812 } else {
1813 false
1814 }
1815 }
1816
1817 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1818 self.active_pane
1819 .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1820 }
1821
1822 pub fn open_abs_path(
1823 &mut self,
1824 abs_path: PathBuf,
1825 visible: bool,
1826 cx: &mut ViewContext<Self>,
1827 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
1828 cx.spawn(|workspace, mut cx| async move {
1829 let open_paths_task_result = workspace
1830 .update(&mut cx, |workspace, cx| {
1831 workspace.open_paths(vec![abs_path.clone()], visible, cx)
1832 })
1833 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
1834 .await;
1835 anyhow::ensure!(
1836 open_paths_task_result.len() == 1,
1837 "open abs path {abs_path:?} task returned incorrect number of results"
1838 );
1839 match open_paths_task_result
1840 .into_iter()
1841 .next()
1842 .expect("ensured single task result")
1843 {
1844 Some(open_result) => {
1845 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
1846 }
1847 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
1848 }
1849 })
1850 }
1851
1852 pub fn open_path(
1853 &mut self,
1854 path: impl Into<ProjectPath>,
1855 pane: Option<WeakViewHandle<Pane>>,
1856 focus_item: bool,
1857 cx: &mut ViewContext<Self>,
1858 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1859 let pane = pane.unwrap_or_else(|| {
1860 self.last_active_center_pane.clone().unwrap_or_else(|| {
1861 self.panes
1862 .first()
1863 .expect("There must be an active pane")
1864 .downgrade()
1865 })
1866 });
1867
1868 let task = self.load_path(path.into(), cx);
1869 cx.spawn(|_, mut cx| async move {
1870 let (project_entry_id, build_item) = task.await?;
1871 pane.update(&mut cx, |pane, cx| {
1872 pane.open_item(project_entry_id, focus_item, cx, build_item)
1873 })
1874 })
1875 }
1876
1877 pub(crate) fn load_path(
1878 &mut self,
1879 path: ProjectPath,
1880 cx: &mut ViewContext<Self>,
1881 ) -> Task<
1882 Result<(
1883 ProjectEntryId,
1884 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
1885 )>,
1886 > {
1887 let project = self.project().clone();
1888 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1889 cx.spawn(|_, mut cx| async move {
1890 let (project_entry_id, project_item) = project_item.await?;
1891 let build_item = cx.update(|cx| {
1892 cx.default_global::<ProjectItemBuilders>()
1893 .get(&project_item.model_type())
1894 .ok_or_else(|| anyhow!("no item builder for project item"))
1895 .cloned()
1896 })?;
1897 let build_item =
1898 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
1899 Ok((project_entry_id, build_item))
1900 })
1901 }
1902
1903 pub fn open_project_item<T>(
1904 &mut self,
1905 project_item: ModelHandle<T::Item>,
1906 cx: &mut ViewContext<Self>,
1907 ) -> ViewHandle<T>
1908 where
1909 T: ProjectItem,
1910 {
1911 use project::Item as _;
1912
1913 let entry_id = project_item.read(cx).entry_id(cx);
1914 if let Some(item) = entry_id
1915 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1916 .and_then(|item| item.downcast())
1917 {
1918 self.activate_item(&item, cx);
1919 return item;
1920 }
1921
1922 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1923 self.add_item(Box::new(item.clone()), cx);
1924 item
1925 }
1926
1927 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1928 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
1929 self.active_pane.update(cx, |pane, cx| {
1930 pane.add_item(Box::new(shared_screen), false, true, None, cx)
1931 });
1932 }
1933 }
1934
1935 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1936 let result = self.panes.iter().find_map(|pane| {
1937 pane.read(cx)
1938 .index_for_item(item)
1939 .map(|ix| (pane.clone(), ix))
1940 });
1941 if let Some((pane, ix)) = result {
1942 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1943 true
1944 } else {
1945 false
1946 }
1947 }
1948
1949 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1950 let panes = self.center.panes();
1951 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1952 cx.focus(&pane);
1953 } else {
1954 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1955 }
1956 }
1957
1958 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1959 let panes = self.center.panes();
1960 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1961 let next_ix = (ix + 1) % panes.len();
1962 let next_pane = panes[next_ix].clone();
1963 cx.focus(&next_pane);
1964 }
1965 }
1966
1967 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1968 let panes = self.center.panes();
1969 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1970 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1971 let prev_pane = panes[prev_ix].clone();
1972 cx.focus(&prev_pane);
1973 }
1974 }
1975
1976 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1977 if self.active_pane != pane {
1978 self.active_pane = pane.clone();
1979 self.status_bar.update(cx, |status_bar, cx| {
1980 status_bar.set_active_pane(&self.active_pane, cx);
1981 });
1982 self.active_item_path_changed(cx);
1983 self.last_active_center_pane = Some(pane.downgrade());
1984 }
1985
1986 self.dismiss_zoomed_items_to_reveal(None, cx);
1987 if pane.read(cx).is_zoomed() {
1988 self.zoomed = Some(pane.downgrade().into_any());
1989 } else {
1990 self.zoomed = None;
1991 }
1992 self.zoomed_position = None;
1993 self.update_active_view_for_followers(cx);
1994
1995 cx.notify();
1996 }
1997
1998 fn handle_pane_event(
1999 &mut self,
2000 pane: ViewHandle<Pane>,
2001 event: &pane::Event,
2002 cx: &mut ViewContext<Self>,
2003 ) {
2004 match event {
2005 pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
2006 pane::Event::Split(direction) => {
2007 self.split_pane(pane, *direction, cx);
2008 }
2009 pane::Event::Remove => self.remove_pane(pane, cx),
2010 pane::Event::ActivateItem { local } => {
2011 if *local {
2012 self.unfollow(&pane, cx);
2013 }
2014 if &pane == self.active_pane() {
2015 self.active_item_path_changed(cx);
2016 }
2017 }
2018 pane::Event::ChangeItemTitle => {
2019 if pane == self.active_pane {
2020 self.active_item_path_changed(cx);
2021 }
2022 self.update_window_edited(cx);
2023 }
2024 pane::Event::RemoveItem { item_id } => {
2025 self.update_window_edited(cx);
2026 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
2027 if entry.get().id() == pane.id() {
2028 entry.remove();
2029 }
2030 }
2031 }
2032 pane::Event::Focus => {
2033 self.handle_pane_focused(pane.clone(), cx);
2034 }
2035 pane::Event::ZoomIn => {
2036 if pane == self.active_pane {
2037 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
2038 if pane.read(cx).has_focus() {
2039 self.zoomed = Some(pane.downgrade().into_any());
2040 self.zoomed_position = None;
2041 }
2042 cx.notify();
2043 }
2044 }
2045 pane::Event::ZoomOut => {
2046 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2047 if self.zoomed_position.is_none() {
2048 self.zoomed = None;
2049 }
2050 cx.notify();
2051 }
2052 }
2053
2054 self.serialize_workspace(cx);
2055 }
2056
2057 pub fn split_pane(
2058 &mut self,
2059 pane: ViewHandle<Pane>,
2060 direction: SplitDirection,
2061 cx: &mut ViewContext<Self>,
2062 ) -> Option<ViewHandle<Pane>> {
2063 let item = pane.read(cx).active_item()?;
2064 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
2065 let new_pane = self.add_pane(cx);
2066 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
2067 self.center.split(&pane, &new_pane, direction).unwrap();
2068 Some(new_pane)
2069 } else {
2070 None
2071 };
2072 cx.notify();
2073 maybe_pane_handle
2074 }
2075
2076 pub fn split_pane_with_item(
2077 &mut self,
2078 pane_to_split: WeakViewHandle<Pane>,
2079 split_direction: SplitDirection,
2080 from: WeakViewHandle<Pane>,
2081 item_id_to_move: usize,
2082 cx: &mut ViewContext<Self>,
2083 ) {
2084 let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; };
2085 let Some(from) = from.upgrade(cx) else { return; };
2086
2087 let new_pane = self.add_pane(cx);
2088 self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
2089 self.center
2090 .split(&pane_to_split, &new_pane, split_direction)
2091 .unwrap();
2092 cx.notify();
2093 }
2094
2095 pub fn split_pane_with_project_entry(
2096 &mut self,
2097 pane_to_split: WeakViewHandle<Pane>,
2098 split_direction: SplitDirection,
2099 project_entry: ProjectEntryId,
2100 cx: &mut ViewContext<Self>,
2101 ) -> Option<Task<Result<()>>> {
2102 let pane_to_split = pane_to_split.upgrade(cx)?;
2103 let new_pane = self.add_pane(cx);
2104 self.center
2105 .split(&pane_to_split, &new_pane, split_direction)
2106 .unwrap();
2107
2108 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
2109 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
2110 Some(cx.foreground().spawn(async move {
2111 task.await?;
2112 Ok(())
2113 }))
2114 }
2115
2116 pub fn move_item(
2117 &mut self,
2118 source: ViewHandle<Pane>,
2119 destination: ViewHandle<Pane>,
2120 item_id_to_move: usize,
2121 destination_index: usize,
2122 cx: &mut ViewContext<Self>,
2123 ) {
2124 let item_to_move = source
2125 .read(cx)
2126 .items()
2127 .enumerate()
2128 .find(|(_, item_handle)| item_handle.id() == item_id_to_move);
2129
2130 if item_to_move.is_none() {
2131 log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
2132 return;
2133 }
2134 let (item_ix, item_handle) = item_to_move.unwrap();
2135 let item_handle = item_handle.clone();
2136
2137 if source != destination {
2138 // Close item from previous pane
2139 source.update(cx, |source, cx| {
2140 source.remove_item(item_ix, false, cx);
2141 });
2142 }
2143
2144 // This automatically removes duplicate items in the pane
2145 destination.update(cx, |destination, cx| {
2146 destination.add_item(item_handle, true, true, Some(destination_index), cx);
2147 cx.focus_self();
2148 });
2149 }
2150
2151 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
2152 if self.center.remove(&pane).unwrap() {
2153 self.force_remove_pane(&pane, cx);
2154 self.unfollow(&pane, cx);
2155 self.last_leaders_by_pane.remove(&pane.downgrade());
2156 for removed_item in pane.read(cx).items() {
2157 self.panes_by_item.remove(&removed_item.id());
2158 }
2159
2160 cx.notify();
2161 } else {
2162 self.active_item_path_changed(cx);
2163 }
2164 }
2165
2166 pub fn panes(&self) -> &[ViewHandle<Pane>] {
2167 &self.panes
2168 }
2169
2170 pub fn active_pane(&self) -> &ViewHandle<Pane> {
2171 &self.active_pane
2172 }
2173
2174 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
2175 if let Some(remote_id) = remote_id {
2176 self.remote_entity_subscription = Some(
2177 self.app_state
2178 .client
2179 .add_view_for_remote_entity(remote_id, cx),
2180 );
2181 } else {
2182 self.remote_entity_subscription.take();
2183 }
2184 }
2185
2186 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2187 self.leader_state.followers.remove(&peer_id);
2188 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
2189 for state in states_by_pane.into_values() {
2190 for item in state.items_by_leader_view_id.into_values() {
2191 item.set_leader_replica_id(None, cx);
2192 }
2193 }
2194 }
2195 cx.notify();
2196 }
2197
2198 pub fn toggle_follow(
2199 &mut self,
2200 leader_id: PeerId,
2201 cx: &mut ViewContext<Self>,
2202 ) -> Option<Task<Result<()>>> {
2203 let pane = self.active_pane().clone();
2204
2205 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
2206 if leader_id == prev_leader_id {
2207 return None;
2208 }
2209 }
2210
2211 self.last_leaders_by_pane
2212 .insert(pane.downgrade(), leader_id);
2213 self.follower_states_by_leader
2214 .entry(leader_id)
2215 .or_default()
2216 .insert(pane.clone(), Default::default());
2217 cx.notify();
2218
2219 let project_id = self.project.read(cx).remote_id()?;
2220 let request = self.app_state.client.request(proto::Follow {
2221 project_id,
2222 leader_id: Some(leader_id),
2223 });
2224
2225 Some(cx.spawn(|this, mut cx| async move {
2226 let response = request.await?;
2227 this.update(&mut cx, |this, _| {
2228 let state = this
2229 .follower_states_by_leader
2230 .get_mut(&leader_id)
2231 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
2232 .ok_or_else(|| anyhow!("following interrupted"))?;
2233 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
2234 Some(ViewId::from_proto(active_view_id)?)
2235 } else {
2236 None
2237 };
2238 Ok::<_, anyhow::Error>(())
2239 })??;
2240 Self::add_views_from_leader(
2241 this.clone(),
2242 leader_id,
2243 vec![pane],
2244 response.views,
2245 &mut cx,
2246 )
2247 .await?;
2248 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
2249 Ok(())
2250 }))
2251 }
2252
2253 pub fn follow_next_collaborator(
2254 &mut self,
2255 _: &FollowNextCollaborator,
2256 cx: &mut ViewContext<Self>,
2257 ) -> Option<Task<Result<()>>> {
2258 let collaborators = self.project.read(cx).collaborators();
2259 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
2260 let mut collaborators = collaborators.keys().copied();
2261 for peer_id in collaborators.by_ref() {
2262 if peer_id == leader_id {
2263 break;
2264 }
2265 }
2266 collaborators.next()
2267 } else if let Some(last_leader_id) =
2268 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
2269 {
2270 if collaborators.contains_key(last_leader_id) {
2271 Some(*last_leader_id)
2272 } else {
2273 None
2274 }
2275 } else {
2276 None
2277 };
2278
2279 next_leader_id
2280 .or_else(|| collaborators.keys().copied().next())
2281 .and_then(|leader_id| self.toggle_follow(leader_id, cx))
2282 }
2283
2284 pub fn unfollow(
2285 &mut self,
2286 pane: &ViewHandle<Pane>,
2287 cx: &mut ViewContext<Self>,
2288 ) -> Option<PeerId> {
2289 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
2290 let leader_id = *leader_id;
2291 if let Some(state) = states_by_pane.remove(pane) {
2292 for (_, item) in state.items_by_leader_view_id {
2293 item.set_leader_replica_id(None, cx);
2294 }
2295
2296 if states_by_pane.is_empty() {
2297 self.follower_states_by_leader.remove(&leader_id);
2298 if let Some(project_id) = self.project.read(cx).remote_id() {
2299 self.app_state
2300 .client
2301 .send(proto::Unfollow {
2302 project_id,
2303 leader_id: Some(leader_id),
2304 })
2305 .log_err();
2306 }
2307 }
2308
2309 cx.notify();
2310 return Some(leader_id);
2311 }
2312 }
2313 None
2314 }
2315
2316 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2317 self.follower_states_by_leader.contains_key(&peer_id)
2318 }
2319
2320 pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
2321 self.leader_state.followers.contains(&peer_id)
2322 }
2323
2324 fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2325 // TODO: There should be a better system in place for this
2326 // (https://github.com/zed-industries/zed/issues/1290)
2327 let is_fullscreen = cx.window_is_fullscreen();
2328 let container_theme = if is_fullscreen {
2329 let mut container_theme = theme.titlebar.container;
2330 container_theme.padding.left = container_theme.padding.right;
2331 container_theme
2332 } else {
2333 theme.titlebar.container
2334 };
2335
2336 enum TitleBar {}
2337 MouseEventHandler::<TitleBar, _>::new(0, cx, |_, cx| {
2338 Stack::new()
2339 .with_children(
2340 self.titlebar_item
2341 .as_ref()
2342 .map(|item| ChildView::new(item, cx)),
2343 )
2344 .contained()
2345 .with_style(container_theme)
2346 })
2347 .on_click(MouseButton::Left, |event, _, cx| {
2348 if event.click_count == 2 {
2349 cx.zoom_window();
2350 }
2351 })
2352 .constrained()
2353 .with_height(theme.titlebar.height)
2354 .into_any_named("titlebar")
2355 }
2356
2357 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2358 let active_entry = self.active_project_path(cx);
2359 self.project
2360 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2361 self.update_window_title(cx);
2362 }
2363
2364 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2365 let project = self.project().read(cx);
2366 let mut title = String::new();
2367
2368 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2369 let filename = path
2370 .path
2371 .file_name()
2372 .map(|s| s.to_string_lossy())
2373 .or_else(|| {
2374 Some(Cow::Borrowed(
2375 project
2376 .worktree_for_id(path.worktree_id, cx)?
2377 .read(cx)
2378 .root_name(),
2379 ))
2380 });
2381
2382 if let Some(filename) = filename {
2383 title.push_str(filename.as_ref());
2384 title.push_str(" β ");
2385 }
2386 }
2387
2388 for (i, name) in project.worktree_root_names(cx).enumerate() {
2389 if i > 0 {
2390 title.push_str(", ");
2391 }
2392 title.push_str(name);
2393 }
2394
2395 if title.is_empty() {
2396 title = "empty project".to_string();
2397 }
2398
2399 if project.is_remote() {
2400 title.push_str(" β");
2401 } else if project.is_shared() {
2402 title.push_str(" β");
2403 }
2404
2405 cx.set_window_title(&title);
2406 }
2407
2408 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2409 let is_edited = !self.project.read(cx).is_read_only()
2410 && self
2411 .items(cx)
2412 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2413 if is_edited != self.window_edited {
2414 self.window_edited = is_edited;
2415 cx.set_window_edited(self.window_edited)
2416 }
2417 }
2418
2419 fn render_disconnected_overlay(
2420 &self,
2421 cx: &mut ViewContext<Workspace>,
2422 ) -> Option<AnyElement<Workspace>> {
2423 if self.project.read(cx).is_read_only() {
2424 enum DisconnectedOverlay {}
2425 Some(
2426 MouseEventHandler::<DisconnectedOverlay, _>::new(0, cx, |_, cx| {
2427 let theme = &theme::current(cx);
2428 Label::new(
2429 "Your connection to the remote project has been lost.",
2430 theme.workspace.disconnected_overlay.text.clone(),
2431 )
2432 .aligned()
2433 .contained()
2434 .with_style(theme.workspace.disconnected_overlay.container)
2435 })
2436 .with_cursor_style(CursorStyle::Arrow)
2437 .capture_all()
2438 .into_any_named("disconnected overlay"),
2439 )
2440 } else {
2441 None
2442 }
2443 }
2444
2445 fn render_notifications(
2446 &self,
2447 theme: &theme::Workspace,
2448 cx: &AppContext,
2449 ) -> Option<AnyElement<Workspace>> {
2450 if self.notifications.is_empty() {
2451 None
2452 } else {
2453 Some(
2454 Flex::column()
2455 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2456 ChildView::new(notification.as_any(), cx)
2457 .contained()
2458 .with_style(theme.notification)
2459 }))
2460 .constrained()
2461 .with_width(theme.notifications.width)
2462 .contained()
2463 .with_style(theme.notifications.container)
2464 .aligned()
2465 .bottom()
2466 .right()
2467 .into_any(),
2468 )
2469 }
2470 }
2471
2472 // RPC handlers
2473
2474 async fn handle_follow(
2475 this: WeakViewHandle<Self>,
2476 envelope: TypedEnvelope<proto::Follow>,
2477 _: Arc<Client>,
2478 mut cx: AsyncAppContext,
2479 ) -> Result<proto::FollowResponse> {
2480 this.update(&mut cx, |this, cx| {
2481 let client = &this.app_state.client;
2482 this.leader_state
2483 .followers
2484 .insert(envelope.original_sender_id()?);
2485
2486 let active_view_id = this.active_item(cx).and_then(|i| {
2487 Some(
2488 i.to_followable_item_handle(cx)?
2489 .remote_id(client, cx)?
2490 .to_proto(),
2491 )
2492 });
2493
2494 cx.notify();
2495
2496 Ok(proto::FollowResponse {
2497 active_view_id,
2498 views: this
2499 .panes()
2500 .iter()
2501 .flat_map(|pane| {
2502 let leader_id = this.leader_for_pane(pane);
2503 pane.read(cx).items().filter_map({
2504 let cx = &cx;
2505 move |item| {
2506 let item = item.to_followable_item_handle(cx)?;
2507 let id = item.remote_id(client, cx)?.to_proto();
2508 let variant = item.to_state_proto(cx)?;
2509 Some(proto::View {
2510 id: Some(id),
2511 leader_id,
2512 variant: Some(variant),
2513 })
2514 }
2515 })
2516 })
2517 .collect(),
2518 })
2519 })?
2520 }
2521
2522 async fn handle_unfollow(
2523 this: WeakViewHandle<Self>,
2524 envelope: TypedEnvelope<proto::Unfollow>,
2525 _: Arc<Client>,
2526 mut cx: AsyncAppContext,
2527 ) -> Result<()> {
2528 this.update(&mut cx, |this, cx| {
2529 this.leader_state
2530 .followers
2531 .remove(&envelope.original_sender_id()?);
2532 cx.notify();
2533 Ok(())
2534 })?
2535 }
2536
2537 async fn handle_update_followers(
2538 this: WeakViewHandle<Self>,
2539 envelope: TypedEnvelope<proto::UpdateFollowers>,
2540 _: Arc<Client>,
2541 cx: AsyncAppContext,
2542 ) -> Result<()> {
2543 let leader_id = envelope.original_sender_id()?;
2544 this.read_with(&cx, |this, _| {
2545 this.leader_updates_tx
2546 .unbounded_send((leader_id, envelope.payload))
2547 })??;
2548 Ok(())
2549 }
2550
2551 async fn process_leader_update(
2552 this: &WeakViewHandle<Self>,
2553 leader_id: PeerId,
2554 update: proto::UpdateFollowers,
2555 cx: &mut AsyncAppContext,
2556 ) -> Result<()> {
2557 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2558 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2559 this.update(cx, |this, _| {
2560 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2561 for state in state.values_mut() {
2562 state.active_view_id =
2563 if let Some(active_view_id) = update_active_view.id.clone() {
2564 Some(ViewId::from_proto(active_view_id)?)
2565 } else {
2566 None
2567 };
2568 }
2569 }
2570 anyhow::Ok(())
2571 })??;
2572 }
2573 proto::update_followers::Variant::UpdateView(update_view) => {
2574 let variant = update_view
2575 .variant
2576 .ok_or_else(|| anyhow!("missing update view variant"))?;
2577 let id = update_view
2578 .id
2579 .ok_or_else(|| anyhow!("missing update view id"))?;
2580 let mut tasks = Vec::new();
2581 this.update(cx, |this, cx| {
2582 let project = this.project.clone();
2583 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2584 for state in state.values_mut() {
2585 let view_id = ViewId::from_proto(id.clone())?;
2586 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2587 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2588 }
2589 }
2590 }
2591 anyhow::Ok(())
2592 })??;
2593 try_join_all(tasks).await.log_err();
2594 }
2595 proto::update_followers::Variant::CreateView(view) => {
2596 let panes = this.read_with(cx, |this, _| {
2597 this.follower_states_by_leader
2598 .get(&leader_id)
2599 .into_iter()
2600 .flat_map(|states_by_pane| states_by_pane.keys())
2601 .cloned()
2602 .collect()
2603 })?;
2604 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2605 }
2606 }
2607 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2608 Ok(())
2609 }
2610
2611 async fn add_views_from_leader(
2612 this: WeakViewHandle<Self>,
2613 leader_id: PeerId,
2614 panes: Vec<ViewHandle<Pane>>,
2615 views: Vec<proto::View>,
2616 cx: &mut AsyncAppContext,
2617 ) -> Result<()> {
2618 let project = this.read_with(cx, |this, _| this.project.clone())?;
2619 let replica_id = project
2620 .read_with(cx, |project, _| {
2621 project
2622 .collaborators()
2623 .get(&leader_id)
2624 .map(|c| c.replica_id)
2625 })
2626 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2627
2628 let item_builders = cx.update(|cx| {
2629 cx.default_global::<FollowableItemBuilders>()
2630 .values()
2631 .map(|b| b.0)
2632 .collect::<Vec<_>>()
2633 });
2634
2635 let mut item_tasks_by_pane = HashMap::default();
2636 for pane in panes {
2637 let mut item_tasks = Vec::new();
2638 let mut leader_view_ids = Vec::new();
2639 for view in &views {
2640 let Some(id) = &view.id else { continue };
2641 let id = ViewId::from_proto(id.clone())?;
2642 let mut variant = view.variant.clone();
2643 if variant.is_none() {
2644 Err(anyhow!("missing variant"))?;
2645 }
2646 for build_item in &item_builders {
2647 let task = cx.update(|cx| {
2648 build_item(pane.clone(), project.clone(), id, &mut variant, cx)
2649 });
2650 if let Some(task) = task {
2651 item_tasks.push(task);
2652 leader_view_ids.push(id);
2653 break;
2654 } else {
2655 assert!(variant.is_some());
2656 }
2657 }
2658 }
2659
2660 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2661 }
2662
2663 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2664 let items = futures::future::try_join_all(item_tasks).await?;
2665 this.update(cx, |this, cx| {
2666 let state = this
2667 .follower_states_by_leader
2668 .get_mut(&leader_id)?
2669 .get_mut(&pane)?;
2670
2671 for (id, item) in leader_view_ids.into_iter().zip(items) {
2672 item.set_leader_replica_id(Some(replica_id), cx);
2673 state.items_by_leader_view_id.insert(id, item);
2674 }
2675
2676 Some(())
2677 })?;
2678 }
2679 Ok(())
2680 }
2681
2682 fn update_active_view_for_followers(&self, cx: &AppContext) {
2683 if self.active_pane.read(cx).has_focus() {
2684 self.update_followers(
2685 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
2686 id: self.active_item(cx).and_then(|item| {
2687 item.to_followable_item_handle(cx)?
2688 .remote_id(&self.app_state.client, cx)
2689 .map(|id| id.to_proto())
2690 }),
2691 leader_id: self.leader_for_pane(&self.active_pane),
2692 }),
2693 cx,
2694 );
2695 } else {
2696 self.update_followers(
2697 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
2698 id: None,
2699 leader_id: None,
2700 }),
2701 cx,
2702 );
2703 }
2704 }
2705
2706 fn update_followers(
2707 &self,
2708 update: proto::update_followers::Variant,
2709 cx: &AppContext,
2710 ) -> Option<()> {
2711 let project_id = self.project.read(cx).remote_id()?;
2712 if !self.leader_state.followers.is_empty() {
2713 self.app_state
2714 .client
2715 .send(proto::UpdateFollowers {
2716 project_id,
2717 follower_ids: self.leader_state.followers.iter().copied().collect(),
2718 variant: Some(update),
2719 })
2720 .log_err();
2721 }
2722 None
2723 }
2724
2725 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2726 self.follower_states_by_leader
2727 .iter()
2728 .find_map(|(leader_id, state)| {
2729 if state.contains_key(pane) {
2730 Some(*leader_id)
2731 } else {
2732 None
2733 }
2734 })
2735 }
2736
2737 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2738 cx.notify();
2739
2740 let call = self.active_call()?;
2741 let room = call.read(cx).room()?.read(cx);
2742 let participant = room.remote_participant_for_peer_id(leader_id)?;
2743 let mut items_to_activate = Vec::new();
2744 match participant.location {
2745 call::ParticipantLocation::SharedProject { project_id } => {
2746 if Some(project_id) == self.project.read(cx).remote_id() {
2747 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2748 if let Some(item) = state
2749 .active_view_id
2750 .and_then(|id| state.items_by_leader_view_id.get(&id))
2751 {
2752 items_to_activate.push((pane.clone(), item.boxed_clone()));
2753 } else if let Some(shared_screen) =
2754 self.shared_screen_for_peer(leader_id, pane, cx)
2755 {
2756 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2757 }
2758 }
2759 }
2760 }
2761 call::ParticipantLocation::UnsharedProject => {}
2762 call::ParticipantLocation::External => {
2763 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2764 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2765 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2766 }
2767 }
2768 }
2769 }
2770
2771 for (pane, item) in items_to_activate {
2772 let pane_was_focused = pane.read(cx).has_focus();
2773 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2774 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2775 } else {
2776 pane.update(cx, |pane, cx| {
2777 pane.add_item(item.boxed_clone(), false, false, None, cx)
2778 });
2779 }
2780
2781 if pane_was_focused {
2782 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2783 }
2784 }
2785
2786 None
2787 }
2788
2789 fn shared_screen_for_peer(
2790 &self,
2791 peer_id: PeerId,
2792 pane: &ViewHandle<Pane>,
2793 cx: &mut ViewContext<Self>,
2794 ) -> Option<ViewHandle<SharedScreen>> {
2795 let call = self.active_call()?;
2796 let room = call.read(cx).room()?.read(cx);
2797 let participant = room.remote_participant_for_peer_id(peer_id)?;
2798 let track = participant.video_tracks.values().next()?.clone();
2799 let user = participant.user.clone();
2800
2801 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2802 if item.read(cx).peer_id == peer_id {
2803 return Some(item);
2804 }
2805 }
2806
2807 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2808 }
2809
2810 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2811 if active {
2812 cx.background()
2813 .spawn(persistence::DB.update_timestamp(self.database_id()))
2814 .detach();
2815 } else {
2816 for pane in &self.panes {
2817 pane.update(cx, |pane, cx| {
2818 if let Some(item) = pane.active_item() {
2819 item.workspace_deactivated(cx);
2820 }
2821 if matches!(
2822 settings::get::<WorkspaceSettings>(cx).autosave,
2823 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
2824 ) {
2825 for item in pane.items() {
2826 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2827 .detach_and_log_err(cx);
2828 }
2829 }
2830 });
2831 }
2832 }
2833 }
2834
2835 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2836 self.active_call.as_ref().map(|(call, _)| call)
2837 }
2838
2839 fn on_active_call_event(
2840 &mut self,
2841 _: ModelHandle<ActiveCall>,
2842 event: &call::room::Event,
2843 cx: &mut ViewContext<Self>,
2844 ) {
2845 match event {
2846 call::room::Event::ParticipantLocationChanged { participant_id }
2847 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2848 self.leader_updated(*participant_id, cx);
2849 }
2850 _ => {}
2851 }
2852 }
2853
2854 pub fn database_id(&self) -> WorkspaceId {
2855 self.database_id
2856 }
2857
2858 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2859 let project = self.project().read(cx);
2860
2861 if project.is_local() {
2862 Some(
2863 project
2864 .visible_worktrees(cx)
2865 .map(|worktree| worktree.read(cx).abs_path())
2866 .collect::<Vec<_>>()
2867 .into(),
2868 )
2869 } else {
2870 None
2871 }
2872 }
2873
2874 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2875 match member {
2876 Member::Axis(PaneAxis { members, .. }) => {
2877 for child in members.iter() {
2878 self.remove_panes(child.clone(), cx)
2879 }
2880 }
2881 Member::Pane(pane) => {
2882 self.force_remove_pane(&pane, cx);
2883 }
2884 }
2885 }
2886
2887 fn force_remove_pane(&mut self, pane: &ViewHandle<Pane>, cx: &mut ViewContext<Workspace>) {
2888 self.panes.retain(|p| p != pane);
2889 cx.focus(self.panes.last().unwrap());
2890 if self.last_active_center_pane == Some(pane.downgrade()) {
2891 self.last_active_center_pane = None;
2892 }
2893 cx.notify();
2894 }
2895
2896 fn serialize_workspace(&self, cx: &ViewContext<Self>) {
2897 fn serialize_pane_handle(
2898 pane_handle: &ViewHandle<Pane>,
2899 cx: &AppContext,
2900 ) -> SerializedPane {
2901 let (items, active) = {
2902 let pane = pane_handle.read(cx);
2903 let active_item_id = pane.active_item().map(|item| item.id());
2904 (
2905 pane.items()
2906 .filter_map(|item_handle| {
2907 Some(SerializedItem {
2908 kind: Arc::from(item_handle.serialized_item_kind()?),
2909 item_id: item_handle.id(),
2910 active: Some(item_handle.id()) == active_item_id,
2911 })
2912 })
2913 .collect::<Vec<_>>(),
2914 pane.has_focus(),
2915 )
2916 };
2917
2918 SerializedPane::new(items, active)
2919 }
2920
2921 fn build_serialized_pane_group(
2922 pane_group: &Member,
2923 cx: &AppContext,
2924 ) -> SerializedPaneGroup {
2925 match pane_group {
2926 Member::Axis(PaneAxis { axis, members, .. }) => SerializedPaneGroup::Group {
2927 axis: *axis,
2928 children: members
2929 .iter()
2930 .map(|member| build_serialized_pane_group(member, cx))
2931 .collect::<Vec<_>>(),
2932 },
2933 Member::Pane(pane_handle) => {
2934 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2935 }
2936 }
2937 }
2938
2939 fn build_serialized_docks(this: &Workspace, cx: &ViewContext<Workspace>) -> DockStructure {
2940 let left_dock = this.left_dock.read(cx);
2941 let left_visible = left_dock.is_open();
2942 let left_active_panel = left_dock.visible_panel().and_then(|panel| {
2943 Some(
2944 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2945 .to_string(),
2946 )
2947 });
2948 let left_dock_zoom = left_dock
2949 .visible_panel()
2950 .map(|panel| panel.is_zoomed(cx))
2951 .unwrap_or(false);
2952
2953 let right_dock = this.right_dock.read(cx);
2954 let right_visible = right_dock.is_open();
2955 let right_active_panel = right_dock.visible_panel().and_then(|panel| {
2956 Some(
2957 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2958 .to_string(),
2959 )
2960 });
2961 let right_dock_zoom = right_dock
2962 .visible_panel()
2963 .map(|panel| panel.is_zoomed(cx))
2964 .unwrap_or(false);
2965
2966 let bottom_dock = this.bottom_dock.read(cx);
2967 let bottom_visible = bottom_dock.is_open();
2968 let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| {
2969 Some(
2970 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2971 .to_string(),
2972 )
2973 });
2974 let bottom_dock_zoom = bottom_dock
2975 .visible_panel()
2976 .map(|panel| panel.is_zoomed(cx))
2977 .unwrap_or(false);
2978
2979 DockStructure {
2980 left: DockData {
2981 visible: left_visible,
2982 active_panel: left_active_panel,
2983 zoom: left_dock_zoom,
2984 },
2985 right: DockData {
2986 visible: right_visible,
2987 active_panel: right_active_panel,
2988 zoom: right_dock_zoom,
2989 },
2990 bottom: DockData {
2991 visible: bottom_visible,
2992 active_panel: bottom_active_panel,
2993 zoom: bottom_dock_zoom,
2994 },
2995 }
2996 }
2997
2998 if let Some(location) = self.location(cx) {
2999 // Load bearing special case:
3000 // - with_local_workspace() relies on this to not have other stuff open
3001 // when you open your log
3002 if !location.paths().is_empty() {
3003 let center_group = build_serialized_pane_group(&self.center.root, cx);
3004 let docks = build_serialized_docks(self, cx);
3005
3006 let serialized_workspace = SerializedWorkspace {
3007 id: self.database_id,
3008 location,
3009 center_group,
3010 bounds: Default::default(),
3011 display: Default::default(),
3012 docks,
3013 };
3014
3015 cx.background()
3016 .spawn(persistence::DB.save_workspace(serialized_workspace))
3017 .detach();
3018 }
3019 }
3020 }
3021
3022 pub(crate) fn load_workspace(
3023 workspace: WeakViewHandle<Workspace>,
3024 serialized_workspace: SerializedWorkspace,
3025 paths_to_open: Vec<Option<ProjectPath>>,
3026 cx: &mut AppContext,
3027 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
3028 cx.spawn(|mut cx| async move {
3029 let result = async_iife! {{
3030 let (project, old_center_pane) =
3031 workspace.read_with(&cx, |workspace, _| {
3032 (
3033 workspace.project().clone(),
3034 workspace.last_active_center_pane.clone(),
3035 )
3036 })?;
3037
3038 let mut center_items = None;
3039 let mut center_group = None;
3040 // Traverse the splits tree and add to things
3041 if let Some((group, active_pane, items)) = serialized_workspace
3042 .center_group
3043 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
3044 .await {
3045 center_items = Some(items);
3046 center_group = Some((group, active_pane))
3047 }
3048
3049 let resulting_list = cx.read(|cx| {
3050 let mut opened_items = center_items
3051 .unwrap_or_default()
3052 .into_iter()
3053 .filter_map(|item| {
3054 let item = item?;
3055 let project_path = item.project_path(cx)?;
3056 Some((project_path, item))
3057 })
3058 .collect::<HashMap<_, _>>();
3059
3060 paths_to_open
3061 .into_iter()
3062 .map(|path_to_open| {
3063 path_to_open.map(|path_to_open| {
3064 Ok(opened_items.remove(&path_to_open))
3065 })
3066 .transpose()
3067 .map(|item| item.flatten())
3068 .transpose()
3069 })
3070 .collect::<Vec<_>>()
3071 });
3072
3073 // Remove old panes from workspace panes list
3074 workspace.update(&mut cx, |workspace, cx| {
3075 if let Some((center_group, active_pane)) = center_group {
3076 workspace.remove_panes(workspace.center.root.clone(), cx);
3077
3078 // Swap workspace center group
3079 workspace.center = PaneGroup::with_root(center_group);
3080
3081 // Change the focus to the workspace first so that we retrigger focus in on the pane.
3082 cx.focus_self();
3083
3084 if let Some(active_pane) = active_pane {
3085 cx.focus(&active_pane);
3086 } else {
3087 cx.focus(workspace.panes.last().unwrap());
3088 }
3089 } else {
3090 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
3091 if let Some(old_center_handle) = old_center_handle {
3092 cx.focus(&old_center_handle)
3093 } else {
3094 cx.focus_self()
3095 }
3096 }
3097
3098 let docks = serialized_workspace.docks;
3099 workspace.left_dock.update(cx, |dock, cx| {
3100 dock.set_open(docks.left.visible, cx);
3101 if let Some(active_panel) = docks.left.active_panel {
3102 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3103 dock.activate_panel(ix, cx);
3104 }
3105 }
3106 dock.active_panel()
3107 .map(|panel| {
3108 panel.set_zoomed(docks.left.zoom, cx)
3109 });
3110 if docks.left.visible && docks.left.zoom {
3111 cx.focus_self()
3112 }
3113 });
3114 // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
3115 workspace.right_dock.update(cx, |dock, cx| {
3116 dock.set_open(docks.right.visible, cx);
3117 if let Some(active_panel) = docks.right.active_panel {
3118 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3119 dock.activate_panel(ix, cx);
3120
3121 }
3122 }
3123 dock.active_panel()
3124 .map(|panel| {
3125 panel.set_zoomed(docks.right.zoom, cx)
3126 });
3127
3128 if docks.right.visible && docks.right.zoom {
3129 cx.focus_self()
3130 }
3131 });
3132 workspace.bottom_dock.update(cx, |dock, cx| {
3133 dock.set_open(docks.bottom.visible, cx);
3134 if let Some(active_panel) = docks.bottom.active_panel {
3135 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3136 dock.activate_panel(ix, cx);
3137 }
3138 }
3139
3140 dock.active_panel()
3141 .map(|panel| {
3142 panel.set_zoomed(docks.bottom.zoom, cx)
3143 });
3144
3145 if docks.bottom.visible && docks.bottom.zoom {
3146 cx.focus_self()
3147 }
3148 });
3149
3150
3151 cx.notify();
3152 })?;
3153
3154 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3155 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3156
3157 Ok::<_, anyhow::Error>(resulting_list)
3158 }};
3159
3160 result.await.unwrap_or_default()
3161 })
3162 }
3163
3164 #[cfg(any(test, feature = "test-support"))]
3165 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
3166 let app_state = Arc::new(AppState {
3167 languages: project.read(cx).languages().clone(),
3168 client: project.read(cx).client(),
3169 user_store: project.read(cx).user_store(),
3170 fs: project.read(cx).fs().clone(),
3171 build_window_options: |_, _, _| Default::default(),
3172 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
3173 background_actions: || &[],
3174 });
3175 Self::new(0, project, app_state, cx)
3176 }
3177
3178 fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
3179 let dock = match position {
3180 DockPosition::Left => &self.left_dock,
3181 DockPosition::Right => &self.right_dock,
3182 DockPosition::Bottom => &self.bottom_dock,
3183 };
3184 let active_panel = dock.read(cx).visible_panel()?;
3185 let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
3186 dock.read(cx).render_placeholder(cx)
3187 } else {
3188 ChildView::new(dock, cx).into_any()
3189 };
3190
3191 Some(
3192 element
3193 .constrained()
3194 .dynamically(move |constraint, _, cx| match position {
3195 DockPosition::Left | DockPosition::Right => SizeConstraint::new(
3196 Vector2F::new(20., constraint.min.y()),
3197 Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
3198 ),
3199 DockPosition::Bottom => SizeConstraint::new(
3200 Vector2F::new(constraint.min.x(), 20.),
3201 Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
3202 ),
3203 })
3204 .into_any(),
3205 )
3206 }
3207}
3208
3209fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3210 ZED_WINDOW_POSITION
3211 .zip(*ZED_WINDOW_SIZE)
3212 .map(|(position, size)| {
3213 WindowBounds::Fixed(RectF::new(
3214 cx.platform().screens()[0].bounds().origin() + position,
3215 size,
3216 ))
3217 })
3218}
3219
3220async fn open_items(
3221 serialized_workspace: Option<SerializedWorkspace>,
3222 workspace: &WeakViewHandle<Workspace>,
3223 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3224 app_state: Arc<AppState>,
3225 mut cx: AsyncAppContext,
3226) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
3227 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3228
3229 if let Some(serialized_workspace) = serialized_workspace {
3230 let workspace = workspace.clone();
3231 let restored_items = cx
3232 .update(|cx| {
3233 Workspace::load_workspace(
3234 workspace,
3235 serialized_workspace,
3236 project_paths_to_open
3237 .iter()
3238 .map(|(_, project_path)| project_path)
3239 .cloned()
3240 .collect(),
3241 cx,
3242 )
3243 })
3244 .await;
3245
3246 let restored_project_paths = cx.read(|cx| {
3247 restored_items
3248 .iter()
3249 .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
3250 .collect::<HashSet<_>>()
3251 });
3252
3253 opened_items = restored_items;
3254 project_paths_to_open
3255 .iter_mut()
3256 .for_each(|(_, project_path)| {
3257 if let Some(project_path_to_open) = project_path {
3258 if restored_project_paths.contains(project_path_to_open) {
3259 *project_path = None;
3260 }
3261 }
3262 });
3263 } else {
3264 for _ in 0..project_paths_to_open.len() {
3265 opened_items.push(None);
3266 }
3267 }
3268 assert!(opened_items.len() == project_paths_to_open.len());
3269
3270 let tasks =
3271 project_paths_to_open
3272 .into_iter()
3273 .enumerate()
3274 .map(|(i, (abs_path, project_path))| {
3275 let workspace = workspace.clone();
3276 cx.spawn(|mut cx| {
3277 let fs = app_state.fs.clone();
3278 async move {
3279 let file_project_path = project_path?;
3280 if fs.is_file(&abs_path).await {
3281 Some((
3282 i,
3283 workspace
3284 .update(&mut cx, |workspace, cx| {
3285 workspace.open_path(file_project_path, None, true, cx)
3286 })
3287 .log_err()?
3288 .await,
3289 ))
3290 } else {
3291 None
3292 }
3293 }
3294 })
3295 });
3296
3297 for maybe_opened_path in futures::future::join_all(tasks.into_iter())
3298 .await
3299 .into_iter()
3300 {
3301 if let Some((i, path_open_result)) = maybe_opened_path {
3302 opened_items[i] = Some(path_open_result);
3303 }
3304 }
3305
3306 opened_items
3307}
3308
3309fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3310 const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
3311 const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
3312 const MESSAGE_ID: usize = 2;
3313
3314 if workspace
3315 .read_with(cx, |workspace, cx| {
3316 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3317 })
3318 .unwrap_or(false)
3319 {
3320 return;
3321 }
3322
3323 if db::kvp::KEY_VALUE_STORE
3324 .read_kvp(NEW_DOCK_HINT_KEY)
3325 .ok()
3326 .flatten()
3327 .is_some()
3328 {
3329 if !workspace
3330 .read_with(cx, |workspace, cx| {
3331 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3332 })
3333 .unwrap_or(false)
3334 {
3335 cx.update(|cx| {
3336 cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
3337 let entry = tracker
3338 .entry(TypeId::of::<MessageNotification>())
3339 .or_default();
3340 if !entry.contains(&MESSAGE_ID) {
3341 entry.push(MESSAGE_ID);
3342 }
3343 });
3344 });
3345 }
3346
3347 return;
3348 }
3349
3350 cx.spawn(|_| async move {
3351 db::kvp::KEY_VALUE_STORE
3352 .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
3353 .await
3354 .ok();
3355 })
3356 .detach();
3357
3358 workspace
3359 .update(cx, |workspace, cx| {
3360 workspace.show_notification_once(2, cx, |cx| {
3361 cx.add_view(|_| {
3362 MessageNotification::new_element(|text, _| {
3363 Text::new(
3364 "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
3365 text,
3366 )
3367 .with_custom_runs(vec![26..32, 34..46], |_, bounds, scene, cx| {
3368 let code_span_background_color = settings::get::<ThemeSettings>(cx)
3369 .theme
3370 .editor
3371 .document_highlight_read_background;
3372
3373 scene.push_quad(gpui::Quad {
3374 bounds,
3375 background: Some(code_span_background_color),
3376 border: Default::default(),
3377 corner_radius: 2.0,
3378 })
3379 })
3380 .into_any()
3381 })
3382 .with_click_message("Read more about the new panel system")
3383 .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
3384 })
3385 })
3386 })
3387 .ok();
3388}
3389
3390fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3391 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3392
3393 workspace
3394 .update(cx, |workspace, cx| {
3395 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3396 workspace.show_notification_once(0, cx, |cx| {
3397 cx.add_view(|_| {
3398 MessageNotification::new("Failed to load any database file.")
3399 .with_click_message("Click to let us know about this error")
3400 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
3401 })
3402 });
3403 } else {
3404 let backup_path = (*db::BACKUP_DB_PATH).read();
3405 if let Some(backup_path) = backup_path.clone() {
3406 workspace.show_notification_once(1, cx, move |cx| {
3407 cx.add_view(move |_| {
3408 MessageNotification::new(format!(
3409 "Database file was corrupted. Old database backed up to {}",
3410 backup_path.display()
3411 ))
3412 .with_click_message("Click to show old database in finder")
3413 .on_click(move |cx| {
3414 cx.platform().open_url(&backup_path.to_string_lossy())
3415 })
3416 })
3417 });
3418 }
3419 }
3420 })
3421 .log_err();
3422}
3423
3424impl Entity for Workspace {
3425 type Event = Event;
3426}
3427
3428impl View for Workspace {
3429 fn ui_name() -> &'static str {
3430 "Workspace"
3431 }
3432
3433 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
3434 let theme = theme::current(cx).clone();
3435 Stack::new()
3436 .with_child(
3437 Flex::column()
3438 .with_child(self.render_titlebar(&theme, cx))
3439 .with_child(
3440 Stack::new()
3441 .with_child({
3442 let project = self.project.clone();
3443 Flex::row()
3444 .with_children(self.render_dock(DockPosition::Left, cx))
3445 .with_child(
3446 Flex::column()
3447 .with_child(
3448 FlexItem::new(
3449 self.center.render(
3450 &project,
3451 &theme,
3452 &self.follower_states_by_leader,
3453 self.active_call(),
3454 self.active_pane(),
3455 self.zoomed
3456 .as_ref()
3457 .and_then(|zoomed| zoomed.upgrade(cx))
3458 .as_ref(),
3459 &self.app_state,
3460 cx,
3461 ),
3462 )
3463 .flex(1., true),
3464 )
3465 .with_children(
3466 self.render_dock(DockPosition::Bottom, cx),
3467 )
3468 .flex(1., true),
3469 )
3470 .with_children(self.render_dock(DockPosition::Right, cx))
3471 })
3472 .with_child(Overlay::new(
3473 Stack::new()
3474 .with_children(self.zoomed.as_ref().and_then(|zoomed| {
3475 enum ZoomBackground {}
3476 let zoomed = zoomed.upgrade(cx)?;
3477
3478 let mut foreground_style =
3479 theme.workspace.zoomed_pane_foreground;
3480 if let Some(zoomed_dock_position) = self.zoomed_position {
3481 foreground_style =
3482 theme.workspace.zoomed_panel_foreground;
3483 let margin = foreground_style.margin.top;
3484 let border = foreground_style.border.top;
3485
3486 // Only include a margin and border on the opposite side.
3487 foreground_style.margin.top = 0.;
3488 foreground_style.margin.left = 0.;
3489 foreground_style.margin.bottom = 0.;
3490 foreground_style.margin.right = 0.;
3491 foreground_style.border.top = false;
3492 foreground_style.border.left = false;
3493 foreground_style.border.bottom = false;
3494 foreground_style.border.right = false;
3495 match zoomed_dock_position {
3496 DockPosition::Left => {
3497 foreground_style.margin.right = margin;
3498 foreground_style.border.right = border;
3499 }
3500 DockPosition::Right => {
3501 foreground_style.margin.left = margin;
3502 foreground_style.border.left = border;
3503 }
3504 DockPosition::Bottom => {
3505 foreground_style.margin.top = margin;
3506 foreground_style.border.top = border;
3507 }
3508 }
3509 }
3510
3511 Some(
3512 ChildView::new(&zoomed, cx)
3513 .contained()
3514 .with_style(foreground_style)
3515 .aligned()
3516 .contained()
3517 .with_style(theme.workspace.zoomed_background)
3518 .mouse::<ZoomBackground>(0)
3519 .capture_all()
3520 .on_down(
3521 MouseButton::Left,
3522 |_, this: &mut Self, cx| {
3523 this.zoom_out(cx);
3524 },
3525 ),
3526 )
3527 }))
3528 .with_children(self.modal.as_ref().map(|modal| {
3529 ChildView::new(modal.view.as_any(), cx)
3530 .contained()
3531 .with_style(theme.workspace.modal)
3532 .aligned()
3533 .top()
3534 }))
3535 .with_children(self.render_notifications(&theme.workspace, cx)),
3536 ))
3537 .flex(1.0, true),
3538 )
3539 .with_child(ChildView::new(&self.status_bar, cx))
3540 .contained()
3541 .with_background_color(theme.workspace.background),
3542 )
3543 .with_children(DragAndDrop::render(cx))
3544 .with_children(self.render_disconnected_overlay(cx))
3545 .into_any_named("workspace")
3546 }
3547
3548 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
3549 if cx.is_self_focused() {
3550 cx.focus(&self.active_pane);
3551 }
3552 }
3553}
3554
3555impl ViewId {
3556 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3557 Ok(Self {
3558 creator: message
3559 .creator
3560 .ok_or_else(|| anyhow!("creator is missing"))?,
3561 id: message.id,
3562 })
3563 }
3564
3565 pub(crate) fn to_proto(&self) -> proto::ViewId {
3566 proto::ViewId {
3567 creator: Some(self.creator),
3568 id: self.id,
3569 }
3570 }
3571}
3572
3573pub trait WorkspaceHandle {
3574 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3575}
3576
3577impl WorkspaceHandle for ViewHandle<Workspace> {
3578 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3579 self.read(cx)
3580 .worktrees(cx)
3581 .flat_map(|worktree| {
3582 let worktree_id = worktree.read(cx).id();
3583 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3584 worktree_id,
3585 path: f.path.clone(),
3586 })
3587 })
3588 .collect::<Vec<_>>()
3589 }
3590}
3591
3592impl std::fmt::Debug for OpenPaths {
3593 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3594 f.debug_struct("OpenPaths")
3595 .field("paths", &self.paths)
3596 .finish()
3597 }
3598}
3599
3600pub struct WorkspaceCreated(pub WeakViewHandle<Workspace>);
3601
3602pub fn activate_workspace_for_project(
3603 cx: &mut AsyncAppContext,
3604 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
3605) -> Option<WeakViewHandle<Workspace>> {
3606 for window_id in cx.window_ids() {
3607 let handle = cx
3608 .update_window(window_id, |cx| {
3609 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
3610 let project = workspace_handle.read(cx).project.clone();
3611 if project.update(cx, &predicate) {
3612 cx.activate_window();
3613 return Some(workspace_handle.clone());
3614 }
3615 }
3616 None
3617 })
3618 .flatten();
3619
3620 if let Some(handle) = handle {
3621 return Some(handle.downgrade());
3622 }
3623 }
3624 None
3625}
3626
3627pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3628 DB.last_workspace().await.log_err().flatten()
3629}
3630
3631#[allow(clippy::type_complexity)]
3632pub fn open_paths(
3633 abs_paths: &[PathBuf],
3634 app_state: &Arc<AppState>,
3635 requesting_window_id: Option<usize>,
3636 cx: &mut AppContext,
3637) -> Task<
3638 Result<(
3639 WeakViewHandle<Workspace>,
3640 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
3641 )>,
3642> {
3643 let app_state = app_state.clone();
3644 let abs_paths = abs_paths.to_vec();
3645 cx.spawn(|mut cx| async move {
3646 // Open paths in existing workspace if possible
3647 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
3648 project.contains_paths(&abs_paths, cx)
3649 });
3650
3651 if let Some(existing) = existing {
3652 Ok((
3653 existing.clone(),
3654 existing
3655 .update(&mut cx, |workspace, cx| {
3656 workspace.open_paths(abs_paths, true, cx)
3657 })?
3658 .await,
3659 ))
3660 } else {
3661 Ok(cx
3662 .update(|cx| {
3663 Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx)
3664 })
3665 .await)
3666 }
3667 })
3668}
3669
3670pub fn open_new(
3671 app_state: &Arc<AppState>,
3672 cx: &mut AppContext,
3673 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
3674) -> Task<()> {
3675 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
3676 cx.spawn(|mut cx| async move {
3677 let (workspace, opened_paths) = task.await;
3678
3679 workspace
3680 .update(&mut cx, |workspace, cx| {
3681 if opened_paths.is_empty() {
3682 init(workspace, cx)
3683 }
3684 })
3685 .log_err();
3686 })
3687}
3688
3689pub fn create_and_open_local_file(
3690 path: &'static Path,
3691 cx: &mut ViewContext<Workspace>,
3692 default_content: impl 'static + Send + FnOnce() -> Rope,
3693) -> Task<Result<Box<dyn ItemHandle>>> {
3694 cx.spawn(|workspace, mut cx| async move {
3695 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
3696 if !fs.is_file(path).await {
3697 fs.create_file(path, Default::default()).await?;
3698 fs.save(path, &default_content(), Default::default())
3699 .await?;
3700 }
3701
3702 let mut items = workspace
3703 .update(&mut cx, |workspace, cx| {
3704 workspace.with_local_workspace(cx, |workspace, cx| {
3705 workspace.open_paths(vec![path.to_path_buf()], false, cx)
3706 })
3707 })?
3708 .await?
3709 .await;
3710
3711 let item = items.pop().flatten();
3712 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
3713 })
3714}
3715
3716pub fn join_remote_project(
3717 project_id: u64,
3718 follow_user_id: u64,
3719 app_state: Arc<AppState>,
3720 cx: &mut AppContext,
3721) -> Task<Result<()>> {
3722 cx.spawn(|mut cx| async move {
3723 let existing_workspace = cx
3724 .window_ids()
3725 .into_iter()
3726 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
3727 .find(|workspace| {
3728 cx.read_window(workspace.window_id(), |cx| {
3729 workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
3730 })
3731 .unwrap_or(false)
3732 });
3733
3734 let workspace = if let Some(existing_workspace) = existing_workspace {
3735 existing_workspace.downgrade()
3736 } else {
3737 let active_call = cx.read(ActiveCall::global);
3738 let room = active_call
3739 .read_with(&cx, |call, _| call.room().cloned())
3740 .ok_or_else(|| anyhow!("not in a call"))?;
3741 let project = room
3742 .update(&mut cx, |room, cx| {
3743 room.join_project(
3744 project_id,
3745 app_state.languages.clone(),
3746 app_state.fs.clone(),
3747 cx,
3748 )
3749 })
3750 .await?;
3751
3752 let window_bounds_override = window_bounds_env_override(&cx);
3753 let (_, workspace) = cx.add_window(
3754 (app_state.build_window_options)(
3755 window_bounds_override,
3756 None,
3757 cx.platform().as_ref(),
3758 ),
3759 |cx| Workspace::new(0, project, app_state.clone(), cx),
3760 );
3761 (app_state.initialize_workspace)(
3762 workspace.downgrade(),
3763 false,
3764 app_state.clone(),
3765 cx.clone(),
3766 )
3767 .await
3768 .log_err();
3769
3770 workspace.downgrade()
3771 };
3772
3773 cx.activate_window(workspace.window_id());
3774 cx.platform().activate(true);
3775
3776 workspace.update(&mut cx, |workspace, cx| {
3777 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
3778 let follow_peer_id = room
3779 .read(cx)
3780 .remote_participants()
3781 .iter()
3782 .find(|(_, participant)| participant.user.id == follow_user_id)
3783 .map(|(_, p)| p.peer_id)
3784 .or_else(|| {
3785 // If we couldn't follow the given user, follow the host instead.
3786 let collaborator = workspace
3787 .project()
3788 .read(cx)
3789 .collaborators()
3790 .values()
3791 .find(|collaborator| collaborator.replica_id == 0)?;
3792 Some(collaborator.peer_id)
3793 });
3794
3795 if let Some(follow_peer_id) = follow_peer_id {
3796 if !workspace.is_being_followed(follow_peer_id) {
3797 workspace
3798 .toggle_follow(follow_peer_id, cx)
3799 .map(|follow| follow.detach_and_log_err(cx));
3800 }
3801 }
3802 }
3803 })?;
3804
3805 anyhow::Ok(())
3806 })
3807}
3808
3809pub fn restart(_: &Restart, cx: &mut AppContext) {
3810 let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
3811 cx.spawn(|mut cx| async move {
3812 let mut workspaces = cx
3813 .window_ids()
3814 .into_iter()
3815 .filter_map(|window_id| {
3816 Some(
3817 cx.root_view(window_id)?
3818 .clone()
3819 .downcast::<Workspace>()?
3820 .downgrade(),
3821 )
3822 })
3823 .collect::<Vec<_>>();
3824
3825 // If multiple windows have unsaved changes, and need a save prompt,
3826 // prompt in the active window before switching to a different window.
3827 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
3828
3829 if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
3830 let answer = cx.prompt(
3831 workspace.window_id(),
3832 PromptLevel::Info,
3833 "Are you sure you want to restart?",
3834 &["Restart", "Cancel"],
3835 );
3836
3837 if let Some(mut answer) = answer {
3838 let answer = answer.next().await;
3839 if answer != Some(0) {
3840 return Ok(());
3841 }
3842 }
3843 }
3844
3845 // If the user cancels any save prompt, then keep the app open.
3846 for workspace in workspaces {
3847 if !workspace
3848 .update(&mut cx, |workspace, cx| {
3849 workspace.prepare_to_close(true, cx)
3850 })?
3851 .await?
3852 {
3853 return Ok(());
3854 }
3855 }
3856 cx.platform().restart();
3857 anyhow::Ok(())
3858 })
3859 .detach_and_log_err(cx);
3860}
3861
3862fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
3863 let mut parts = value.split(',');
3864 let width: usize = parts.next()?.parse().ok()?;
3865 let height: usize = parts.next()?.parse().ok()?;
3866 Some(vec2f(width as f32, height as f32))
3867}
3868
3869#[cfg(test)]
3870mod tests {
3871 use super::*;
3872 use crate::{
3873 dock::test::{TestPanel, TestPanelEvent},
3874 item::test::{TestItem, TestItemEvent, TestProjectItem},
3875 };
3876 use fs::FakeFs;
3877 use gpui::{executor::Deterministic, test::EmptyView, TestAppContext};
3878 use project::{Project, ProjectEntryId};
3879 use serde_json::json;
3880 use settings::SettingsStore;
3881 use std::{cell::RefCell, rc::Rc};
3882
3883 #[gpui::test]
3884 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
3885 init_test(cx);
3886
3887 let fs = FakeFs::new(cx.background());
3888 let project = Project::test(fs, [], cx).await;
3889 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3890
3891 // Adding an item with no ambiguity renders the tab without detail.
3892 let item1 = cx.add_view(window_id, |_| {
3893 let mut item = TestItem::new();
3894 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
3895 item
3896 });
3897 workspace.update(cx, |workspace, cx| {
3898 workspace.add_item(Box::new(item1.clone()), cx);
3899 });
3900 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
3901
3902 // Adding an item that creates ambiguity increases the level of detail on
3903 // both tabs.
3904 let item2 = cx.add_view(window_id, |_| {
3905 let mut item = TestItem::new();
3906 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3907 item
3908 });
3909 workspace.update(cx, |workspace, cx| {
3910 workspace.add_item(Box::new(item2.clone()), cx);
3911 });
3912 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3913 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3914
3915 // Adding an item that creates ambiguity increases the level of detail only
3916 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
3917 // we stop at the highest detail available.
3918 let item3 = cx.add_view(window_id, |_| {
3919 let mut item = TestItem::new();
3920 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3921 item
3922 });
3923 workspace.update(cx, |workspace, cx| {
3924 workspace.add_item(Box::new(item3.clone()), cx);
3925 });
3926 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3927 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3928 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3929 }
3930
3931 #[gpui::test]
3932 async fn test_tracking_active_path(cx: &mut TestAppContext) {
3933 init_test(cx);
3934
3935 let fs = FakeFs::new(cx.background());
3936 fs.insert_tree(
3937 "/root1",
3938 json!({
3939 "one.txt": "",
3940 "two.txt": "",
3941 }),
3942 )
3943 .await;
3944 fs.insert_tree(
3945 "/root2",
3946 json!({
3947 "three.txt": "",
3948 }),
3949 )
3950 .await;
3951
3952 let project = Project::test(fs, ["root1".as_ref()], cx).await;
3953 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3954 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3955 let worktree_id = project.read_with(cx, |project, cx| {
3956 project.worktrees(cx).next().unwrap().read(cx).id()
3957 });
3958
3959 let item1 = cx.add_view(window_id, |cx| {
3960 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3961 });
3962 let item2 = cx.add_view(window_id, |cx| {
3963 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3964 });
3965
3966 // Add an item to an empty pane
3967 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3968 project.read_with(cx, |project, cx| {
3969 assert_eq!(
3970 project.active_entry(),
3971 project
3972 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3973 .map(|e| e.id)
3974 );
3975 });
3976 assert_eq!(
3977 cx.current_window_title(window_id).as_deref(),
3978 Some("one.txt β root1")
3979 );
3980
3981 // Add a second item to a non-empty pane
3982 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3983 assert_eq!(
3984 cx.current_window_title(window_id).as_deref(),
3985 Some("two.txt β root1")
3986 );
3987 project.read_with(cx, |project, cx| {
3988 assert_eq!(
3989 project.active_entry(),
3990 project
3991 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3992 .map(|e| e.id)
3993 );
3994 });
3995
3996 // Close the active item
3997 pane.update(cx, |pane, cx| {
3998 pane.close_active_item(&Default::default(), cx).unwrap()
3999 })
4000 .await
4001 .unwrap();
4002 assert_eq!(
4003 cx.current_window_title(window_id).as_deref(),
4004 Some("one.txt β root1")
4005 );
4006 project.read_with(cx, |project, cx| {
4007 assert_eq!(
4008 project.active_entry(),
4009 project
4010 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4011 .map(|e| e.id)
4012 );
4013 });
4014
4015 // Add a project folder
4016 project
4017 .update(cx, |project, cx| {
4018 project.find_or_create_local_worktree("/root2", true, cx)
4019 })
4020 .await
4021 .unwrap();
4022 assert_eq!(
4023 cx.current_window_title(window_id).as_deref(),
4024 Some("one.txt β root1, root2")
4025 );
4026
4027 // Remove a project folder
4028 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4029 assert_eq!(
4030 cx.current_window_title(window_id).as_deref(),
4031 Some("one.txt β root2")
4032 );
4033 }
4034
4035 #[gpui::test]
4036 async fn test_close_window(cx: &mut TestAppContext) {
4037 init_test(cx);
4038
4039 let fs = FakeFs::new(cx.background());
4040 fs.insert_tree("/root", json!({ "one": "" })).await;
4041
4042 let project = Project::test(fs, ["root".as_ref()], cx).await;
4043 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4044
4045 // When there are no dirty items, there's nothing to do.
4046 let item1 = cx.add_view(window_id, |_| TestItem::new());
4047 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4048 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4049 assert!(task.await.unwrap());
4050
4051 // When there are dirty untitled items, prompt to save each one. If the user
4052 // cancels any prompt, then abort.
4053 let item2 = cx.add_view(window_id, |_| TestItem::new().with_dirty(true));
4054 let item3 = cx.add_view(window_id, |cx| {
4055 TestItem::new()
4056 .with_dirty(true)
4057 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4058 });
4059 workspace.update(cx, |w, cx| {
4060 w.add_item(Box::new(item2.clone()), cx);
4061 w.add_item(Box::new(item3.clone()), cx);
4062 });
4063 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4064 cx.foreground().run_until_parked();
4065 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
4066 cx.foreground().run_until_parked();
4067 assert!(!cx.has_pending_prompt(window_id));
4068 assert!(!task.await.unwrap());
4069 }
4070
4071 #[gpui::test]
4072 async fn test_close_pane_items(cx: &mut TestAppContext) {
4073 init_test(cx);
4074
4075 let fs = FakeFs::new(cx.background());
4076
4077 let project = Project::test(fs, None, cx).await;
4078 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4079
4080 let item1 = cx.add_view(window_id, |cx| {
4081 TestItem::new()
4082 .with_dirty(true)
4083 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4084 });
4085 let item2 = cx.add_view(window_id, |cx| {
4086 TestItem::new()
4087 .with_dirty(true)
4088 .with_conflict(true)
4089 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4090 });
4091 let item3 = cx.add_view(window_id, |cx| {
4092 TestItem::new()
4093 .with_dirty(true)
4094 .with_conflict(true)
4095 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4096 });
4097 let item4 = cx.add_view(window_id, |cx| {
4098 TestItem::new()
4099 .with_dirty(true)
4100 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4101 });
4102 let pane = workspace.update(cx, |workspace, cx| {
4103 workspace.add_item(Box::new(item1.clone()), cx);
4104 workspace.add_item(Box::new(item2.clone()), cx);
4105 workspace.add_item(Box::new(item3.clone()), cx);
4106 workspace.add_item(Box::new(item4.clone()), cx);
4107 workspace.active_pane().clone()
4108 });
4109
4110 let close_items = pane.update(cx, |pane, cx| {
4111 pane.activate_item(1, true, true, cx);
4112 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4113 let item1_id = item1.id();
4114 let item3_id = item3.id();
4115 let item4_id = item4.id();
4116 pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id))
4117 });
4118 cx.foreground().run_until_parked();
4119
4120 // There's a prompt to save item 1.
4121 pane.read_with(cx, |pane, _| {
4122 assert_eq!(pane.items_len(), 4);
4123 assert_eq!(pane.active_item().unwrap().id(), item1.id());
4124 });
4125 assert!(cx.has_pending_prompt(window_id));
4126
4127 // Confirm saving item 1.
4128 cx.simulate_prompt_answer(window_id, 0);
4129 cx.foreground().run_until_parked();
4130
4131 // Item 1 is saved. There's a prompt to save item 3.
4132 pane.read_with(cx, |pane, cx| {
4133 assert_eq!(item1.read(cx).save_count, 1);
4134 assert_eq!(item1.read(cx).save_as_count, 0);
4135 assert_eq!(item1.read(cx).reload_count, 0);
4136 assert_eq!(pane.items_len(), 3);
4137 assert_eq!(pane.active_item().unwrap().id(), item3.id());
4138 });
4139 assert!(cx.has_pending_prompt(window_id));
4140
4141 // Cancel saving item 3.
4142 cx.simulate_prompt_answer(window_id, 1);
4143 cx.foreground().run_until_parked();
4144
4145 // Item 3 is reloaded. There's a prompt to save item 4.
4146 pane.read_with(cx, |pane, cx| {
4147 assert_eq!(item3.read(cx).save_count, 0);
4148 assert_eq!(item3.read(cx).save_as_count, 0);
4149 assert_eq!(item3.read(cx).reload_count, 1);
4150 assert_eq!(pane.items_len(), 2);
4151 assert_eq!(pane.active_item().unwrap().id(), item4.id());
4152 });
4153 assert!(cx.has_pending_prompt(window_id));
4154
4155 // Confirm saving item 4.
4156 cx.simulate_prompt_answer(window_id, 0);
4157 cx.foreground().run_until_parked();
4158
4159 // There's a prompt for a path for item 4.
4160 cx.simulate_new_path_selection(|_| Some(Default::default()));
4161 close_items.await.unwrap();
4162
4163 // The requested items are closed.
4164 pane.read_with(cx, |pane, cx| {
4165 assert_eq!(item4.read(cx).save_count, 0);
4166 assert_eq!(item4.read(cx).save_as_count, 1);
4167 assert_eq!(item4.read(cx).reload_count, 0);
4168 assert_eq!(pane.items_len(), 1);
4169 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4170 });
4171 }
4172
4173 #[gpui::test]
4174 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4175 init_test(cx);
4176
4177 let fs = FakeFs::new(cx.background());
4178
4179 let project = Project::test(fs, [], cx).await;
4180 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4181
4182 // Create several workspace items with single project entries, and two
4183 // workspace items with multiple project entries.
4184 let single_entry_items = (0..=4)
4185 .map(|project_entry_id| {
4186 cx.add_view(window_id, |cx| {
4187 TestItem::new()
4188 .with_dirty(true)
4189 .with_project_items(&[TestProjectItem::new(
4190 project_entry_id,
4191 &format!("{project_entry_id}.txt"),
4192 cx,
4193 )])
4194 })
4195 })
4196 .collect::<Vec<_>>();
4197 let item_2_3 = cx.add_view(window_id, |cx| {
4198 TestItem::new()
4199 .with_dirty(true)
4200 .with_singleton(false)
4201 .with_project_items(&[
4202 single_entry_items[2].read(cx).project_items[0].clone(),
4203 single_entry_items[3].read(cx).project_items[0].clone(),
4204 ])
4205 });
4206 let item_3_4 = cx.add_view(window_id, |cx| {
4207 TestItem::new()
4208 .with_dirty(true)
4209 .with_singleton(false)
4210 .with_project_items(&[
4211 single_entry_items[3].read(cx).project_items[0].clone(),
4212 single_entry_items[4].read(cx).project_items[0].clone(),
4213 ])
4214 });
4215
4216 // Create two panes that contain the following project entries:
4217 // left pane:
4218 // multi-entry items: (2, 3)
4219 // single-entry items: 0, 1, 2, 3, 4
4220 // right pane:
4221 // single-entry items: 1
4222 // multi-entry items: (3, 4)
4223 let left_pane = workspace.update(cx, |workspace, cx| {
4224 let left_pane = workspace.active_pane().clone();
4225 workspace.add_item(Box::new(item_2_3.clone()), cx);
4226 for item in single_entry_items {
4227 workspace.add_item(Box::new(item), cx);
4228 }
4229 left_pane.update(cx, |pane, cx| {
4230 pane.activate_item(2, true, true, cx);
4231 });
4232
4233 workspace
4234 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
4235 .unwrap();
4236
4237 left_pane
4238 });
4239
4240 //Need to cause an effect flush in order to respect new focus
4241 workspace.update(cx, |workspace, cx| {
4242 workspace.add_item(Box::new(item_3_4.clone()), cx);
4243 cx.focus(&left_pane);
4244 });
4245
4246 // When closing all of the items in the left pane, we should be prompted twice:
4247 // once for project entry 0, and once for project entry 2. After those two
4248 // prompts, the task should complete.
4249
4250 let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true));
4251 cx.foreground().run_until_parked();
4252 left_pane.read_with(cx, |pane, cx| {
4253 assert_eq!(
4254 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4255 &[ProjectEntryId::from_proto(0)]
4256 );
4257 });
4258 cx.simulate_prompt_answer(window_id, 0);
4259
4260 cx.foreground().run_until_parked();
4261 left_pane.read_with(cx, |pane, cx| {
4262 assert_eq!(
4263 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4264 &[ProjectEntryId::from_proto(2)]
4265 );
4266 });
4267 cx.simulate_prompt_answer(window_id, 0);
4268
4269 cx.foreground().run_until_parked();
4270 close.await.unwrap();
4271 left_pane.read_with(cx, |pane, _| {
4272 assert_eq!(pane.items_len(), 0);
4273 });
4274 }
4275
4276 #[gpui::test]
4277 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
4278 init_test(cx);
4279
4280 let fs = FakeFs::new(cx.background());
4281
4282 let project = Project::test(fs, [], cx).await;
4283 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4284 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4285
4286 let item = cx.add_view(window_id, |cx| {
4287 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4288 });
4289 let item_id = item.id();
4290 workspace.update(cx, |workspace, cx| {
4291 workspace.add_item(Box::new(item.clone()), cx);
4292 });
4293
4294 // Autosave on window change.
4295 item.update(cx, |item, cx| {
4296 cx.update_global(|settings: &mut SettingsStore, cx| {
4297 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4298 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4299 })
4300 });
4301 item.is_dirty = true;
4302 });
4303
4304 // Deactivating the window saves the file.
4305 cx.simulate_window_activation(None);
4306 deterministic.run_until_parked();
4307 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
4308
4309 // Autosave on focus change.
4310 item.update(cx, |item, cx| {
4311 cx.focus_self();
4312 cx.update_global(|settings: &mut SettingsStore, cx| {
4313 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4314 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4315 })
4316 });
4317 item.is_dirty = true;
4318 });
4319
4320 // Blurring the item saves the file.
4321 item.update(cx, |_, cx| cx.blur());
4322 deterministic.run_until_parked();
4323 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
4324
4325 // Deactivating the window still saves the file.
4326 cx.simulate_window_activation(Some(window_id));
4327 item.update(cx, |item, cx| {
4328 cx.focus_self();
4329 item.is_dirty = true;
4330 });
4331 cx.simulate_window_activation(None);
4332
4333 deterministic.run_until_parked();
4334 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4335
4336 // Autosave after delay.
4337 item.update(cx, |item, cx| {
4338 cx.update_global(|settings: &mut SettingsStore, cx| {
4339 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4340 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4341 })
4342 });
4343 item.is_dirty = true;
4344 cx.emit(TestItemEvent::Edit);
4345 });
4346
4347 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4348 deterministic.advance_clock(Duration::from_millis(250));
4349 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4350
4351 // After delay expires, the file is saved.
4352 deterministic.advance_clock(Duration::from_millis(250));
4353 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
4354
4355 // Autosave on focus change, ensuring closing the tab counts as such.
4356 item.update(cx, |item, cx| {
4357 cx.update_global(|settings: &mut SettingsStore, cx| {
4358 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4359 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4360 })
4361 });
4362 item.is_dirty = true;
4363 });
4364
4365 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
4366 .await
4367 .unwrap();
4368 assert!(!cx.has_pending_prompt(window_id));
4369 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4370
4371 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4372 workspace.update(cx, |workspace, cx| {
4373 workspace.add_item(Box::new(item.clone()), cx);
4374 });
4375 item.update(cx, |item, cx| {
4376 item.project_items[0].update(cx, |item, _| {
4377 item.entry_id = None;
4378 });
4379 item.is_dirty = true;
4380 cx.blur();
4381 });
4382 deterministic.run_until_parked();
4383 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4384
4385 // Ensure autosave is prevented for deleted files also when closing the buffer.
4386 let _close_items =
4387 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
4388 deterministic.run_until_parked();
4389 assert!(cx.has_pending_prompt(window_id));
4390 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4391 }
4392
4393 #[gpui::test]
4394 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4395 init_test(cx);
4396
4397 let fs = FakeFs::new(cx.background());
4398
4399 let project = Project::test(fs, [], cx).await;
4400 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4401
4402 let item = cx.add_view(window_id, |cx| {
4403 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4404 });
4405 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4406 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
4407 let toolbar_notify_count = Rc::new(RefCell::new(0));
4408
4409 workspace.update(cx, |workspace, cx| {
4410 workspace.add_item(Box::new(item.clone()), cx);
4411 let toolbar_notification_count = toolbar_notify_count.clone();
4412 cx.observe(&toolbar, move |_, _, _| {
4413 *toolbar_notification_count.borrow_mut() += 1
4414 })
4415 .detach();
4416 });
4417
4418 pane.read_with(cx, |pane, _| {
4419 assert!(!pane.can_navigate_backward());
4420 assert!(!pane.can_navigate_forward());
4421 });
4422
4423 item.update(cx, |item, cx| {
4424 item.set_state("one".to_string(), cx);
4425 });
4426
4427 // Toolbar must be notified to re-render the navigation buttons
4428 assert_eq!(*toolbar_notify_count.borrow(), 1);
4429
4430 pane.read_with(cx, |pane, _| {
4431 assert!(pane.can_navigate_backward());
4432 assert!(!pane.can_navigate_forward());
4433 });
4434
4435 workspace
4436 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
4437 .await
4438 .unwrap();
4439
4440 assert_eq!(*toolbar_notify_count.borrow(), 3);
4441 pane.read_with(cx, |pane, _| {
4442 assert!(!pane.can_navigate_backward());
4443 assert!(pane.can_navigate_forward());
4444 });
4445 }
4446
4447 #[gpui::test]
4448 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4449 init_test(cx);
4450 let fs = FakeFs::new(cx.background());
4451
4452 let project = Project::test(fs, [], cx).await;
4453 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4454
4455 let panel = workspace.update(cx, |workspace, cx| {
4456 let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4457 workspace.add_panel(panel.clone(), cx);
4458
4459 workspace
4460 .right_dock()
4461 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4462
4463 panel
4464 });
4465
4466 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4467 pane.update(cx, |pane, cx| {
4468 let item = cx.add_view(|_| TestItem::new());
4469 pane.add_item(Box::new(item), true, true, None, cx);
4470 });
4471
4472 // Transfer focus from center to panel
4473 workspace.update(cx, |workspace, cx| {
4474 workspace.toggle_panel_focus::<TestPanel>(cx);
4475 });
4476
4477 workspace.read_with(cx, |workspace, cx| {
4478 assert!(workspace.right_dock().read(cx).is_open());
4479 assert!(!panel.is_zoomed(cx));
4480 assert!(panel.has_focus(cx));
4481 });
4482
4483 // Transfer focus from panel to center
4484 workspace.update(cx, |workspace, cx| {
4485 workspace.toggle_panel_focus::<TestPanel>(cx);
4486 });
4487
4488 workspace.read_with(cx, |workspace, cx| {
4489 assert!(workspace.right_dock().read(cx).is_open());
4490 assert!(!panel.is_zoomed(cx));
4491 assert!(!panel.has_focus(cx));
4492 });
4493
4494 // Close the dock
4495 workspace.update(cx, |workspace, cx| {
4496 workspace.toggle_dock(DockPosition::Right, cx);
4497 });
4498
4499 workspace.read_with(cx, |workspace, cx| {
4500 assert!(!workspace.right_dock().read(cx).is_open());
4501 assert!(!panel.is_zoomed(cx));
4502 assert!(!panel.has_focus(cx));
4503 });
4504
4505 // Open the dock
4506 workspace.update(cx, |workspace, cx| {
4507 workspace.toggle_dock(DockPosition::Right, cx);
4508 });
4509
4510 workspace.read_with(cx, |workspace, cx| {
4511 assert!(workspace.right_dock().read(cx).is_open());
4512 assert!(!panel.is_zoomed(cx));
4513 assert!(panel.has_focus(cx));
4514 });
4515
4516 // Focus and zoom panel
4517 panel.update(cx, |panel, cx| {
4518 cx.focus_self();
4519 panel.set_zoomed(true, cx)
4520 });
4521
4522 workspace.read_with(cx, |workspace, cx| {
4523 assert!(workspace.right_dock().read(cx).is_open());
4524 assert!(panel.is_zoomed(cx));
4525 assert!(panel.has_focus(cx));
4526 });
4527
4528 // Transfer focus to the center closes the dock
4529 workspace.update(cx, |workspace, cx| {
4530 workspace.toggle_panel_focus::<TestPanel>(cx);
4531 });
4532
4533 workspace.read_with(cx, |workspace, cx| {
4534 assert!(!workspace.right_dock().read(cx).is_open());
4535 assert!(panel.is_zoomed(cx));
4536 assert!(!panel.has_focus(cx));
4537 });
4538
4539 // Transferring focus back to the panel keeps it zoomed
4540 workspace.update(cx, |workspace, cx| {
4541 workspace.toggle_panel_focus::<TestPanel>(cx);
4542 });
4543
4544 workspace.read_with(cx, |workspace, cx| {
4545 assert!(workspace.right_dock().read(cx).is_open());
4546 assert!(panel.is_zoomed(cx));
4547 assert!(panel.has_focus(cx));
4548 });
4549
4550 // Close the dock while it is zoomed
4551 workspace.update(cx, |workspace, cx| {
4552 workspace.toggle_dock(DockPosition::Right, cx)
4553 });
4554
4555 workspace.read_with(cx, |workspace, cx| {
4556 assert!(!workspace.right_dock().read(cx).is_open());
4557 assert!(panel.is_zoomed(cx));
4558 assert!(workspace.zoomed.is_none());
4559 assert!(!panel.has_focus(cx));
4560 });
4561
4562 // Opening the dock, when it's zoomed, retains focus
4563 workspace.update(cx, |workspace, cx| {
4564 workspace.toggle_dock(DockPosition::Right, cx)
4565 });
4566
4567 workspace.read_with(cx, |workspace, cx| {
4568 assert!(workspace.right_dock().read(cx).is_open());
4569 assert!(panel.is_zoomed(cx));
4570 assert!(workspace.zoomed.is_some());
4571 assert!(panel.has_focus(cx));
4572 });
4573
4574 // Unzoom and close the panel, zoom the active pane.
4575 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
4576 workspace.update(cx, |workspace, cx| {
4577 workspace.toggle_dock(DockPosition::Right, cx)
4578 });
4579 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
4580
4581 // Opening a dock unzooms the pane.
4582 workspace.update(cx, |workspace, cx| {
4583 workspace.toggle_dock(DockPosition::Right, cx)
4584 });
4585 workspace.read_with(cx, |workspace, cx| {
4586 let pane = pane.read(cx);
4587 assert!(!pane.is_zoomed());
4588 assert!(!pane.has_focus());
4589 assert!(workspace.right_dock().read(cx).is_open());
4590 assert!(workspace.zoomed.is_none());
4591 });
4592 }
4593
4594 #[gpui::test]
4595 async fn test_panels(cx: &mut gpui::TestAppContext) {
4596 init_test(cx);
4597 let fs = FakeFs::new(cx.background());
4598
4599 let project = Project::test(fs, [], cx).await;
4600 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4601
4602 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
4603 // Add panel_1 on the left, panel_2 on the right.
4604 let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left));
4605 workspace.add_panel(panel_1.clone(), cx);
4606 workspace
4607 .left_dock()
4608 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
4609 let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4610 workspace.add_panel(panel_2.clone(), cx);
4611 workspace
4612 .right_dock()
4613 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4614
4615 let left_dock = workspace.left_dock();
4616 assert_eq!(
4617 left_dock.read(cx).visible_panel().unwrap().id(),
4618 panel_1.id()
4619 );
4620 assert_eq!(
4621 left_dock.read(cx).active_panel_size(cx).unwrap(),
4622 panel_1.size(cx)
4623 );
4624
4625 left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx));
4626 assert_eq!(
4627 workspace
4628 .right_dock()
4629 .read(cx)
4630 .visible_panel()
4631 .unwrap()
4632 .id(),
4633 panel_2.id()
4634 );
4635
4636 (panel_1, panel_2)
4637 });
4638
4639 // Move panel_1 to the right
4640 panel_1.update(cx, |panel_1, cx| {
4641 panel_1.set_position(DockPosition::Right, cx)
4642 });
4643
4644 workspace.update(cx, |workspace, cx| {
4645 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
4646 // Since it was the only panel on the left, the left dock should now be closed.
4647 assert!(!workspace.left_dock().read(cx).is_open());
4648 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
4649 let right_dock = workspace.right_dock();
4650 assert_eq!(
4651 right_dock.read(cx).visible_panel().unwrap().id(),
4652 panel_1.id()
4653 );
4654 assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4655
4656 // Now we move panel_2Β to the left
4657 panel_2.set_position(DockPosition::Left, cx);
4658 });
4659
4660 workspace.update(cx, |workspace, cx| {
4661 // Since panel_2 was not visible on the right, we don't open the left dock.
4662 assert!(!workspace.left_dock().read(cx).is_open());
4663 // And the right dock is unaffected in it's displaying of panel_1
4664 assert!(workspace.right_dock().read(cx).is_open());
4665 assert_eq!(
4666 workspace
4667 .right_dock()
4668 .read(cx)
4669 .visible_panel()
4670 .unwrap()
4671 .id(),
4672 panel_1.id()
4673 );
4674 });
4675
4676 // Move panel_1 back to the left
4677 panel_1.update(cx, |panel_1, cx| {
4678 panel_1.set_position(DockPosition::Left, cx)
4679 });
4680
4681 workspace.update(cx, |workspace, cx| {
4682 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
4683 let left_dock = workspace.left_dock();
4684 assert!(left_dock.read(cx).is_open());
4685 assert_eq!(
4686 left_dock.read(cx).visible_panel().unwrap().id(),
4687 panel_1.id()
4688 );
4689 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4690 // And right the dock should be closed as it no longer has any panels.
4691 assert!(!workspace.right_dock().read(cx).is_open());
4692
4693 // Now we move panel_1 to the bottom
4694 panel_1.set_position(DockPosition::Bottom, cx);
4695 });
4696
4697 workspace.update(cx, |workspace, cx| {
4698 // Since panel_1 was visible on the left, we close the left dock.
4699 assert!(!workspace.left_dock().read(cx).is_open());
4700 // The bottom dock is sized based on the panel's default size,
4701 // since the panel orientation changed from vertical to horizontal.
4702 let bottom_dock = workspace.bottom_dock();
4703 assert_eq!(
4704 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
4705 panel_1.size(cx),
4706 );
4707 // Close bottom dock and move panel_1 back to the left.
4708 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
4709 panel_1.set_position(DockPosition::Left, cx);
4710 });
4711
4712 // Emit activated event on panel 1
4713 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
4714
4715 // Now the left dock is open and panel_1 is active and focused.
4716 workspace.read_with(cx, |workspace, cx| {
4717 let left_dock = workspace.left_dock();
4718 assert!(left_dock.read(cx).is_open());
4719 assert_eq!(
4720 left_dock.read(cx).visible_panel().unwrap().id(),
4721 panel_1.id()
4722 );
4723 assert!(panel_1.is_focused(cx));
4724 });
4725
4726 // Emit closed event on panel 2, which is not active
4727 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
4728
4729 // Wo don't close the left dock, because panel_2 wasn't the active panel
4730 workspace.read_with(cx, |workspace, cx| {
4731 let left_dock = workspace.left_dock();
4732 assert!(left_dock.read(cx).is_open());
4733 assert_eq!(
4734 left_dock.read(cx).visible_panel().unwrap().id(),
4735 panel_1.id()
4736 );
4737 });
4738
4739 // Emitting a ZoomIn event shows the panel as zoomed.
4740 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
4741 workspace.read_with(cx, |workspace, _| {
4742 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4743 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
4744 });
4745
4746 // Move panel to another dock while it is zoomed
4747 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
4748 workspace.read_with(cx, |workspace, _| {
4749 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4750 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
4751 });
4752
4753 // If focus is transferred to another view that's not a panel or another pane, we still show
4754 // the panel as zoomed.
4755 let focus_receiver = cx.add_view(window_id, |_| EmptyView);
4756 focus_receiver.update(cx, |_, cx| cx.focus_self());
4757 workspace.read_with(cx, |workspace, _| {
4758 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4759 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
4760 });
4761
4762 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
4763 workspace.update(cx, |_, cx| cx.focus_self());
4764 workspace.read_with(cx, |workspace, _| {
4765 assert_eq!(workspace.zoomed, None);
4766 assert_eq!(workspace.zoomed_position, None);
4767 });
4768
4769 // If focus is transferred again to another view that's not a panel or a pane, we won't
4770 // show the panel as zoomed because it wasn't zoomed before.
4771 focus_receiver.update(cx, |_, cx| cx.focus_self());
4772 workspace.read_with(cx, |workspace, _| {
4773 assert_eq!(workspace.zoomed, None);
4774 assert_eq!(workspace.zoomed_position, None);
4775 });
4776
4777 // When focus is transferred back to the panel, it is zoomed again.
4778 panel_1.update(cx, |_, cx| cx.focus_self());
4779 workspace.read_with(cx, |workspace, _| {
4780 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4781 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
4782 });
4783
4784 // Emitting a ZoomOut event unzooms the panel.
4785 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
4786 workspace.read_with(cx, |workspace, _| {
4787 assert_eq!(workspace.zoomed, None);
4788 assert_eq!(workspace.zoomed_position, None);
4789 });
4790
4791 // Emit closed event on panel 1, which is active
4792 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
4793
4794 // Now the left dock is closed, because panel_1 was the active panel
4795 workspace.read_with(cx, |workspace, cx| {
4796 let right_dock = workspace.right_dock();
4797 assert!(!right_dock.read(cx).is_open());
4798 });
4799 }
4800
4801 pub fn init_test(cx: &mut TestAppContext) {
4802 cx.foreground().forbid_parking();
4803 cx.update(|cx| {
4804 cx.set_global(SettingsStore::test(cx));
4805 theme::init((), cx);
4806 language::init(cx);
4807 crate::init_settings(cx);
4808 Project::init_settings(cx);
4809 });
4810 }
4811}