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