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