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