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