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