Render code actions indicator

Antonio Scandurra and Nathan created

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

Change summary

Cargo.lock                               |   1 
crates/editor2/Cargo.toml                |   1 
crates/editor2/src/editor.rs             | 137 +++++++++++--------------
crates/editor2/src/element.rs            |  89 ++++++++--------
crates/gpui2/src/element.rs              |  93 +++++++++++++++-
crates/gpui2/src/taffy.rs                |  31 +++++
crates/gpui2/src/window.rs               |   2 
crates/ui2/src/components/icon_button.rs |   1 
8 files changed, 227 insertions(+), 128 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2781,6 +2781,7 @@ dependencies = [
  "tree-sitter-html",
  "tree-sitter-rust",
  "tree-sitter-typescript",
+ "ui2",
  "unindent",
  "util",
  "workspace2",

crates/editor2/Cargo.toml 🔗

@@ -44,6 +44,7 @@ snippet = { path = "../snippet" }
 sum_tree = { path = "../sum_tree" }
 text = { package="text2", path = "../text2" }
 theme = { package="theme2", path = "../theme2" }
+ui2 = { package = "ui2", path = "../ui2" }
 util = { path = "../util" }
 sqlez = { path = "../sqlez" }
 workspace = { package = "workspace2", path = "../workspace2" }

crates/editor2/src/editor.rs 🔗

@@ -40,9 +40,9 @@ use fuzzy::{StringMatch, StringMatchCandidate};
 use git::diff_hunk_to_display;
 use gpui::{
     action, actions, point, px, relative, rems, size, AnyElement, AppContext, BackgroundExecutor,
-    Bounds, ClipboardItem, Context, DispatchContext, EventEmitter, FocusHandle, FontFeatures,
-    FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, Model, Pixels, Render, Subscription,
-    Task, TextStyle, View, ViewContext, VisualContext, WeakView, WindowContext,
+    Bounds, ClipboardItem, Component, Context, DispatchContext, EventEmitter, FocusHandle,
+    FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, Model, Pixels, Render,
+    Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
@@ -95,6 +95,7 @@ use text::{OffsetUtf16, Rope};
 use theme::{
     ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
 };
+use ui2::IconButton;
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::{
     item::ItemEvent, searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace,
@@ -3846,44 +3847,44 @@ impl Editor {
     //         }))
     //     }
 
-    //     pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
-    //         let mut context_menu = self.context_menu.write();
-    //         if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) {
-    //             *context_menu = None;
-    //             cx.notify();
-    //             return;
-    //         }
-    //         drop(context_menu);
-
-    //         let deployed_from_indicator = action.deployed_from_indicator;
-    //         let mut task = self.code_actions_task.take();
-    //         cx.spawn(|this, mut cx| async move {
-    //             while let Some(prev_task) = task {
-    //                 prev_task.await;
-    //                 task = this.update(&mut cx, |this, _| this.code_actions_task.take())?;
-    //             }
+    pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
+        let mut context_menu = self.context_menu.write();
+        if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) {
+            *context_menu = None;
+            cx.notify();
+            return;
+        }
+        drop(context_menu);
 
-    //             this.update(&mut cx, |this, cx| {
-    //                 if this.focused {
-    //                     if let Some((buffer, actions)) = this.available_code_actions.clone() {
-    //                         this.completion_tasks.clear();
-    //                         this.discard_copilot_suggestion(cx);
-    //                         *this.context_menu.write() =
-    //                             Some(ContextMenu::CodeActions(CodeActionsMenu {
-    //                                 buffer,
-    //                                 actions,
-    //                                 selected_item: Default::default(),
-    //                                 list: Default::default(),
-    //                                 deployed_from_indicator,
-    //                             }));
-    //                     }
-    //                 }
-    //             })?;
+        let deployed_from_indicator = action.deployed_from_indicator;
+        let mut task = self.code_actions_task.take();
+        cx.spawn(|this, mut cx| async move {
+            while let Some(prev_task) = task {
+                prev_task.await;
+                task = this.update(&mut cx, |this, _| this.code_actions_task.take())?;
+            }
 
-    //             Ok::<_, anyhow::Error>(())
-    //         })
-    //         .detach_and_log_err(cx);
-    //     }
+            this.update(&mut cx, |this, cx| {
+                if this.focus_handle.is_focused(cx) {
+                    if let Some((buffer, actions)) = this.available_code_actions.clone() {
+                        this.completion_tasks.clear();
+                        this.discard_copilot_suggestion(cx);
+                        *this.context_menu.write() =
+                            Some(ContextMenu::CodeActions(CodeActionsMenu {
+                                buffer,
+                                actions,
+                                selected_item: Default::default(),
+                                list: Default::default(),
+                                deployed_from_indicator,
+                            }));
+                    }
+                }
+            })?;
+
+            Ok::<_, anyhow::Error>(())
+        })
+        .detach_and_log_err(cx);
+    }
 
     //     pub fn confirm_code_action(
     //         workspace: &mut Workspace,
@@ -4390,41 +4391,29 @@ impl Editor {
         self.discard_copilot_suggestion(cx);
     }
 
-    //     pub fn render_code_actions_indicator(
-    //         &self,
-    //         style: &EditorStyle,
-    //         is_active: bool,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<AnyElement<Self>> {
-    //         if self.available_code_actions.is_some() {
-    //             enum CodeActions {}
-    //             Some(
-    //                 MouseEventHandler::new::<CodeActions, _>(0, cx, |state, _| {
-    //                     Svg::new("icons/bolt.svg").with_color(
-    //                         style
-    //                             .code_actions
-    //                             .indicator
-    //                             .in_state(is_active)
-    //                             .style_for(state)
-    //                             .color,
-    //                     )
-    //                 })
-    //                 .with_cursor_style(CursorStyle::PointingHand)
-    //                 .with_padding(Padding::uniform(3.))
-    //                 .on_down(MouseButton::Left, |_, this, cx| {
-    //                     this.toggle_code_actions(
-    //                         &ToggleCodeActions {
-    //                             deployed_from_indicator: true,
-    //                         },
-    //                         cx,
-    //                     );
-    //                 })
-    //                 .into_any(),
-    //             )
-    //         } else {
-    //             None
-    //         }
-    //     }
+    pub fn render_code_actions_indicator(
+        &self,
+        style: &EditorStyle,
+        is_active: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<AnyElement<Self>> {
+        if self.available_code_actions.is_some() {
+            Some(
+                IconButton::new("code_actions", ui2::Icon::Bolt)
+                    .on_click(|editor: &mut Editor, cx| {
+                        editor.toggle_code_actions(
+                            &ToggleCodeActions {
+                                deployed_from_indicator: true,
+                            },
+                            cx,
+                        );
+                    })
+                    .render(),
+            )
+        } else {
+            None
+        }
+    }
 
     //     pub fn render_fold_indicators(
     //         &self,

crates/editor2/src/element.rs 🔗

@@ -15,7 +15,7 @@ use crate::{
 use anyhow::Result;
 use collections::{BTreeMap, HashMap};
 use gpui::{
-    black, hsla, point, px, relative, size, transparent_black, Action, AnyElement,
+    black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
     BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchContext, DispatchPhase,
     Edges, Element, ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla,
     InputHandler, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, MouseButton,
@@ -447,7 +447,7 @@ impl EditorElement {
     fn paint_gutter(
         &mut self,
         bounds: Bounds<Pixels>,
-        layout: &LayoutState,
+        layout: &mut LayoutState,
         editor: &mut Editor,
         cx: &mut ViewContext<Editor>,
     ) {
@@ -495,14 +495,21 @@ impl EditorElement {
         //     }
         // }
 
-        // todo!("code actions indicator")
-        // if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
-        //     let mut x = 0.;
-        //     let mut y = *row as f32 * line_height - scroll_top;
-        //     x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x) / 2.;
-        //     y += (line_height - indicator.size().y) / 2.;
-        //     indicator.paint(bounds.origin + point(x, y), visible_bounds, editor, cx);
-        // }
+        if let Some(indicator) = layout.code_actions_indicator.as_mut() {
+            let available_space = size(
+                AvailableSpace::MinContent,
+                AvailableSpace::Definite(line_height),
+            );
+            let indicator_size = indicator.element.measure(available_space, editor, cx);
+            let mut x = Pixels::ZERO;
+            let mut y = indicator.row as f32 * line_height - scroll_top;
+            // Center indicator.
+            x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.;
+            y += (line_height - indicator_size.height) / 2.;
+            indicator
+                .element
+                .draw(bounds.origin + point(x, y), available_space, editor, cx);
+        }
     }
 
     fn paint_diff_hunks(
@@ -1776,24 +1783,27 @@ impl EditorElement {
 
         // todo!("context menu")
         // let mut context_menu = None;
-        // let mut code_actions_indicator = None;
-        // if let Some(newest_selection_head) = newest_selection_head {
-        //     if (start_row..end_row).contains(&newest_selection_head.row()) {
-        //         if editor.context_menu_visible() {
-        //             context_menu =
-        //                 editor.render_context_menu(newest_selection_head, style.clone(), cx);
-        //         }
-
-        //         let active = matches!(
-        //             editor.context_menu.read().as_ref(),
-        //             Some(crate::ContextMenu::CodeActions(_))
-        //         );
+        let mut code_actions_indicator = None;
+        if let Some(newest_selection_head) = newest_selection_head {
+            if (start_row..end_row).contains(&newest_selection_head.row()) {
+                //         if editor.context_menu_visible() {
+                //             context_menu =
+                //                 editor.render_context_menu(newest_selection_head, style.clone(), cx);
+                //         }
+
+                let active = matches!(
+                    editor.context_menu.read().as_ref(),
+                    Some(crate::ContextMenu::CodeActions(_))
+                );
 
-        //         code_actions_indicator = editor
-        //             .render_code_actions_indicator(&style, active, cx)
-        //             .map(|indicator| (newest_selection_head.row(), indicator));
-        //     }
-        // }
+                code_actions_indicator = editor
+                    .render_code_actions_indicator(&style, active, cx)
+                    .map(|element| CodeActionsIndicator {
+                        row: newest_selection_head.row(),
+                        element,
+                    });
+            }
+        }
 
         let visible_rows = start_row..start_row + line_layouts.len() as u32;
         // todo!("hover")
@@ -1831,18 +1841,6 @@ impl EditorElement {
         //     );
         // }
 
-        // todo!("code actions")
-        // if let Some((_, indicator)) = code_actions_indicator.as_mut() {
-        //     indicator.layout(
-        //         SizeConstraint::strict_along(
-        //             Axis::Vertical,
-        //             line_height * style.code_actions.vertical_scale,
-        //         ),
-        //         editor,
-        //         cx,
-        //     );
-        // }
-
         // todo!("fold indicators")
         // for fold_indicator in fold_indicators.iter_mut() {
         //     if let Some(indicator) = fold_indicator.as_mut() {
@@ -1942,7 +1940,7 @@ impl EditorElement {
             // blocks,
             selections,
             // context_menu,
-            // code_actions_indicator,
+            code_actions_indicator,
             // fold_indicators,
             tab_invisible,
             space_invisible,
@@ -2493,7 +2491,7 @@ impl Element<Editor> for EditorElement {
         element_state: &mut Self::ElementState,
         cx: &mut gpui::ViewContext<Editor>,
     ) {
-        let layout = self.compute_layout(editor, cx, bounds);
+        let mut layout = self.compute_layout(editor, cx, bounds);
         let gutter_bounds = Bounds {
             origin: bounds.origin,
             size: layout.gutter_size,
@@ -2513,7 +2511,7 @@ impl Element<Editor> for EditorElement {
             );
             self.paint_background(gutter_bounds, text_bounds, &layout, cx);
             if layout.gutter_size.width > Pixels::ZERO {
-                self.paint_gutter(gutter_bounds, &layout, editor, cx);
+                self.paint_gutter(gutter_bounds, &mut layout, editor, cx);
             }
             self.paint_text(text_bounds, &layout, editor, cx);
             let input_handler = ElementInputHandler::new(bounds, cx);
@@ -3144,13 +3142,18 @@ pub struct LayoutState {
     is_singleton: bool,
     max_row: u32,
     // context_menu: Option<(DisplayPoint, AnyElement<Editor>)>,
-    // code_actions_indicator: Option<(u32, AnyElement<Editor>)>,
+    code_actions_indicator: Option<CodeActionsIndicator>,
     // hover_popovers: Option<(DisplayPoint, Vec<AnyElement<Editor>>)>,
     // fold_indicators: Vec<Option<AnyElement<Editor>>>,
     tab_invisible: Line,
     space_invisible: Line,
 }
 
+struct CodeActionsIndicator {
+    row: u32,
+    element: AnyElement<Editor>,
+}
+
 struct PositionMap {
     size: Size<Pixels>,
     line_height: Pixels,

crates/gpui2/src/element.rs 🔗

@@ -63,6 +63,19 @@ trait ElementObject<V> {
     fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
     fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId;
     fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
+    fn measure(
+        &mut self,
+        available_space: Size<AvailableSpace>,
+        view_state: &mut V,
+        cx: &mut ViewContext<V>,
+    ) -> Size<Pixels>;
+    fn draw(
+        &mut self,
+        origin: Point<Pixels>,
+        available_space: Size<AvailableSpace>,
+        view_state: &mut V,
+        cx: &mut ViewContext<V>,
+    );
 }
 
 struct RenderedElement<V: 'static, E: Element<V>> {
@@ -81,6 +94,11 @@ enum ElementRenderPhase<V> {
         layout_id: LayoutId,
         frame_state: Option<V>,
     },
+    LayoutComputed {
+        layout_id: LayoutId,
+        available_space: Size<AvailableSpace>,
+        frame_state: Option<V>,
+    },
     Painted,
 }
 
@@ -137,7 +155,9 @@ where
                 }
             }
             ElementRenderPhase::Start => panic!("must call initialize before layout"),
-            ElementRenderPhase::LayoutRequested { .. } | ElementRenderPhase::Painted => {
+            ElementRenderPhase::LayoutRequested { .. }
+            | ElementRenderPhase::LayoutComputed { .. }
+            | ElementRenderPhase::Painted => {
                 panic!("element rendered twice")
             }
         };
@@ -154,6 +174,11 @@ where
             ElementRenderPhase::LayoutRequested {
                 layout_id,
                 mut frame_state,
+            }
+            | ElementRenderPhase::LayoutComputed {
+                layout_id,
+                mut frame_state,
+                ..
             } => {
                 let bounds = cx.layout_bounds(layout_id);
                 if let Some(id) = self.element.id() {
@@ -173,6 +198,62 @@ where
             _ => panic!("must call layout before paint"),
         };
     }
+
+    fn measure(
+        &mut self,
+        available_space: Size<AvailableSpace>,
+        view_state: &mut V,
+        cx: &mut ViewContext<V>,
+    ) -> Size<Pixels> {
+        if matches!(&self.phase, ElementRenderPhase::Start) {
+            self.initialize(view_state, cx);
+        }
+
+        if matches!(&self.phase, ElementRenderPhase::Initialized { .. }) {
+            self.layout(view_state, cx);
+        }
+
+        let layout_id = match &mut self.phase {
+            ElementRenderPhase::LayoutRequested {
+                layout_id,
+                frame_state,
+            } => {
+                cx.compute_layout(*layout_id, available_space);
+                let layout_id = *layout_id;
+                self.phase = ElementRenderPhase::LayoutComputed {
+                    layout_id,
+                    available_space,
+                    frame_state: frame_state.take(),
+                };
+                layout_id
+            }
+            ElementRenderPhase::LayoutComputed {
+                layout_id,
+                available_space: prev_available_space,
+                ..
+            } => {
+                if available_space != *prev_available_space {
+                    cx.compute_layout(*layout_id, available_space);
+                    *prev_available_space = available_space;
+                }
+                *layout_id
+            }
+            _ => panic!("cannot measure after painting"),
+        };
+
+        cx.layout_bounds(layout_id).size
+    }
+
+    fn draw(
+        &mut self,
+        origin: Point<Pixels>,
+        available_space: Size<AvailableSpace>,
+        view_state: &mut V,
+        cx: &mut ViewContext<V>,
+    ) {
+        self.measure(available_space, view_state, cx);
+        cx.with_element_offset(Some(origin), |cx| self.paint(view_state, cx))
+    }
 }
 
 pub struct AnyElement<V>(Box<dyn ElementObject<V>>);
@@ -206,10 +287,7 @@ impl<V> AnyElement<V> {
         view_state: &mut V,
         cx: &mut ViewContext<V>,
     ) -> Size<Pixels> {
-        self.initialize(view_state, cx);
-        let layout_id = self.layout(view_state, cx);
-        cx.compute_layout(layout_id, available_space);
-        cx.layout_bounds(layout_id).size
+        self.0.measure(available_space, view_state, cx)
     }
 
     /// Initializes this element and performs layout in the available space, then paints it at the given origin.
@@ -220,10 +298,7 @@ impl<V> AnyElement<V> {
         view_state: &mut V,
         cx: &mut ViewContext<V>,
     ) {
-        self.initialize(view_state, cx);
-        let layout_id = self.layout(view_state, cx);
-        cx.compute_layout(layout_id, available_space);
-        cx.with_element_offset(Some(origin), |cx| self.paint(view_state, cx))
+        self.0.draw(origin, available_space, view_state, cx)
     }
 }
 

crates/gpui2/src/taffy.rs 🔗

@@ -1,5 +1,6 @@
 use super::{AbsoluteLength, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Size, Style};
-use collections::HashMap;
+use collections::{HashMap, HashSet};
+use smallvec::SmallVec;
 use std::fmt::Debug;
 use taffy::{
     geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize},
@@ -12,6 +13,7 @@ pub struct TaffyLayoutEngine {
     taffy: Taffy,
     children_to_parents: HashMap<LayoutId, LayoutId>,
     absolute_layout_bounds: HashMap<LayoutId, Bounds<Pixels>>,
+    computed_layouts: HashSet<LayoutId>,
 }
 
 static EXPECT_MESSAGE: &'static str =
@@ -23,9 +25,17 @@ impl TaffyLayoutEngine {
             taffy: Taffy::new(),
             children_to_parents: HashMap::default(),
             absolute_layout_bounds: HashMap::default(),
+            computed_layouts: HashSet::default(),
         }
     }
 
+    pub fn clear(&mut self) {
+        self.taffy.clear();
+        self.children_to_parents.clear();
+        self.absolute_layout_bounds.clear();
+        self.computed_layouts.clear();
+    }
+
     pub fn request_layout(
         &mut self,
         style: &Style,
@@ -115,6 +125,7 @@ impl TaffyLayoutEngine {
     }
 
     pub fn compute_layout(&mut self, id: LayoutId, available_space: Size<AvailableSpace>) {
+        // Leaving this here until we have a better instrumentation approach.
         // println!("Laying out {} children", self.count_all_children(id)?);
         // println!("Max layout depth: {}", self.max_depth(0, id)?);
 
@@ -124,6 +135,22 @@ impl TaffyLayoutEngine {
         //     println!("N{} --> N{}", u64::from(a), u64::from(b));
         // }
         // println!("");
+        //
+
+        if !self.computed_layouts.insert(id) {
+            let mut stack = SmallVec::<[LayoutId; 64]>::new();
+            stack.push(id);
+            while let Some(id) = stack.pop() {
+                self.absolute_layout_bounds.remove(&id);
+                stack.extend(
+                    self.taffy
+                        .children(id.into())
+                        .expect(EXPECT_MESSAGE)
+                        .into_iter()
+                        .map(Into::into),
+                );
+            }
+        }
 
         // let started_at = std::time::Instant::now();
         self.taffy
@@ -397,7 +424,7 @@ where
     }
 }
 
-#[derive(Copy, Clone, Default, Debug)]
+#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)]
 pub enum AvailableSpace {
     /// The amount of space available is the specified number of pixels
     Definite(Pixels),

crates/gpui2/src/window.rs 🔗

@@ -1060,6 +1060,8 @@ impl<'a> WindowContext<'a> {
         self.text_system().start_frame();
 
         let window = &mut *self.window;
+        window.layout_engine.clear();
+
         mem::swap(&mut window.previous_frame, &mut window.current_frame);
         let frame = &mut window.current_frame;
         frame.element_states.clear();

crates/ui2/src/components/icon_button.rs 🔗

@@ -98,6 +98,7 @@ impl<V: 'static> IconButton<V> {
 
         if let Some(click_handler) = self.handlers.click.clone() {
             button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
+                cx.stop_propagation();
                 click_handler(state, cx);
             });
         }