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