Render code actions indicator

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/editor/src/editor.rs         | 30 ++++++++++++++-
crates/editor/src/element.rs        | 58 +++++++++++++++++++++---------
crates/server/src/rpc.rs            |  2 
crates/theme/src/theme.rs           |  2 +
crates/zed/assets/icons/zap.svg     |  3 +
crates/zed/assets/themes/_base.toml |  1 
6 files changed, 74 insertions(+), 22 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -2177,7 +2177,21 @@ impl Editor {
         }))
     }
 
-    pub fn showing_context_menu(&self) -> bool {
+    pub fn render_code_actions_indicator(&self, cx: &AppContext) -> Option<ElementBox> {
+        if self.available_code_actions.is_some() {
+            let style = (self.build_settings)(cx).style;
+            Some(
+                Svg::new("icons/zap.svg")
+                    .with_color(style.code_actions_indicator)
+                    .aligned()
+                    .boxed(),
+            )
+        } else {
+            None
+        }
+    }
+
+    pub fn context_menu_visible(&self) -> bool {
         self.context_menu
             .as_ref()
             .map_or(false, |menu| menu.visible())
@@ -4341,7 +4355,10 @@ impl Editor {
             });
         }
 
-        let buffer = self.buffer.read(cx).snapshot(cx);
+        let display_map = self
+            .display_map
+            .update(cx, |display_map, cx| display_map.snapshot(cx));
+        let buffer = &display_map.buffer_snapshot;
         self.pending_selection = None;
         self.add_selections_state = None;
         self.select_next_state = None;
@@ -4357,7 +4374,7 @@ impl Editor {
             .unwrap();
 
         self.push_to_nav_history(
-            old_cursor_position,
+            old_cursor_position.clone(),
             Some(new_cursor_position.to_point(&buffer)),
             cx,
         );
@@ -4386,6 +4403,12 @@ impl Editor {
         }
 
         if let Some(project) = self.project.as_ref() {
+            if old_cursor_position.to_display_point(&display_map).row()
+                != new_cursor_position.to_display_point(&display_map).row()
+            {
+                self.available_code_actions.take();
+            }
+
             let (buffer, head) = self
                 .buffer
                 .read(cx)
@@ -4894,6 +4917,7 @@ impl EditorSettings {
                     hint_diagnostic: default_diagnostic_style.clone(),
                     invalid_hint_diagnostic: default_diagnostic_style.clone(),
                     autocomplete: Default::default(),
+                    code_actions_indicator: Default::default(),
                 }
             },
         }

crates/editor/src/element.rs 🔗

@@ -281,7 +281,7 @@ impl EditorElement {
         &mut self,
         bounds: RectF,
         visible_bounds: RectF,
-        layout: &LayoutState,
+        layout: &mut LayoutState,
         cx: &mut PaintContext,
     ) {
         let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height;
@@ -295,6 +295,14 @@ impl EditorElement {
                 line.paint(line_origin, visible_bounds, layout.line_height, cx);
             }
         }
+
+        if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
+            let mut x = bounds.width() - layout.gutter_padding;
+            let mut y = *row as f32 * layout.line_height - scroll_top;
+            x += ((layout.gutter_padding + layout.text_offset.x()) - indicator.size().x()) / 2.;
+            y += (layout.line_height - indicator.size().y()) / 2.;
+            indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
+        }
     }
 
     fn paint_text(
@@ -393,20 +401,20 @@ impl EditorElement {
         }
         cx.scene.pop_layer();
 
-        if let Some((position, completions_list)) = layout.completions.as_mut() {
+        if let Some((position, context_menu)) = layout.context_menu.as_mut() {
             cx.scene.push_stacking_context(None);
 
             let cursor_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
             let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
             let y = (position.row() + 1) as f32 * layout.line_height - scroll_top;
             let mut list_origin = content_origin + vec2f(x, y);
-            let list_height = completions_list.size().y();
+            let list_height = context_menu.size().y();
 
             if list_origin.y() + list_height > bounds.lower_left().y() {
                 list_origin.set_y(list_origin.y() - layout.line_height - list_height);
             }
 
-            completions_list.paint(
+            context_menu.paint(
                 list_origin,
                 RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
                 cx,
@@ -918,7 +926,8 @@ impl Element for EditorElement {
             max_row.saturating_sub(1) as f32,
         );
 
-        let mut completions = None;
+        let mut context_menu = None;
+        let mut code_actions_indicator = None;
         self.update_view(cx.app, |view, cx| {
             let clamped = view.clamp_scroll_left(scroll_max.x());
             let autoscrolled;
@@ -939,21 +948,25 @@ impl Element for EditorElement {
                 snapshot = view.snapshot(cx);
             }
 
-            if view.showing_context_menu() {
-                let newest_selection_head = view
-                    .newest_selection::<usize>(&snapshot.buffer_snapshot)
-                    .head()
-                    .to_display_point(&snapshot);
+            let newest_selection_head = view
+                .newest_selection::<usize>(&snapshot.buffer_snapshot)
+                .head()
+                .to_display_point(&snapshot);
 
-                if (start_row..end_row).contains(&newest_selection_head.row()) {
+            if (start_row..end_row).contains(&newest_selection_head.row()) {
+                if view.context_menu_visible() {
                     let list = view.render_context_menu(cx).unwrap();
-                    completions = Some((newest_selection_head, list));
+                    context_menu = Some((newest_selection_head, list));
                 }
+
+                code_actions_indicator = view
+                    .render_code_actions_indicator(cx)
+                    .map(|indicator| (newest_selection_head.row(), indicator));
             }
         });
 
-        if let Some((_, completions_list)) = completions.as_mut() {
-            completions_list.layout(
+        if let Some((_, context_menu)) = context_menu.as_mut() {
+            context_menu.layout(
                 SizeConstraint {
                     min: Vector2F::zero(),
                     max: vec2f(
@@ -965,6 +978,13 @@ impl Element for EditorElement {
             );
         }
 
+        if let Some((_, indicator)) = code_actions_indicator.as_mut() {
+            indicator.layout(
+                SizeConstraint::strict_along(Axis::Vertical, line_height * 0.618),
+                cx,
+            );
+        }
+
         let blocks = self.layout_blocks(
             start_row..end_row,
             &snapshot,
@@ -999,7 +1019,8 @@ impl Element for EditorElement {
                 em_width,
                 em_advance,
                 selections,
-                completions,
+                context_menu,
+                code_actions_indicator,
             },
         )
     }
@@ -1048,8 +1069,8 @@ impl Element for EditorElement {
         paint: &mut PaintState,
         cx: &mut EventContext,
     ) -> bool {
-        if let Some((_, completion_list)) = &mut layout.completions {
-            if completion_list.dispatch_event(event, cx) {
+        if let Some((_, context_menu)) = &mut layout.context_menu {
+            if context_menu.dispatch_event(event, cx) {
                 return true;
             }
         }
@@ -1110,7 +1131,8 @@ pub struct LayoutState {
     highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
     selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
     text_offset: Vector2F,
-    completions: Option<(DisplayPoint, ElementBox)>,
+    context_menu: Option<(DisplayPoint, ElementBox)>,
+    code_actions_indicator: Option<(u32, ElementBox)>,
 }
 
 fn layout_line(

crates/server/src/rpc.rs 🔗

@@ -2475,7 +2475,7 @@ mod tests {
         // Confirm a completion on the guest.
         editor_b.next_notification(&cx_b).await;
         editor_b.update(&mut cx_b, |editor, cx| {
-            assert!(editor.showing_context_menu());
+            assert!(editor.context_menu_visible());
             editor.confirm_completion(&ConfirmCompletion(Some(0)), cx);
             assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
         });

crates/theme/src/theme.rs 🔗

@@ -293,6 +293,7 @@ pub struct EditorStyle {
     pub hint_diagnostic: DiagnosticStyle,
     pub invalid_hint_diagnostic: DiagnosticStyle,
     pub autocomplete: AutocompleteStyle,
+    pub code_actions_indicator: Color,
 }
 
 #[derive(Clone, Deserialize, Default)]
@@ -420,6 +421,7 @@ impl InputEditorStyle {
             hint_diagnostic: default_diagnostic_style.clone(),
             invalid_hint_diagnostic: default_diagnostic_style.clone(),
             autocomplete: Default::default(),
+            code_actions_indicator: Default::default(),
         }
     }
 }

crates/zed/assets/icons/zap.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.00262 12L2.89358 7.9886C2.95207 7.71862 2.77658 7.49963 2.5021 7.49963H0.000671387L6.00037 0L5.10792 4.0108C5.04792 4.27929 5.22341 4.49828 5.4994 4.49828H7.99932L1.99962 11.9979L2.00262 12Z" fill="#FDE047"/>
+</svg>

crates/zed/assets/themes/_base.toml 🔗

@@ -253,6 +253,7 @@ line_number_active = "$text.0.color"
 selection = "$selection.host"
 guest_selections = "$selection.guests"
 error_color = "$status.bad"
+code_actions_indicator = "$text.3.color"
 
 [editor.diagnostic_path_header]
 background = "$state.active_line"