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