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