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