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