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