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