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