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