Wire up code actions in `editor2` (#3302)

Antonio Scandurra created

Release Notes:

- N/A

Change summary

Cargo.lock                                |   1 
crates/editor2/Cargo.toml                 |   1 
crates/editor2/src/editor.rs              | 643 +++++++++++-------------
crates/editor2/src/element.rs             | 207 ++++---
crates/gpui2/src/element.rs               | 111 ++++
crates/gpui2/src/elements/text.rs         |   7 
crates/gpui2/src/elements/uniform_list.rs |  61 +-
crates/gpui2/src/geometry.rs              |   4 
crates/gpui2/src/taffy.rs                 |  31 +
crates/gpui2/src/window.rs                |  24 
crates/ui2/src/components/icon_button.rs  |   1 
11 files changed, 599 insertions(+), 492 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2805,6 +2805,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" }
+ui = { package = "ui2", path = "../ui2" }
 util = { path = "../util" }
 sqlez = { path = "../sqlez" }
 workspace = { package = "workspace2", path = "../workspace2" }

crates/editor2/src/editor.rs 🔗

@@ -39,10 +39,12 @@ use futures::FutureExt;
 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,
+    action, actions, div, point, px, relative, rems, size, uniform_list, AnyElement, AppContext,
+    AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
+    DispatchContext, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight,
+    HighlightStyle, Hsla, InputHandler, Model, MouseButton, ParentElement, Pixels, Render,
+    StatelessInteractive, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View,
+    ViewContext, VisualContext, WeakView, WindowContext,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
@@ -67,7 +69,7 @@ pub use multi_buffer::{
 };
 use ordered_float::OrderedFloat;
 use parking_lot::{Mutex, RwLock};
-use project::{FormatTrigger, Location, Project};
+use project::{FormatTrigger, Location, Project, ProjectTransaction};
 use rand::prelude::*;
 use rpc::proto::*;
 use scroll::{
@@ -95,6 +97,7 @@ use text::{OffsetUtf16, Rope};
 use theme::{
     ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
 };
+use ui::IconButton;
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::{
     item::ItemEvent, searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace,
@@ -384,26 +387,6 @@ actions!(
     UnfoldLines,
 );
 
-// impl_actions!(
-//     editor,
-//     [
-//         SelectNext,
-//         SelectPrevious,
-//         SelectAllMatches,
-//         SelectToBeginningOfLine,
-//         SelectToEndOfLine,
-//         ToggleCodeActions,
-//         MovePageUp,
-//         MovePageDown,
-//         ConfirmCompletion,
-//         ConfirmCodeAction,
-//         ToggleComments,
-//         FoldAt,
-//         UnfoldAt,
-//         GutterHover
-//     ]
-// );
-
 enum DocumentHighlightRead {}
 enum DocumentHighlightWrite {}
 enum InputComposition {}
@@ -919,15 +902,14 @@ impl ContextMenu {
     fn render(
         &self,
         cursor_position: DisplayPoint,
-        style: EditorStyle,
+        style: &EditorStyle,
         workspace: Option<WeakView<Workspace>>,
         cx: &mut ViewContext<Editor>,
     ) -> (DisplayPoint, AnyElement<Editor>) {
-        todo!()
-        // match self {
-        //     ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)),
-        //     ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx),
-        // }
+        match self {
+            ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)),
+            ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx),
+        }
     }
 }
 
@@ -940,29 +922,13 @@ struct CompletionsMenu {
     match_candidates: Arc<[StringMatchCandidate]>,
     matches: Arc<[StringMatch]>,
     selected_item: usize,
-    list: UniformListState,
-}
-
-// todo!(this is fake)
-#[derive(Clone, Default)]
-struct UniformListState;
-
-// todo!(this is fake)
-impl UniformListState {
-    pub fn scroll_to(&mut self, target: ScrollTarget) {}
-}
-
-// todo!(this is somewhat fake)
-#[derive(Debug)]
-pub enum ScrollTarget {
-    Show(usize),
-    Center(usize),
+    scroll_handle: UniformListScrollHandle,
 }
 
 impl CompletionsMenu {
     fn select_first(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
         self.selected_item = 0;
-        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.scroll_handle.scroll_to_item(self.selected_item);
         self.attempt_resolve_selected_completion_documentation(project, cx);
         cx.notify();
     }
@@ -973,7 +939,7 @@ impl CompletionsMenu {
         } else {
             self.selected_item = self.matches.len() - 1;
         }
-        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.scroll_handle.scroll_to_item(self.selected_item);
         self.attempt_resolve_selected_completion_documentation(project, cx);
         cx.notify();
     }
@@ -984,14 +950,14 @@ impl CompletionsMenu {
         } else {
             self.selected_item = 0;
         }
-        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.scroll_handle.scroll_to_item(self.selected_item);
         self.attempt_resolve_selected_completion_documentation(project, cx);
         cx.notify();
     }
 
     fn select_last(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
         self.selected_item = self.matches.len() - 1;
-        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.scroll_handle.scroll_to_item(self.selected_item);
         self.attempt_resolve_selected_completion_documentation(project, cx);
         cx.notify();
     }
@@ -1252,13 +1218,13 @@ impl CompletionsMenu {
 
     fn render(
         &self,
-        style: EditorStyle,
+        style: &EditorStyle,
         workspace: Option<WeakView<Workspace>>,
         cx: &mut ViewContext<Editor>,
-    ) {
+    ) -> AnyElement<Editor> {
         todo!("old implementation below")
     }
-    // ) -> AnyElement<Editor> {
+
     //     enum CompletionTag {}
 
     //     let settings = EditorSettings>(cx);
@@ -1527,14 +1493,14 @@ struct CodeActionsMenu {
     actions: Arc<[CodeAction]>,
     buffer: Model<Buffer>,
     selected_item: usize,
-    list: UniformListState,
+    scroll_handle: UniformListScrollHandle,
     deployed_from_indicator: bool,
 }
 
 impl CodeActionsMenu {
     fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
         self.selected_item = 0;
-        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.scroll_handle.scroll_to_item(self.selected_item);
         cx.notify()
     }
 
@@ -1544,7 +1510,7 @@ impl CodeActionsMenu {
         } else {
             self.selected_item = self.actions.len() - 1;
         }
-        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.scroll_handle.scroll_to_item(self.selected_item);
         cx.notify();
     }
 
@@ -1554,13 +1520,13 @@ impl CodeActionsMenu {
         } else {
             self.selected_item = 0;
         }
-        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.scroll_handle.scroll_to_item(self.selected_item);
         cx.notify();
     }
 
     fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
         self.selected_item = self.actions.len() - 1;
-        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.scroll_handle.scroll_to_item(self.selected_item);
         cx.notify()
     }
 
@@ -1571,83 +1537,69 @@ impl CodeActionsMenu {
     fn render(
         &self,
         mut cursor_position: DisplayPoint,
-        style: EditorStyle,
+        style: &EditorStyle,
         cx: &mut ViewContext<Editor>,
     ) -> (DisplayPoint, AnyElement<Editor>) {
-        todo!("old version below")
-    }
-    //     enum ActionTag {}
+        let actions = self.actions.clone();
+        let selected_item = self.selected_item;
+        let element = uniform_list(
+            "code_actions_menu",
+            self.actions.len(),
+            move |editor, range, cx| {
+                actions[range.clone()]
+                    .iter()
+                    .enumerate()
+                    .map(|(ix, action)| {
+                        let item_ix = range.start + ix;
+                        let selected = selected_item == item_ix;
+                        let colors = cx.theme().colors();
+                        div()
+                            .px_2()
+                            .text_color(colors.text)
+                            .when(selected, |style| {
+                                style
+                                    .bg(colors.element_active)
+                                    .text_color(colors.text_accent)
+                            })
+                            .hover(|style| {
+                                style
+                                    .bg(colors.element_hover)
+                                    .text_color(colors.text_accent)
+                            })
+                            .on_mouse_down(MouseButton::Left, move |editor: &mut Editor, _, cx| {
+                                cx.stop_propagation();
+                                editor
+                                    .confirm_code_action(
+                                        &ConfirmCodeAction {
+                                            item_ix: Some(item_ix),
+                                        },
+                                        cx,
+                                    )
+                                    .map(|task| task.detach_and_log_err(cx));
+                            })
+                            .child(action.lsp_action.title.clone())
+                    })
+                    .collect()
+            },
+        )
+        .bg(cx.theme().colors().element_background)
+        .px_2()
+        .py_1()
+        .with_width_from_item(
+            self.actions
+                .iter()
+                .enumerate()
+                .max_by_key(|(_, action)| action.lsp_action.title.chars().count())
+                .map(|(ix, _)| ix),
+        )
+        .render();
 
-    //     let container_style = style.autocomplete.container;
-    //     let actions = self.actions.clone();
-    //     let selected_item = self.selected_item;
-    //     let element = UniformList::new(
-    //         self.list.clone(),
-    //         actions.len(),
-    //         cx,
-    //         move |_, range, items, cx| {
-    //             let start_ix = range.start;
-    //             for (ix, action) in actions[range].iter().enumerate() {
-    //                 let item_ix = start_ix + ix;
-    //                 items.push(
-    //                     MouseEventHandler::new::<ActionTag, _>(item_ix, cx, |state, _| {
-    //                         let item_style = if item_ix == selected_item {
-    //                             style.autocomplete.selected_item
-    //                         } else if state.hovered() {
-    //                             style.autocomplete.hovered_item
-    //                         } else {
-    //                             style.autocomplete.item
-    //                         };
-
-    //                         Text::new(action.lsp_action.title.clone(), style.text.clone())
-    //                             .with_soft_wrap(false)
-    //                             .contained()
-    //                             .with_style(item_style)
-    //                     })
-    //                     .with_cursor_style(CursorStyle::PointingHand)
-    //                     .on_down(MouseButton::Left, move |_, this, cx| {
-    //                         let workspace = this
-    //                             .workspace
-    //                             .as_ref()
-    //                             .and_then(|(workspace, _)| workspace.upgrade(cx));
-    //                         cx.window_context().defer(move |cx| {
-    //                             if let Some(workspace) = workspace {
-    //                                 workspace.update(cx, |workspace, cx| {
-    //                                     if let Some(task) = Editor::confirm_code_action(
-    //                                         workspace,
-    //                                         &ConfirmCodeAction {
-    //                                             item_ix: Some(item_ix),
-    //                                         },
-    //                                         cx,
-    //                                     ) {
-    //                                         task.detach_and_log_err(cx);
-    //                                     }
-    //                                 });
-    //                             }
-    //                         });
-    //                     })
-    //                     .into_any(),
-    //                 );
-    //             }
-    //         },
-    //     )
-    //     .with_width_from_item(
-    //         self.actions
-    //             .iter()
-    //             .enumerate()
-    //             .max_by_key(|(_, action)| action.lsp_action.title.chars().count())
-    //             .map(|(ix, _)| ix),
-    //     )
-    //     .contained()
-    //     .with_style(container_style)
-    //     .into_any();
-
-    //     if self.deployed_from_indicator {
-    //         *cursor_position.column_mut() = 0;
-    //     }
+        if self.deployed_from_indicator {
+            *cursor_position.column_mut() = 0;
+        }
 
-    //     (cursor_position, element)
-    // }
+        (cursor_position, element)
+    }
 }
 
 pub struct CopilotState {
@@ -3660,7 +3612,7 @@ impl Editor {
                         completions: Arc::new(RwLock::new(completions.into())),
                         matches: Vec::new().into(),
                         selected_item: 0,
-                        list: Default::default(),
+                        scroll_handle: UniformListScrollHandle::new(),
                     };
                     menu.filter(query.as_deref(), cx.background_executor().clone())
                         .await;
@@ -3846,156 +3798,161 @@ 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(),
+                                scroll_handle: UniformListScrollHandle::default(),
+                                deployed_from_indicator,
+                            }));
+                        cx.notify();
+                    }
+                }
+            })?;
 
-    //     pub fn confirm_code_action(
-    //         workspace: &mut Workspace,
-    //         action: &ConfirmCodeAction,
-    //         cx: &mut ViewContext<Workspace>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
-    //         let actions_menu = if let ContextMenu::CodeActions(menu) =
-    //             editor.update(cx, |editor, cx| editor.hide_context_menu(cx))?
-    //         {
-    //             menu
-    //         } else {
-    //             return None;
-    //         };
-    //         let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item);
-    //         let action = actions_menu.actions.get(action_ix)?.clone();
-    //         let title = action.lsp_action.title.clone();
-    //         let buffer = actions_menu.buffer;
+            Ok::<_, anyhow::Error>(())
+        })
+        .detach_and_log_err(cx);
+    }
 
-    //         let apply_code_actions = workspace.project().clone().update(cx, |project, cx| {
-    //             project.apply_code_action(buffer, action, true, cx)
-    //         });
-    //         let editor = editor.downgrade();
-    //         Some(cx.spawn(|workspace, cx| async move {
-    //             let project_transaction = apply_code_actions.await?;
-    //             Self::open_project_transaction(&editor, workspace, project_transaction, title, cx).await
-    //         }))
-    //     }
+    pub fn confirm_code_action(
+        &mut self,
+        action: &ConfirmCodeAction,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let actions_menu = if let ContextMenu::CodeActions(menu) = self.hide_context_menu(cx)? {
+            menu
+        } else {
+            return None;
+        };
+        let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item);
+        let action = actions_menu.actions.get(action_ix)?.clone();
+        let title = action.lsp_action.title.clone();
+        let buffer = actions_menu.buffer;
+        let workspace = self.workspace()?;
 
-    //     async fn open_project_transaction(
-    //         this: &WeakViewHandle<Editor
-    //         workspace: WeakViewHandle<Workspace
-    //         transaction: ProjectTransaction,
-    //         title: String,
-    //         mut cx: AsyncAppContext,
-    //     ) -> Result<()> {
-    //         let replica_id = this.read_with(&cx, |this, cx| this.replica_id(cx))?;
-
-    //         let mut entries = transaction.0.into_iter().collect::<Vec<_>>();
-    //         entries.sort_unstable_by_key(|(buffer, _)| {
-    //             buffer.read_with(&cx, |buffer, _| buffer.file().map(|f| f.path().clone()))
-    //         });
+        let apply_code_actions = workspace
+            .read(cx)
+            .project()
+            .clone()
+            .update(cx, |project, cx| {
+                project.apply_code_action(buffer, action, true, cx)
+            });
+        let workspace = workspace.downgrade();
+        Some(cx.spawn(|editor, cx| async move {
+            let project_transaction = apply_code_actions.await?;
+            Self::open_project_transaction(&editor, workspace, project_transaction, title, cx).await
+        }))
+    }
 
-    //         // If the project transaction's edits are all contained within this editor, then
-    //         // avoid opening a new editor to display them.
+    async fn open_project_transaction(
+        this: &WeakView<Editor>,
+        workspace: WeakView<Workspace>,
+        transaction: ProjectTransaction,
+        title: String,
+        mut cx: AsyncWindowContext,
+    ) -> Result<()> {
+        let replica_id = this.update(&mut cx, |this, cx| this.replica_id(cx))?;
 
-    //         if let Some((buffer, transaction)) = entries.first() {
-    //             if entries.len() == 1 {
-    //                 let excerpt = this.read_with(&cx, |editor, cx| {
-    //                     editor
-    //                         .buffer()
-    //                         .read(cx)
-    //                         .excerpt_containing(editor.selections.newest_anchor().head(), cx)
-    //                 })?;
-    //                 if let Some((_, excerpted_buffer, excerpt_range)) = excerpt {
-    //                     if excerpted_buffer == *buffer {
-    //                         let all_edits_within_excerpt = buffer.read_with(&cx, |buffer, _| {
-    //                             let excerpt_range = excerpt_range.to_offset(buffer);
-    //                             buffer
-    //                                 .edited_ranges_for_transaction::<usize>(transaction)
-    //                                 .all(|range| {
-    //                                     excerpt_range.start <= range.start
-    //                                         && excerpt_range.end >= range.end
-    //                                 })
-    //                         });
-
-    //                         if all_edits_within_excerpt {
-    //                             return Ok(());
-    //                         }
-    //                     }
-    //                 }
-    //             }
-    //         } else {
-    //             return Ok(());
-    //         }
+        let mut entries = transaction.0.into_iter().collect::<Vec<_>>();
+        cx.update(|_, cx| {
+            entries.sort_unstable_by_key(|(buffer, _)| {
+                buffer.read(cx).file().map(|f| f.path().clone())
+            });
+        })?;
+
+        // If the project transaction's edits are all contained within this editor, then
+        // avoid opening a new editor to display them.
+
+        if let Some((buffer, transaction)) = entries.first() {
+            if entries.len() == 1 {
+                let excerpt = this.update(&mut cx, |editor, cx| {
+                    editor
+                        .buffer()
+                        .read(cx)
+                        .excerpt_containing(editor.selections.newest_anchor().head(), cx)
+                })?;
+                if let Some((_, excerpted_buffer, excerpt_range)) = excerpt {
+                    if excerpted_buffer == *buffer {
+                        let all_edits_within_excerpt = buffer.read_with(&cx, |buffer, _| {
+                            let excerpt_range = excerpt_range.to_offset(buffer);
+                            buffer
+                                .edited_ranges_for_transaction::<usize>(transaction)
+                                .all(|range| {
+                                    excerpt_range.start <= range.start
+                                        && excerpt_range.end >= range.end
+                                })
+                        })?;
 
-    //         let mut ranges_to_highlight = Vec::new();
-    //         let excerpt_buffer = cx.build_model(|cx| {
-    //             let mut multibuffer = MultiBuffer::new(replica_id).with_title(title);
-    //             for (buffer_handle, transaction) in &entries {
-    //                 let buffer = buffer_handle.read(cx);
-    //                 ranges_to_highlight.extend(
-    //                     multibuffer.push_excerpts_with_context_lines(
-    //                         buffer_handle.clone(),
-    //                         buffer
-    //                             .edited_ranges_for_transaction::<usize>(transaction)
-    //                             .collect(),
-    //                         1,
-    //                         cx,
-    //                     ),
-    //                 );
-    //             }
-    //             multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)), cx);
-    //             multibuffer
-    //         });
+                        if all_edits_within_excerpt {
+                            return Ok(());
+                        }
+                    }
+                }
+            }
+        } else {
+            return Ok(());
+        }
 
-    //         workspace.update(&mut cx, |workspace, cx| {
-    //             let project = workspace.project().clone();
-    //             let editor =
-    //                 cx.add_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx));
-    //             workspace.add_item(Box::new(editor.clone()), cx);
-    //             editor.update(cx, |editor, cx| {
-    //                 editor.highlight_background::<Self>(
-    //                     ranges_to_highlight,
-    //                     |theme| theme.editor.highlighted_line_background,
-    //                     cx,
-    //                 );
-    //             });
-    //         })?;
+        let mut ranges_to_highlight = Vec::new();
+        let excerpt_buffer = cx.build_model(|cx| {
+            let mut multibuffer = MultiBuffer::new(replica_id).with_title(title);
+            for (buffer_handle, transaction) in &entries {
+                let buffer = buffer_handle.read(cx);
+                ranges_to_highlight.extend(
+                    multibuffer.push_excerpts_with_context_lines(
+                        buffer_handle.clone(),
+                        buffer
+                            .edited_ranges_for_transaction::<usize>(transaction)
+                            .collect(),
+                        1,
+                        cx,
+                    ),
+                );
+            }
+            multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)), cx);
+            multibuffer
+        })?;
+
+        workspace.update(&mut cx, |workspace, cx| {
+            let project = workspace.project().clone();
+            let editor =
+                cx.build_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx));
+            workspace.add_item(Box::new(editor.clone()), cx);
+            editor.update(cx, |editor, cx| {
+                editor.highlight_background::<Self>(
+                    ranges_to_highlight,
+                    |theme| theme.editor_highlighted_line_background,
+                    cx,
+                );
+            });
+        })?;
 
-    //         Ok(())
-    //     }
+        Ok(())
+    }
 
     fn refresh_code_actions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
         let project = self.project.clone()?;
@@ -4390,41 +4347,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_indicator", ui::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,
@@ -4491,29 +4436,27 @@ impl Editor {
     //     }
 
     pub fn context_menu_visible(&self) -> bool {
-        false
-        // todo!("context menu")
-        // self.context_menu
-        //     .read()
-        //     .as_ref()
-        //     .map_or(false, |menu| menu.visible())
+        self.context_menu
+            .read()
+            .as_ref()
+            .map_or(false, |menu| menu.visible())
     }
 
-    //     pub fn render_context_menu(
-    //         &self,
-    //         cursor_position: DisplayPoint,
-    //         style: EditorStyle,
-    //         cx: &mut ViewContext<Editor>,
-    //     ) -> Option<(DisplayPoint, AnyElement<Editor>)> {
-    //         self.context_menu.read().as_ref().map(|menu| {
-    //             menu.render(
-    //                 cursor_position,
-    //                 style,
-    //                 self.workspace.as_ref().map(|(w, _)| w.clone()),
-    //                 cx,
-    //             )
-    //         })
-    //     }
+    pub fn render_context_menu(
+        &self,
+        cursor_position: DisplayPoint,
+        style: &EditorStyle,
+        cx: &mut ViewContext<Editor>,
+    ) -> Option<(DisplayPoint, AnyElement<Editor>)> {
+        self.context_menu.read().as_ref().map(|menu| {
+            menu.render(
+                cursor_position,
+                style,
+                self.workspace.as_ref().map(|(w, _)| w.clone()),
+                cx,
+            )
+        })
+    }
 
     fn hide_context_menu(&mut self, cx: &mut ViewContext<Self>) -> Option<ContextMenu> {
         cx.notify();
@@ -5954,29 +5897,29 @@ impl Editor {
         });
     }
 
-    //     pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext<Self>) {
-    //         if let Some(context_menu) = self.context_menu.write().as_mut() {
-    //             context_menu.select_first(self.project.as_ref(), cx);
-    //         }
-    //     }
+    pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext<Self>) {
+        if let Some(context_menu) = self.context_menu.write().as_mut() {
+            context_menu.select_first(self.project.as_ref(), cx);
+        }
+    }
 
-    //     pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext<Self>) {
-    //         if let Some(context_menu) = self.context_menu.write().as_mut() {
-    //             context_menu.select_prev(self.project.as_ref(), cx);
-    //         }
-    //     }
+    pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext<Self>) {
+        if let Some(context_menu) = self.context_menu.write().as_mut() {
+            context_menu.select_prev(self.project.as_ref(), cx);
+        }
+    }
 
-    //     pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext<Self>) {
-    //         if let Some(context_menu) = self.context_menu.write().as_mut() {
-    //             context_menu.select_next(self.project.as_ref(), cx);
-    //         }
-    //     }
+    pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext<Self>) {
+        if let Some(context_menu) = self.context_menu.write().as_mut() {
+            context_menu.select_next(self.project.as_ref(), cx);
+        }
+    }
 
-    //     pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext<Self>) {
-    //         if let Some(context_menu) = self.context_menu.write().as_mut() {
-    //             context_menu.select_last(self.project.as_ref(), cx);
-    //         }
-    //     }
+    pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext<Self>) {
+        if let Some(context_menu) = self.context_menu.write().as_mut() {
+            context_menu.select_last(self.project.as_ref(), cx);
+        }
+    }
 
     pub fn move_to_previous_word_start(
         &mut 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(
@@ -596,7 +603,7 @@ impl EditorElement {
     fn paint_text(
         &mut self,
         bounds: Bounds<Pixels>,
-        layout: &LayoutState,
+        layout: &mut LayoutState,
         editor: &mut Editor,
         cx: &mut ViewContext<Editor>,
     ) {
@@ -787,48 +794,46 @@ impl EditorElement {
                 )
             }
 
-            cx.stack(0, |cx| {
+            cx.with_z_index(0, |cx| {
                 for cursor in cursors {
                     cursor.paint(content_origin, cx);
                 }
             });
-            // cx.scene().push_layer(Some(bounds));
-
-            // cx.scene().pop_layer();
 
-            // if let Some((position, context_menu)) = layout.context_menu.as_mut() {
-            //     cx.scene().push_stacking_context(None, None);
-            //     let cursor_row_layout =
-            //         &layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
-            //     let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
-            //     let y = (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top;
-            //     let mut list_origin = content_origin + point(x, y);
-            //     let list_width = context_menu.size().x;
-            //     let list_height = context_menu.size().y;
-
-            //     // Snap the right edge of the list to the right edge of the window if
-            //     // its horizontal bounds overflow.
-            //     if list_origin.x + list_width > cx.window_size().x {
-            //         list_origin.set_x((cx.window_size().x - list_width).max(0.));
-            //     }
-
-            //     if list_origin.y + list_height > bounds.max_y {
-            //         list_origin
-            //             .set_y(list_origin.y - layout.position_map.line_height - list_height);
-            //     }
+            if let Some((position, context_menu)) = layout.context_menu.as_mut() {
+                cx.with_z_index(1, |cx| {
+                    let line_height = self.style.text.line_height_in_pixels(cx.rem_size());
+                    let available_space = size(
+                        AvailableSpace::MinContent,
+                        AvailableSpace::Definite(
+                            (12. * line_height).min((bounds.size.height - line_height) / 2.),
+                        ),
+                    );
+                    let context_menu_size = context_menu.measure(available_space, editor, cx);
+
+                    let cursor_row_layout = &layout.position_map.line_layouts
+                        [(position.row() - start_row) as usize]
+                        .line;
+                    let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
+                    let y =
+                        (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top;
+                    let mut list_origin = content_origin + point(x, y);
+                    let list_width = context_menu_size.width;
+                    let list_height = context_menu_size.height;
+
+                    // Snap the right edge of the list to the right edge of the window if
+                    // its horizontal bounds overflow.
+                    if list_origin.x + list_width > cx.viewport_size().width {
+                        list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO);
+                    }
 
-            //     context_menu.paint(
-            //         list_origin,
-            //         Bounds::<Pixels>::from_points(
-            //             gpui::Point::<Pixels>::zero(),
-            //             point(f32::MAX, f32::MAX),
-            //         ), // Let content bleed outside of editor
-            //         editor,
-            //         cx,
-            //     );
+                    if list_origin.y + list_height > bounds.lower_right().y {
+                        list_origin.y -= layout.position_map.line_height - list_height;
+                    }
 
-            //     cx.scene().pop_stacking_context();
-            // }
+                    context_menu.draw(list_origin, available_space, editor, cx);
+                })
+            }
 
             // if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() {
             //     cx.scene().push_stacking_context(None, None);
@@ -1774,26 +1779,28 @@ impl EditorElement {
             snapshot = editor.snapshot(cx);
         }
 
-        // 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 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, &self.style, cx);
+                }
 
-        //         let active = matches!(
-        //             editor.context_menu.read().as_ref(),
-        //             Some(crate::ContextMenu::CodeActions(_))
-        //         );
+                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 +1838,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() {
@@ -1941,8 +1936,8 @@ impl EditorElement {
             display_hunks,
             // blocks,
             selections,
-            // context_menu,
-            // code_actions_indicator,
+            context_menu,
+            code_actions_indicator,
             // fold_indicators,
             tab_invisible,
             space_invisible,
@@ -2493,7 +2488,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,
@@ -2503,21 +2498,24 @@ impl Element<Editor> for EditorElement {
             size: layout.text_size,
         };
 
-        cx.with_content_mask(ContentMask { bounds }, |cx| {
-            self.paint_mouse_listeners(
-                bounds,
-                gutter_bounds,
-                text_bounds,
-                &layout.position_map,
-                cx,
-            );
-            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_text(text_bounds, &layout, editor, cx);
-            let input_handler = ElementInputHandler::new(bounds, cx);
-            cx.handle_input(&editor.focus_handle, input_handler);
+        // We call with_z_index to establish a new stacking context.
+        cx.with_z_index(0, |cx| {
+            cx.with_content_mask(ContentMask { bounds }, |cx| {
+                self.paint_mouse_listeners(
+                    bounds,
+                    gutter_bounds,
+                    text_bounds,
+                    &layout.position_map,
+                    cx,
+                );
+                self.paint_background(gutter_bounds, text_bounds, &layout, cx);
+                if layout.gutter_size.width > Pixels::ZERO {
+                    self.paint_gutter(gutter_bounds, &mut layout, editor, cx);
+                }
+                self.paint_text(text_bounds, &mut layout, editor, cx);
+                let input_handler = ElementInputHandler::new(bounds, cx);
+                cx.handle_input(&editor.focus_handle, input_handler);
+            });
         });
     }
 }
@@ -3143,14 +3141,19 @@ pub struct LayoutState {
     show_scrollbars: bool,
     is_singleton: bool,
     max_row: u32,
-    // context_menu: Option<(DisplayPoint, AnyElement<Editor>)>,
-    // code_actions_indicator: Option<(u32, AnyElement<Editor>)>,
+    context_menu: Option<(DisplayPoint, 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,
@@ -4123,7 +4126,7 @@ fn build_key_listeners(
         build_action_listener(Editor::unfold_at),
         build_action_listener(Editor::fold_selected_ranges),
         build_action_listener(Editor::show_completions),
-        // build_action_listener(Editor::toggle_code_actions), todo!()
+        build_action_listener(Editor::toggle_code_actions),
         // build_action_listener(Editor::open_excerpts), todo!()
         build_action_listener(Editor::toggle_soft_wrap),
         build_action_listener(Editor::toggle_inlay_hints),
@@ -4139,13 +4142,21 @@ fn build_key_listeners(
         build_action_listener(Editor::restart_language_server),
         build_action_listener(Editor::show_character_palette),
         // build_action_listener(Editor::confirm_completion), todo!()
-        // build_action_listener(Editor::confirm_code_action), todo!()
+        build_action_listener(|editor, action, cx| {
+            editor
+                .confirm_code_action(action, cx)
+                .map(|task| task.detach_and_log_err(cx));
+        }),
         // build_action_listener(Editor::rename), todo!()
         // build_action_listener(Editor::confirm_rename), todo!()
         // build_action_listener(Editor::find_all_references), todo!()
         build_action_listener(Editor::next_copilot_suggestion),
         build_action_listener(Editor::previous_copilot_suggestion),
         build_action_listener(Editor::copilot_suggest),
+        build_action_listener(Editor::context_menu_first),
+        build_action_listener(Editor::context_menu_prev),
+        build_action_listener(Editor::context_menu_next),
+        build_action_listener(Editor::context_menu_last),
         build_key_listener(
             move |editor, key_down: &KeyDownEvent, dispatch_context, phase, cx| {
                 if phase == DispatchPhase::Bubble {

crates/gpui2/src/element.rs 🔗

@@ -1,4 +1,6 @@
-use crate::{BorrowWindow, Bounds, ElementId, LayoutId, Pixels, ViewContext};
+use crate::{
+    AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size, ViewContext,
+};
 use derive_more::{Deref, DerefMut};
 pub(crate) use smallvec::SmallVec;
 use std::{any::Any, mem};
@@ -61,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>> {
@@ -79,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,
 }
 
@@ -135,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")
             }
         };
@@ -152,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() {
@@ -171,6 +198,65 @@ 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,
+        mut origin: Point<Pixels>,
+        available_space: Size<AvailableSpace>,
+        view_state: &mut V,
+        cx: &mut ViewContext<V>,
+    ) {
+        self.measure(available_space, view_state, cx);
+        // Ignore the element offset when drawing this element, as the origin is already specified
+        // in absolute terms.
+        origin -= cx.element_offset();
+        cx.with_element_offset(Some(origin), |cx| self.paint(view_state, cx))
+    }
 }
 
 pub struct AnyElement<V>(Box<dyn ElementObject<V>>);
@@ -196,6 +282,27 @@ impl<V> AnyElement<V> {
     pub fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
         self.0.paint(view_state, cx)
     }
+
+    /// Initializes this element and performs layout within the given available space to determine its size.
+    pub fn measure(
+        &mut self,
+        available_space: Size<AvailableSpace>,
+        view_state: &mut V,
+        cx: &mut ViewContext<V>,
+    ) -> Size<Pixels> {
+        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.
+    pub fn draw(
+        &mut self,
+        origin: Point<Pixels>,
+        available_space: Size<AvailableSpace>,
+        view_state: &mut V,
+        cx: &mut ViewContext<V>,
+    ) {
+        self.0.draw(origin, available_space, view_state, cx)
+    }
 }
 
 pub trait Component<V> {

crates/gpui2/src/elements/text.rs 🔗

@@ -101,7 +101,12 @@ impl<V: 'static> Element<V> for Text<V> {
                     .map(|line| line.wrap_count() + 1)
                     .sum::<usize>();
                 let size = Size {
-                    width: lines.iter().map(|line| line.layout.width).max().unwrap(),
+                    width: lines
+                        .iter()
+                        .map(|line| line.layout.width)
+                        .max()
+                        .unwrap()
+                        .ceil(),
                     height: line_height * line_count,
                 };
 

crates/gpui2/src/elements/uniform_list.rs 🔗

@@ -30,6 +30,7 @@ where
         id: id.clone(),
         style,
         item_count,
+        item_to_measure_index: 0,
         render_items: Box::new(move |view, visible_range, cx| {
             f(view, visible_range, cx)
                 .into_iter()
@@ -45,6 +46,7 @@ pub struct UniformList<V: 'static> {
     id: ElementId,
     style: StyleRefinement,
     item_count: usize,
+    item_to_measure_index: usize,
     render_items: Box<
         dyn for<'a> Fn(
             &'a mut V,
@@ -56,7 +58,7 @@ pub struct UniformList<V: 'static> {
     scroll_handle: Option<UniformListScrollHandle>,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Default)]
 pub struct UniformListScrollHandle(Arc<Mutex<Option<ScrollHandleState>>>);
 
 #[derive(Clone, Debug)]
@@ -112,7 +114,7 @@ impl<V: 'static> Element<V> for UniformList<V> {
         cx: &mut ViewContext<V>,
     ) -> Self::ElementState {
         element_state.unwrap_or_else(|| {
-            let item_size = self.measure_first_item(view_state, None, cx);
+            let item_size = self.measure_item(view_state, None, cx);
             UniformListState {
                 interactive: InteractiveElementState::default(),
                 item_size,
@@ -158,7 +160,6 @@ impl<V: 'static> Element<V> for UniformList<V> {
         cx: &mut ViewContext<V>,
     ) {
         let style = self.computed_style();
-        style.paint(bounds, cx);
 
         let border = style.border_widths.to_pixels(cx.rem_size());
         let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
@@ -170,10 +171,12 @@ impl<V: 'static> Element<V> for UniformList<V> {
         );
 
         cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
+            style.paint(bounds, cx);
+
             let content_size;
             if self.item_count > 0 {
                 let item_height = self
-                    .measure_first_item(view_state, Some(padded_bounds.size.width), cx)
+                    .measure_item(view_state, Some(padded_bounds.size.width), cx)
                     .height;
                 if let Some(scroll_handle) = self.scroll_handle.clone() {
                     scroll_handle.0.lock().replace(ScrollHandleState {
@@ -207,19 +210,13 @@ impl<V: 'static> Element<V> for UniformList<V> {
 
                 cx.with_z_index(1, |cx| {
                     for (item, ix) in items.iter_mut().zip(visible_range) {
-                        item.initialize(view_state, cx);
-
-                        let layout_id = item.layout(view_state, cx);
-                        cx.compute_layout(
-                            layout_id,
-                            Size {
-                                width: AvailableSpace::Definite(bounds.size.width),
-                                height: AvailableSpace::Definite(item_height),
-                            },
-                        );
-                        let offset =
+                        let item_origin =
                             padded_bounds.origin + point(px(0.), item_height * ix + scroll_offset);
-                        cx.with_element_offset(Some(offset), |cx| item.paint(view_state, cx))
+                        let available_space = size(
+                            AvailableSpace::Definite(padded_bounds.size.width),
+                            AvailableSpace::Definite(item_height),
+                        );
+                        item.draw(item_origin, available_space, view_state, cx);
                     }
                 });
             } else {
@@ -245,27 +242,31 @@ impl<V: 'static> Element<V> for UniformList<V> {
 }
 
 impl<V> UniformList<V> {
-    fn measure_first_item(
+    pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
+        self.item_to_measure_index = item_index.unwrap_or(0);
+        self
+    }
+
+    fn measure_item(
         &self,
         view_state: &mut V,
         list_width: Option<Pixels>,
         cx: &mut ViewContext<V>,
     ) -> Size<Pixels> {
-        let mut items = (self.render_items)(view_state, 0..1, cx);
-        debug_assert!(items.len() == 1);
+        if self.item_count == 0 {
+            return Size::default();
+        }
+
+        let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
+        let mut items = (self.render_items)(view_state, item_ix..item_ix + 1, cx);
         let mut item_to_measure = items.pop().unwrap();
-        item_to_measure.initialize(view_state, cx);
-        let layout_id = item_to_measure.layout(view_state, cx);
-        cx.compute_layout(
-            layout_id,
-            Size {
-                width: list_width.map_or(AvailableSpace::MinContent, |width| {
-                    AvailableSpace::Definite(width)
-                }),
-                height: AvailableSpace::MinContent,
-            },
+        let available_space = size(
+            list_width.map_or(AvailableSpace::MinContent, |width| {
+                AvailableSpace::Definite(width)
+            }),
+            AvailableSpace::MinContent,
         );
-        cx.layout_bounds(layout_id).size
+        item_to_measure.measure(available_space, view_state, cx)
     }
 
     pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {

crates/gpui2/src/geometry.rs 🔗

@@ -785,6 +785,10 @@ impl Pixels {
         Self(self.0.round())
     }
 
+    pub fn ceil(&self) -> Self {
+        Self(self.0.ceil())
+    }
+
     pub fn scale(&self, factor: f32) -> ScaledPixels {
         ScaledPixels(self.0 * factor)
     }

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 🔗

@@ -206,7 +206,7 @@ pub struct Window {
     display_id: DisplayId,
     sprite_atlas: Arc<dyn PlatformAtlas>,
     rem_size: Pixels,
-    content_size: Size<Pixels>,
+    viewport_size: Size<Pixels>,
     pub(crate) layout_engine: TaffyLayoutEngine,
     pub(crate) root_view: Option<AnyView>,
     pub(crate) element_id_stack: GlobalElementId,
@@ -305,7 +305,7 @@ impl Window {
             display_id,
             sprite_atlas,
             rem_size: px(16.),
-            content_size,
+            viewport_size: content_size,
             layout_engine: TaffyLayoutEngine::new(),
             root_view: None,
             element_id_stack: GlobalElementId::default(),
@@ -630,7 +630,7 @@ impl<'a> WindowContext<'a> {
 
     fn window_bounds_changed(&mut self) {
         self.window.scale_factor = self.window.platform_window.scale_factor();
-        self.window.content_size = self.window.platform_window.content_size();
+        self.window.viewport_size = self.window.platform_window.content_size();
         self.window.bounds = self.window.platform_window.bounds();
         self.window.display_id = self.window.platform_window.display().id();
         self.window.dirty = true;
@@ -645,6 +645,10 @@ impl<'a> WindowContext<'a> {
         self.window.bounds
     }
 
+    pub fn viewport_size(&self) -> Size<Pixels> {
+        self.window.viewport_size
+    }
+
     pub fn is_window_active(&self) -> bool {
         self.window.active
     }
@@ -738,7 +742,7 @@ impl<'a> WindowContext<'a> {
 
     /// Called during painting to invoke the given closure in a new stacking context. The given
     /// z-index is interpreted relative to the previous call to `stack`.
-    pub fn stack<R>(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R {
+    pub fn with_z_index<R>(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R {
         self.window.current_frame.z_index_stack.push(z_index);
         let result = f(self);
         self.window.current_frame.z_index_stack.pop();
@@ -1036,13 +1040,13 @@ impl<'a> WindowContext<'a> {
 
         self.start_frame();
 
-        self.stack(0, |cx| {
-            let available_space = cx.window.content_size.map(Into::into);
+        self.with_z_index(0, |cx| {
+            let available_space = cx.window.viewport_size.map(Into::into);
             root_view.draw(available_space, cx);
         });
 
         if let Some(active_drag) = self.app.active_drag.take() {
-            self.stack(1, |cx| {
+            self.with_z_index(1, |cx| {
                 let offset = cx.mouse_position() - active_drag.cursor_offset;
                 cx.with_element_offset(Some(offset), |cx| {
                     let available_space =
@@ -1052,7 +1056,7 @@ impl<'a> WindowContext<'a> {
                 });
             });
         } else if let Some(active_tooltip) = self.app.active_tooltip.take() {
-            self.stack(1, |cx| {
+            self.with_z_index(1, |cx| {
                 cx.with_element_offset(Some(active_tooltip.cursor_offset), |cx| {
                     let available_space =
                         size(AvailableSpace::MinContent, AvailableSpace::MinContent);
@@ -1101,6 +1105,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();
@@ -1750,7 +1756,7 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
             .unwrap_or_else(|| ContentMask {
                 bounds: Bounds {
                     origin: Point::default(),
-                    size: self.window().content_size,
+                    size: self.window().viewport_size,
                 },
             })
     }

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);
             });
         }