WIP add basic context menu and make progress toward adding quick actions to it

Keith Simmons created

Change summary

Cargo.lock                                 |  1 
crates/editor/Cargo.toml                   |  1 
crates/editor/src/editor.rs                | 14 ++++-
crates/editor/src/element.rs               | 24 +++++++++
crates/editor/src/mouse_context_menu.rs    | 61 ++++++++++++++++++++++++
crates/editor/src/selections_collection.rs | 38 ++++++++++++++
6 files changed, 135 insertions(+), 4 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1611,6 +1611,7 @@ dependencies = [
  "anyhow",
  "clock",
  "collections",
+ "context_menu",
  "ctor",
  "env_logger",
  "futures",

crates/editor/Cargo.toml 🔗

@@ -23,6 +23,7 @@ test-support = [
 text = { path = "../text" }
 clock = { path = "../clock" }
 collections = { path = "../collections" }
+context_menu = { path = "../context_menu" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }

crates/editor/src/editor.rs 🔗

@@ -4,6 +4,7 @@ mod highlight_matching_bracket;
 mod hover_popover;
 pub mod items;
 mod link_go_to_definition;
+mod mouse_context_menu;
 pub mod movement;
 mod multi_buffer;
 pub mod selections_collection;
@@ -319,6 +320,7 @@ pub fn init(cx: &mut MutableAppContext) {
 
     hover_popover::init(cx);
     link_go_to_definition::init(cx);
+    mouse_context_menu::init(cx);
 
     workspace::register_project_item::<Editor>(cx);
     workspace::register_followable_item::<Editor>(cx);
@@ -425,6 +427,7 @@ pub struct Editor {
     background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>,
     nav_history: Option<ItemNavHistory>,
     context_menu: Option<ContextMenu>,
+    mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
     completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
     next_completion_id: CompletionId,
     available_code_actions: Option<(ModelHandle<Buffer>, Arc<[CodeAction]>)>,
@@ -1010,11 +1013,11 @@ impl Editor {
             background_highlights: Default::default(),
             nav_history: None,
             context_menu: None,
+            mouse_context_menu: cx.add_view(|cx| context_menu::ContextMenu::new(cx)),
             completion_tasks: Default::default(),
             next_completion_id: 0,
             available_code_actions: Default::default(),
             code_actions_task: Default::default(),
-
             document_highlights_task: Default::default(),
             pending_rename: Default::default(),
             searchable: true,
@@ -1596,7 +1599,7 @@ impl Editor {
                 s.delete(newest_selection.id)
             }
 
-            s.set_pending_range(start..end, mode);
+            s.set_pending_anchor_range(start..end, mode);
         });
     }
 
@@ -5780,7 +5783,12 @@ impl View for Editor {
             });
         }
 
-        EditorElement::new(self.handle.clone(), style.clone(), self.cursor_shape).boxed()
+        Stack::new()
+            .with_child(
+                EditorElement::new(self.handle.clone(), style.clone(), self.cursor_shape).boxed(),
+            )
+            .with_child(ChildView::new(&self.mouse_context_menu).boxed())
+            .boxed()
     }
 
     fn ui_name() -> &'static str {

crates/editor/src/element.rs 🔗

@@ -7,6 +7,7 @@ use crate::{
     display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
     hover_popover::HoverAt,
     link_go_to_definition::{CmdChanged, GoToFetchedDefinition, UpdateGoToDefinitionLink},
+    mouse_context_menu::DeployMouseContextMenu,
     EditorStyle,
 };
 use clock::ReplicaId;
@@ -152,6 +153,24 @@ impl EditorElement {
         true
     }
 
+    fn mouse_right_down(
+        &self,
+        position: Vector2F,
+        layout: &mut LayoutState,
+        paint: &mut PaintState,
+        cx: &mut EventContext,
+    ) -> bool {
+        if !paint.text_bounds.contains_point(position) {
+            return false;
+        }
+
+        let snapshot = self.snapshot(cx.app);
+        let (point, _) = paint.point_for_position(&snapshot, layout, position);
+
+        cx.dispatch_action(DeployMouseContextMenu { position, point });
+        true
+    }
+
     fn mouse_up(&self, _position: Vector2F, cx: &mut EventContext) -> bool {
         if self.view(cx.app.as_ref()).is_selecting() {
             cx.dispatch_action(Select(SelectPhase::End));
@@ -1482,6 +1501,11 @@ impl Element for EditorElement {
                 paint,
                 cx,
             ),
+            Event::MouseDown(MouseEvent {
+                button: MouseButton::Right,
+                position,
+                ..
+            }) => self.mouse_right_down(*position, layout, paint, cx),
             Event::MouseUp(MouseEvent {
                 button: MouseButton::Left,
                 position,

crates/editor/src/mouse_context_menu.rs 🔗

@@ -0,0 +1,61 @@
+use context_menu::{ContextMenu, ContextMenuItem};
+use gpui::{
+    geometry::vector::Vector2F, impl_internal_actions, MutableAppContext, Task, ViewContext,
+    ViewHandle,
+};
+
+use crate::{
+    DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, Rename, SelectMode,
+};
+
+#[derive(Clone, PartialEq)]
+pub struct DeployMouseContextMenu {
+    pub position: Vector2F,
+    pub point: DisplayPoint,
+}
+
+impl_internal_actions!(editor, [DeployMouseContextMenu]);
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(deploy_context_menu);
+}
+
+pub struct MouseContextMenuState {
+    pub context_menu: ViewHandle<ContextMenu>,
+    pub task: Option<Task<()>>,
+}
+
+pub fn deploy_context_menu(
+    editor: &mut Editor,
+    &DeployMouseContextMenu { position, point }: &DeployMouseContextMenu,
+    cx: &mut ViewContext<Editor>,
+) {
+    // Don't show context menu for inline editors
+    if editor.mode() != EditorMode::Full {
+        return;
+    }
+
+    // Don't show the context menu if there isn't a project associated with this editor
+    if editor.project.is_none() {
+        return;
+    }
+
+    // Move the cursor to the clicked location so that dispatched actions make sense
+    editor.change_selections(None, cx, |s| {
+        s.clear_disjoint();
+        s.set_pending_display_range(point..point, SelectMode::Character);
+    });
+
+    editor.mouse_context_menu.update(cx, |menu, cx| {
+        menu.show(
+            position,
+            vec![
+                ContextMenuItem::item("Rename Symbol", Rename),
+                ContextMenuItem::item("Go To Definition", GoToDefinition),
+                ContextMenuItem::item("Find All References", FindAllReferences),
+            ],
+            cx,
+        );
+    });
+    cx.notify();
+}

crates/editor/src/selections_collection.rs 🔗

@@ -384,7 +384,7 @@ impl<'a> MutableSelectionsCollection<'a> {
         }
     }
 
-    pub fn set_pending_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
+    pub fn set_pending_anchor_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
         self.collection.pending = Some(PendingSelection {
             selection: Selection {
                 id: post_inc(&mut self.collection.next_selection_id),
@@ -398,6 +398,42 @@ impl<'a> MutableSelectionsCollection<'a> {
         self.selections_changed = true;
     }
 
+    pub fn set_pending_display_range(&mut self, range: Range<DisplayPoint>, mode: SelectMode) {
+        let (start, end, reversed) = {
+            let display_map = self.display_map();
+            let buffer = self.buffer();
+            let mut start = range.start;
+            let mut end = range.end;
+            let reversed = if start > end {
+                mem::swap(&mut start, &mut end);
+                true
+            } else {
+                false
+            };
+
+            let end_bias = if end > start { Bias::Left } else { Bias::Right };
+            (
+                buffer.anchor_before(start.to_point(&display_map)),
+                buffer.anchor_at(end.to_point(&display_map), end_bias),
+                reversed,
+            )
+        };
+
+        let new_pending = PendingSelection {
+            selection: Selection {
+                id: post_inc(&mut self.collection.next_selection_id),
+                start,
+                end,
+                reversed,
+                goal: SelectionGoal::None,
+            },
+            mode,
+        };
+
+        self.collection.pending = Some(new_pending);
+        self.selections_changed = true;
+    }
+
     pub fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
         self.collection.pending = Some(PendingSelection { selection, mode });
         self.selections_changed = true;