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