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