Start on wiring up render_parsed_markdown

Antonio Scandurra created

Change summary

crates/editor2/src/editor.rs | 203 ++++++++++++++++---------------------
crates/fuzzy2/src/strings.rs |   2 
2 files changed, 90 insertions(+), 115 deletions(-)

Detailed changes

crates/editor2/src/editor.rs 🔗

@@ -40,7 +40,7 @@ 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, InteractiveText, KeyContext, Model, MouseButton, ParentElement, Pixels,
     Render, RenderOnce, SharedString, Styled, StyledText, Subscription, Task, TextRun, TextStyle,
@@ -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,71 +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>(
-//     element_id: impl Into<ElementId>,
-//     parsed: &language::ParsedMarkdown,
-//     editor_style: &EditorStyle,
-//     workspace: Option<WeakView<Workspace>>,
-//     cx: &mut ViewContext<Editor>,
-// ) -> InteractiveText {
-//     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 {
@@ -1254,6 +1254,18 @@ impl CompletionsMenu {
         let selected_item = self.selected_item;
         let style = style.clone();
 
+        let multiline_docs = {
+            let mat = &self.matches[selected_item];
+            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,
+            }
+        };
         let list = uniform_list(
             cx.view().clone(),
             "completions",
@@ -1332,51 +1344,12 @@ impl CompletionsMenu {
         .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.id("multiline_docs").overflow_y_scroll())
+            })
+            .into_any_element()
     }
 
     pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {

crates/fuzzy2/src/strings.rs 🔗

@@ -66,6 +66,8 @@ impl StringMatch {
                     if end == **next_start {
                         end += self.char_len_at_index(end);
                         positions.next();
+                    } else {
+                        break;
                     }
                 }