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