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