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