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