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