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