Add action `editor::OpenContextMenu` (#21494)

Michael Sloan created

This addresses the editor context menu portion of #17819.

Release Notes:

- Added `editor::OpenContextMenu` action to open context menu at current
cursor position.

Change summary

assets/keymaps/default-linux.json       |  4 +
crates/editor/src/actions.rs            |  1 
crates/editor/src/editor.rs             | 38 +++++++++++----
crates/editor/src/element.rs            | 29 ++++++-----
crates/editor/src/mouse_context_menu.rs | 65 ++++++++++++--------------
5 files changed, 77 insertions(+), 60 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -108,7 +108,9 @@
       "ctrl-'": "editor::ToggleHunkDiff",
       "ctrl-\"": "editor::ExpandAllHunkDiffs",
       "ctrl-i": "editor::ShowSignatureHelp",
-      "alt-g b": "editor::ToggleGitBlame"
+      "alt-g b": "editor::ToggleGitBlame",
+      "menu": "editor::OpenContextMenu",
+      "shift-f10": "editor::OpenContextMenu"
     }
   },
   {

crates/editor/src/actions.rs 🔗

@@ -296,6 +296,7 @@ gpui::actions!(
         NewlineBelow,
         NextInlineCompletion,
         NextScreen,
+        OpenContextMenu,
         OpenExcerpts,
         OpenExcerptsSplit,
         OpenProposedChangesEditor,

crates/editor/src/editor.rs 🔗

@@ -13075,6 +13075,12 @@ impl Editor {
         cx.write_to_clipboard(ClipboardItem::new_string(lines));
     }
 
+    pub fn open_context_menu(&mut self, _: &OpenContextMenu, cx: &mut ViewContext<Self>) {
+        self.request_autoscroll(Autoscroll::newest(), cx);
+        let position = self.selections.newest_display(cx).start;
+        mouse_context_menu::deploy_context_menu(self, None, position, cx);
+    }
+
     pub fn inlay_hint_cache(&self) -> &InlayHintCache {
         &self.inlay_hint_cache
     }
@@ -13296,6 +13302,23 @@ impl Editor {
             .get(&type_id)
             .and_then(|item| item.to_any().downcast_ref::<T>())
     }
+
+    fn character_size(&self, cx: &mut ViewContext<Self>) -> gpui::Point<Pixels> {
+        let text_layout_details = self.text_layout_details(cx);
+        let style = &text_layout_details.editor_style;
+        let font_id = cx.text_system().resolve_font(&style.text.font());
+        let font_size = style.text.font_size.to_pixels(cx.rem_size());
+        let line_height = style.text.line_height_in_pixels(cx.rem_size());
+
+        let em_width = cx
+            .text_system()
+            .typographic_bounds(font_id, font_size, 'm')
+            .unwrap()
+            .size
+            .width;
+
+        gpui::Point::new(em_width, line_height)
+    }
 }
 
 fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize {
@@ -14725,17 +14748,10 @@ impl ViewInputHandler for Editor {
         cx: &mut ViewContext<Self>,
     ) -> Option<gpui::Bounds<Pixels>> {
         let text_layout_details = self.text_layout_details(cx);
-        let style = &text_layout_details.editor_style;
-        let font_id = cx.text_system().resolve_font(&style.text.font());
-        let font_size = style.text.font_size.to_pixels(cx.rem_size());
-        let line_height = style.text.line_height_in_pixels(cx.rem_size());
-
-        let em_width = cx
-            .text_system()
-            .typographic_bounds(font_id, font_size, 'm')
-            .unwrap()
-            .size
-            .width;
+        let gpui::Point {
+            x: em_width,
+            y: line_height,
+        } = self.character_size(cx);
 
         let snapshot = self.snapshot(cx);
         let scroll_position = snapshot.scroll_position();

crates/editor/src/element.rs 🔗

@@ -169,6 +169,7 @@ impl EditorElement {
 
         crate::rust_analyzer_ext::apply_related_actions(view, cx);
         crate::clangd_ext::apply_related_actions(view, cx);
+        register_action(view, cx, Editor::open_context_menu);
         register_action(view, cx, Editor::move_left);
         register_action(view, cx, Editor::move_right);
         register_action(view, cx, Editor::move_down);
@@ -595,7 +596,7 @@ impl EditorElement {
             position_map.point_for_position(text_hitbox.bounds, event.position);
         mouse_context_menu::deploy_context_menu(
             editor,
-            event.position,
+            Some(event.position),
             point_for_position.previous_valid,
             cx,
         );
@@ -2730,6 +2731,7 @@ impl EditorElement {
         &self,
         editor_snapshot: &EditorSnapshot,
         visible_range: Range<DisplayRow>,
+        content_origin: gpui::Point<Pixels>,
         cx: &mut WindowContext,
     ) -> Option<AnyElement> {
         let position = self.editor.update(cx, |editor, cx| {
@@ -2747,16 +2749,11 @@ impl EditorElement {
             let mouse_context_menu = editor.mouse_context_menu.as_ref()?;
             let (source_display_point, position) = match mouse_context_menu.position {
                 MenuPosition::PinnedToScreen(point) => (None, point),
-                MenuPosition::PinnedToEditor {
-                    source,
-                    offset_x,
-                    offset_y,
-                } => {
+                MenuPosition::PinnedToEditor { source, offset } => {
                     let source_display_point = source.to_display_point(editor_snapshot);
-                    let mut source_point = editor.to_pixel_point(source, editor_snapshot, cx)?;
-                    source_point.x += offset_x;
-                    source_point.y += offset_y;
-                    (Some(source_display_point), source_point)
+                    let source_point = editor.to_pixel_point(source, editor_snapshot, cx)?;
+                    let position = content_origin + source_point + offset;
+                    (Some(source_display_point), position)
                 }
             };
 
@@ -4325,8 +4322,8 @@ fn deploy_blame_entry_context_menu(
     });
 
     editor.update(cx, move |editor, cx| {
-        editor.mouse_context_menu = Some(MouseContextMenu::pinned_to_screen(
-            position,
+        editor.mouse_context_menu = Some(MouseContextMenu::new(
+            MenuPosition::PinnedToScreen(position),
             context_menu,
             cx,
         ));
@@ -5578,8 +5575,12 @@ impl Element for EditorElement {
                         );
                     }
 
-                    let mouse_context_menu =
-                        self.layout_mouse_context_menu(&snapshot, start_row..end_row, cx);
+                    let mouse_context_menu = self.layout_mouse_context_menu(
+                        &snapshot,
+                        start_row..end_row,
+                        content_origin,
+                        cx,
+                    );
 
                     cx.with_element_namespace("crease_toggles", |cx| {
                         self.prepaint_crease_toggles(

crates/editor/src/mouse_context_menu.rs 🔗

@@ -20,8 +20,7 @@ pub enum MenuPosition {
     /// Disappears when the position is no longer visible.
     PinnedToEditor {
         source: multi_buffer::Anchor,
-        offset_x: Pixels,
-        offset_y: Pixels,
+        offset: Point<Pixels>,
     },
 }
 
@@ -48,36 +47,22 @@ impl MouseContextMenu {
         context_menu: View<ui::ContextMenu>,
         cx: &mut ViewContext<Editor>,
     ) -> Option<Self> {
-        let context_menu_focus = context_menu.focus_handle(cx);
-        cx.focus(&context_menu_focus);
-
-        let _subscription = cx.subscribe(
-            &context_menu,
-            move |editor, _, _event: &DismissEvent, cx| {
-                editor.mouse_context_menu.take();
-                if context_menu_focus.contains_focused(cx) {
-                    editor.focus(cx);
-                }
-            },
-        );
-
         let editor_snapshot = editor.snapshot(cx);
-        let source_point = editor.to_pixel_point(source, &editor_snapshot, cx)?;
-        let offset = position - source_point;
-
-        Some(Self {
-            position: MenuPosition::PinnedToEditor {
-                source,
-                offset_x: offset.x,
-                offset_y: offset.y,
-            },
-            context_menu,
-            _subscription,
-        })
+        let content_origin = editor.last_bounds?.origin
+            + Point {
+                x: editor.gutter_dimensions.width,
+                y: Pixels(0.0),
+            };
+        let source_position = editor.to_pixel_point(source, &editor_snapshot, cx)?;
+        let menu_position = MenuPosition::PinnedToEditor {
+            source,
+            offset: position - (source_position + content_origin),
+        };
+        return Some(MouseContextMenu::new(menu_position, context_menu, cx));
     }
 
-    pub(crate) fn pinned_to_screen(
-        position: Point<Pixels>,
+    pub(crate) fn new(
+        position: MenuPosition,
         context_menu: View<ui::ContextMenu>,
         cx: &mut ViewContext<Editor>,
     ) -> Self {
@@ -95,7 +80,7 @@ impl MouseContextMenu {
         );
 
         Self {
-            position: MenuPosition::PinnedToScreen(position),
+            position,
             context_menu,
             _subscription,
         }
@@ -119,7 +104,7 @@ fn display_ranges<'a>(
 
 pub fn deploy_context_menu(
     editor: &mut Editor,
-    position: Point<Pixels>,
+    position: Option<Point<Pixels>>,
     point: DisplayPoint,
     cx: &mut ViewContext<Editor>,
 ) {
@@ -213,8 +198,18 @@ pub fn deploy_context_menu(
         })
     };
 
-    editor.mouse_context_menu =
-        MouseContextMenu::pinned_to_editor(editor, source_anchor, position, context_menu, cx);
+    editor.mouse_context_menu = match position {
+        Some(position) => {
+            MouseContextMenu::pinned_to_editor(editor, source_anchor, position, context_menu, cx)
+        }
+        None => {
+            let menu_position = MenuPosition::PinnedToEditor {
+                source: source_anchor,
+                offset: editor.character_size(cx),
+            };
+            Some(MouseContextMenu::new(menu_position, context_menu, cx))
+        }
+    };
     cx.notify();
 }
 
@@ -248,7 +243,9 @@ mod tests {
             }
         "});
         cx.editor(|editor, _app| assert!(editor.mouse_context_menu.is_none()));
-        cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
+        cx.update_editor(|editor, cx| {
+            deploy_context_menu(editor, Some(Default::default()), point, cx)
+        });
 
         cx.assert_editor_state(indoc! {"
             fn test() {