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 ViewHandle<Workspace>,
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, workspace, id, state, cx| {
366 I::from_state_proto(pane, workspace, 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 this = this
2852 .upgrade(cx)
2853 .ok_or_else(|| anyhow!("workspace dropped"))?;
2854 let project = this
2855 .read_with(cx, |this, _| this.project.clone())
2856 .ok_or_else(|| anyhow!("window dropped"))?;
2857
2858 let replica_id = project
2859 .read_with(cx, |project, _| {
2860 project
2861 .collaborators()
2862 .get(&leader_id)
2863 .map(|c| c.replica_id)
2864 })
2865 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2866
2867 let item_builders = cx.update(|cx| {
2868 cx.default_global::<FollowableItemBuilders>()
2869 .values()
2870 .map(|b| b.0)
2871 .collect::<Vec<_>>()
2872 });
2873
2874 let mut item_tasks_by_pane = HashMap::default();
2875 for pane in panes {
2876 let mut item_tasks = Vec::new();
2877 let mut leader_view_ids = Vec::new();
2878 for view in &views {
2879 let Some(id) = &view.id else { continue };
2880 let id = ViewId::from_proto(id.clone())?;
2881 let mut variant = view.variant.clone();
2882 if variant.is_none() {
2883 Err(anyhow!("missing view variant"))?;
2884 }
2885 for build_item in &item_builders {
2886 let task = cx
2887 .update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx));
2888 if let Some(task) = task {
2889 item_tasks.push(task);
2890 leader_view_ids.push(id);
2891 break;
2892 } else {
2893 assert!(variant.is_some());
2894 }
2895 }
2896 }
2897
2898 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2899 }
2900
2901 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2902 let items = futures::future::try_join_all(item_tasks).await?;
2903 this.update(cx, |this, cx| {
2904 let state = this
2905 .follower_states_by_leader
2906 .get_mut(&leader_id)?
2907 .get_mut(&pane)?;
2908
2909 for (id, item) in leader_view_ids.into_iter().zip(items) {
2910 item.set_leader_replica_id(Some(replica_id), cx);
2911 state.items_by_leader_view_id.insert(id, item);
2912 }
2913
2914 Some(())
2915 });
2916 }
2917 Ok(())
2918 }
2919
2920 fn update_active_view_for_followers(&self, cx: &AppContext) {
2921 if self.active_pane.read(cx).has_focus() {
2922 self.update_followers(
2923 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
2924 id: self.active_item(cx).and_then(|item| {
2925 item.to_followable_item_handle(cx)?
2926 .remote_id(&self.app_state.client, cx)
2927 .map(|id| id.to_proto())
2928 }),
2929 leader_id: self.leader_for_pane(&self.active_pane),
2930 }),
2931 cx,
2932 );
2933 } else {
2934 self.update_followers(
2935 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
2936 id: None,
2937 leader_id: None,
2938 }),
2939 cx,
2940 );
2941 }
2942 }
2943
2944 fn update_followers(
2945 &self,
2946 update: proto::update_followers::Variant,
2947 cx: &AppContext,
2948 ) -> Option<()> {
2949 let project_id = self.project.read(cx).remote_id()?;
2950 if !self.leader_state.followers.is_empty() {
2951 self.app_state
2952 .client
2953 .send(proto::UpdateFollowers {
2954 project_id,
2955 follower_ids: self.leader_state.followers.iter().copied().collect(),
2956 variant: Some(update),
2957 })
2958 .log_err();
2959 }
2960 None
2961 }
2962
2963 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2964 self.follower_states_by_leader
2965 .iter()
2966 .find_map(|(leader_id, state)| {
2967 if state.contains_key(pane) {
2968 Some(*leader_id)
2969 } else {
2970 None
2971 }
2972 })
2973 }
2974
2975 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2976 cx.notify();
2977
2978 let call = self.active_call()?;
2979 let room = call.read(cx).room()?.read(cx);
2980 let participant = room.remote_participant_for_peer_id(leader_id)?;
2981 let mut items_to_activate = Vec::new();
2982 match participant.location {
2983 call::ParticipantLocation::SharedProject { project_id } => {
2984 if Some(project_id) == self.project.read(cx).remote_id() {
2985 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2986 if let Some(item) = state
2987 .active_view_id
2988 .and_then(|id| state.items_by_leader_view_id.get(&id))
2989 {
2990 items_to_activate.push((pane.clone(), item.boxed_clone()));
2991 } else if let Some(shared_screen) =
2992 self.shared_screen_for_peer(leader_id, pane, cx)
2993 {
2994 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2995 }
2996 }
2997 }
2998 }
2999 call::ParticipantLocation::UnsharedProject => {}
3000 call::ParticipantLocation::External => {
3001 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
3002 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
3003 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
3004 }
3005 }
3006 }
3007 }
3008
3009 for (pane, item) in items_to_activate {
3010 let pane_was_focused = pane.read(cx).has_focus();
3011 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
3012 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
3013 } else {
3014 pane.update(cx, |pane, cx| {
3015 pane.add_item(item.boxed_clone(), false, false, None, cx)
3016 });
3017 }
3018
3019 if pane_was_focused {
3020 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
3021 }
3022 }
3023
3024 None
3025 }
3026
3027 fn shared_screen_for_peer(
3028 &self,
3029 peer_id: PeerId,
3030 pane: &ViewHandle<Pane>,
3031 cx: &mut ViewContext<Self>,
3032 ) -> Option<ViewHandle<SharedScreen>> {
3033 let call = self.active_call()?;
3034 let room = call.read(cx).room()?.read(cx);
3035 let participant = room.remote_participant_for_peer_id(peer_id)?;
3036 let track = participant.video_tracks.values().next()?.clone();
3037 let user = participant.user.clone();
3038
3039 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3040 if item.read(cx).peer_id == peer_id {
3041 return Some(item);
3042 }
3043 }
3044
3045 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3046 }
3047
3048 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
3049 if active {
3050 cx.background()
3051 .spawn(persistence::DB.update_timestamp(self.database_id()))
3052 .detach();
3053 } else {
3054 for pane in &self.panes {
3055 pane.update(cx, |pane, cx| {
3056 if let Some(item) = pane.active_item() {
3057 item.workspace_deactivated(cx);
3058 }
3059 if matches!(
3060 settings::get::<WorkspaceSettings>(cx).autosave,
3061 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3062 ) {
3063 for item in pane.items() {
3064 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3065 .detach_and_log_err(cx);
3066 }
3067 }
3068 });
3069 }
3070 }
3071 }
3072
3073 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
3074 self.active_call.as_ref().map(|(call, _)| call)
3075 }
3076
3077 fn on_active_call_event(
3078 &mut self,
3079 _: ModelHandle<ActiveCall>,
3080 event: &call::room::Event,
3081 cx: &mut ViewContext<Self>,
3082 ) {
3083 match event {
3084 call::room::Event::ParticipantLocationChanged { participant_id }
3085 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3086 self.leader_updated(*participant_id, cx);
3087 }
3088 _ => {}
3089 }
3090 }
3091
3092 pub fn database_id(&self) -> WorkspaceId {
3093 self.database_id
3094 }
3095
3096 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
3097 let project = self.project().read(cx);
3098
3099 if project.is_local() {
3100 Some(
3101 project
3102 .visible_worktrees(cx)
3103 .map(|worktree| worktree.read(cx).abs_path())
3104 .collect::<Vec<_>>()
3105 .into(),
3106 )
3107 } else {
3108 None
3109 }
3110 }
3111
3112 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3113 match member {
3114 Member::Axis(PaneAxis { members, .. }) => {
3115 for child in members.iter() {
3116 self.remove_panes(child.clone(), cx)
3117 }
3118 }
3119 Member::Pane(pane) => {
3120 self.force_remove_pane(&pane, cx);
3121 }
3122 }
3123 }
3124
3125 fn force_remove_pane(&mut self, pane: &ViewHandle<Pane>, cx: &mut ViewContext<Workspace>) {
3126 self.panes.retain(|p| p != pane);
3127 cx.focus(self.panes.last().unwrap());
3128 if self.last_active_center_pane == Some(pane.downgrade()) {
3129 self.last_active_center_pane = None;
3130 }
3131 cx.notify();
3132 }
3133
3134 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
3135 self._schedule_serialize = Some(cx.spawn(|this, cx| async move {
3136 cx.background().timer(Duration::from_millis(100)).await;
3137 this.read_with(&cx, |this, cx| this.serialize_workspace(cx))
3138 .ok();
3139 }));
3140 }
3141
3142 fn serialize_workspace(&self, cx: &ViewContext<Self>) {
3143 fn serialize_pane_handle(
3144 pane_handle: &ViewHandle<Pane>,
3145 cx: &AppContext,
3146 ) -> SerializedPane {
3147 let (items, active) = {
3148 let pane = pane_handle.read(cx);
3149 let active_item_id = pane.active_item().map(|item| item.id());
3150 (
3151 pane.items()
3152 .filter_map(|item_handle| {
3153 Some(SerializedItem {
3154 kind: Arc::from(item_handle.serialized_item_kind()?),
3155 item_id: item_handle.id(),
3156 active: Some(item_handle.id()) == active_item_id,
3157 })
3158 })
3159 .collect::<Vec<_>>(),
3160 pane.has_focus(),
3161 )
3162 };
3163
3164 SerializedPane::new(items, active)
3165 }
3166
3167 fn build_serialized_pane_group(
3168 pane_group: &Member,
3169 cx: &AppContext,
3170 ) -> SerializedPaneGroup {
3171 match pane_group {
3172 Member::Axis(PaneAxis {
3173 axis,
3174 members,
3175 flexes,
3176 bounding_boxes: _,
3177 }) => SerializedPaneGroup::Group {
3178 axis: *axis,
3179 children: members
3180 .iter()
3181 .map(|member| build_serialized_pane_group(member, cx))
3182 .collect::<Vec<_>>(),
3183 flexes: Some(flexes.borrow().clone()),
3184 },
3185 Member::Pane(pane_handle) => {
3186 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
3187 }
3188 }
3189 }
3190
3191 fn build_serialized_docks(this: &Workspace, cx: &ViewContext<Workspace>) -> DockStructure {
3192 let left_dock = this.left_dock.read(cx);
3193 let left_visible = left_dock.is_open();
3194 let left_active_panel = left_dock.visible_panel().and_then(|panel| {
3195 Some(
3196 cx.view_ui_name(panel.as_any().window(), panel.id())?
3197 .to_string(),
3198 )
3199 });
3200 let left_dock_zoom = left_dock
3201 .visible_panel()
3202 .map(|panel| panel.is_zoomed(cx))
3203 .unwrap_or(false);
3204
3205 let right_dock = this.right_dock.read(cx);
3206 let right_visible = right_dock.is_open();
3207 let right_active_panel = right_dock.visible_panel().and_then(|panel| {
3208 Some(
3209 cx.view_ui_name(panel.as_any().window(), panel.id())?
3210 .to_string(),
3211 )
3212 });
3213 let right_dock_zoom = right_dock
3214 .visible_panel()
3215 .map(|panel| panel.is_zoomed(cx))
3216 .unwrap_or(false);
3217
3218 let bottom_dock = this.bottom_dock.read(cx);
3219 let bottom_visible = bottom_dock.is_open();
3220 let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| {
3221 Some(
3222 cx.view_ui_name(panel.as_any().window(), panel.id())?
3223 .to_string(),
3224 )
3225 });
3226 let bottom_dock_zoom = bottom_dock
3227 .visible_panel()
3228 .map(|panel| panel.is_zoomed(cx))
3229 .unwrap_or(false);
3230
3231 DockStructure {
3232 left: DockData {
3233 visible: left_visible,
3234 active_panel: left_active_panel,
3235 zoom: left_dock_zoom,
3236 },
3237 right: DockData {
3238 visible: right_visible,
3239 active_panel: right_active_panel,
3240 zoom: right_dock_zoom,
3241 },
3242 bottom: DockData {
3243 visible: bottom_visible,
3244 active_panel: bottom_active_panel,
3245 zoom: bottom_dock_zoom,
3246 },
3247 }
3248 }
3249
3250 if let Some(location) = self.location(cx) {
3251 // Load bearing special case:
3252 // - with_local_workspace() relies on this to not have other stuff open
3253 // when you open your log
3254 if !location.paths().is_empty() {
3255 let center_group = build_serialized_pane_group(&self.center.root, cx);
3256 let docks = build_serialized_docks(self, cx);
3257
3258 let serialized_workspace = SerializedWorkspace {
3259 id: self.database_id,
3260 location,
3261 center_group,
3262 bounds: Default::default(),
3263 display: Default::default(),
3264 docks,
3265 };
3266
3267 cx.background()
3268 .spawn(persistence::DB.save_workspace(serialized_workspace))
3269 .detach();
3270 }
3271 }
3272 }
3273
3274 pub(crate) fn load_workspace(
3275 workspace: WeakViewHandle<Workspace>,
3276 serialized_workspace: SerializedWorkspace,
3277 paths_to_open: Vec<Option<ProjectPath>>,
3278 cx: &mut AppContext,
3279 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
3280 cx.spawn(|mut cx| async move {
3281 let result = async_iife! {{
3282 let (project, old_center_pane) =
3283 workspace.read_with(&cx, |workspace, _| {
3284 (
3285 workspace.project().clone(),
3286 workspace.last_active_center_pane.clone(),
3287 )
3288 })?;
3289
3290 let mut center_items = None;
3291 let mut center_group = None;
3292 // Traverse the splits tree and add to things
3293 if let Some((group, active_pane, items)) = serialized_workspace
3294 .center_group
3295 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
3296 .await {
3297 center_items = Some(items);
3298 center_group = Some((group, active_pane))
3299 }
3300
3301 let resulting_list = cx.read(|cx| {
3302 let mut opened_items = center_items
3303 .unwrap_or_default()
3304 .into_iter()
3305 .filter_map(|item| {
3306 let item = item?;
3307 let project_path = item.project_path(cx)?;
3308 Some((project_path, item))
3309 })
3310 .collect::<HashMap<_, _>>();
3311
3312 paths_to_open
3313 .into_iter()
3314 .map(|path_to_open| {
3315 path_to_open.map(|path_to_open| {
3316 Ok(opened_items.remove(&path_to_open))
3317 })
3318 .transpose()
3319 .map(|item| item.flatten())
3320 .transpose()
3321 })
3322 .collect::<Vec<_>>()
3323 });
3324
3325 // Remove old panes from workspace panes list
3326 workspace.update(&mut cx, |workspace, cx| {
3327 if let Some((center_group, active_pane)) = center_group {
3328 workspace.remove_panes(workspace.center.root.clone(), cx);
3329
3330 // Swap workspace center group
3331 workspace.center = PaneGroup::with_root(center_group);
3332
3333 // Change the focus to the workspace first so that we retrigger focus in on the pane.
3334 cx.focus_self();
3335
3336 if let Some(active_pane) = active_pane {
3337 cx.focus(&active_pane);
3338 } else {
3339 cx.focus(workspace.panes.last().unwrap());
3340 }
3341 } else {
3342 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
3343 if let Some(old_center_handle) = old_center_handle {
3344 cx.focus(&old_center_handle)
3345 } else {
3346 cx.focus_self()
3347 }
3348 }
3349
3350 let docks = serialized_workspace.docks;
3351 workspace.left_dock.update(cx, |dock, cx| {
3352 dock.set_open(docks.left.visible, cx);
3353 if let Some(active_panel) = docks.left.active_panel {
3354 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3355 dock.activate_panel(ix, cx);
3356 }
3357 }
3358 dock.active_panel()
3359 .map(|panel| {
3360 panel.set_zoomed(docks.left.zoom, cx)
3361 });
3362 if docks.left.visible && docks.left.zoom {
3363 cx.focus_self()
3364 }
3365 });
3366 // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
3367 workspace.right_dock.update(cx, |dock, cx| {
3368 dock.set_open(docks.right.visible, cx);
3369 if let Some(active_panel) = docks.right.active_panel {
3370 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3371 dock.activate_panel(ix, cx);
3372
3373 }
3374 }
3375 dock.active_panel()
3376 .map(|panel| {
3377 panel.set_zoomed(docks.right.zoom, cx)
3378 });
3379
3380 if docks.right.visible && docks.right.zoom {
3381 cx.focus_self()
3382 }
3383 });
3384 workspace.bottom_dock.update(cx, |dock, cx| {
3385 dock.set_open(docks.bottom.visible, cx);
3386 if let Some(active_panel) = docks.bottom.active_panel {
3387 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3388 dock.activate_panel(ix, cx);
3389 }
3390 }
3391
3392 dock.active_panel()
3393 .map(|panel| {
3394 panel.set_zoomed(docks.bottom.zoom, cx)
3395 });
3396
3397 if docks.bottom.visible && docks.bottom.zoom {
3398 cx.focus_self()
3399 }
3400 });
3401
3402
3403 cx.notify();
3404 })?;
3405
3406 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3407 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3408
3409 Ok::<_, anyhow::Error>(resulting_list)
3410 }};
3411
3412 result.await.unwrap_or_default()
3413 })
3414 }
3415
3416 #[cfg(any(test, feature = "test-support"))]
3417 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
3418 let client = project.read(cx).client();
3419 let user_store = project.read(cx).user_store();
3420
3421 let channel_store =
3422 cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
3423 let app_state = Arc::new(AppState {
3424 languages: project.read(cx).languages().clone(),
3425 client,
3426 user_store,
3427 channel_store,
3428 fs: project.read(cx).fs().clone(),
3429 build_window_options: |_, _, _| Default::default(),
3430 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
3431 background_actions: || &[],
3432 });
3433 Self::new(0, project, app_state, cx)
3434 }
3435
3436 fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
3437 let dock = match position {
3438 DockPosition::Left => &self.left_dock,
3439 DockPosition::Right => &self.right_dock,
3440 DockPosition::Bottom => &self.bottom_dock,
3441 };
3442 let active_panel = dock.read(cx).visible_panel()?;
3443 let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
3444 dock.read(cx).render_placeholder(cx)
3445 } else {
3446 ChildView::new(dock, cx).into_any()
3447 };
3448
3449 Some(
3450 element
3451 .constrained()
3452 .dynamically(move |constraint, _, cx| match position {
3453 DockPosition::Left | DockPosition::Right => SizeConstraint::new(
3454 Vector2F::new(20., constraint.min.y()),
3455 Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
3456 ),
3457 DockPosition::Bottom => SizeConstraint::new(
3458 Vector2F::new(constraint.min.x(), 20.),
3459 Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
3460 ),
3461 })
3462 .into_any(),
3463 )
3464 }
3465}
3466
3467fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3468 ZED_WINDOW_POSITION
3469 .zip(*ZED_WINDOW_SIZE)
3470 .map(|(position, size)| {
3471 WindowBounds::Fixed(RectF::new(
3472 cx.platform().screens()[0].bounds().origin() + position,
3473 size,
3474 ))
3475 })
3476}
3477
3478async fn open_items(
3479 serialized_workspace: Option<SerializedWorkspace>,
3480 workspace: &WeakViewHandle<Workspace>,
3481 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3482 app_state: Arc<AppState>,
3483 mut cx: AsyncAppContext,
3484) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
3485 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3486
3487 if let Some(serialized_workspace) = serialized_workspace {
3488 let workspace = workspace.clone();
3489 let restored_items = cx
3490 .update(|cx| {
3491 Workspace::load_workspace(
3492 workspace,
3493 serialized_workspace,
3494 project_paths_to_open
3495 .iter()
3496 .map(|(_, project_path)| project_path)
3497 .cloned()
3498 .collect(),
3499 cx,
3500 )
3501 })
3502 .await;
3503
3504 let restored_project_paths = cx.read(|cx| {
3505 restored_items
3506 .iter()
3507 .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
3508 .collect::<HashSet<_>>()
3509 });
3510
3511 opened_items = restored_items;
3512 project_paths_to_open
3513 .iter_mut()
3514 .for_each(|(_, project_path)| {
3515 if let Some(project_path_to_open) = project_path {
3516 if restored_project_paths.contains(project_path_to_open) {
3517 *project_path = None;
3518 }
3519 }
3520 });
3521 } else {
3522 for _ in 0..project_paths_to_open.len() {
3523 opened_items.push(None);
3524 }
3525 }
3526 assert!(opened_items.len() == project_paths_to_open.len());
3527
3528 let tasks =
3529 project_paths_to_open
3530 .into_iter()
3531 .enumerate()
3532 .map(|(i, (abs_path, project_path))| {
3533 let workspace = workspace.clone();
3534 cx.spawn(|mut cx| {
3535 let fs = app_state.fs.clone();
3536 async move {
3537 let file_project_path = project_path?;
3538 if fs.is_file(&abs_path).await {
3539 Some((
3540 i,
3541 workspace
3542 .update(&mut cx, |workspace, cx| {
3543 workspace.open_path(file_project_path, None, true, cx)
3544 })
3545 .log_err()?
3546 .await,
3547 ))
3548 } else {
3549 None
3550 }
3551 }
3552 })
3553 });
3554
3555 for maybe_opened_path in futures::future::join_all(tasks.into_iter())
3556 .await
3557 .into_iter()
3558 {
3559 if let Some((i, path_open_result)) = maybe_opened_path {
3560 opened_items[i] = Some(path_open_result);
3561 }
3562 }
3563
3564 opened_items
3565}
3566
3567fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3568 const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
3569 const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
3570 const MESSAGE_ID: usize = 2;
3571
3572 if workspace
3573 .read_with(cx, |workspace, cx| {
3574 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3575 })
3576 .unwrap_or(false)
3577 {
3578 return;
3579 }
3580
3581 if db::kvp::KEY_VALUE_STORE
3582 .read_kvp(NEW_DOCK_HINT_KEY)
3583 .ok()
3584 .flatten()
3585 .is_some()
3586 {
3587 if !workspace
3588 .read_with(cx, |workspace, cx| {
3589 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3590 })
3591 .unwrap_or(false)
3592 {
3593 cx.update(|cx| {
3594 cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
3595 let entry = tracker
3596 .entry(TypeId::of::<MessageNotification>())
3597 .or_default();
3598 if !entry.contains(&MESSAGE_ID) {
3599 entry.push(MESSAGE_ID);
3600 }
3601 });
3602 });
3603 }
3604
3605 return;
3606 }
3607
3608 cx.spawn(|_| async move {
3609 db::kvp::KEY_VALUE_STORE
3610 .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
3611 .await
3612 .ok();
3613 })
3614 .detach();
3615
3616 workspace
3617 .update(cx, |workspace, cx| {
3618 workspace.show_notification_once(2, cx, |cx| {
3619 cx.add_view(|_| {
3620 MessageNotification::new_element(|text, _| {
3621 Text::new(
3622 "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
3623 text,
3624 )
3625 .with_custom_runs(vec![26..32, 34..46], |_, bounds, scene, cx| {
3626 let code_span_background_color = settings::get::<ThemeSettings>(cx)
3627 .theme
3628 .editor
3629 .document_highlight_read_background;
3630
3631 scene.push_quad(gpui::Quad {
3632 bounds,
3633 background: Some(code_span_background_color),
3634 border: Default::default(),
3635 corner_radii: (2.0).into(),
3636 })
3637 })
3638 .into_any()
3639 })
3640 .with_click_message("Read more about the new panel system")
3641 .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
3642 })
3643 })
3644 })
3645 .ok();
3646}
3647
3648fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3649 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3650
3651 workspace
3652 .update(cx, |workspace, cx| {
3653 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3654 workspace.show_notification_once(0, cx, |cx| {
3655 cx.add_view(|_| {
3656 MessageNotification::new("Failed to load the database file.")
3657 .with_click_message("Click to let us know about this error")
3658 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
3659 })
3660 });
3661 }
3662 })
3663 .log_err();
3664}
3665
3666impl Entity for Workspace {
3667 type Event = Event;
3668}
3669
3670impl View for Workspace {
3671 fn ui_name() -> &'static str {
3672 "Workspace"
3673 }
3674
3675 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
3676 let theme = theme::current(cx).clone();
3677 Stack::new()
3678 .with_child(
3679 Flex::column()
3680 .with_child(self.render_titlebar(&theme, cx))
3681 .with_child(
3682 Stack::new()
3683 .with_child({
3684 let project = self.project.clone();
3685 Flex::row()
3686 .with_children(self.render_dock(DockPosition::Left, cx))
3687 .with_child(
3688 Flex::column()
3689 .with_child(
3690 FlexItem::new(
3691 self.center.render(
3692 &project,
3693 &theme,
3694 &self.follower_states_by_leader,
3695 self.active_call(),
3696 self.active_pane(),
3697 self.zoomed
3698 .as_ref()
3699 .and_then(|zoomed| zoomed.upgrade(cx))
3700 .as_ref(),
3701 &self.app_state,
3702 cx,
3703 ),
3704 )
3705 .flex(1., true),
3706 )
3707 .with_children(
3708 self.render_dock(DockPosition::Bottom, cx),
3709 )
3710 .flex(1., true),
3711 )
3712 .with_children(self.render_dock(DockPosition::Right, cx))
3713 })
3714 .with_child(Overlay::new(
3715 Stack::new()
3716 .with_children(self.zoomed.as_ref().and_then(|zoomed| {
3717 enum ZoomBackground {}
3718 let zoomed = zoomed.upgrade(cx)?;
3719
3720 let mut foreground_style =
3721 theme.workspace.zoomed_pane_foreground;
3722 if let Some(zoomed_dock_position) = self.zoomed_position {
3723 foreground_style =
3724 theme.workspace.zoomed_panel_foreground;
3725 let margin = foreground_style.margin.top;
3726 let border = foreground_style.border.top;
3727
3728 // Only include a margin and border on the opposite side.
3729 foreground_style.margin.top = 0.;
3730 foreground_style.margin.left = 0.;
3731 foreground_style.margin.bottom = 0.;
3732 foreground_style.margin.right = 0.;
3733 foreground_style.border.top = false;
3734 foreground_style.border.left = false;
3735 foreground_style.border.bottom = false;
3736 foreground_style.border.right = false;
3737 match zoomed_dock_position {
3738 DockPosition::Left => {
3739 foreground_style.margin.right = margin;
3740 foreground_style.border.right = border;
3741 }
3742 DockPosition::Right => {
3743 foreground_style.margin.left = margin;
3744 foreground_style.border.left = border;
3745 }
3746 DockPosition::Bottom => {
3747 foreground_style.margin.top = margin;
3748 foreground_style.border.top = border;
3749 }
3750 }
3751 }
3752
3753 Some(
3754 ChildView::new(&zoomed, cx)
3755 .contained()
3756 .with_style(foreground_style)
3757 .aligned()
3758 .contained()
3759 .with_style(theme.workspace.zoomed_background)
3760 .mouse::<ZoomBackground>(0)
3761 .capture_all()
3762 .on_down(
3763 MouseButton::Left,
3764 |_, this: &mut Self, cx| {
3765 this.zoom_out(cx);
3766 },
3767 ),
3768 )
3769 }))
3770 .with_children(self.modal.as_ref().map(|modal| {
3771 // Prevent clicks within the modal from falling
3772 // through to the rest of the workspace.
3773 enum ModalBackground {}
3774 MouseEventHandler::new::<ModalBackground, _>(
3775 0,
3776 cx,
3777 |_, cx| ChildView::new(modal.view.as_any(), cx),
3778 )
3779 .on_click(MouseButton::Left, |_, _, _| {})
3780 .contained()
3781 .with_style(theme.workspace.modal)
3782 .aligned()
3783 .top()
3784 }))
3785 .with_children(self.render_notifications(&theme.workspace, cx)),
3786 ))
3787 .provide_resize_bounds::<WorkspaceBounds>()
3788 .flex(1.0, true),
3789 )
3790 .with_child(ChildView::new(&self.status_bar, cx))
3791 .contained()
3792 .with_background_color(theme.workspace.background),
3793 )
3794 .with_children(DragAndDrop::render(cx))
3795 .with_children(self.render_disconnected_overlay(cx))
3796 .into_any_named("workspace")
3797 }
3798
3799 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
3800 if cx.is_self_focused() {
3801 cx.focus(&self.active_pane);
3802 }
3803 }
3804}
3805
3806impl ViewId {
3807 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3808 Ok(Self {
3809 creator: message
3810 .creator
3811 .ok_or_else(|| anyhow!("creator is missing"))?,
3812 id: message.id,
3813 })
3814 }
3815
3816 pub(crate) fn to_proto(&self) -> proto::ViewId {
3817 proto::ViewId {
3818 creator: Some(self.creator),
3819 id: self.id,
3820 }
3821 }
3822}
3823
3824pub trait WorkspaceHandle {
3825 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3826}
3827
3828impl WorkspaceHandle for ViewHandle<Workspace> {
3829 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3830 self.read(cx)
3831 .worktrees(cx)
3832 .flat_map(|worktree| {
3833 let worktree_id = worktree.read(cx).id();
3834 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3835 worktree_id,
3836 path: f.path.clone(),
3837 })
3838 })
3839 .collect::<Vec<_>>()
3840 }
3841}
3842
3843impl std::fmt::Debug for OpenPaths {
3844 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3845 f.debug_struct("OpenPaths")
3846 .field("paths", &self.paths)
3847 .finish()
3848 }
3849}
3850
3851pub struct WorkspaceCreated(pub WeakViewHandle<Workspace>);
3852
3853pub fn activate_workspace_for_project(
3854 cx: &mut AsyncAppContext,
3855 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
3856) -> Option<WeakViewHandle<Workspace>> {
3857 for window in cx.windows() {
3858 let handle = window
3859 .update(cx, |cx| {
3860 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
3861 let project = workspace_handle.read(cx).project.clone();
3862 if project.update(cx, &predicate) {
3863 cx.activate_window();
3864 return Some(workspace_handle.clone());
3865 }
3866 }
3867 None
3868 })
3869 .flatten();
3870
3871 if let Some(handle) = handle {
3872 return Some(handle.downgrade());
3873 }
3874 }
3875 None
3876}
3877
3878pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3879 DB.last_workspace().await.log_err().flatten()
3880}
3881
3882#[allow(clippy::type_complexity)]
3883pub fn open_paths(
3884 abs_paths: &[PathBuf],
3885 app_state: &Arc<AppState>,
3886 requesting_window: Option<WindowHandle<Workspace>>,
3887 cx: &mut AppContext,
3888) -> Task<
3889 Result<(
3890 WeakViewHandle<Workspace>,
3891 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
3892 )>,
3893> {
3894 let app_state = app_state.clone();
3895 let abs_paths = abs_paths.to_vec();
3896 cx.spawn(|mut cx| async move {
3897 // Open paths in existing workspace if possible
3898 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
3899 project.contains_paths(&abs_paths, cx)
3900 });
3901
3902 if let Some(existing) = existing {
3903 Ok((
3904 existing.clone(),
3905 existing
3906 .update(&mut cx, |workspace, cx| {
3907 workspace.open_paths(abs_paths, true, cx)
3908 })?
3909 .await,
3910 ))
3911 } else {
3912 Ok(cx
3913 .update(|cx| {
3914 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
3915 })
3916 .await)
3917 }
3918 })
3919}
3920
3921pub fn open_new(
3922 app_state: &Arc<AppState>,
3923 cx: &mut AppContext,
3924 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
3925) -> Task<()> {
3926 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
3927 cx.spawn(|mut cx| async move {
3928 let (workspace, opened_paths) = task.await;
3929
3930 workspace
3931 .update(&mut cx, |workspace, cx| {
3932 if opened_paths.is_empty() {
3933 init(workspace, cx)
3934 }
3935 })
3936 .log_err();
3937 })
3938}
3939
3940pub fn create_and_open_local_file(
3941 path: &'static Path,
3942 cx: &mut ViewContext<Workspace>,
3943 default_content: impl 'static + Send + FnOnce() -> Rope,
3944) -> Task<Result<Box<dyn ItemHandle>>> {
3945 cx.spawn(|workspace, mut cx| async move {
3946 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
3947 if !fs.is_file(path).await {
3948 fs.create_file(path, Default::default()).await?;
3949 fs.save(path, &default_content(), Default::default())
3950 .await?;
3951 }
3952
3953 let mut items = workspace
3954 .update(&mut cx, |workspace, cx| {
3955 workspace.with_local_workspace(cx, |workspace, cx| {
3956 workspace.open_paths(vec![path.to_path_buf()], false, cx)
3957 })
3958 })?
3959 .await?
3960 .await;
3961
3962 let item = items.pop().flatten();
3963 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
3964 })
3965}
3966
3967pub fn join_remote_project(
3968 project_id: u64,
3969 follow_user_id: u64,
3970 app_state: Arc<AppState>,
3971 cx: &mut AppContext,
3972) -> Task<Result<()>> {
3973 cx.spawn(|mut cx| async move {
3974 let existing_workspace = cx
3975 .windows()
3976 .into_iter()
3977 .find_map(|window| {
3978 window.downcast::<Workspace>().and_then(|window| {
3979 window.read_root_with(&cx, |workspace, cx| {
3980 if workspace.project().read(cx).remote_id() == Some(project_id) {
3981 Some(cx.handle().downgrade())
3982 } else {
3983 None
3984 }
3985 })
3986 })
3987 })
3988 .flatten();
3989
3990 let workspace = if let Some(existing_workspace) = existing_workspace {
3991 existing_workspace
3992 } else {
3993 let active_call = cx.read(ActiveCall::global);
3994 let room = active_call
3995 .read_with(&cx, |call, _| call.room().cloned())
3996 .ok_or_else(|| anyhow!("not in a call"))?;
3997 let project = room
3998 .update(&mut cx, |room, cx| {
3999 room.join_project(
4000 project_id,
4001 app_state.languages.clone(),
4002 app_state.fs.clone(),
4003 cx,
4004 )
4005 })
4006 .await?;
4007
4008 let window_bounds_override = window_bounds_env_override(&cx);
4009 let window = cx.add_window(
4010 (app_state.build_window_options)(
4011 window_bounds_override,
4012 None,
4013 cx.platform().as_ref(),
4014 ),
4015 |cx| Workspace::new(0, project, app_state.clone(), cx),
4016 );
4017 let workspace = window.root(&cx).unwrap();
4018 (app_state.initialize_workspace)(
4019 workspace.downgrade(),
4020 false,
4021 app_state.clone(),
4022 cx.clone(),
4023 )
4024 .await
4025 .log_err();
4026
4027 workspace.downgrade()
4028 };
4029
4030 workspace.window().activate(&mut cx);
4031 cx.platform().activate(true);
4032
4033 workspace.update(&mut cx, |workspace, cx| {
4034 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4035 let follow_peer_id = room
4036 .read(cx)
4037 .remote_participants()
4038 .iter()
4039 .find(|(_, participant)| participant.user.id == follow_user_id)
4040 .map(|(_, p)| p.peer_id)
4041 .or_else(|| {
4042 // If we couldn't follow the given user, follow the host instead.
4043 let collaborator = workspace
4044 .project()
4045 .read(cx)
4046 .collaborators()
4047 .values()
4048 .find(|collaborator| collaborator.replica_id == 0)?;
4049 Some(collaborator.peer_id)
4050 });
4051
4052 if let Some(follow_peer_id) = follow_peer_id {
4053 if !workspace.is_being_followed(follow_peer_id) {
4054 workspace
4055 .toggle_follow(follow_peer_id, cx)
4056 .map(|follow| follow.detach_and_log_err(cx));
4057 }
4058 }
4059 }
4060 })?;
4061
4062 anyhow::Ok(())
4063 })
4064}
4065
4066pub fn restart(_: &Restart, cx: &mut AppContext) {
4067 let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
4068 cx.spawn(|mut cx| async move {
4069 let mut workspace_windows = cx
4070 .windows()
4071 .into_iter()
4072 .filter_map(|window| window.downcast::<Workspace>())
4073 .collect::<Vec<_>>();
4074
4075 // If multiple windows have unsaved changes, and need a save prompt,
4076 // prompt in the active window before switching to a different window.
4077 workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
4078
4079 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4080 let answer = window.prompt(
4081 PromptLevel::Info,
4082 "Are you sure you want to restart?",
4083 &["Restart", "Cancel"],
4084 &mut cx,
4085 );
4086
4087 if let Some(mut answer) = answer {
4088 let answer = answer.next().await;
4089 if answer != Some(0) {
4090 return Ok(());
4091 }
4092 }
4093 }
4094
4095 // If the user cancels any save prompt, then keep the app open.
4096 for window in workspace_windows {
4097 if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| {
4098 workspace.prepare_to_close(true, cx)
4099 }) {
4100 if !should_close.await? {
4101 return Ok(());
4102 }
4103 }
4104 }
4105 cx.platform().restart();
4106 anyhow::Ok(())
4107 })
4108 .detach_and_log_err(cx);
4109}
4110
4111fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
4112 let mut parts = value.split(',');
4113 let width: usize = parts.next()?.parse().ok()?;
4114 let height: usize = parts.next()?.parse().ok()?;
4115 Some(vec2f(width as f32, height as f32))
4116}
4117
4118#[cfg(test)]
4119mod tests {
4120 use super::*;
4121 use crate::{
4122 dock::test::{TestPanel, TestPanelEvent},
4123 item::test::{TestItem, TestItemEvent, TestProjectItem},
4124 };
4125 use fs::FakeFs;
4126 use gpui::{executor::Deterministic, test::EmptyView, TestAppContext};
4127 use project::{Project, ProjectEntryId};
4128 use serde_json::json;
4129 use settings::SettingsStore;
4130 use std::{cell::RefCell, rc::Rc};
4131
4132 #[gpui::test]
4133 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4134 init_test(cx);
4135
4136 let fs = FakeFs::new(cx.background());
4137 let project = Project::test(fs, [], cx).await;
4138 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4139 let workspace = window.root(cx);
4140
4141 // Adding an item with no ambiguity renders the tab without detail.
4142 let item1 = window.add_view(cx, |_| {
4143 let mut item = TestItem::new();
4144 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4145 item
4146 });
4147 workspace.update(cx, |workspace, cx| {
4148 workspace.add_item(Box::new(item1.clone()), cx);
4149 });
4150 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
4151
4152 // Adding an item that creates ambiguity increases the level of detail on
4153 // both tabs.
4154 let item2 = window.add_view(cx, |_| {
4155 let mut item = TestItem::new();
4156 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4157 item
4158 });
4159 workspace.update(cx, |workspace, cx| {
4160 workspace.add_item(Box::new(item2.clone()), cx);
4161 });
4162 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4163 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4164
4165 // Adding an item that creates ambiguity increases the level of detail only
4166 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4167 // we stop at the highest detail available.
4168 let item3 = window.add_view(cx, |_| {
4169 let mut item = TestItem::new();
4170 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4171 item
4172 });
4173 workspace.update(cx, |workspace, cx| {
4174 workspace.add_item(Box::new(item3.clone()), cx);
4175 });
4176 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4177 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4178 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4179 }
4180
4181 #[gpui::test]
4182 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4183 init_test(cx);
4184
4185 let fs = FakeFs::new(cx.background());
4186 fs.insert_tree(
4187 "/root1",
4188 json!({
4189 "one.txt": "",
4190 "two.txt": "",
4191 }),
4192 )
4193 .await;
4194 fs.insert_tree(
4195 "/root2",
4196 json!({
4197 "three.txt": "",
4198 }),
4199 )
4200 .await;
4201
4202 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4203 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4204 let workspace = window.root(cx);
4205 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4206 let worktree_id = project.read_with(cx, |project, cx| {
4207 project.worktrees(cx).next().unwrap().read(cx).id()
4208 });
4209
4210 let item1 = window.add_view(cx, |cx| {
4211 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4212 });
4213 let item2 = window.add_view(cx, |cx| {
4214 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4215 });
4216
4217 // Add an item to an empty pane
4218 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4219 project.read_with(cx, |project, cx| {
4220 assert_eq!(
4221 project.active_entry(),
4222 project
4223 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4224 .map(|e| e.id)
4225 );
4226 });
4227 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4228
4229 // Add a second item to a non-empty pane
4230 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4231 assert_eq!(window.current_title(cx).as_deref(), Some("two.txt β root1"));
4232 project.read_with(cx, |project, cx| {
4233 assert_eq!(
4234 project.active_entry(),
4235 project
4236 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4237 .map(|e| e.id)
4238 );
4239 });
4240
4241 // Close the active item
4242 pane.update(cx, |pane, cx| {
4243 pane.close_active_item(&Default::default(), cx).unwrap()
4244 })
4245 .await
4246 .unwrap();
4247 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4248 project.read_with(cx, |project, cx| {
4249 assert_eq!(
4250 project.active_entry(),
4251 project
4252 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4253 .map(|e| e.id)
4254 );
4255 });
4256
4257 // Add a project folder
4258 project
4259 .update(cx, |project, cx| {
4260 project.find_or_create_local_worktree("/root2", true, cx)
4261 })
4262 .await
4263 .unwrap();
4264 assert_eq!(
4265 window.current_title(cx).as_deref(),
4266 Some("one.txt β root1, root2")
4267 );
4268
4269 // Remove a project folder
4270 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4271 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root2"));
4272 }
4273
4274 #[gpui::test]
4275 async fn test_close_window(cx: &mut TestAppContext) {
4276 init_test(cx);
4277
4278 let fs = FakeFs::new(cx.background());
4279 fs.insert_tree("/root", json!({ "one": "" })).await;
4280
4281 let project = Project::test(fs, ["root".as_ref()], cx).await;
4282 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4283 let workspace = window.root(cx);
4284
4285 // When there are no dirty items, there's nothing to do.
4286 let item1 = window.add_view(cx, |_| TestItem::new());
4287 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4288 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4289 assert!(task.await.unwrap());
4290
4291 // When there are dirty untitled items, prompt to save each one. If the user
4292 // cancels any prompt, then abort.
4293 let item2 = window.add_view(cx, |_| TestItem::new().with_dirty(true));
4294 let item3 = window.add_view(cx, |cx| {
4295 TestItem::new()
4296 .with_dirty(true)
4297 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4298 });
4299 workspace.update(cx, |w, cx| {
4300 w.add_item(Box::new(item2.clone()), cx);
4301 w.add_item(Box::new(item3.clone()), cx);
4302 });
4303 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4304 cx.foreground().run_until_parked();
4305 window.simulate_prompt_answer(2, cx); // cancel
4306 cx.foreground().run_until_parked();
4307 assert!(!window.has_pending_prompt(cx));
4308 assert!(!task.await.unwrap());
4309 }
4310
4311 #[gpui::test]
4312 async fn test_close_pane_items(cx: &mut TestAppContext) {
4313 init_test(cx);
4314
4315 let fs = FakeFs::new(cx.background());
4316
4317 let project = Project::test(fs, None, cx).await;
4318 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4319 let workspace = window.root(cx);
4320
4321 let item1 = window.add_view(cx, |cx| {
4322 TestItem::new()
4323 .with_dirty(true)
4324 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4325 });
4326 let item2 = window.add_view(cx, |cx| {
4327 TestItem::new()
4328 .with_dirty(true)
4329 .with_conflict(true)
4330 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4331 });
4332 let item3 = window.add_view(cx, |cx| {
4333 TestItem::new()
4334 .with_dirty(true)
4335 .with_conflict(true)
4336 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4337 });
4338 let item4 = window.add_view(cx, |cx| {
4339 TestItem::new()
4340 .with_dirty(true)
4341 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4342 });
4343 let pane = workspace.update(cx, |workspace, cx| {
4344 workspace.add_item(Box::new(item1.clone()), cx);
4345 workspace.add_item(Box::new(item2.clone()), cx);
4346 workspace.add_item(Box::new(item3.clone()), cx);
4347 workspace.add_item(Box::new(item4.clone()), cx);
4348 workspace.active_pane().clone()
4349 });
4350
4351 let close_items = pane.update(cx, |pane, cx| {
4352 pane.activate_item(1, true, true, cx);
4353 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4354 let item1_id = item1.id();
4355 let item3_id = item3.id();
4356 let item4_id = item4.id();
4357 pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id))
4358 });
4359 cx.foreground().run_until_parked();
4360
4361 // There's a prompt to save item 1.
4362 pane.read_with(cx, |pane, _| {
4363 assert_eq!(pane.items_len(), 4);
4364 assert_eq!(pane.active_item().unwrap().id(), item1.id());
4365 });
4366 assert!(window.has_pending_prompt(cx));
4367
4368 // Confirm saving item 1.
4369 window.simulate_prompt_answer(0, cx);
4370 cx.foreground().run_until_parked();
4371
4372 // Item 1 is saved. There's a prompt to save item 3.
4373 pane.read_with(cx, |pane, cx| {
4374 assert_eq!(item1.read(cx).save_count, 1);
4375 assert_eq!(item1.read(cx).save_as_count, 0);
4376 assert_eq!(item1.read(cx).reload_count, 0);
4377 assert_eq!(pane.items_len(), 3);
4378 assert_eq!(pane.active_item().unwrap().id(), item3.id());
4379 });
4380 assert!(window.has_pending_prompt(cx));
4381
4382 // Cancel saving item 3.
4383 window.simulate_prompt_answer(1, cx);
4384 cx.foreground().run_until_parked();
4385
4386 // Item 3 is reloaded. There's a prompt to save item 4.
4387 pane.read_with(cx, |pane, cx| {
4388 assert_eq!(item3.read(cx).save_count, 0);
4389 assert_eq!(item3.read(cx).save_as_count, 0);
4390 assert_eq!(item3.read(cx).reload_count, 1);
4391 assert_eq!(pane.items_len(), 2);
4392 assert_eq!(pane.active_item().unwrap().id(), item4.id());
4393 });
4394 assert!(window.has_pending_prompt(cx));
4395
4396 // Confirm saving item 4.
4397 window.simulate_prompt_answer(0, cx);
4398 cx.foreground().run_until_parked();
4399
4400 // There's a prompt for a path for item 4.
4401 cx.simulate_new_path_selection(|_| Some(Default::default()));
4402 close_items.await.unwrap();
4403
4404 // The requested items are closed.
4405 pane.read_with(cx, |pane, cx| {
4406 assert_eq!(item4.read(cx).save_count, 0);
4407 assert_eq!(item4.read(cx).save_as_count, 1);
4408 assert_eq!(item4.read(cx).reload_count, 0);
4409 assert_eq!(pane.items_len(), 1);
4410 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4411 });
4412 }
4413
4414 #[gpui::test]
4415 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4416 init_test(cx);
4417
4418 let fs = FakeFs::new(cx.background());
4419
4420 let project = Project::test(fs, [], cx).await;
4421 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4422 let workspace = window.root(cx);
4423
4424 // Create several workspace items with single project entries, and two
4425 // workspace items with multiple project entries.
4426 let single_entry_items = (0..=4)
4427 .map(|project_entry_id| {
4428 window.add_view(cx, |cx| {
4429 TestItem::new()
4430 .with_dirty(true)
4431 .with_project_items(&[TestProjectItem::new(
4432 project_entry_id,
4433 &format!("{project_entry_id}.txt"),
4434 cx,
4435 )])
4436 })
4437 })
4438 .collect::<Vec<_>>();
4439 let item_2_3 = window.add_view(cx, |cx| {
4440 TestItem::new()
4441 .with_dirty(true)
4442 .with_singleton(false)
4443 .with_project_items(&[
4444 single_entry_items[2].read(cx).project_items[0].clone(),
4445 single_entry_items[3].read(cx).project_items[0].clone(),
4446 ])
4447 });
4448 let item_3_4 = window.add_view(cx, |cx| {
4449 TestItem::new()
4450 .with_dirty(true)
4451 .with_singleton(false)
4452 .with_project_items(&[
4453 single_entry_items[3].read(cx).project_items[0].clone(),
4454 single_entry_items[4].read(cx).project_items[0].clone(),
4455 ])
4456 });
4457
4458 // Create two panes that contain the following project entries:
4459 // left pane:
4460 // multi-entry items: (2, 3)
4461 // single-entry items: 0, 1, 2, 3, 4
4462 // right pane:
4463 // single-entry items: 1
4464 // multi-entry items: (3, 4)
4465 let left_pane = workspace.update(cx, |workspace, cx| {
4466 let left_pane = workspace.active_pane().clone();
4467 workspace.add_item(Box::new(item_2_3.clone()), cx);
4468 for item in single_entry_items {
4469 workspace.add_item(Box::new(item), cx);
4470 }
4471 left_pane.update(cx, |pane, cx| {
4472 pane.activate_item(2, true, true, cx);
4473 });
4474
4475 workspace
4476 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4477 .unwrap();
4478
4479 left_pane
4480 });
4481
4482 //Need to cause an effect flush in order to respect new focus
4483 workspace.update(cx, |workspace, cx| {
4484 workspace.add_item(Box::new(item_3_4.clone()), cx);
4485 cx.focus(&left_pane);
4486 });
4487
4488 // When closing all of the items in the left pane, we should be prompted twice:
4489 // once for project entry 0, and once for project entry 2. After those two
4490 // prompts, the task should complete.
4491
4492 let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true));
4493 cx.foreground().run_until_parked();
4494 left_pane.read_with(cx, |pane, cx| {
4495 assert_eq!(
4496 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4497 &[ProjectEntryId::from_proto(0)]
4498 );
4499 });
4500 window.simulate_prompt_answer(0, cx);
4501
4502 cx.foreground().run_until_parked();
4503 left_pane.read_with(cx, |pane, cx| {
4504 assert_eq!(
4505 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4506 &[ProjectEntryId::from_proto(2)]
4507 );
4508 });
4509 window.simulate_prompt_answer(0, cx);
4510
4511 cx.foreground().run_until_parked();
4512 close.await.unwrap();
4513 left_pane.read_with(cx, |pane, _| {
4514 assert_eq!(pane.items_len(), 0);
4515 });
4516 }
4517
4518 #[gpui::test]
4519 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
4520 init_test(cx);
4521
4522 let fs = FakeFs::new(cx.background());
4523
4524 let project = Project::test(fs, [], cx).await;
4525 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4526 let workspace = window.root(cx);
4527 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4528
4529 let item = window.add_view(cx, |cx| {
4530 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4531 });
4532 let item_id = item.id();
4533 workspace.update(cx, |workspace, cx| {
4534 workspace.add_item(Box::new(item.clone()), cx);
4535 });
4536
4537 // Autosave on window change.
4538 item.update(cx, |item, cx| {
4539 cx.update_global(|settings: &mut SettingsStore, cx| {
4540 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4541 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4542 })
4543 });
4544 item.is_dirty = true;
4545 });
4546
4547 // Deactivating the window saves the file.
4548 window.simulate_deactivation(cx);
4549 deterministic.run_until_parked();
4550 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
4551
4552 // Autosave on focus change.
4553 item.update(cx, |item, cx| {
4554 cx.focus_self();
4555 cx.update_global(|settings: &mut SettingsStore, cx| {
4556 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4557 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4558 })
4559 });
4560 item.is_dirty = true;
4561 });
4562
4563 // Blurring the item saves the file.
4564 item.update(cx, |_, cx| cx.blur());
4565 deterministic.run_until_parked();
4566 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
4567
4568 // Deactivating the window still saves the file.
4569 window.simulate_activation(cx);
4570 item.update(cx, |item, cx| {
4571 cx.focus_self();
4572 item.is_dirty = true;
4573 });
4574 window.simulate_deactivation(cx);
4575
4576 deterministic.run_until_parked();
4577 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4578
4579 // Autosave after delay.
4580 item.update(cx, |item, cx| {
4581 cx.update_global(|settings: &mut SettingsStore, cx| {
4582 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4583 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4584 })
4585 });
4586 item.is_dirty = true;
4587 cx.emit(TestItemEvent::Edit);
4588 });
4589
4590 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4591 deterministic.advance_clock(Duration::from_millis(250));
4592 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4593
4594 // After delay expires, the file is saved.
4595 deterministic.advance_clock(Duration::from_millis(250));
4596 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
4597
4598 // Autosave on focus change, ensuring closing the tab counts as such.
4599 item.update(cx, |item, cx| {
4600 cx.update_global(|settings: &mut SettingsStore, cx| {
4601 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4602 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4603 })
4604 });
4605 item.is_dirty = true;
4606 });
4607
4608 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
4609 .await
4610 .unwrap();
4611 assert!(!window.has_pending_prompt(cx));
4612 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4613
4614 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4615 workspace.update(cx, |workspace, cx| {
4616 workspace.add_item(Box::new(item.clone()), cx);
4617 });
4618 item.update(cx, |item, cx| {
4619 item.project_items[0].update(cx, |item, _| {
4620 item.entry_id = None;
4621 });
4622 item.is_dirty = true;
4623 cx.blur();
4624 });
4625 deterministic.run_until_parked();
4626 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4627
4628 // Ensure autosave is prevented for deleted files also when closing the buffer.
4629 let _close_items =
4630 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
4631 deterministic.run_until_parked();
4632 assert!(window.has_pending_prompt(cx));
4633 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4634 }
4635
4636 #[gpui::test]
4637 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4638 init_test(cx);
4639
4640 let fs = FakeFs::new(cx.background());
4641
4642 let project = Project::test(fs, [], cx).await;
4643 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4644 let workspace = window.root(cx);
4645
4646 let item = window.add_view(cx, |cx| {
4647 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4648 });
4649 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4650 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
4651 let toolbar_notify_count = Rc::new(RefCell::new(0));
4652
4653 workspace.update(cx, |workspace, cx| {
4654 workspace.add_item(Box::new(item.clone()), cx);
4655 let toolbar_notification_count = toolbar_notify_count.clone();
4656 cx.observe(&toolbar, move |_, _, _| {
4657 *toolbar_notification_count.borrow_mut() += 1
4658 })
4659 .detach();
4660 });
4661
4662 pane.read_with(cx, |pane, _| {
4663 assert!(!pane.can_navigate_backward());
4664 assert!(!pane.can_navigate_forward());
4665 });
4666
4667 item.update(cx, |item, cx| {
4668 item.set_state("one".to_string(), cx);
4669 });
4670
4671 // Toolbar must be notified to re-render the navigation buttons
4672 assert_eq!(*toolbar_notify_count.borrow(), 1);
4673
4674 pane.read_with(cx, |pane, _| {
4675 assert!(pane.can_navigate_backward());
4676 assert!(!pane.can_navigate_forward());
4677 });
4678
4679 workspace
4680 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
4681 .await
4682 .unwrap();
4683
4684 assert_eq!(*toolbar_notify_count.borrow(), 3);
4685 pane.read_with(cx, |pane, _| {
4686 assert!(!pane.can_navigate_backward());
4687 assert!(pane.can_navigate_forward());
4688 });
4689 }
4690
4691 #[gpui::test]
4692 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4693 init_test(cx);
4694 let fs = FakeFs::new(cx.background());
4695
4696 let project = Project::test(fs, [], cx).await;
4697 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4698 let workspace = window.root(cx);
4699
4700 let panel = workspace.update(cx, |workspace, cx| {
4701 let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4702 workspace.add_panel(panel.clone(), cx);
4703
4704 workspace
4705 .right_dock()
4706 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4707
4708 panel
4709 });
4710
4711 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4712 pane.update(cx, |pane, cx| {
4713 let item = cx.add_view(|_| TestItem::new());
4714 pane.add_item(Box::new(item), true, true, None, cx);
4715 });
4716
4717 // Transfer focus from center to panel
4718 workspace.update(cx, |workspace, cx| {
4719 workspace.toggle_panel_focus::<TestPanel>(cx);
4720 });
4721
4722 workspace.read_with(cx, |workspace, cx| {
4723 assert!(workspace.right_dock().read(cx).is_open());
4724 assert!(!panel.is_zoomed(cx));
4725 assert!(panel.has_focus(cx));
4726 });
4727
4728 // Transfer focus from panel to center
4729 workspace.update(cx, |workspace, cx| {
4730 workspace.toggle_panel_focus::<TestPanel>(cx);
4731 });
4732
4733 workspace.read_with(cx, |workspace, cx| {
4734 assert!(workspace.right_dock().read(cx).is_open());
4735 assert!(!panel.is_zoomed(cx));
4736 assert!(!panel.has_focus(cx));
4737 });
4738
4739 // Close the dock
4740 workspace.update(cx, |workspace, cx| {
4741 workspace.toggle_dock(DockPosition::Right, cx);
4742 });
4743
4744 workspace.read_with(cx, |workspace, cx| {
4745 assert!(!workspace.right_dock().read(cx).is_open());
4746 assert!(!panel.is_zoomed(cx));
4747 assert!(!panel.has_focus(cx));
4748 });
4749
4750 // Open the dock
4751 workspace.update(cx, |workspace, cx| {
4752 workspace.toggle_dock(DockPosition::Right, cx);
4753 });
4754
4755 workspace.read_with(cx, |workspace, cx| {
4756 assert!(workspace.right_dock().read(cx).is_open());
4757 assert!(!panel.is_zoomed(cx));
4758 assert!(panel.has_focus(cx));
4759 });
4760
4761 // Focus and zoom panel
4762 panel.update(cx, |panel, cx| {
4763 cx.focus_self();
4764 panel.set_zoomed(true, cx)
4765 });
4766
4767 workspace.read_with(cx, |workspace, cx| {
4768 assert!(workspace.right_dock().read(cx).is_open());
4769 assert!(panel.is_zoomed(cx));
4770 assert!(panel.has_focus(cx));
4771 });
4772
4773 // Transfer focus to the center closes the dock
4774 workspace.update(cx, |workspace, cx| {
4775 workspace.toggle_panel_focus::<TestPanel>(cx);
4776 });
4777
4778 workspace.read_with(cx, |workspace, cx| {
4779 assert!(!workspace.right_dock().read(cx).is_open());
4780 assert!(panel.is_zoomed(cx));
4781 assert!(!panel.has_focus(cx));
4782 });
4783
4784 // Transferring focus back to the panel keeps it zoomed
4785 workspace.update(cx, |workspace, cx| {
4786 workspace.toggle_panel_focus::<TestPanel>(cx);
4787 });
4788
4789 workspace.read_with(cx, |workspace, cx| {
4790 assert!(workspace.right_dock().read(cx).is_open());
4791 assert!(panel.is_zoomed(cx));
4792 assert!(panel.has_focus(cx));
4793 });
4794
4795 // Close the dock while it is zoomed
4796 workspace.update(cx, |workspace, cx| {
4797 workspace.toggle_dock(DockPosition::Right, cx)
4798 });
4799
4800 workspace.read_with(cx, |workspace, cx| {
4801 assert!(!workspace.right_dock().read(cx).is_open());
4802 assert!(panel.is_zoomed(cx));
4803 assert!(workspace.zoomed.is_none());
4804 assert!(!panel.has_focus(cx));
4805 });
4806
4807 // Opening the dock, when it's zoomed, retains focus
4808 workspace.update(cx, |workspace, cx| {
4809 workspace.toggle_dock(DockPosition::Right, cx)
4810 });
4811
4812 workspace.read_with(cx, |workspace, cx| {
4813 assert!(workspace.right_dock().read(cx).is_open());
4814 assert!(panel.is_zoomed(cx));
4815 assert!(workspace.zoomed.is_some());
4816 assert!(panel.has_focus(cx));
4817 });
4818
4819 // Unzoom and close the panel, zoom the active pane.
4820 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
4821 workspace.update(cx, |workspace, cx| {
4822 workspace.toggle_dock(DockPosition::Right, cx)
4823 });
4824 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
4825
4826 // Opening a dock unzooms the pane.
4827 workspace.update(cx, |workspace, cx| {
4828 workspace.toggle_dock(DockPosition::Right, cx)
4829 });
4830 workspace.read_with(cx, |workspace, cx| {
4831 let pane = pane.read(cx);
4832 assert!(!pane.is_zoomed());
4833 assert!(!pane.has_focus());
4834 assert!(workspace.right_dock().read(cx).is_open());
4835 assert!(workspace.zoomed.is_none());
4836 });
4837 }
4838
4839 #[gpui::test]
4840 async fn test_panels(cx: &mut gpui::TestAppContext) {
4841 init_test(cx);
4842 let fs = FakeFs::new(cx.background());
4843
4844 let project = Project::test(fs, [], cx).await;
4845 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4846 let workspace = window.root(cx);
4847
4848 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
4849 // Add panel_1 on the left, panel_2 on the right.
4850 let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left));
4851 workspace.add_panel(panel_1.clone(), cx);
4852 workspace
4853 .left_dock()
4854 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
4855 let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4856 workspace.add_panel(panel_2.clone(), cx);
4857 workspace
4858 .right_dock()
4859 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4860
4861 let left_dock = workspace.left_dock();
4862 assert_eq!(
4863 left_dock.read(cx).visible_panel().unwrap().id(),
4864 panel_1.id()
4865 );
4866 assert_eq!(
4867 left_dock.read(cx).active_panel_size(cx).unwrap(),
4868 panel_1.size(cx)
4869 );
4870
4871 left_dock.update(cx, |left_dock, cx| {
4872 left_dock.resize_active_panel(Some(1337.), cx)
4873 });
4874 assert_eq!(
4875 workspace
4876 .right_dock()
4877 .read(cx)
4878 .visible_panel()
4879 .unwrap()
4880 .id(),
4881 panel_2.id()
4882 );
4883
4884 (panel_1, panel_2)
4885 });
4886
4887 // Move panel_1 to the right
4888 panel_1.update(cx, |panel_1, cx| {
4889 panel_1.set_position(DockPosition::Right, cx)
4890 });
4891
4892 workspace.update(cx, |workspace, cx| {
4893 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
4894 // Since it was the only panel on the left, the left dock should now be closed.
4895 assert!(!workspace.left_dock().read(cx).is_open());
4896 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
4897 let right_dock = workspace.right_dock();
4898 assert_eq!(
4899 right_dock.read(cx).visible_panel().unwrap().id(),
4900 panel_1.id()
4901 );
4902 assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4903
4904 // Now we move panel_2Β to the left
4905 panel_2.set_position(DockPosition::Left, cx);
4906 });
4907
4908 workspace.update(cx, |workspace, cx| {
4909 // Since panel_2 was not visible on the right, we don't open the left dock.
4910 assert!(!workspace.left_dock().read(cx).is_open());
4911 // And the right dock is unaffected in it's displaying of panel_1
4912 assert!(workspace.right_dock().read(cx).is_open());
4913 assert_eq!(
4914 workspace
4915 .right_dock()
4916 .read(cx)
4917 .visible_panel()
4918 .unwrap()
4919 .id(),
4920 panel_1.id()
4921 );
4922 });
4923
4924 // Move panel_1 back to the left
4925 panel_1.update(cx, |panel_1, cx| {
4926 panel_1.set_position(DockPosition::Left, cx)
4927 });
4928
4929 workspace.update(cx, |workspace, cx| {
4930 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
4931 let left_dock = workspace.left_dock();
4932 assert!(left_dock.read(cx).is_open());
4933 assert_eq!(
4934 left_dock.read(cx).visible_panel().unwrap().id(),
4935 panel_1.id()
4936 );
4937 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4938 // And right the dock should be closed as it no longer has any panels.
4939 assert!(!workspace.right_dock().read(cx).is_open());
4940
4941 // Now we move panel_1 to the bottom
4942 panel_1.set_position(DockPosition::Bottom, cx);
4943 });
4944
4945 workspace.update(cx, |workspace, cx| {
4946 // Since panel_1 was visible on the left, we close the left dock.
4947 assert!(!workspace.left_dock().read(cx).is_open());
4948 // The bottom dock is sized based on the panel's default size,
4949 // since the panel orientation changed from vertical to horizontal.
4950 let bottom_dock = workspace.bottom_dock();
4951 assert_eq!(
4952 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
4953 panel_1.size(cx),
4954 );
4955 // Close bottom dock and move panel_1 back to the left.
4956 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
4957 panel_1.set_position(DockPosition::Left, cx);
4958 });
4959
4960 // Emit activated event on panel 1
4961 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
4962
4963 // Now the left dock is open and panel_1 is active and focused.
4964 workspace.read_with(cx, |workspace, cx| {
4965 let left_dock = workspace.left_dock();
4966 assert!(left_dock.read(cx).is_open());
4967 assert_eq!(
4968 left_dock.read(cx).visible_panel().unwrap().id(),
4969 panel_1.id()
4970 );
4971 assert!(panel_1.is_focused(cx));
4972 });
4973
4974 // Emit closed event on panel 2, which is not active
4975 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
4976
4977 // Wo don't close the left dock, because panel_2 wasn't the active panel
4978 workspace.read_with(cx, |workspace, cx| {
4979 let left_dock = workspace.left_dock();
4980 assert!(left_dock.read(cx).is_open());
4981 assert_eq!(
4982 left_dock.read(cx).visible_panel().unwrap().id(),
4983 panel_1.id()
4984 );
4985 });
4986
4987 // Emitting a ZoomIn event shows the panel as zoomed.
4988 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
4989 workspace.read_with(cx, |workspace, _| {
4990 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4991 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
4992 });
4993
4994 // Move panel to another dock while it is zoomed
4995 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
4996 workspace.read_with(cx, |workspace, _| {
4997 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4998 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
4999 });
5000
5001 // If focus is transferred to another view that's not a panel or another pane, we still show
5002 // the panel as zoomed.
5003 let focus_receiver = window.add_view(cx, |_| EmptyView);
5004 focus_receiver.update(cx, |_, cx| cx.focus_self());
5005 workspace.read_with(cx, |workspace, _| {
5006 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5007 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5008 });
5009
5010 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5011 workspace.update(cx, |_, cx| cx.focus_self());
5012 workspace.read_with(cx, |workspace, _| {
5013 assert_eq!(workspace.zoomed, None);
5014 assert_eq!(workspace.zoomed_position, None);
5015 });
5016
5017 // If focus is transferred again to another view that's not a panel or a pane, we won't
5018 // show the panel as zoomed because it wasn't zoomed before.
5019 focus_receiver.update(cx, |_, cx| cx.focus_self());
5020 workspace.read_with(cx, |workspace, _| {
5021 assert_eq!(workspace.zoomed, None);
5022 assert_eq!(workspace.zoomed_position, None);
5023 });
5024
5025 // When focus is transferred back to the panel, it is zoomed again.
5026 panel_1.update(cx, |_, cx| cx.focus_self());
5027 workspace.read_with(cx, |workspace, _| {
5028 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5029 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5030 });
5031
5032 // Emitting a ZoomOut event unzooms the panel.
5033 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
5034 workspace.read_with(cx, |workspace, _| {
5035 assert_eq!(workspace.zoomed, None);
5036 assert_eq!(workspace.zoomed_position, None);
5037 });
5038
5039 // Emit closed event on panel 1, which is active
5040 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5041
5042 // Now the left dock is closed, because panel_1 was the active panel
5043 workspace.read_with(cx, |workspace, cx| {
5044 let right_dock = workspace.right_dock();
5045 assert!(!right_dock.read(cx).is_open());
5046 });
5047 }
5048
5049 pub fn init_test(cx: &mut TestAppContext) {
5050 cx.foreground().forbid_parking();
5051 cx.update(|cx| {
5052 cx.set_global(SettingsStore::test(cx));
5053 theme::init((), cx);
5054 language::init(cx);
5055 crate::init_settings(cx);
5056 Project::init_settings(cx);
5057 });
5058 }
5059}