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 DockPosition::Bottom => SizeConstraint::new(
2840 Vector2F::new(constraint.min.x(), 20.),
2841 Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
2842 ),
2843 })
2844 .into_any(),
2845 )
2846 }
2847}
2848
2849async fn open_items(
2850 serialized_workspace: Option<SerializedWorkspace>,
2851 workspace: &WeakViewHandle<Workspace>,
2852 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
2853 app_state: Arc<AppState>,
2854 mut cx: AsyncAppContext,
2855) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
2856 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
2857
2858 if let Some(serialized_workspace) = serialized_workspace {
2859 let workspace = workspace.clone();
2860 let restored_items = cx
2861 .update(|cx| {
2862 Workspace::load_workspace(
2863 workspace,
2864 serialized_workspace,
2865 project_paths_to_open
2866 .iter()
2867 .map(|(_, project_path)| project_path)
2868 .cloned()
2869 .collect(),
2870 cx,
2871 )
2872 })
2873 .await;
2874
2875 let restored_project_paths = cx.read(|cx| {
2876 restored_items
2877 .iter()
2878 .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
2879 .collect::<HashSet<_>>()
2880 });
2881
2882 opened_items = restored_items;
2883 project_paths_to_open
2884 .iter_mut()
2885 .for_each(|(_, project_path)| {
2886 if let Some(project_path_to_open) = project_path {
2887 if restored_project_paths.contains(project_path_to_open) {
2888 *project_path = None;
2889 }
2890 }
2891 });
2892 } else {
2893 for _ in 0..project_paths_to_open.len() {
2894 opened_items.push(None);
2895 }
2896 }
2897 assert!(opened_items.len() == project_paths_to_open.len());
2898
2899 let tasks =
2900 project_paths_to_open
2901 .into_iter()
2902 .enumerate()
2903 .map(|(i, (abs_path, project_path))| {
2904 let workspace = workspace.clone();
2905 cx.spawn(|mut cx| {
2906 let fs = app_state.fs.clone();
2907 async move {
2908 let file_project_path = project_path?;
2909 if fs.is_file(&abs_path).await {
2910 Some((
2911 i,
2912 workspace
2913 .update(&mut cx, |workspace, cx| {
2914 workspace.open_path(file_project_path, None, true, cx)
2915 })
2916 .log_err()?
2917 .await,
2918 ))
2919 } else {
2920 None
2921 }
2922 }
2923 })
2924 });
2925
2926 for maybe_opened_path in futures::future::join_all(tasks.into_iter())
2927 .await
2928 .into_iter()
2929 {
2930 if let Some((i, path_open_result)) = maybe_opened_path {
2931 opened_items[i] = Some(path_open_result);
2932 }
2933 }
2934
2935 opened_items
2936}
2937
2938fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
2939 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
2940
2941 workspace
2942 .update(cx, |workspace, cx| {
2943 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
2944 workspace.show_notification_once(0, cx, |cx| {
2945 cx.add_view(|_| {
2946 MessageNotification::new("Failed to load any database file.")
2947 .with_click_message("Click to let us know about this error")
2948 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
2949 })
2950 });
2951 } else {
2952 let backup_path = (*db::BACKUP_DB_PATH).read();
2953 if let Some(backup_path) = backup_path.clone() {
2954 workspace.show_notification_once(0, cx, move |cx| {
2955 cx.add_view(move |_| {
2956 MessageNotification::new(format!(
2957 "Database file was corrupted. Old database backed up to {}",
2958 backup_path.display()
2959 ))
2960 .with_click_message("Click to show old database in finder")
2961 .on_click(move |cx| {
2962 cx.platform().open_url(&backup_path.to_string_lossy())
2963 })
2964 })
2965 });
2966 }
2967 }
2968 })
2969 .log_err();
2970}
2971
2972impl Entity for Workspace {
2973 type Event = Event;
2974}
2975
2976impl View for Workspace {
2977 fn ui_name() -> &'static str {
2978 "Workspace"
2979 }
2980
2981 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2982 let theme = theme::current(cx).clone();
2983 Stack::new()
2984 .with_child(
2985 Flex::column()
2986 .with_child(self.render_titlebar(&theme, cx))
2987 .with_child(
2988 Stack::new()
2989 .with_child({
2990 let project = self.project.clone();
2991 Flex::row()
2992 .with_children(self.render_dock(DockPosition::Left, cx))
2993 .with_child(
2994 Flex::column()
2995 .with_child(
2996 FlexItem::new(
2997 self.center.render(
2998 &project,
2999 &theme,
3000 &self.follower_states_by_leader,
3001 self.active_call(),
3002 self.active_pane(),
3003 self.zoomed
3004 .as_ref()
3005 .and_then(|zoomed| zoomed.upgrade(cx))
3006 .as_ref(),
3007 &self.app_state,
3008 cx,
3009 ),
3010 )
3011 .flex(1., true),
3012 )
3013 .with_children(
3014 self.render_dock(DockPosition::Bottom, cx),
3015 )
3016 .flex(1., true),
3017 )
3018 .with_children(self.render_dock(DockPosition::Right, cx))
3019 })
3020 .with_child(Overlay::new(
3021 Stack::new()
3022 .with_children(self.zoomed.as_ref().and_then(|zoomed| {
3023 enum ZoomBackground {}
3024 let zoomed = zoomed.upgrade(cx)?;
3025 Some(
3026 ChildView::new(&zoomed, cx)
3027 .contained()
3028 .with_style(theme.workspace.zoomed_foreground)
3029 .aligned()
3030 .contained()
3031 .with_style(theme.workspace.zoomed_background)
3032 .mouse::<ZoomBackground>(0)
3033 .capture_all()
3034 .on_down(
3035 MouseButton::Left,
3036 |_, this: &mut Self, cx| {
3037 this.zoom_out(cx);
3038 },
3039 ),
3040 )
3041 }))
3042 .with_children(self.modal.as_ref().map(|modal| {
3043 ChildView::new(modal, cx)
3044 .contained()
3045 .with_style(theme.workspace.modal)
3046 .aligned()
3047 .top()
3048 }))
3049 .with_children(self.render_notifications(&theme.workspace, cx)),
3050 ))
3051 .flex(1.0, true),
3052 )
3053 .with_child(ChildView::new(&self.status_bar, cx))
3054 .contained()
3055 .with_background_color(theme.workspace.background),
3056 )
3057 .with_children(DragAndDrop::render(cx))
3058 .with_children(self.render_disconnected_overlay(cx))
3059 .into_any_named("workspace")
3060 }
3061
3062 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
3063 if cx.is_self_focused() {
3064 cx.focus(&self.active_pane);
3065 }
3066 }
3067}
3068
3069impl ViewId {
3070 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3071 Ok(Self {
3072 creator: message
3073 .creator
3074 .ok_or_else(|| anyhow!("creator is missing"))?,
3075 id: message.id,
3076 })
3077 }
3078
3079 pub(crate) fn to_proto(&self) -> proto::ViewId {
3080 proto::ViewId {
3081 creator: Some(self.creator),
3082 id: self.id,
3083 }
3084 }
3085}
3086
3087pub trait WorkspaceHandle {
3088 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3089}
3090
3091impl WorkspaceHandle for ViewHandle<Workspace> {
3092 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3093 self.read(cx)
3094 .worktrees(cx)
3095 .flat_map(|worktree| {
3096 let worktree_id = worktree.read(cx).id();
3097 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3098 worktree_id,
3099 path: f.path.clone(),
3100 })
3101 })
3102 .collect::<Vec<_>>()
3103 }
3104}
3105
3106impl std::fmt::Debug for OpenPaths {
3107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3108 f.debug_struct("OpenPaths")
3109 .field("paths", &self.paths)
3110 .finish()
3111 }
3112}
3113
3114pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
3115
3116pub fn activate_workspace_for_project(
3117 cx: &mut AsyncAppContext,
3118 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
3119) -> Option<WeakViewHandle<Workspace>> {
3120 for window_id in cx.window_ids() {
3121 let handle = cx
3122 .update_window(window_id, |cx| {
3123 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
3124 let project = workspace_handle.read(cx).project.clone();
3125 if project.update(cx, &predicate) {
3126 cx.activate_window();
3127 return Some(workspace_handle.clone());
3128 }
3129 }
3130 None
3131 })
3132 .flatten();
3133
3134 if let Some(handle) = handle {
3135 return Some(handle.downgrade());
3136 }
3137 }
3138 None
3139}
3140
3141pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3142 DB.last_workspace().await.log_err().flatten()
3143}
3144
3145#[allow(clippy::type_complexity)]
3146pub fn open_paths(
3147 abs_paths: &[PathBuf],
3148 app_state: &Arc<AppState>,
3149 requesting_window_id: Option<usize>,
3150 cx: &mut AppContext,
3151) -> Task<
3152 Result<(
3153 WeakViewHandle<Workspace>,
3154 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
3155 )>,
3156> {
3157 let app_state = app_state.clone();
3158 let abs_paths = abs_paths.to_vec();
3159 cx.spawn(|mut cx| async move {
3160 // Open paths in existing workspace if possible
3161 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
3162 project.contains_paths(&abs_paths, cx)
3163 });
3164
3165 if let Some(existing) = existing {
3166 Ok((
3167 existing.clone(),
3168 existing
3169 .update(&mut cx, |workspace, cx| {
3170 workspace.open_paths(abs_paths, true, cx)
3171 })?
3172 .await,
3173 ))
3174 } else {
3175 Ok(cx
3176 .update(|cx| {
3177 Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx)
3178 })
3179 .await)
3180 }
3181 })
3182}
3183
3184pub fn open_new(
3185 app_state: &Arc<AppState>,
3186 cx: &mut AppContext,
3187 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
3188) -> Task<()> {
3189 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
3190 cx.spawn(|mut cx| async move {
3191 let (workspace, opened_paths) = task.await;
3192
3193 workspace
3194 .update(&mut cx, |workspace, cx| {
3195 if opened_paths.is_empty() {
3196 init(workspace, cx)
3197 }
3198 })
3199 .log_err();
3200 })
3201}
3202
3203pub fn create_and_open_local_file(
3204 path: &'static Path,
3205 cx: &mut ViewContext<Workspace>,
3206 default_content: impl 'static + Send + FnOnce() -> Rope,
3207) -> Task<Result<Box<dyn ItemHandle>>> {
3208 cx.spawn(|workspace, mut cx| async move {
3209 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
3210 if !fs.is_file(path).await {
3211 fs.create_file(path, Default::default()).await?;
3212 fs.save(path, &default_content(), Default::default())
3213 .await?;
3214 }
3215
3216 let mut items = workspace
3217 .update(&mut cx, |workspace, cx| {
3218 workspace.with_local_workspace(cx, |workspace, cx| {
3219 workspace.open_paths(vec![path.to_path_buf()], false, cx)
3220 })
3221 })?
3222 .await?
3223 .await;
3224
3225 let item = items.pop().flatten();
3226 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
3227 })
3228}
3229
3230pub fn join_remote_project(
3231 project_id: u64,
3232 follow_user_id: u64,
3233 app_state: Arc<AppState>,
3234 cx: &mut AppContext,
3235) -> Task<Result<()>> {
3236 cx.spawn(|mut cx| async move {
3237 let existing_workspace = cx
3238 .window_ids()
3239 .into_iter()
3240 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
3241 .find(|workspace| {
3242 cx.read_window(workspace.window_id(), |cx| {
3243 workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
3244 })
3245 .unwrap_or(false)
3246 });
3247
3248 let workspace = if let Some(existing_workspace) = existing_workspace {
3249 existing_workspace.downgrade()
3250 } else {
3251 let active_call = cx.read(ActiveCall::global);
3252 let room = active_call
3253 .read_with(&cx, |call, _| call.room().cloned())
3254 .ok_or_else(|| anyhow!("not in a call"))?;
3255 let project = room
3256 .update(&mut cx, |room, cx| {
3257 room.join_project(
3258 project_id,
3259 app_state.languages.clone(),
3260 app_state.fs.clone(),
3261 cx,
3262 )
3263 })
3264 .await?;
3265
3266 let (_, workspace) = cx.add_window(
3267 (app_state.build_window_options)(None, None, cx.platform().as_ref()),
3268 |cx| Workspace::new(0, project, app_state.clone(), cx),
3269 );
3270 (app_state.initialize_workspace)(
3271 workspace.downgrade(),
3272 false,
3273 app_state.clone(),
3274 cx.clone(),
3275 )
3276 .await
3277 .log_err();
3278
3279 workspace.downgrade()
3280 };
3281
3282 cx.activate_window(workspace.window_id());
3283 cx.platform().activate(true);
3284
3285 workspace.update(&mut cx, |workspace, cx| {
3286 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
3287 let follow_peer_id = room
3288 .read(cx)
3289 .remote_participants()
3290 .iter()
3291 .find(|(_, participant)| participant.user.id == follow_user_id)
3292 .map(|(_, p)| p.peer_id)
3293 .or_else(|| {
3294 // If we couldn't follow the given user, follow the host instead.
3295 let collaborator = workspace
3296 .project()
3297 .read(cx)
3298 .collaborators()
3299 .values()
3300 .find(|collaborator| collaborator.replica_id == 0)?;
3301 Some(collaborator.peer_id)
3302 });
3303
3304 if let Some(follow_peer_id) = follow_peer_id {
3305 if !workspace.is_being_followed(follow_peer_id) {
3306 workspace
3307 .toggle_follow(follow_peer_id, cx)
3308 .map(|follow| follow.detach_and_log_err(cx));
3309 }
3310 }
3311 }
3312 })?;
3313
3314 anyhow::Ok(())
3315 })
3316}
3317
3318pub fn restart(_: &Restart, cx: &mut AppContext) {
3319 let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
3320 cx.spawn(|mut cx| async move {
3321 let mut workspaces = cx
3322 .window_ids()
3323 .into_iter()
3324 .filter_map(|window_id| {
3325 Some(
3326 cx.root_view(window_id)?
3327 .clone()
3328 .downcast::<Workspace>()?
3329 .downgrade(),
3330 )
3331 })
3332 .collect::<Vec<_>>();
3333
3334 // If multiple windows have unsaved changes, and need a save prompt,
3335 // prompt in the active window before switching to a different window.
3336 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
3337
3338 if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
3339 let answer = cx.prompt(
3340 workspace.window_id(),
3341 PromptLevel::Info,
3342 "Are you sure you want to restart?",
3343 &["Restart", "Cancel"],
3344 );
3345
3346 if let Some(mut answer) = answer {
3347 let answer = answer.next().await;
3348 if answer != Some(0) {
3349 return Ok(());
3350 }
3351 }
3352 }
3353
3354 // If the user cancels any save prompt, then keep the app open.
3355 for workspace in workspaces {
3356 if !workspace
3357 .update(&mut cx, |workspace, cx| {
3358 workspace.prepare_to_close(true, cx)
3359 })?
3360 .await?
3361 {
3362 return Ok(());
3363 }
3364 }
3365 cx.platform().restart();
3366 anyhow::Ok(())
3367 })
3368 .detach_and_log_err(cx);
3369}
3370
3371fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
3372 let mut parts = value.split(',');
3373 let width: usize = parts.next()?.parse().ok()?;
3374 let height: usize = parts.next()?.parse().ok()?;
3375 Some(vec2f(width as f32, height as f32))
3376}
3377
3378fn default_true() -> bool {
3379 true
3380}
3381
3382#[cfg(test)]
3383mod tests {
3384 use super::*;
3385 use crate::{
3386 dock::test::{TestPanel, TestPanelEvent},
3387 item::test::{TestItem, TestItemEvent, TestProjectItem},
3388 };
3389 use fs::FakeFs;
3390 use gpui::{executor::Deterministic, test::EmptyView, TestAppContext};
3391 use project::{Project, ProjectEntryId};
3392 use serde_json::json;
3393 use settings::SettingsStore;
3394 use std::{cell::RefCell, rc::Rc};
3395
3396 #[gpui::test]
3397 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
3398 init_test(cx);
3399
3400 let fs = FakeFs::new(cx.background());
3401 let project = Project::test(fs, [], cx).await;
3402 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3403
3404 // Adding an item with no ambiguity renders the tab without detail.
3405 let item1 = cx.add_view(window_id, |_| {
3406 let mut item = TestItem::new();
3407 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
3408 item
3409 });
3410 workspace.update(cx, |workspace, cx| {
3411 workspace.add_item(Box::new(item1.clone()), cx);
3412 });
3413 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
3414
3415 // Adding an item that creates ambiguity increases the level of detail on
3416 // both tabs.
3417 let item2 = cx.add_view(window_id, |_| {
3418 let mut item = TestItem::new();
3419 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3420 item
3421 });
3422 workspace.update(cx, |workspace, cx| {
3423 workspace.add_item(Box::new(item2.clone()), cx);
3424 });
3425 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3426 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3427
3428 // Adding an item that creates ambiguity increases the level of detail only
3429 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
3430 // we stop at the highest detail available.
3431 let item3 = cx.add_view(window_id, |_| {
3432 let mut item = TestItem::new();
3433 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3434 item
3435 });
3436 workspace.update(cx, |workspace, cx| {
3437 workspace.add_item(Box::new(item3.clone()), cx);
3438 });
3439 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3440 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3441 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3442 }
3443
3444 #[gpui::test]
3445 async fn test_tracking_active_path(cx: &mut TestAppContext) {
3446 init_test(cx);
3447
3448 let fs = FakeFs::new(cx.background());
3449 fs.insert_tree(
3450 "/root1",
3451 json!({
3452 "one.txt": "",
3453 "two.txt": "",
3454 }),
3455 )
3456 .await;
3457 fs.insert_tree(
3458 "/root2",
3459 json!({
3460 "three.txt": "",
3461 }),
3462 )
3463 .await;
3464
3465 let project = Project::test(fs, ["root1".as_ref()], cx).await;
3466 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3467 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3468 let worktree_id = project.read_with(cx, |project, cx| {
3469 project.worktrees(cx).next().unwrap().read(cx).id()
3470 });
3471
3472 let item1 = cx.add_view(window_id, |cx| {
3473 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3474 });
3475 let item2 = cx.add_view(window_id, |cx| {
3476 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3477 });
3478
3479 // Add an item to an empty pane
3480 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3481 project.read_with(cx, |project, cx| {
3482 assert_eq!(
3483 project.active_entry(),
3484 project
3485 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3486 .map(|e| e.id)
3487 );
3488 });
3489 assert_eq!(
3490 cx.current_window_title(window_id).as_deref(),
3491 Some("one.txt β root1")
3492 );
3493
3494 // Add a second item to a non-empty pane
3495 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3496 assert_eq!(
3497 cx.current_window_title(window_id).as_deref(),
3498 Some("two.txt β root1")
3499 );
3500 project.read_with(cx, |project, cx| {
3501 assert_eq!(
3502 project.active_entry(),
3503 project
3504 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3505 .map(|e| e.id)
3506 );
3507 });
3508
3509 // Close the active item
3510 pane.update(cx, |pane, cx| {
3511 pane.close_active_item(&Default::default(), cx).unwrap()
3512 })
3513 .await
3514 .unwrap();
3515 assert_eq!(
3516 cx.current_window_title(window_id).as_deref(),
3517 Some("one.txt β root1")
3518 );
3519 project.read_with(cx, |project, cx| {
3520 assert_eq!(
3521 project.active_entry(),
3522 project
3523 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3524 .map(|e| e.id)
3525 );
3526 });
3527
3528 // Add a project folder
3529 project
3530 .update(cx, |project, cx| {
3531 project.find_or_create_local_worktree("/root2", true, cx)
3532 })
3533 .await
3534 .unwrap();
3535 assert_eq!(
3536 cx.current_window_title(window_id).as_deref(),
3537 Some("one.txt β root1, root2")
3538 );
3539
3540 // Remove a project folder
3541 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3542 assert_eq!(
3543 cx.current_window_title(window_id).as_deref(),
3544 Some("one.txt β root2")
3545 );
3546 }
3547
3548 #[gpui::test]
3549 async fn test_close_window(cx: &mut TestAppContext) {
3550 init_test(cx);
3551
3552 let fs = FakeFs::new(cx.background());
3553 fs.insert_tree("/root", json!({ "one": "" })).await;
3554
3555 let project = Project::test(fs, ["root".as_ref()], cx).await;
3556 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3557
3558 // When there are no dirty items, there's nothing to do.
3559 let item1 = cx.add_view(window_id, |_| TestItem::new());
3560 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3561 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3562 assert!(task.await.unwrap());
3563
3564 // When there are dirty untitled items, prompt to save each one. If the user
3565 // cancels any prompt, then abort.
3566 let item2 = cx.add_view(window_id, |_| TestItem::new().with_dirty(true));
3567 let item3 = cx.add_view(window_id, |cx| {
3568 TestItem::new()
3569 .with_dirty(true)
3570 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3571 });
3572 workspace.update(cx, |w, cx| {
3573 w.add_item(Box::new(item2.clone()), cx);
3574 w.add_item(Box::new(item3.clone()), cx);
3575 });
3576 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3577 cx.foreground().run_until_parked();
3578 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
3579 cx.foreground().run_until_parked();
3580 assert!(!cx.has_pending_prompt(window_id));
3581 assert!(!task.await.unwrap());
3582 }
3583
3584 #[gpui::test]
3585 async fn test_close_pane_items(cx: &mut TestAppContext) {
3586 init_test(cx);
3587
3588 let fs = FakeFs::new(cx.background());
3589
3590 let project = Project::test(fs, None, cx).await;
3591 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3592
3593 let item1 = cx.add_view(window_id, |cx| {
3594 TestItem::new()
3595 .with_dirty(true)
3596 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3597 });
3598 let item2 = cx.add_view(window_id, |cx| {
3599 TestItem::new()
3600 .with_dirty(true)
3601 .with_conflict(true)
3602 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3603 });
3604 let item3 = cx.add_view(window_id, |cx| {
3605 TestItem::new()
3606 .with_dirty(true)
3607 .with_conflict(true)
3608 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
3609 });
3610 let item4 = cx.add_view(window_id, |cx| {
3611 TestItem::new()
3612 .with_dirty(true)
3613 .with_project_items(&[TestProjectItem::new_untitled(cx)])
3614 });
3615 let pane = workspace.update(cx, |workspace, cx| {
3616 workspace.add_item(Box::new(item1.clone()), cx);
3617 workspace.add_item(Box::new(item2.clone()), cx);
3618 workspace.add_item(Box::new(item3.clone()), cx);
3619 workspace.add_item(Box::new(item4.clone()), cx);
3620 workspace.active_pane().clone()
3621 });
3622
3623 let close_items = pane.update(cx, |pane, cx| {
3624 pane.activate_item(1, true, true, cx);
3625 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3626 let item1_id = item1.id();
3627 let item3_id = item3.id();
3628 let item4_id = item4.id();
3629 pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id))
3630 });
3631 cx.foreground().run_until_parked();
3632
3633 // There's a prompt to save item 1.
3634 pane.read_with(cx, |pane, _| {
3635 assert_eq!(pane.items_len(), 4);
3636 assert_eq!(pane.active_item().unwrap().id(), item1.id());
3637 });
3638 assert!(cx.has_pending_prompt(window_id));
3639
3640 // Confirm saving item 1.
3641 cx.simulate_prompt_answer(window_id, 0);
3642 cx.foreground().run_until_parked();
3643
3644 // Item 1 is saved. There's a prompt to save item 3.
3645 pane.read_with(cx, |pane, cx| {
3646 assert_eq!(item1.read(cx).save_count, 1);
3647 assert_eq!(item1.read(cx).save_as_count, 0);
3648 assert_eq!(item1.read(cx).reload_count, 0);
3649 assert_eq!(pane.items_len(), 3);
3650 assert_eq!(pane.active_item().unwrap().id(), item3.id());
3651 });
3652 assert!(cx.has_pending_prompt(window_id));
3653
3654 // Cancel saving item 3.
3655 cx.simulate_prompt_answer(window_id, 1);
3656 cx.foreground().run_until_parked();
3657
3658 // Item 3 is reloaded. There's a prompt to save item 4.
3659 pane.read_with(cx, |pane, cx| {
3660 assert_eq!(item3.read(cx).save_count, 0);
3661 assert_eq!(item3.read(cx).save_as_count, 0);
3662 assert_eq!(item3.read(cx).reload_count, 1);
3663 assert_eq!(pane.items_len(), 2);
3664 assert_eq!(pane.active_item().unwrap().id(), item4.id());
3665 });
3666 assert!(cx.has_pending_prompt(window_id));
3667
3668 // Confirm saving item 4.
3669 cx.simulate_prompt_answer(window_id, 0);
3670 cx.foreground().run_until_parked();
3671
3672 // There's a prompt for a path for item 4.
3673 cx.simulate_new_path_selection(|_| Some(Default::default()));
3674 close_items.await.unwrap();
3675
3676 // The requested items are closed.
3677 pane.read_with(cx, |pane, cx| {
3678 assert_eq!(item4.read(cx).save_count, 0);
3679 assert_eq!(item4.read(cx).save_as_count, 1);
3680 assert_eq!(item4.read(cx).reload_count, 0);
3681 assert_eq!(pane.items_len(), 1);
3682 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3683 });
3684 }
3685
3686 #[gpui::test]
3687 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
3688 init_test(cx);
3689
3690 let fs = FakeFs::new(cx.background());
3691
3692 let project = Project::test(fs, [], cx).await;
3693 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3694
3695 // Create several workspace items with single project entries, and two
3696 // workspace items with multiple project entries.
3697 let single_entry_items = (0..=4)
3698 .map(|project_entry_id| {
3699 cx.add_view(window_id, |cx| {
3700 TestItem::new()
3701 .with_dirty(true)
3702 .with_project_items(&[TestProjectItem::new(
3703 project_entry_id,
3704 &format!("{project_entry_id}.txt"),
3705 cx,
3706 )])
3707 })
3708 })
3709 .collect::<Vec<_>>();
3710 let item_2_3 = cx.add_view(window_id, |cx| {
3711 TestItem::new()
3712 .with_dirty(true)
3713 .with_singleton(false)
3714 .with_project_items(&[
3715 single_entry_items[2].read(cx).project_items[0].clone(),
3716 single_entry_items[3].read(cx).project_items[0].clone(),
3717 ])
3718 });
3719 let item_3_4 = cx.add_view(window_id, |cx| {
3720 TestItem::new()
3721 .with_dirty(true)
3722 .with_singleton(false)
3723 .with_project_items(&[
3724 single_entry_items[3].read(cx).project_items[0].clone(),
3725 single_entry_items[4].read(cx).project_items[0].clone(),
3726 ])
3727 });
3728
3729 // Create two panes that contain the following project entries:
3730 // left pane:
3731 // multi-entry items: (2, 3)
3732 // single-entry items: 0, 1, 2, 3, 4
3733 // right pane:
3734 // single-entry items: 1
3735 // multi-entry items: (3, 4)
3736 let left_pane = workspace.update(cx, |workspace, cx| {
3737 let left_pane = workspace.active_pane().clone();
3738 workspace.add_item(Box::new(item_2_3.clone()), cx);
3739 for item in single_entry_items {
3740 workspace.add_item(Box::new(item), cx);
3741 }
3742 left_pane.update(cx, |pane, cx| {
3743 pane.activate_item(2, true, true, cx);
3744 });
3745
3746 workspace
3747 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3748 .unwrap();
3749
3750 left_pane
3751 });
3752
3753 //Need to cause an effect flush in order to respect new focus
3754 workspace.update(cx, |workspace, cx| {
3755 workspace.add_item(Box::new(item_3_4.clone()), cx);
3756 cx.focus(&left_pane);
3757 });
3758
3759 // When closing all of the items in the left pane, we should be prompted twice:
3760 // once for project entry 0, and once for project entry 2. After those two
3761 // prompts, the task should complete.
3762
3763 let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true));
3764 cx.foreground().run_until_parked();
3765 left_pane.read_with(cx, |pane, cx| {
3766 assert_eq!(
3767 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3768 &[ProjectEntryId::from_proto(0)]
3769 );
3770 });
3771 cx.simulate_prompt_answer(window_id, 0);
3772
3773 cx.foreground().run_until_parked();
3774 left_pane.read_with(cx, |pane, cx| {
3775 assert_eq!(
3776 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3777 &[ProjectEntryId::from_proto(2)]
3778 );
3779 });
3780 cx.simulate_prompt_answer(window_id, 0);
3781
3782 cx.foreground().run_until_parked();
3783 close.await.unwrap();
3784 left_pane.read_with(cx, |pane, _| {
3785 assert_eq!(pane.items_len(), 0);
3786 });
3787 }
3788
3789 #[gpui::test]
3790 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3791 init_test(cx);
3792
3793 let fs = FakeFs::new(cx.background());
3794
3795 let project = Project::test(fs, [], cx).await;
3796 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3797 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3798
3799 let item = cx.add_view(window_id, |cx| {
3800 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3801 });
3802 let item_id = item.id();
3803 workspace.update(cx, |workspace, cx| {
3804 workspace.add_item(Box::new(item.clone()), cx);
3805 });
3806
3807 // Autosave on window change.
3808 item.update(cx, |item, cx| {
3809 cx.update_global(|settings: &mut SettingsStore, cx| {
3810 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
3811 settings.autosave = Some(AutosaveSetting::OnWindowChange);
3812 })
3813 });
3814 item.is_dirty = true;
3815 });
3816
3817 // Deactivating the window saves the file.
3818 cx.simulate_window_activation(None);
3819 deterministic.run_until_parked();
3820 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3821
3822 // Autosave on focus change.
3823 item.update(cx, |item, cx| {
3824 cx.focus_self();
3825 cx.update_global(|settings: &mut SettingsStore, cx| {
3826 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
3827 settings.autosave = Some(AutosaveSetting::OnFocusChange);
3828 })
3829 });
3830 item.is_dirty = true;
3831 });
3832
3833 // Blurring the item saves the file.
3834 item.update(cx, |_, cx| cx.blur());
3835 deterministic.run_until_parked();
3836 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3837
3838 // Deactivating the window still saves the file.
3839 cx.simulate_window_activation(Some(window_id));
3840 item.update(cx, |item, cx| {
3841 cx.focus_self();
3842 item.is_dirty = true;
3843 });
3844 cx.simulate_window_activation(None);
3845
3846 deterministic.run_until_parked();
3847 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3848
3849 // Autosave after delay.
3850 item.update(cx, |item, cx| {
3851 cx.update_global(|settings: &mut SettingsStore, cx| {
3852 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
3853 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
3854 })
3855 });
3856 item.is_dirty = true;
3857 cx.emit(TestItemEvent::Edit);
3858 });
3859
3860 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3861 deterministic.advance_clock(Duration::from_millis(250));
3862 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3863
3864 // After delay expires, the file is saved.
3865 deterministic.advance_clock(Duration::from_millis(250));
3866 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3867
3868 // Autosave on focus change, ensuring closing the tab counts as such.
3869 item.update(cx, |item, cx| {
3870 cx.update_global(|settings: &mut SettingsStore, cx| {
3871 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
3872 settings.autosave = Some(AutosaveSetting::OnFocusChange);
3873 })
3874 });
3875 item.is_dirty = true;
3876 });
3877
3878 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
3879 .await
3880 .unwrap();
3881 assert!(!cx.has_pending_prompt(window_id));
3882 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3883
3884 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3885 workspace.update(cx, |workspace, cx| {
3886 workspace.add_item(Box::new(item.clone()), cx);
3887 });
3888 item.update(cx, |item, cx| {
3889 item.project_items[0].update(cx, |item, _| {
3890 item.entry_id = None;
3891 });
3892 item.is_dirty = true;
3893 cx.blur();
3894 });
3895 deterministic.run_until_parked();
3896 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3897
3898 // Ensure autosave is prevented for deleted files also when closing the buffer.
3899 let _close_items =
3900 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
3901 deterministic.run_until_parked();
3902 assert!(cx.has_pending_prompt(window_id));
3903 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3904 }
3905
3906 #[gpui::test]
3907 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
3908 init_test(cx);
3909
3910 let fs = FakeFs::new(cx.background());
3911
3912 let project = Project::test(fs, [], cx).await;
3913 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3914
3915 let item = cx.add_view(window_id, |cx| {
3916 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3917 });
3918 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3919 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3920 let toolbar_notify_count = Rc::new(RefCell::new(0));
3921
3922 workspace.update(cx, |workspace, cx| {
3923 workspace.add_item(Box::new(item.clone()), cx);
3924 let toolbar_notification_count = toolbar_notify_count.clone();
3925 cx.observe(&toolbar, move |_, _, _| {
3926 *toolbar_notification_count.borrow_mut() += 1
3927 })
3928 .detach();
3929 });
3930
3931 pane.read_with(cx, |pane, _| {
3932 assert!(!pane.can_navigate_backward());
3933 assert!(!pane.can_navigate_forward());
3934 });
3935
3936 item.update(cx, |item, cx| {
3937 item.set_state("one".to_string(), cx);
3938 });
3939
3940 // Toolbar must be notified to re-render the navigation buttons
3941 assert_eq!(*toolbar_notify_count.borrow(), 1);
3942
3943 pane.read_with(cx, |pane, _| {
3944 assert!(pane.can_navigate_backward());
3945 assert!(!pane.can_navigate_forward());
3946 });
3947
3948 workspace
3949 .update(cx, |workspace, cx| {
3950 Pane::go_back(workspace, Some(pane.downgrade()), cx)
3951 })
3952 .await
3953 .unwrap();
3954
3955 assert_eq!(*toolbar_notify_count.borrow(), 3);
3956 pane.read_with(cx, |pane, _| {
3957 assert!(!pane.can_navigate_backward());
3958 assert!(pane.can_navigate_forward());
3959 });
3960 }
3961
3962 #[gpui::test]
3963 async fn test_panels(cx: &mut gpui::TestAppContext) {
3964 init_test(cx);
3965 let fs = FakeFs::new(cx.background());
3966
3967 let project = Project::test(fs, [], cx).await;
3968 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3969
3970 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
3971 // Add panel_1 on the left, panel_2 on the right.
3972 let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left));
3973 workspace.add_panel(panel_1.clone(), cx);
3974 workspace
3975 .left_dock()
3976 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
3977 let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right));
3978 workspace.add_panel(panel_2.clone(), cx);
3979 workspace
3980 .right_dock()
3981 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
3982
3983 let left_dock = workspace.left_dock();
3984 assert_eq!(
3985 left_dock.read(cx).active_panel().unwrap().id(),
3986 panel_1.id()
3987 );
3988 assert_eq!(
3989 left_dock.read(cx).active_panel_size(cx).unwrap(),
3990 panel_1.size(cx)
3991 );
3992
3993 left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx));
3994 assert_eq!(
3995 workspace.right_dock().read(cx).active_panel().unwrap().id(),
3996 panel_2.id()
3997 );
3998
3999 (panel_1, panel_2)
4000 });
4001
4002 // Move panel_1 to the right
4003 panel_1.update(cx, |panel_1, cx| {
4004 panel_1.set_position(DockPosition::Right, cx)
4005 });
4006
4007 workspace.update(cx, |workspace, cx| {
4008 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
4009 // Since it was the only panel on the left, the left dock should now be closed.
4010 assert!(!workspace.left_dock().read(cx).is_open());
4011 assert!(workspace.left_dock().read(cx).active_panel().is_none());
4012 let right_dock = workspace.right_dock();
4013 assert_eq!(
4014 right_dock.read(cx).active_panel().unwrap().id(),
4015 panel_1.id()
4016 );
4017 assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4018
4019 // Now we move panel_2Β to the left
4020 panel_2.set_position(DockPosition::Left, cx);
4021 });
4022
4023 workspace.update(cx, |workspace, cx| {
4024 // Since panel_2 was not visible on the right, we don't open the left dock.
4025 assert!(!workspace.left_dock().read(cx).is_open());
4026 // And the right dock is unaffected in it's displaying of panel_1
4027 assert!(workspace.right_dock().read(cx).is_open());
4028 assert_eq!(
4029 workspace.right_dock().read(cx).active_panel().unwrap().id(),
4030 panel_1.id()
4031 );
4032 });
4033
4034 // Move panel_1 back to the left
4035 panel_1.update(cx, |panel_1, cx| {
4036 panel_1.set_position(DockPosition::Left, cx)
4037 });
4038
4039 workspace.update(cx, |workspace, cx| {
4040 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
4041 let left_dock = workspace.left_dock();
4042 assert!(left_dock.read(cx).is_open());
4043 assert_eq!(
4044 left_dock.read(cx).active_panel().unwrap().id(),
4045 panel_1.id()
4046 );
4047 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4048 // And right the dock should be closed as it no longer has any panels.
4049 assert!(!workspace.right_dock().read(cx).is_open());
4050
4051 // Now we move panel_1 to the bottom
4052 panel_1.set_position(DockPosition::Bottom, cx);
4053 });
4054
4055 workspace.update(cx, |workspace, cx| {
4056 // Since panel_1 was visible on the left, we close the left dock.
4057 assert!(!workspace.left_dock().read(cx).is_open());
4058 // The bottom dock is sized based on the panel's default size,
4059 // since the panel orientation changed from vertical to horizontal.
4060 let bottom_dock = workspace.bottom_dock();
4061 assert_eq!(
4062 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
4063 panel_1.size(cx),
4064 );
4065 // Close bottom dock and move panel_1 back to the left.
4066 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
4067 panel_1.set_position(DockPosition::Left, cx);
4068 });
4069
4070 // Emit activated event on panel 1
4071 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
4072
4073 // Now the left dock is open and panel_1 is active and focused.
4074 workspace.read_with(cx, |workspace, cx| {
4075 let left_dock = workspace.left_dock();
4076 assert!(left_dock.read(cx).is_open());
4077 assert_eq!(
4078 left_dock.read(cx).active_panel().unwrap().id(),
4079 panel_1.id()
4080 );
4081 assert!(panel_1.is_focused(cx));
4082 });
4083
4084 // Emit closed event on panel 2, which is not active
4085 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
4086
4087 // Wo don't close the left dock, because panel_2 wasn't the active panel
4088 workspace.read_with(cx, |workspace, cx| {
4089 let left_dock = workspace.left_dock();
4090 assert!(left_dock.read(cx).is_open());
4091 assert_eq!(
4092 left_dock.read(cx).active_panel().unwrap().id(),
4093 panel_1.id()
4094 );
4095 });
4096
4097 // Emitting a ZoomIn event shows the panel as zoomed.
4098 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
4099 workspace.read_with(cx, |workspace, _| {
4100 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4101 });
4102
4103 // If focus is transferred to another view that's not a panel or another pane, we still show
4104 // the panel as zoomed.
4105 let focus_receiver = cx.add_view(window_id, |_| EmptyView);
4106 focus_receiver.update(cx, |_, cx| cx.focus_self());
4107 workspace.read_with(cx, |workspace, _| {
4108 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4109 });
4110
4111 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
4112 workspace.update(cx, |_, cx| cx.focus_self());
4113 workspace.read_with(cx, |workspace, _| {
4114 assert_eq!(workspace.zoomed, None);
4115 });
4116
4117 // If focus is transferred again to another view that's not a panel or a pane, we won't
4118 // show the panel as zoomed because it wasn't zoomed before.
4119 focus_receiver.update(cx, |_, cx| cx.focus_self());
4120 workspace.read_with(cx, |workspace, _| {
4121 assert_eq!(workspace.zoomed, None);
4122 });
4123
4124 // When focus is transferred back to the panel, it is zoomed again.
4125 panel_1.update(cx, |_, cx| cx.focus_self());
4126 workspace.read_with(cx, |workspace, _| {
4127 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4128 });
4129
4130 // Emitting a ZoomOut event unzooms the panel.
4131 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
4132 workspace.read_with(cx, |workspace, _| {
4133 assert_eq!(workspace.zoomed, None);
4134 });
4135
4136 // Emit closed event on panel 1, which is active
4137 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
4138
4139 // Now the left dock is closed, because panel_1 was the active panel
4140 workspace.read_with(cx, |workspace, cx| {
4141 let left_dock = workspace.left_dock();
4142 assert!(!left_dock.read(cx).is_open());
4143 });
4144 }
4145
4146 pub fn init_test(cx: &mut TestAppContext) {
4147 cx.foreground().forbid_parking();
4148 cx.update(|cx| {
4149 cx.set_global(SettingsStore::test(cx));
4150 theme::init((), cx);
4151 language::init(cx);
4152 crate::init_settings(cx);
4153 });
4154 }
4155}