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}