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