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