Support clicking on a completion to confirm it

Antonio Scandurra created

Change summary

crates/editor/src/editor.rs         | 95 ++++++++++++++++++++----------
crates/theme/src/theme.rs           |  1 
crates/zed/assets/themes/_base.toml |  4 +
crates/zed/assets/themes/black.toml |  1 
crates/zed/assets/themes/dark.toml  |  2 
crates/zed/assets/themes/light.toml |  2 
6 files changed, 71 insertions(+), 34 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -23,6 +23,7 @@ use gpui::{
     fonts::{self, HighlightStyle, TextStyle},
     geometry::vector::{vec2f, Vector2F},
     keymap::Binding,
+    platform::CursorStyle,
     text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle,
     MutableAppContext, RenderContext, Task, View, ViewContext, WeakModelHandle, WeakViewHandle,
 };
@@ -123,7 +124,7 @@ action!(FoldSelectedRanges);
 action!(Scroll, Vector2F);
 action!(Select, SelectPhase);
 action!(ShowCompletions);
-action!(ConfirmCompletion);
+action!(ConfirmCompletion, Option<usize>);
 
 pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpener>>) {
     path_openers.push(Box::new(items::BufferOpener));
@@ -139,7 +140,11 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
             Input("\n".into()),
             Some("Editor && mode == auto_height"),
         ),
-        Binding::new("enter", ConfirmCompletion, Some("Editor && completing")),
+        Binding::new(
+            "enter",
+            ConfirmCompletion(None),
+            Some("Editor && completing"),
+        ),
         Binding::new("tab", Tab, Some("Editor")),
         Binding::new("shift-tab", Outdent, Some("Editor")),
         Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")),
@@ -297,11 +302,13 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
     cx.add_action(Editor::unfold);
     cx.add_action(Editor::fold_selected_ranges);
     cx.add_action(Editor::show_completions);
-    cx.add_action(|editor: &mut Editor, _: &ConfirmCompletion, cx| {
-        if let Some(task) = editor.confirm_completion(cx) {
-            task.detach_and_log_err(cx);
-        }
-    });
+    cx.add_action(
+        |editor: &mut Editor, &ConfirmCompletion(ix): &ConfirmCompletion, cx| {
+            if let Some(task) = editor.confirm_completion(ix, cx) {
+                task.detach_and_log_err(cx);
+            }
+        },
+    );
 }
 
 trait SelectionExt {
@@ -1669,11 +1676,15 @@ impl Editor {
         self.completion_state.take()
     }
 
-    fn confirm_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
+    fn confirm_completion(
+        &mut self,
+        completion_ix: Option<usize>,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
         let completion_state = self.hide_completions(cx)?;
         let mat = completion_state
             .matches
-            .get(completion_state.selected_item)?;
+            .get(completion_ix.unwrap_or(completion_state.selected_item))?;
         let completion = completion_state.completions.get(mat.candidate_id)?;
 
         if completion.is_snippet() {
@@ -1702,6 +1713,8 @@ impl Editor {
     }
 
     pub fn render_completions(&self, cx: &AppContext) -> Option<ElementBox> {
+        enum CompletionTag {}
+
         self.completion_state.as_ref().map(|state| {
             let build_settings = self.build_settings.clone();
             let settings = build_settings(cx);
@@ -1715,30 +1728,48 @@ impl Editor {
                     let settings = build_settings(cx);
                     let start_ix = range.start;
                     for (ix, mat) in matches[range].iter().enumerate() {
-                        let item_style = if start_ix + ix == selected_item {
-                            settings.style.autocomplete.selected_item
-                        } else {
-                            settings.style.autocomplete.item
-                        };
                         let completion = &completions[mat.candidate_id];
+                        let item_ix = start_ix + ix;
                         items.push(
-                            Text::new(completion.label.text.clone(), settings.style.text.clone())
-                                .with_soft_wrap(false)
-                                .with_highlights(combine_syntax_and_fuzzy_match_highlights(
-                                    &completion.label.text,
-                                    settings.style.text.color.into(),
-                                    completion.label.runs.iter().filter_map(
-                                        |(range, highlight_id)| {
-                                            highlight_id
-                                                .style(&settings.style.syntax)
-                                                .map(|style| (range.clone(), style))
-                                        },
-                                    ),
-                                    &mat.positions,
-                                ))
-                                .contained()
-                                .with_style(item_style)
-                                .boxed(),
+                            MouseEventHandler::new::<CompletionTag, _, _, _>(
+                                mat.candidate_id,
+                                cx,
+                                |state, _| {
+                                    let item_style = if item_ix == selected_item {
+                                        settings.style.autocomplete.selected_item
+                                    } else if state.hovered {
+                                        settings.style.autocomplete.hovered_item
+                                    } else {
+                                        settings.style.autocomplete.item
+                                    };
+
+                                    Text::new(
+                                        completion.label.text.clone(),
+                                        settings.style.text.clone(),
+                                    )
+                                    .with_soft_wrap(false)
+                                    .with_highlights(combine_syntax_and_fuzzy_match_highlights(
+                                        &completion.label.text,
+                                        settings.style.text.color.into(),
+                                        completion.label.runs.iter().filter_map(
+                                            |(range, highlight_id)| {
+                                                highlight_id
+                                                    .style(&settings.style.syntax)
+                                                    .map(|style| (range.clone(), style))
+                                            },
+                                        ),
+                                        &mat.positions,
+                                    ))
+                                    .contained()
+                                    .with_style(item_style)
+                                    .boxed()
+                                },
+                            )
+                            .with_cursor_style(CursorStyle::PointingHand)
+                            .on_mouse_down(move |cx| {
+                                cx.dispatch_action(ConfirmCompletion(Some(item_ix)));
+                            })
+                            .boxed(),
                         );
                     }
                 },
@@ -6952,7 +6983,7 @@ mod tests {
 
         let apply_additional_edits = editor.update(&mut cx, |editor, cx| {
             editor.move_down(&MoveDown, cx);
-            let apply_additional_edits = editor.confirm_completion(cx).unwrap();
+            let apply_additional_edits = editor.confirm_completion(None, cx).unwrap();
             assert_eq!(
                 editor.text(cx),
                 "

crates/theme/src/theme.rs 🔗

@@ -328,6 +328,7 @@ pub struct AutocompleteStyle {
     pub container: ContainerStyle,
     pub item: ContainerStyle,
     pub selected_item: ContainerStyle,
+    pub hovered_item: ContainerStyle,
     pub match_highlight: HighlightStyle,
 }
 

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

@@ -322,6 +322,10 @@ match_highlight = { color = "$editor.syntax.keyword.color", weight = "$editor.sy
 
 [editor.autocomplete.selected_item]
 extends = "$editor.autocomplete.item"
+background = "$state.selected"
+
+[editor.autocomplete.hovered_item]
+extends = "$editor.autocomplete.item"
 background = "$state.hover"
 
 [project_diagnostics]

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

@@ -40,6 +40,7 @@ bad = "#b7372e"
 active_line = "#161313"
 highlighted_line = "#faca5033"
 hover = "#00000033"
+selected = "#00000088"
 
 [editor.syntax]
 keyword = { color = "#0086c0", weight = "bold" }

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

@@ -40,6 +40,7 @@ bad = "#b7372e"
 active_line = "#00000022"
 highlighted_line = "#faca5033"
 hover = "#00000033"
+selected = "#00000088"
 
 [editor.syntax]
 keyword = { color = "#0086c0", weight = "bold" }
@@ -51,7 +52,6 @@ comment = "#6a9955"
 property = "#4e94ce"
 variant = "#4fc1ff"
 constant = "#9cdcfe"
-
 title = { color = "#9cdcfe", weight = "bold" }
 emphasis = "#4ec9b0"
 "emphasis.strong" = { color = "#4ec9b0", weight = "bold" }

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

@@ -40,6 +40,7 @@ bad = "#b7372e"
 active_line = "#00000008"
 highlighted_line = "#faca5033"
 hover = "#0000000D"
+selected = "#0000001c"
 
 [editor.syntax]
 keyword = { color = "#0000fa", weight = "bold" }
@@ -51,7 +52,6 @@ comment = "#6a9955"
 property = "#4e94ce"
 variant = "#4fc1ff"
 constant = "#5a9ccc"
-
 title = { color = "#5a9ccc", weight = "bold" }
 emphasis = "#267f29"
 "emphasis.strong" = { color = "#267f29", weight = "bold" }