editor: Hide mouse context menu when modal is opened (#29127)

redforks created

Closes #28787 

The context menu appears before the modal because it is a Deferred
element, which is always displayed above normal elements.

Release Notes:

Previously, the editor context menu appeared before the Command Palette.
This commit ensures the editor context menu is hidden when a modal,
including the Command Palette, is opened.

Change summary

crates/editor/src/editor_tests.rs   | 62 ++++++++++++++++++++++++++++++-
crates/editor/src/items.rs          | 10 ++++
crates/workspace/src/modal_layer.rs |  9 +++-
crates/workspace/src/workspace.rs   |  8 ++++
4 files changed, 84 insertions(+), 5 deletions(-)

Detailed changes

crates/editor/src/editor_tests.rs 🔗

@@ -12,8 +12,8 @@ use crate::{
 use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
 use futures::StreamExt;
 use gpui::{
-    BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
-    WindowBounds, WindowOptions, div,
+    BackgroundExecutor, DismissEvent, SemanticVersion, TestAppContext, UpdateGlobal,
+    VisualTestContext, WindowBounds, WindowOptions, div,
 };
 use indoc::indoc;
 use language::{
@@ -19549,6 +19549,64 @@ println!("5");
     });
 }
 
+#[gpui::test]
+async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
+    struct EmptyModalView {
+        focus_handle: gpui::FocusHandle,
+    }
+    impl EventEmitter<DismissEvent> for EmptyModalView {}
+    impl Render for EmptyModalView {
+        fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
+            div()
+        }
+    }
+    impl Focusable for EmptyModalView {
+        fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
+            self.focus_handle.clone()
+        }
+    }
+    impl workspace::ModalView for EmptyModalView {}
+    fn new_empty_modal_view(cx: &App) -> EmptyModalView {
+        EmptyModalView {
+            focus_handle: cx.focus_handle(),
+        }
+    }
+
+    init_test(cx, |_| {});
+
+    let fs = FakeFs::new(cx.executor());
+    let project = Project::test(fs, [], cx).await;
+    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+    let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
+    let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
+    let editor = cx.new_window_entity(|window, cx| {
+        Editor::new(
+            EditorMode::full(),
+            buffer,
+            Some(project.clone()),
+            window,
+            cx,
+        )
+    });
+    workspace
+        .update(cx, |workspace, window, cx| {
+            workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
+        })
+        .unwrap();
+    editor.update_in(cx, |editor, window, cx| {
+        editor.open_context_menu(&OpenContextMenu, window, cx);
+        assert!(editor.mouse_context_menu.is_some());
+    });
+    workspace
+        .update(cx, |workspace, window, cx| {
+            workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
+        })
+        .unwrap();
+    cx.read(|cx| {
+        assert!(editor.read(cx).mouse_context_menu.is_none());
+    });
+}
+
 fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
     let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
     point..point

crates/editor/src/items.rs 🔗

@@ -928,9 +928,17 @@ impl Item for Editor {
         &mut self,
         workspace: &mut Workspace,
         _window: &mut Window,
-        _: &mut Context<Self>,
+        cx: &mut Context<Self>,
     ) {
         self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
+        if let Some(workspace) = &workspace.weak_handle().upgrade() {
+            cx.subscribe(&workspace, |editor, _, event: &workspace::Event, _cx| {
+                if matches!(event, workspace::Event::ModalOpened) {
+                    editor.mouse_context_menu.take();
+                }
+            })
+            .detach();
+        }
     }
 
     fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) {

crates/workspace/src/modal_layer.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::{
-    AnyView, DismissEvent, Entity, FocusHandle, Focusable as _, ManagedView, MouseButton,
-    Subscription,
+    AnyView, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable as _, ManagedView,
+    MouseButton, Subscription,
 };
 use ui::prelude::*;
 
@@ -56,6 +56,10 @@ pub struct ModalLayer {
     dismiss_on_focus_lost: bool,
 }
 
+pub(crate) struct ModalOpenedEvent;
+
+impl EventEmitter<ModalOpenedEvent> for ModalLayer {}
+
 impl Default for ModalLayer {
     fn default() -> Self {
         Self::new()
@@ -84,6 +88,7 @@ impl ModalLayer {
         }
         let new_modal = cx.new(|cx| build_view(window, cx));
         self.show_modal(new_modal, window, cx);
+        cx.emit(ModalOpenedEvent);
     }
 
     fn show_modal<V>(&mut self, new_modal: Entity<V>, window: &mut Window, cx: &mut Context<Self>)

crates/workspace/src/workspace.rs 🔗

@@ -781,6 +781,7 @@ pub enum Event {
         language: &'static str,
     },
     ZoomChanged,
+    ModalOpened,
 }
 
 #[derive(Debug)]
@@ -1051,6 +1052,13 @@ impl Workspace {
         cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
         let modal_layer = cx.new(|_| ModalLayer::new());
         let toast_layer = cx.new(|_| ToastLayer::new());
+        cx.subscribe(
+            &modal_layer,
+            |_, _, _: &modal_layer::ModalOpenedEvent, cx| {
+                cx.emit(Event::ModalOpened);
+            },
+        )
+        .detach();
 
         let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout;
         let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx);