Finish autocomplete (#3404)

Antonio Scandurra created

![image](https://github.com/zed-industries/zed/assets/482957/f1f40bec-4e8b-457b-8801-ce10ecb6fc80)


TODO:

- [x] Respect max height imposed by editor element
- [x] Ensure mouse down event handler in `EditorElement` doesn't prevent
links from being clicked

Release Notes:

- N/A

Change summary

crates/editor2/src/editor.rs         | 687 ++++++++++++-----------------
crates/editor2/src/editor_tests.rs   |  69 ---
crates/editor2/src/element.rs        |  19 
crates/fuzzy2/src/strings.rs         |  28 +
crates/gpui2/src/elements/text.rs    |   6 
crates/gpui2/src/style.rs            | 161 ++++++
crates/gpui2/src/text_system.rs      |  10 
crates/gpui2/src/text_system/line.rs |  36 
8 files changed, 527 insertions(+), 489 deletions(-)

Detailed changes

crates/editor2/src/editor.rs 🔗

@@ -40,11 +40,11 @@ use fuzzy::{StringMatch, StringMatchCandidate};
 use git::diff_hunk_to_display;
 use gpui::{
     actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement,
-    AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context,
+    AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, ElementId,
     EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle,
-    Hsla, InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, RenderOnce,
-    SharedString, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View,
-    ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
+    Hsla, InputHandler, InteractiveText, KeyContext, Model, MouseButton, ParentElement, Pixels,
+    Render, RenderOnce, SharedString, Styled, StyledText, Subscription, Task, TextRun, TextStyle,
+    UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
@@ -54,9 +54,10 @@ use itertools::Itertools;
 pub use language::{char_kind, CharKind};
 use language::{
     language_settings::{self, all_language_settings, InlayHintSettings},
-    point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion,
-    CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, LanguageRegistry,
-    LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
+    markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel,
+    Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language,
+    LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal,
+    TransactionId,
 };
 use lazy_static::lazy_static;
 use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
@@ -97,7 +98,7 @@ use text::{OffsetUtf16, Rope};
 use theme::{
     ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
 };
-use ui::{h_stack, v_stack, HighlightedLabel, IconButton, StyledExt, Tooltip};
+use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, StyledExt, Tooltip};
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::{
     item::{ItemEvent, ItemHandle},
@@ -115,70 +116,70 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis
 
 pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
 
-// pub fn render_parsed_markdown<Tag: 'static>(
-//     parsed: &language::ParsedMarkdown,
-//     editor_style: &EditorStyle,
-//     workspace: Option<WeakView<Workspace>>,
-//     cx: &mut ViewContext<Editor>,
-// ) -> Text {
-//     enum RenderedMarkdown {}
-
-//     let parsed = parsed.clone();
-//     let view_id = cx.view_id();
-//     let code_span_background_color = editor_style.document_highlight_read_background;
-
-//     let mut region_id = 0;
-
-//     todo!()
-//     // Text::new(parsed.text, editor_style.text.clone())
-//     //     .with_highlights(
-//     //         parsed
-//     //             .highlights
-//     //             .iter()
-//     //             .filter_map(|(range, highlight)| {
-//     //                 let highlight = highlight.to_highlight_style(&editor_style.syntax)?;
-//     //                 Some((range.clone(), highlight))
-//     //             })
-//     //             .collect::<Vec<_>>(),
-//     //     )
-//     //     .with_custom_runs(parsed.region_ranges, move |ix, bounds, cx| {
-//     //         region_id += 1;
-//     //         let region = parsed.regions[ix].clone();
-
-//     //         if let Some(link) = region.link {
-//     //             cx.scene().push_cursor_region(CursorRegion {
-//     //                 bounds,
-//     //                 style: CursorStyle::PointingHand,
-//     //             });
-//     //             cx.scene().push_mouse_region(
-//     //                 MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds)
-//     //                     .on_down::<Editor, _>(MouseButton::Left, move |_, _, cx| match &link {
-//     //                         markdown::Link::Web { url } => cx.platform().open_url(url),
-//     //                         markdown::Link::Path { path } => {
-//     //                             if let Some(workspace) = &workspace {
-//     //                                 _ = workspace.update(cx, |workspace, cx| {
-//     //                                     workspace.open_abs_path(path.clone(), false, cx).detach();
-//     //                                 });
-//     //                             }
-//     //                         }
-//     //                     }),
-//     //             );
-//     //         }
-
-//     //         if region.code {
-//     //             cx.draw_quad(Quad {
-//     //                 bounds,
-//     //                 background: Some(code_span_background_color),
-//     //                 corner_radii: (2.0).into(),
-//     //                 order: todo!(),
-//     //                 content_mask: todo!(),
-//     //                 border_color: todo!(),
-//     //                 border_widths: todo!(),
-//     //             });
-//     //         }
-//     //     })
-//     //     .with_soft_wrap(true)
-// }
+pub fn render_parsed_markdown(
+    element_id: impl Into<ElementId>,
+    parsed: &language::ParsedMarkdown,
+    editor_style: &EditorStyle,
+    workspace: Option<WeakView<Workspace>>,
+    cx: &mut ViewContext<Editor>,
+) -> InteractiveText {
+    let code_span_background_color = cx
+        .theme()
+        .colors()
+        .editor_document_highlight_read_background;
+
+    let highlights = gpui::combine_highlights(
+        parsed.highlights.iter().filter_map(|(range, highlight)| {
+            let highlight = highlight.to_highlight_style(&editor_style.syntax)?;
+            Some((range.clone(), highlight))
+        }),
+        parsed
+            .regions
+            .iter()
+            .zip(&parsed.region_ranges)
+            .filter_map(|(region, range)| {
+                if region.code {
+                    Some((
+                        range.clone(),
+                        HighlightStyle {
+                            background_color: Some(code_span_background_color),
+                            ..Default::default()
+                        },
+                    ))
+                } else {
+                    None
+                }
+            }),
+    );
+    let runs = text_runs_for_highlights(&parsed.text, &editor_style.text, highlights);
+
+    // todo!("add the ability to change cursor style for link ranges")
+    let mut links = Vec::new();
+    let mut link_ranges = Vec::new();
+    for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) {
+        if let Some(link) = region.link.clone() {
+            links.push(link);
+            link_ranges.push(range.clone());
+        }
+    }
+
+    InteractiveText::new(
+        element_id,
+        StyledText::new(parsed.text.clone()).with_runs(runs),
+    )
+    .on_click(link_ranges, move |clicked_range_ix, cx| {
+        match &links[clicked_range_ix] {
+            markdown::Link::Web { url } => cx.open_url(url),
+            markdown::Link::Path { path } => {
+                if let Some(workspace) = &workspace {
+                    _ = workspace.update(cx, |workspace, cx| {
+                        workspace.open_abs_path(path.clone(), false, cx).detach();
+                    });
+                }
+            }
+        }
+    })
+}
 
 #[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectNext {
@@ -905,12 +906,16 @@ impl ContextMenu {
         &self,
         cursor_position: DisplayPoint,
         style: &EditorStyle,
+        max_height: Pixels,
         workspace: Option<WeakView<Workspace>>,
         cx: &mut ViewContext<Editor>,
     ) -> (DisplayPoint, AnyElement) {
         match self {
-            ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)),
-            ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx),
+            ContextMenu::Completions(menu) => (
+                cursor_position,
+                menu.render(style, max_height, workspace, cx),
+            ),
+            ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, max_height, cx),
         }
     }
 }
@@ -1222,6 +1227,7 @@ impl CompletionsMenu {
     fn render(
         &self,
         style: &EditorStyle,
+        max_height: Pixels,
         workspace: Option<WeakView<Workspace>>,
         cx: &mut ViewContext<Editor>,
     ) -> AnyElement {
@@ -1251,7 +1257,28 @@ impl CompletionsMenu {
         let completions = self.completions.clone();
         let matches = self.matches.clone();
         let selected_item = self.selected_item;
+        let style = style.clone();
 
+        let multiline_docs = {
+            let mat = &self.matches[selected_item];
+            let multiline_docs = match &self.completions.read()[mat.candidate_id].documentation {
+                Some(Documentation::MultiLinePlainText(text)) => {
+                    Some(div().child(SharedString::from(text.clone())))
+                }
+                Some(Documentation::MultiLineMarkdown(parsed)) => Some(div().child(
+                    render_parsed_markdown("completions_markdown", parsed, &style, workspace, cx),
+                )),
+                _ => None,
+            };
+            multiline_docs.map(|div| {
+                div.id("multiline_docs")
+                    .max_h(max_height)
+                    .overflow_y_scroll()
+                    // Prevent a mouse down on documentation from being propagated to the editor,
+                    // because that would move the cursor.
+                    .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
+            })
+        };
         let list = uniform_list(
             cx.view().clone(),
             "completions",
@@ -1274,151 +1301,69 @@ impl CompletionsMenu {
                             &None
                         };
 
-                        // todo!("highlights")
-                        // let highlights = combine_syntax_and_fuzzy_match_highlights(
-                        //     &completion.label.text,
-                        //     style.text.color.into(),
-                        //     styled_runs_for_code_label(&completion.label, &style.syntax),
-                        //     &mat.positions,
-                        // )
-
-                        // todo!("documentation")
-                        // MouseEventHandler::new::<CompletionTag, _>(mat.candidate_id, cx, |state, _| {
-                        //     let completion_label = HighlightedLabel::new(
-                        //         completion.label.text.clone(),
-                        //         combine_syntax_and_fuzzy_match_highlights(
-                        //             &completion.label.text,
-                        //             style.text.color.into(),
-                        //             styled_runs_for_code_label(&completion.label, &style.syntax),
-                        //             &mat.positions,
-                        //         ),
-                        //     );
-                        //     Text::new(completion.label.text.clone(), style.text.clone())
-                        //         .with_soft_wrap(false)
-                        //         .with_highlights();
-
-                        //     if let Some(Documentation::SingleLine(text)) = documentation {
-                        //         h_stack()
-                        //             .child(completion_label)
-                        //             .with_children((|| {
-                        //                 let text_style = TextStyle {
-                        //                     color: style.autocomplete.inline_docs_color,
-                        //                     font_size: style.text.font_size
-                        //                         * style.autocomplete.inline_docs_size_percent,
-                        //                     ..style.text.clone()
-                        //                 };
-
-                        //                 let label = Text::new(text.clone(), text_style)
-                        //                     .aligned()
-                        //                     .constrained()
-                        //                     .dynamically(move |constraint, _, _| gpui::SizeConstraint {
-                        //                         min: constraint.min,
-                        //                         max: vec2f(constraint.max.x(), constraint.min.y()),
-                        //                     });
-
-                        //                 if Some(item_ix) == widest_completion_ix {
-                        //                     Some(
-                        //                         label
-                        //                             .contained()
-                        //                             .with_style(style.autocomplete.inline_docs_container)
-                        //                             .into_any(),
-                        //                     )
-                        //                 } else {
-                        //                     Some(label.flex_float().into_any())
-                        //                 }
-                        //             })())
-                        //             .into_any()
-                        //     } else {
-                        //         completion_label.into_any()
-                        //     }
-                        //     .contained()
-                        //     .with_style(item_style)
-                        //     .constrained()
-                        //     .dynamically(move |constraint, _, _| {
-                        //         if Some(item_ix) == widest_completion_ix {
-                        //             constraint
-                        //         } else {
-                        //             gpui::SizeConstraint {
-                        //                 min: constraint.min,
-                        //                 max: constraint.min,
-                        //             }
-                        //         }
-                        //     })
-                        // })
-                        // .with_cursor_style(CursorStyle::PointingHand)
-                        // .on_down(MouseButton::Left, move |_, this, cx| {
-                        //     this.confirm_completion(
-                        //         &ConfirmCompletion {
-                        //             item_ix: Some(item_ix),
-                        //         },
-                        //         cx,
-                        //     )
-                        //     .map(|task| task.detach());
-                        // })
-                        // .constrained()
-                        //
+                        let highlights = gpui::combine_highlights(
+                            mat.ranges().map(|range| (range, FontWeight::BOLD.into())),
+                            styled_runs_for_code_label(&completion.label, &style.syntax).map(
+                                |(range, mut highlight)| {
+                                    // Ignore font weight for syntax highlighting, as we'll use it
+                                    // for fuzzy matches.
+                                    highlight.font_weight = None;
+                                    (range, highlight)
+                                },
+                            ),
+                        );
+                        let completion_label = StyledText::new(completion.label.text.clone())
+                            .with_runs(text_runs_for_highlights(
+                                &completion.label.text,
+                                &style.text,
+                                highlights,
+                            ));
+                        let documentation_label =
+                            if let Some(Documentation::SingleLine(text)) = documentation {
+                                Some(SharedString::from(text.clone()))
+                            } else {
+                                None
+                            };
+
                         div()
                             .id(mat.candidate_id)
+                            .min_w(px(300.))
+                            .max_w(px(700.))
                             .whitespace_nowrap()
                             .overflow_hidden()
                             .bg(gpui::green())
                             .hover(|style| style.bg(gpui::blue()))
                             .when(item_ix == selected_item, |div| div.bg(gpui::red()))
-                            .child(SharedString::from(completion.label.text.clone()))
-                            .min_w(px(300.))
-                            .max_w(px(700.))
+                            .on_mouse_down(
+                                MouseButton::Left,
+                                cx.listener(move |editor, event, cx| {
+                                    cx.stop_propagation();
+                                    editor
+                                        .confirm_completion(
+                                            &ConfirmCompletion {
+                                                item_ix: Some(item_ix),
+                                            },
+                                            cx,
+                                        )
+                                        .map(|task| task.detach_and_log_err(cx));
+                                }),
+                            )
+                            .child(completion_label)
+                            .children(documentation_label)
                     })
                     .collect()
             },
         )
+        .max_h(max_height)
         .track_scroll(self.scroll_handle.clone())
         .with_width_from_item(widest_completion_ix);
 
-        list.into_any_element()
-        // todo!("multiline documentation")
-        //     enum MultiLineDocumentation {}
-
-        //     Flex::row()
-        //         .with_child(list.flex(1., false))
-        //         .with_children({
-        //             let mat = &self.matches[selected_item];
-        //             let completions = self.completions.read();
-        //             let completion = &completions[mat.candidate_id];
-        //             let documentation = &completion.documentation;
-
-        //             match documentation {
-        //                 Some(Documentation::MultiLinePlainText(text)) => Some(
-        //                     Flex::column()
-        //                         .scrollable::<MultiLineDocumentation>(0, None, cx)
-        //                         .with_child(
-        //                             Text::new(text.clone(), style.text.clone()).with_soft_wrap(true),
-        //                         )
-        //                         .contained()
-        //                         .with_style(style.autocomplete.alongside_docs_container)
-        //                         .constrained()
-        //                         .with_max_width(style.autocomplete.alongside_docs_max_width)
-        //                         .flex(1., false),
-        //                 ),
-
-        //                 Some(Documentation::MultiLineMarkdown(parsed)) => Some(
-        //                     Flex::column()
-        //                         .scrollable::<MultiLineDocumentation>(0, None, cx)
-        //                         .with_child(render_parsed_markdown::<MultiLineDocumentation>(
-        //                             parsed, &style, workspace, cx,
-        //                         ))
-        //                         .contained()
-        //                         .with_style(style.autocomplete.alongside_docs_container)
-        //                         .constrained()
-        //                         .with_max_width(style.autocomplete.alongside_docs_max_width)
-        //                         .flex(1., false),
-        //                 ),
-
-        //                 _ => None,
-        //             }
-        //         })
-        //         .contained()
-        //         .with_style(style.autocomplete.container)
-        //         .into_any()
+        Popover::new()
+            .child(list)
+            .when_some(multiline_docs, |popover, multiline_docs| {
+                popover.aside(multiline_docs)
+            })
+            .into_any_element()
     }
 
     pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
@@ -1535,6 +1480,7 @@ impl CodeActionsMenu {
         &self,
         mut cursor_position: DisplayPoint,
         style: &EditorStyle,
+        max_height: Pixels,
         cx: &mut ViewContext<Editor>,
     ) -> (DisplayPoint, AnyElement) {
         let actions = self.actions.clone();
@@ -1589,6 +1535,7 @@ impl CodeActionsMenu {
         .elevation_1(cx)
         .px_2()
         .py_1()
+        .max_h(max_height)
         .track_scroll(self.scroll_handle.clone())
         .with_width_from_item(
             self.actions
@@ -3695,135 +3642,135 @@ impl Editor {
         self.completion_tasks.push((id, task));
     }
 
-    //     pub fn confirm_completion(
-    //         &mut self,
-    //         action: &ConfirmCompletion,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         use language::ToOffset as _;
+    pub fn confirm_completion(
+        &mut self,
+        action: &ConfirmCompletion,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        use language::ToOffset as _;
 
-    //         let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
-    //             menu
-    //         } else {
-    //             return None;
-    //         };
+        let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
+            menu
+        } else {
+            return None;
+        };
 
-    //         let mat = completions_menu
-    //             .matches
-    //             .get(action.item_ix.unwrap_or(completions_menu.selected_item))?;
-    //         let buffer_handle = completions_menu.buffer;
-    //         let completions = completions_menu.completions.read();
-    //         let completion = completions.get(mat.candidate_id)?;
-
-    //         let snippet;
-    //         let text;
-    //         if completion.is_snippet() {
-    //             snippet = Some(Snippet::parse(&completion.new_text).log_err()?);
-    //             text = snippet.as_ref().unwrap().text.clone();
-    //         } else {
-    //             snippet = None;
-    //             text = completion.new_text.clone();
-    //         };
-    //         let selections = self.selections.all::<usize>(cx);
-    //         let buffer = buffer_handle.read(cx);
-    //         let old_range = completion.old_range.to_offset(buffer);
-    //         let old_text = buffer.text_for_range(old_range.clone()).collect::<String>();
-
-    //         let newest_selection = self.selections.newest_anchor();
-    //         if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) {
-    //             return None;
-    //         }
+        let mat = completions_menu
+            .matches
+            .get(action.item_ix.unwrap_or(completions_menu.selected_item))?;
+        let buffer_handle = completions_menu.buffer;
+        let completions = completions_menu.completions.read();
+        let completion = completions.get(mat.candidate_id)?;
+
+        let snippet;
+        let text;
+        if completion.is_snippet() {
+            snippet = Some(Snippet::parse(&completion.new_text).log_err()?);
+            text = snippet.as_ref().unwrap().text.clone();
+        } else {
+            snippet = None;
+            text = completion.new_text.clone();
+        };
+        let selections = self.selections.all::<usize>(cx);
+        let buffer = buffer_handle.read(cx);
+        let old_range = completion.old_range.to_offset(buffer);
+        let old_text = buffer.text_for_range(old_range.clone()).collect::<String>();
 
-    //         let lookbehind = newest_selection
-    //             .start
-    //             .text_anchor
-    //             .to_offset(buffer)
-    //             .saturating_sub(old_range.start);
-    //         let lookahead = old_range
-    //             .end
-    //             .saturating_sub(newest_selection.end.text_anchor.to_offset(buffer));
-    //         let mut common_prefix_len = old_text
-    //             .bytes()
-    //             .zip(text.bytes())
-    //             .take_while(|(a, b)| a == b)
-    //             .count();
-
-    //         let snapshot = self.buffer.read(cx).snapshot(cx);
-    //         let mut range_to_replace: Option<Range<isize>> = None;
-    //         let mut ranges = Vec::new();
-    //         for selection in &selections {
-    //             if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) {
-    //                 let start = selection.start.saturating_sub(lookbehind);
-    //                 let end = selection.end + lookahead;
-    //                 if selection.id == newest_selection.id {
-    //                     range_to_replace = Some(
-    //                         ((start + common_prefix_len) as isize - selection.start as isize)
-    //                             ..(end as isize - selection.start as isize),
-    //                     );
-    //                 }
-    //                 ranges.push(start + common_prefix_len..end);
-    //             } else {
-    //                 common_prefix_len = 0;
-    //                 ranges.clear();
-    //                 ranges.extend(selections.iter().map(|s| {
-    //                     if s.id == newest_selection.id {
-    //                         range_to_replace = Some(
-    //                             old_range.start.to_offset_utf16(&snapshot).0 as isize
-    //                                 - selection.start as isize
-    //                                 ..old_range.end.to_offset_utf16(&snapshot).0 as isize
-    //                                     - selection.start as isize,
-    //                         );
-    //                         old_range.clone()
-    //                     } else {
-    //                         s.start..s.end
-    //                     }
-    //                 }));
-    //                 break;
-    //             }
-    //         }
-    //         let text = &text[common_prefix_len..];
+        let newest_selection = self.selections.newest_anchor();
+        if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) {
+            return None;
+        }
 
-    //         cx.emit(Event::InputHandled {
-    //             utf16_range_to_replace: range_to_replace,
-    //             text: text.into(),
-    //         });
+        let lookbehind = newest_selection
+            .start
+            .text_anchor
+            .to_offset(buffer)
+            .saturating_sub(old_range.start);
+        let lookahead = old_range
+            .end
+            .saturating_sub(newest_selection.end.text_anchor.to_offset(buffer));
+        let mut common_prefix_len = old_text
+            .bytes()
+            .zip(text.bytes())
+            .take_while(|(a, b)| a == b)
+            .count();
 
-    //         self.transact(cx, |this, cx| {
-    //             if let Some(mut snippet) = snippet {
-    //                 snippet.text = text.to_string();
-    //                 for tabstop in snippet.tabstops.iter_mut().flatten() {
-    //                     tabstop.start -= common_prefix_len as isize;
-    //                     tabstop.end -= common_prefix_len as isize;
-    //                 }
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let mut range_to_replace: Option<Range<isize>> = None;
+        let mut ranges = Vec::new();
+        for selection in &selections {
+            if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) {
+                let start = selection.start.saturating_sub(lookbehind);
+                let end = selection.end + lookahead;
+                if selection.id == newest_selection.id {
+                    range_to_replace = Some(
+                        ((start + common_prefix_len) as isize - selection.start as isize)
+                            ..(end as isize - selection.start as isize),
+                    );
+                }
+                ranges.push(start + common_prefix_len..end);
+            } else {
+                common_prefix_len = 0;
+                ranges.clear();
+                ranges.extend(selections.iter().map(|s| {
+                    if s.id == newest_selection.id {
+                        range_to_replace = Some(
+                            old_range.start.to_offset_utf16(&snapshot).0 as isize
+                                - selection.start as isize
+                                ..old_range.end.to_offset_utf16(&snapshot).0 as isize
+                                    - selection.start as isize,
+                        );
+                        old_range.clone()
+                    } else {
+                        s.start..s.end
+                    }
+                }));
+                break;
+            }
+        }
+        let text = &text[common_prefix_len..];
 
-    //                 this.insert_snippet(&ranges, snippet, cx).log_err();
-    //             } else {
-    //                 this.buffer.update(cx, |buffer, cx| {
-    //                     buffer.edit(
-    //                         ranges.iter().map(|range| (range.clone(), text)),
-    //                         this.autoindent_mode.clone(),
-    //                         cx,
-    //                     );
-    //                 });
-    //             }
+        cx.emit(EditorEvent::InputHandled {
+            utf16_range_to_replace: range_to_replace,
+            text: text.into(),
+        });
 
-    //             this.refresh_copilot_suggestions(true, cx);
-    //         });
+        self.transact(cx, |this, cx| {
+            if let Some(mut snippet) = snippet {
+                snippet.text = text.to_string();
+                for tabstop in snippet.tabstops.iter_mut().flatten() {
+                    tabstop.start -= common_prefix_len as isize;
+                    tabstop.end -= common_prefix_len as isize;
+                }
 
-    //         let project = self.project.clone()?;
-    //         let apply_edits = project.update(cx, |project, cx| {
-    //             project.apply_additional_edits_for_completion(
-    //                 buffer_handle,
-    //                 completion.clone(),
-    //                 true,
-    //                 cx,
-    //             )
-    //         });
-    //         Some(cx.foreground().spawn(async move {
-    //             apply_edits.await?;
-    //             Ok(())
-    //         }))
-    //     }
+                this.insert_snippet(&ranges, snippet, cx).log_err();
+            } else {
+                this.buffer.update(cx, |buffer, cx| {
+                    buffer.edit(
+                        ranges.iter().map(|range| (range.clone(), text)),
+                        this.autoindent_mode.clone(),
+                        cx,
+                    );
+                });
+            }
+
+            this.refresh_copilot_suggestions(true, cx);
+        });
+
+        let project = self.project.clone()?;
+        let apply_edits = project.update(cx, |project, cx| {
+            project.apply_additional_edits_for_completion(
+                buffer_handle,
+                completion.clone(),
+                true,
+                cx,
+            )
+        });
+        Some(cx.foreground_executor().spawn(async move {
+            apply_edits.await?;
+            Ok(())
+        }))
+    }
 
     pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
         let mut context_menu = self.context_menu.write();
@@ -4446,12 +4393,14 @@ impl Editor {
         &self,
         cursor_position: DisplayPoint,
         style: &EditorStyle,
+        max_height: Pixels,
         cx: &mut ViewContext<Editor>,
     ) -> Option<(DisplayPoint, AnyElement)> {
         self.context_menu.read().as_ref().map(|menu| {
             menu.render(
                 cursor_position,
                 style,
+                max_height,
                 self.workspace.as_ref().map(|(w, _)| w.clone()),
                 cx,
             )
@@ -10078,75 +10027,29 @@ pub fn diagnostic_style(
     }
 }
 
-pub fn combine_syntax_and_fuzzy_match_highlights(
+pub fn text_runs_for_highlights(
     text: &str,
-    default_style: HighlightStyle,
-    syntax_ranges: impl Iterator<Item = (Range<usize>, HighlightStyle)>,
-    match_indices: &[usize],
-) -> Vec<(Range<usize>, HighlightStyle)> {
-    let mut result = Vec::new();
-    let mut match_indices = match_indices.iter().copied().peekable();
-
-    for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())])
-    {
-        syntax_highlight.font_weight = None;
-
-        // Add highlights for any fuzzy match characters before the next
-        // syntax highlight range.
-        while let Some(&match_index) = match_indices.peek() {
-            if match_index >= range.start {
-                break;
-            }
-            match_indices.next();
-            let end_index = char_ix_after(match_index, text);
-            let mut match_style = default_style;
-            match_style.font_weight = Some(FontWeight::BOLD);
-            result.push((match_index..end_index, match_style));
-        }
-
-        if range.start == usize::MAX {
-            break;
-        }
-
-        // Add highlights for any fuzzy match characters within the
-        // syntax highlight range.
-        let mut offset = range.start;
-        while let Some(&match_index) = match_indices.peek() {
-            if match_index >= range.end {
-                break;
-            }
-
-            match_indices.next();
-            if match_index > offset {
-                result.push((offset..match_index, syntax_highlight));
-            }
-
-            let mut end_index = char_ix_after(match_index, text);
-            while let Some(&next_match_index) = match_indices.peek() {
-                if next_match_index == end_index && next_match_index < range.end {
-                    end_index = char_ix_after(next_match_index, text);
-                    match_indices.next();
-                } else {
-                    break;
-                }
-            }
-
-            let mut match_style = syntax_highlight;
-            match_style.font_weight = Some(FontWeight::BOLD);
-            result.push((match_index..end_index, match_style));
-            offset = end_index;
-        }
-
-        if offset < range.end {
-            result.push((offset..range.end, syntax_highlight));
-        }
+    default_style: &TextStyle,
+    highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
+) -> Vec<TextRun> {
+    let mut runs = Vec::new();
+    let mut ix = 0;
+    for (range, highlight) in highlights {
+        if ix < range.start {
+            runs.push(default_style.clone().to_run(range.start - ix));
+        }
+        runs.push(
+            default_style
+                .clone()
+                .highlight(highlight)
+                .to_run(range.len()),
+        );
+        ix = range.end;
     }
-
-    fn char_ix_after(ix: usize, text: &str) -> usize {
-        ix + text[ix..].chars().next().unwrap().len_utf8()
+    if ix < text.len() {
+        runs.push(default_style.to_run(text.len() - ix));
     }
-
-    result
+    runs
 }
 
 pub fn styled_runs_for_code_label<'a>(

crates/editor2/src/editor_tests.rs 🔗

@@ -6740,75 +6740,6 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
 //     );
 // }
 
-#[test]
-fn test_combine_syntax_and_fuzzy_match_highlights() {
-    let string = "abcdefghijklmnop";
-    let syntax_ranges = [
-        (
-            0..3,
-            HighlightStyle {
-                color: Some(Hsla::red()),
-                ..Default::default()
-            },
-        ),
-        (
-            4..8,
-            HighlightStyle {
-                color: Some(Hsla::green()),
-                ..Default::default()
-            },
-        ),
-    ];
-    let match_indices = [4, 6, 7, 8];
-    assert_eq!(
-        combine_syntax_and_fuzzy_match_highlights(
-            string,
-            Default::default(),
-            syntax_ranges.into_iter(),
-            &match_indices,
-        ),
-        &[
-            (
-                0..3,
-                HighlightStyle {
-                    color: Some(Hsla::red()),
-                    ..Default::default()
-                },
-            ),
-            (
-                4..5,
-                HighlightStyle {
-                    color: Some(Hsla::green()),
-                    font_weight: Some(gpui::FontWeight::BOLD),
-                    ..Default::default()
-                },
-            ),
-            (
-                5..6,
-                HighlightStyle {
-                    color: Some(Hsla::green()),
-                    ..Default::default()
-                },
-            ),
-            (
-                6..8,
-                HighlightStyle {
-                    color: Some(Hsla::green()),
-                    font_weight: Some(gpui::FontWeight::BOLD),
-                    ..Default::default()
-                },
-            ),
-            (
-                8..9,
-                HighlightStyle {
-                    font_weight: Some(gpui::FontWeight::BOLD),
-                    ..Default::default()
-                },
-            ),
-        ]
-    );
-}
-
 #[gpui::test]
 async fn go_to_prev_overlapping_diagnostic(
     executor: BackgroundExecutor,

crates/editor2/src/element.rs 🔗

@@ -268,7 +268,11 @@ impl EditorElement {
         });
         register_action(view, cx, Editor::restart_language_server);
         register_action(view, cx, Editor::show_character_palette);
-        // on_action(cx, Editor::confirm_completion); todo!()
+        register_action(view, cx, |editor, action, cx| {
+            editor
+                .confirm_completion(action, cx)
+                .map(|task| task.detach_and_log_err(cx));
+        });
         register_action(view, cx, |editor, action, cx| {
             editor
                 .confirm_code_action(action, cx)
@@ -1022,14 +1026,8 @@ impl EditorElement {
 
                 if let Some((position, mut context_menu)) = layout.context_menu.take() {
                     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((text_bounds.size.height - line_height) / 2.),
-                            ),
-                        );
+                        let available_space =
+                            size(AvailableSpace::MinContent, AvailableSpace::MinContent);
                         let context_menu_size = context_menu.measure(available_space, cx);
 
                         let cursor_row_layout = &layout.position_map.line_layouts
@@ -1974,8 +1972,9 @@ impl EditorElement {
             if let Some(newest_selection_head) = newest_selection_head {
                 if (start_row..end_row).contains(&newest_selection_head.row()) {
                     if editor.context_menu_visible() {
+                        let max_height = (12. * line_height).min((bounds.size.height - line_height) / 2.);
                         context_menu =
-                            editor.render_context_menu(newest_selection_head, &self.style, cx);
+                            editor.render_context_menu(newest_selection_head, &self.style, max_height, cx);
                     }
 
                     let active = matches!(

crates/fuzzy2/src/strings.rs 🔗

@@ -6,6 +6,8 @@ use gpui::BackgroundExecutor;
 use std::{
     borrow::Cow,
     cmp::{self, Ordering},
+    iter,
+    ops::Range,
     sync::atomic::AtomicBool,
 };
 
@@ -54,6 +56,32 @@ pub struct StringMatch {
     pub string: String,
 }
 
+impl StringMatch {
+    pub fn ranges<'a>(&'a self) -> impl 'a + Iterator<Item = Range<usize>> {
+        let mut positions = self.positions.iter().peekable();
+        iter::from_fn(move || {
+            while let Some(start) = positions.next().copied() {
+                let mut end = start + self.char_len_at_index(start);
+                while let Some(next_start) = positions.peek() {
+                    if end == **next_start {
+                        end += self.char_len_at_index(end);
+                        positions.next();
+                    } else {
+                        break;
+                    }
+                }
+
+                return Some(start..end);
+            }
+            None
+        })
+    }
+
+    fn char_len_at_index(&self, ix: usize) -> usize {
+        self.string[ix..].chars().next().unwrap().len_utf8()
+    }
+}
+
 impl PartialEq for StringMatch {
     fn eq(&self, other: &Self) -> bool {
         self.cmp(other).is_eq()

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

@@ -244,13 +244,17 @@ impl TextState {
 
         let line_height = element_state.line_height;
         let mut line_origin = bounds.origin;
+        let mut line_start_ix = 0;
         for line in &element_state.lines {
             let line_bottom = line_origin.y + line.size(line_height).height;
             if position.y > line_bottom {
                 line_origin.y = line_bottom;
+                line_start_ix += line.len() + 1;
             } else {
                 let position_within_line = position - line_origin;
-                return line.index_for_position(position_within_line, line_height);
+                let index_within_line =
+                    line.index_for_position(position_within_line, line_height)?;
+                return Some(line_start_ix + index_within_line);
             }
         }
 

crates/gpui2/src/style.rs 🔗

@@ -1,9 +1,12 @@
+use std::{iter, mem, ops::Range};
+
 use crate::{
     black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
     Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
     FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
     SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
 };
+use collections::HashSet;
 use refineable::{Cascade, Refineable};
 use smallvec::SmallVec;
 pub use taffy::style::{
@@ -168,7 +171,8 @@ impl Default for TextStyle {
 }
 
 impl TextStyle {
-    pub fn highlight(mut self, style: HighlightStyle) -> Self {
+    pub fn highlight(mut self, style: impl Into<HighlightStyle>) -> Self {
+        let style = style.into();
         if let Some(weight) = style.font_weight {
             self.font_weight = weight;
         }
@@ -502,6 +506,24 @@ impl From<Hsla> for HighlightStyle {
     }
 }
 
+impl From<FontWeight> for HighlightStyle {
+    fn from(font_weight: FontWeight) -> Self {
+        Self {
+            font_weight: Some(font_weight),
+            ..Default::default()
+        }
+    }
+}
+
+impl From<FontStyle> for HighlightStyle {
+    fn from(font_style: FontStyle) -> Self {
+        Self {
+            font_style: Some(font_style),
+            ..Default::default()
+        }
+    }
+}
+
 impl From<Rgba> for HighlightStyle {
     fn from(color: Rgba) -> Self {
         Self {
@@ -510,3 +532,140 @@ impl From<Rgba> for HighlightStyle {
         }
     }
 }
+
+pub fn combine_highlights(
+    a: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
+    b: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
+) -> impl Iterator<Item = (Range<usize>, HighlightStyle)> {
+    let mut endpoints = Vec::new();
+    let mut highlights = Vec::new();
+    for (range, highlight) in a.into_iter().chain(b) {
+        if !range.is_empty() {
+            let highlight_id = highlights.len();
+            endpoints.push((range.start, highlight_id, true));
+            endpoints.push((range.end, highlight_id, false));
+            highlights.push(highlight);
+        }
+    }
+    endpoints.sort_unstable_by_key(|(position, _, _)| *position);
+    let mut endpoints = endpoints.into_iter().peekable();
+
+    let mut active_styles = HashSet::default();
+    let mut ix = 0;
+    iter::from_fn(move || {
+        while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() {
+            let prev_index = mem::replace(&mut ix, *endpoint_ix);
+            if ix > prev_index && !active_styles.is_empty() {
+                let mut current_style = HighlightStyle::default();
+                for highlight_id in &active_styles {
+                    current_style.highlight(highlights[*highlight_id]);
+                }
+                return Some((prev_index..ix, current_style));
+            }
+
+            if *is_start {
+                active_styles.insert(*highlight_id);
+            } else {
+                active_styles.remove(highlight_id);
+            }
+            endpoints.next();
+        }
+        None
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::{blue, green, red, yellow};
+
+    use super::*;
+
+    #[test]
+    fn test_combine_highlights() {
+        assert_eq!(
+            combine_highlights(
+                [
+                    (0..5, green().into()),
+                    (4..10, FontWeight::BOLD.into()),
+                    (15..20, yellow().into()),
+                ],
+                [
+                    (2..6, FontStyle::Italic.into()),
+                    (1..3, blue().into()),
+                    (21..23, red().into()),
+                ]
+            )
+            .collect::<Vec<_>>(),
+            [
+                (
+                    0..1,
+                    HighlightStyle {
+                        color: Some(green()),
+                        ..Default::default()
+                    }
+                ),
+                (
+                    1..2,
+                    HighlightStyle {
+                        color: Some(blue()),
+                        ..Default::default()
+                    }
+                ),
+                (
+                    2..3,
+                    HighlightStyle {
+                        color: Some(blue()),
+                        font_style: Some(FontStyle::Italic),
+                        ..Default::default()
+                    }
+                ),
+                (
+                    3..4,
+                    HighlightStyle {
+                        color: Some(green()),
+                        font_style: Some(FontStyle::Italic),
+                        ..Default::default()
+                    }
+                ),
+                (
+                    4..5,
+                    HighlightStyle {
+                        color: Some(green()),
+                        font_weight: Some(FontWeight::BOLD),
+                        font_style: Some(FontStyle::Italic),
+                        ..Default::default()
+                    }
+                ),
+                (
+                    5..6,
+                    HighlightStyle {
+                        font_weight: Some(FontWeight::BOLD),
+                        font_style: Some(FontStyle::Italic),
+                        ..Default::default()
+                    }
+                ),
+                (
+                    6..10,
+                    HighlightStyle {
+                        font_weight: Some(FontWeight::BOLD),
+                        ..Default::default()
+                    }
+                ),
+                (
+                    15..20,
+                    HighlightStyle {
+                        color: Some(yellow()),
+                        ..Default::default()
+                    }
+                ),
+                (
+                    21..23,
+                    HighlightStyle {
+                        color: Some(red()),
+                        ..Default::default()
+                    }
+                )
+            ]
+        );
+    }
+}

crates/gpui2/src/text_system.rs 🔗

@@ -290,7 +290,15 @@ impl TextSystem {
                 text: SharedString::from(line_text),
             });
 
-            line_start = line_end + 1; // Skip `\n` character.
+            // Skip `\n` character.
+            line_start = line_end + 1;
+            if let Some(run) = runs.peek_mut() {
+                run.len = run.len.saturating_sub(1);
+                if run.len == 0 {
+                    runs.next();
+                }
+            }
+
             font_runs.clear();
         }
 

crates/gpui2/src/text_system/line.rs 🔗

@@ -40,7 +40,6 @@ impl ShapedLine {
             &self.layout,
             line_height,
             &self.decoration_runs,
-            None,
             &[],
             cx,
         )?;
@@ -74,7 +73,6 @@ impl WrappedLine {
             &self.layout.unwrapped_layout,
             line_height,
             &self.decoration_runs,
-            self.wrap_width,
             &self.wrap_boundaries,
             cx,
         )?;
@@ -88,7 +86,6 @@ fn paint_line(
     layout: &LineLayout,
     line_height: Pixels,
     decoration_runs: &[DecorationRun],
-    wrap_width: Option<Pixels>,
     wrap_boundaries: &[WrapBoundary],
     cx: &mut WindowContext<'_>,
 ) -> Result<()> {
@@ -113,24 +110,28 @@ fn paint_line(
 
             if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
                 wraps.next();
-                if let Some((background_origin, background_color)) = current_background.take() {
+                if let Some((background_origin, background_color)) = current_background.as_mut() {
                     cx.paint_quad(
                         Bounds {
-                            origin: background_origin,
+                            origin: *background_origin,
                             size: size(glyph_origin.x - background_origin.x, line_height),
                         },
                         Corners::default(),
-                        background_color,
+                        *background_color,
                         Edges::default(),
                         transparent_black(),
                     );
+                    background_origin.x = origin.x;
+                    background_origin.y += line_height;
                 }
-                if let Some((underline_origin, underline_style)) = current_underline.take() {
+                if let Some((underline_origin, underline_style)) = current_underline.as_mut() {
                     cx.paint_underline(
-                        underline_origin,
+                        *underline_origin,
                         glyph_origin.x - underline_origin.x,
-                        &underline_style,
+                        underline_style,
                     );
+                    underline_origin.x = origin.x;
+                    underline_origin.y += line_height;
                 }
 
                 glyph_origin.x = origin.x;
@@ -149,7 +150,7 @@ fn paint_line(
                     }
                     if let Some(run_background) = style_run.background_color {
                         current_background
-                            .get_or_insert((point(glyph_origin.x, origin.y), run_background));
+                            .get_or_insert((point(glyph_origin.x, glyph_origin.y), run_background));
                     }
 
                     if let Some((_, underline_style)) = &mut current_underline {
@@ -161,7 +162,7 @@ fn paint_line(
                         current_underline.get_or_insert((
                             point(
                                 glyph_origin.x,
-                                origin.y + baseline_offset.y + (layout.descent * 0.618),
+                                glyph_origin.y + baseline_offset.y + (layout.descent * 0.618),
                             ),
                             UnderlineStyle {
                                 color: Some(run_underline.color.unwrap_or(style_run.color)),
@@ -228,12 +229,18 @@ fn paint_line(
         }
     }
 
+    let mut last_line_end_x = origin.x + layout.width;
+    if let Some(boundary) = wrap_boundaries.last() {
+        let run = &layout.runs[boundary.run_ix];
+        let glyph = &run.glyphs[boundary.glyph_ix];
+        last_line_end_x -= glyph.position.x;
+    }
+
     if let Some((background_origin, background_color)) = current_background.take() {
-        let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width);
         cx.paint_quad(
             Bounds {
                 origin: background_origin,
-                size: size(line_end_x - background_origin.x, line_height),
+                size: size(last_line_end_x - background_origin.x, line_height),
             },
             Corners::default(),
             background_color,
@@ -243,10 +250,9 @@ fn paint_line(
     }
 
     if let Some((underline_start, underline_style)) = current_underline.take() {
-        let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width);
         cx.paint_underline(
             underline_start,
-            line_end_x - underline_start.x,
+            last_line_end_x - underline_start.x,
             &underline_style,
         );
     }