mouse_context_menu.rs

  1use crate::{
  2    DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToImplementation,
  3    GoToTypeDefinition, Rename, RevealInFinder, SelectMode, ToggleCodeActions,
  4};
  5use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
  6
  7pub struct MouseContextMenu {
  8    pub(crate) position: Point<Pixels>,
  9    pub(crate) context_menu: View<ui::ContextMenu>,
 10    _subscription: Subscription,
 11}
 12
 13impl MouseContextMenu {
 14    pub(crate) fn new(
 15        position: Point<Pixels>,
 16        context_menu: View<ui::ContextMenu>,
 17        cx: &mut ViewContext<Editor>,
 18    ) -> Self {
 19        let context_menu_focus = context_menu.focus_handle(cx);
 20        cx.focus(&context_menu_focus);
 21
 22        let _subscription =
 23            cx.subscribe(&context_menu, move |this, _, _event: &DismissEvent, cx| {
 24                this.mouse_context_menu.take();
 25                if context_menu_focus.contains_focused(cx) {
 26                    this.focus(cx);
 27                }
 28            });
 29
 30        Self {
 31            position,
 32            context_menu,
 33            _subscription,
 34        }
 35    }
 36}
 37
 38pub fn deploy_context_menu(
 39    editor: &mut Editor,
 40    position: Point<Pixels>,
 41    point: DisplayPoint,
 42    cx: &mut ViewContext<Editor>,
 43) {
 44    if !editor.is_focused(cx) {
 45        editor.focus(cx);
 46    }
 47
 48    // Don't show context menu for inline editors
 49    if editor.mode() != EditorMode::Full {
 50        return;
 51    }
 52
 53    let context_menu = if let Some(custom) = editor.custom_context_menu.take() {
 54        let menu = custom(editor, point, cx);
 55        editor.custom_context_menu = Some(custom);
 56        if menu.is_none() {
 57            return;
 58        }
 59        menu.unwrap()
 60    } else {
 61        // Don't show the context menu if there isn't a project associated with this editor
 62        if editor.project.is_none() {
 63            return;
 64        }
 65
 66        // Move the cursor to the clicked location so that dispatched actions make sense
 67        editor.change_selections(None, cx, |s| {
 68            s.clear_disjoint();
 69            s.set_pending_display_range(point..point, SelectMode::Character);
 70        });
 71
 72        ui::ContextMenu::build(cx, |menu, _cx| {
 73            menu.action("Rename Symbol", Box::new(Rename))
 74                .action("Go to Definition", Box::new(GoToDefinition))
 75                .action("Go to Type Definition", Box::new(GoToTypeDefinition))
 76                .action("Go to Implementation", Box::new(GoToImplementation))
 77                .action("Find All References", Box::new(FindAllReferences))
 78                .action(
 79                    "Code Actions",
 80                    Box::new(ToggleCodeActions {
 81                        deployed_from_indicator: false,
 82                    }),
 83                )
 84                .separator()
 85                .action("Reveal in Finder", Box::new(RevealInFinder))
 86        })
 87    };
 88    let mouse_context_menu = MouseContextMenu::new(position, context_menu, cx);
 89    editor.mouse_context_menu = Some(mouse_context_menu);
 90    cx.notify();
 91}
 92
 93#[cfg(test)]
 94mod tests {
 95    use super::*;
 96    use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
 97    use indoc::indoc;
 98
 99    #[gpui::test]
100    async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
101        init_test(cx, |_| {});
102
103        let mut cx = EditorLspTestContext::new_rust(
104            lsp::ServerCapabilities {
105                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
106                ..Default::default()
107            },
108            cx,
109        )
110        .await;
111
112        cx.set_state(indoc! {"
113            fn teˇst() {
114                do_work();
115            }
116        "});
117        let point = cx.display_point(indoc! {"
118            fn test() {
119                do_wˇork();
120            }
121        "});
122        cx.editor(|editor, _app| assert!(editor.mouse_context_menu.is_none()));
123        cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
124
125        cx.assert_editor_state(indoc! {"
126            fn test() {
127                do_wˇork();
128            }
129        "});
130        cx.editor(|editor, _app| assert!(editor.mouse_context_menu.is_some()));
131    }
132}