mouse_context_menu.rs

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