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