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