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