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