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