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