mouse_context_menu.rs

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