Selectable popover text (#12918)

Ephram , Conrad Irwin , and Antonio created

Release Notes:

- Fixed #5236
- Added the ability to select and copy text from information popovers



https://github.com/zed-industries/zed/assets/50590465/d5c86623-342b-474b-913e-d07cc3f76de4

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Antonio <ascii@zed.dev>

Change summary

Cargo.lock                                    |   1 
crates/editor/Cargo.toml                      |   1 
crates/editor/src/editor.rs                   |   5 
crates/editor/src/element.rs                  |   3 
crates/editor/src/hover_popover.rs            | 571 +++++++++-----------
crates/gpui/src/text_system/line.rs           |   8 
crates/markdown/examples/markdown.rs          |  82 +-
crates/markdown/examples/markdown_as_child.rs | 120 ++++
crates/markdown/src/markdown.rs               | 164 +++++
crates/markdown/src/parser.rs                 |  15 
crates/project/src/project.rs                 |   2 
crates/recent_projects/src/dev_servers.rs     |  24 
12 files changed, 596 insertions(+), 400 deletions(-)

Detailed changes

Cargo.lock ๐Ÿ”—

@@ -3605,6 +3605,7 @@ dependencies = [
  "linkify",
  "log",
  "lsp",
+ "markdown",
  "multi_buffer",
  "ordered-float 2.10.0",
  "parking_lot",

crates/editor/Cargo.toml ๐Ÿ”—

@@ -49,6 +49,7 @@ lazy_static.workspace = true
 linkify.workspace = true
 log.workspace = true
 lsp.workspace = true
+markdown.workspace = true
 multi_buffer.workspace = true
 ordered-float.workspace = true
 parking_lot.workspace = true

crates/editor/src/editor.rs ๐Ÿ”—

@@ -11604,8 +11604,11 @@ impl Editor {
         if let Some(blame) = self.blame.as_ref() {
             blame.update(cx, GitBlame::blur)
         }
+        if !self.hover_state.focused(cx) {
+            hide_hover(self, cx);
+        }
+
         self.hide_context_menu(cx);
-        hide_hover(self, cx);
         cx.emit(EditorEvent::Blurred);
         cx.notify();
     }

crates/editor/src/element.rs ๐Ÿ”—

@@ -3740,6 +3740,9 @@ impl EditorElement {
             move |event: &MouseMoveEvent, phase, cx| {
                 if phase == DispatchPhase::Bubble {
                     editor.update(cx, |editor, cx| {
+                        if editor.hover_state.focused(cx) {
+                            return;
+                        }
                         if event.pressed_button == Some(MouseButton::Left)
                             || event.pressed_button == Some(MouseButton::Middle)
                         {

crates/editor/src/hover_popover.rs ๐Ÿ”—

@@ -5,24 +5,26 @@ use crate::{
     Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
     EditorStyle, Hover, RangeToAnchorExt,
 };
-use futures::{stream::FuturesUnordered, FutureExt};
 use gpui::{
-    div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, MouseButton,
-    ParentElement, Pixels, ScrollHandle, SharedString, Size, StatefulInteractiveElement, Styled,
-    Task, ViewContext, WeakView,
+    div, px, AnyElement, AsyncWindowContext, CursorStyle, FontWeight, Hsla, InteractiveElement,
+    IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, SharedString, Size,
+    StatefulInteractiveElement, StyleRefinement, Styled, Task, TextStyleRefinement, View,
+    ViewContext, WeakView,
 };
-use language::{markdown, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
-
+use itertools::Itertools;
+use language::{DiagnosticEntry, Language, LanguageRegistry};
 use lsp::DiagnosticSeverity;
+use markdown::{Markdown, MarkdownStyle};
 use multi_buffer::ToOffset;
-use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
+use project::{HoverBlock, InlayHintLabelPart};
 use settings::Settings;
-use smol::stream::StreamExt;
+use std::rc::Rc;
+use std::{borrow::Cow, cell::RefCell};
 use std::{ops::Range, sync::Arc, time::Duration};
+use theme::ThemeSettings;
 use ui::{prelude::*, window_is_transparent, Tooltip};
 use util::TryFutureExt;
 use workspace::Workspace;
-
 pub const HOVER_DELAY_MILLIS: u64 = 350;
 pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
 
@@ -40,6 +42,9 @@ pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
 /// depending on whether a point to hover over is provided.
 pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContext<Editor>) {
     if EditorSettings::get_global(cx).hover_popover_enabled {
+        if show_keyboard_hover(editor, cx) {
+            return;
+        }
         if let Some(anchor) = anchor {
             show_hover(editor, anchor, false, cx);
         } else {
@@ -48,6 +53,20 @@ pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContex
     }
 }
 
+pub fn show_keyboard_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
+    let info_popovers = editor.hover_state.info_popovers.clone();
+    for p in info_popovers {
+        let keyboard_grace = p.keyboard_grace.borrow();
+        if *keyboard_grace {
+            if let Some(anchor) = p.anchor {
+                show_hover(editor, anchor, false, cx);
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
 pub struct InlayHover {
     pub range: InlayHighlight,
     pub tooltip: HoverBlock,
@@ -113,12 +132,14 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
 
                 let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
                 let blocks = vec![inlay_hover.tooltip];
-                let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
+                let parsed_content = parse_blocks(&blocks, &language_registry, None, &mut cx).await;
 
                 let hover_popover = InfoPopover {
                     symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
                     parsed_content,
                     scroll_handle: ScrollHandle::new(),
+                    keyboard_grace: Rc::new(RefCell::new(false)),
+                    anchor: None,
                 };
 
                 this.update(&mut cx, |this, cx| {
@@ -291,39 +312,40 @@ fn show_hover(
             let snapshot = this.update(&mut cx, |this, cx| this.snapshot(cx))?;
             let mut hover_highlights = Vec::with_capacity(hovers_response.len());
             let mut info_popovers = Vec::with_capacity(hovers_response.len());
-            let mut info_popover_tasks = hovers_response
-                .into_iter()
-                .map(|hover_result| async {
-                    // Create symbol range of anchors for highlighting and filtering of future requests.
-                    let range = hover_result
-                        .range
-                        .and_then(|range| {
-                            let start = snapshot
-                                .buffer_snapshot
-                                .anchor_in_excerpt(excerpt_id, range.start)?;
-                            let end = snapshot
-                                .buffer_snapshot
-                                .anchor_in_excerpt(excerpt_id, range.end)?;
-
-                            Some(start..end)
-                        })
-                        .unwrap_or_else(|| anchor..anchor);
-
-                    let blocks = hover_result.contents;
-                    let language = hover_result.language;
-                    let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
-
-                    (
-                        range.clone(),
-                        InfoPopover {
-                            symbol_range: RangeInEditor::Text(range),
-                            parsed_content,
-                            scroll_handle: ScrollHandle::new(),
-                        },
-                    )
-                })
-                .collect::<FuturesUnordered<_>>();
-            while let Some((highlight_range, info_popover)) = info_popover_tasks.next().await {
+            let mut info_popover_tasks = Vec::with_capacity(hovers_response.len());
+
+            for hover_result in hovers_response {
+                // Create symbol range of anchors for highlighting and filtering of future requests.
+                let range = hover_result
+                    .range
+                    .and_then(|range| {
+                        let start = snapshot
+                            .buffer_snapshot
+                            .anchor_in_excerpt(excerpt_id, range.start)?;
+                        let end = snapshot
+                            .buffer_snapshot
+                            .anchor_in_excerpt(excerpt_id, range.end)?;
+
+                        Some(start..end)
+                    })
+                    .unwrap_or_else(|| anchor..anchor);
+
+                let blocks = hover_result.contents;
+                let language = hover_result.language;
+                let parsed_content =
+                    parse_blocks(&blocks, &language_registry, language, &mut cx).await;
+                info_popover_tasks.push((
+                    range.clone(),
+                    InfoPopover {
+                        symbol_range: RangeInEditor::Text(range),
+                        parsed_content,
+                        scroll_handle: ScrollHandle::new(),
+                        keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
+                        anchor: Some(anchor),
+                    },
+                ));
+            }
+            for (highlight_range, info_popover) in info_popover_tasks {
                 hover_highlights.push(highlight_range);
                 info_popovers.push(info_popover);
             }
@@ -357,72 +379,81 @@ async fn parse_blocks(
     blocks: &[HoverBlock],
     language_registry: &Arc<LanguageRegistry>,
     language: Option<Arc<Language>>,
-) -> markdown::ParsedMarkdown {
-    let mut text = String::new();
-    let mut highlights = Vec::new();
-    let mut region_ranges = Vec::new();
-    let mut regions = Vec::new();
-
-    for block in blocks {
-        match &block.kind {
-            HoverBlockKind::PlainText => {
-                markdown::new_paragraph(&mut text, &mut Vec::new());
-                text.push_str(&block.text.replace("\\n", "\n"));
-            }
+    cx: &mut AsyncWindowContext,
+) -> Option<View<Markdown>> {
+    let fallback_language_name = if let Some(ref l) = language {
+        let l = Arc::clone(l);
+        Some(l.lsp_id().clone())
+    } else {
+        None
+    };
 
-            HoverBlockKind::Markdown => {
-                markdown::parse_markdown_block(
-                    &block.text.replace("\\n", "\n"),
-                    language_registry,
-                    language.clone(),
-                    &mut text,
-                    &mut highlights,
-                    &mut region_ranges,
-                    &mut regions,
-                )
-                .await
+    let combined_text = blocks
+        .iter()
+        .map(|block| match &block.kind {
+            project::HoverBlockKind::PlainText | project::HoverBlockKind::Markdown => {
+                Cow::Borrowed(block.text.trim())
             }
-
-            HoverBlockKind::Code { language } => {
-                if let Some(language) = language_registry
-                    .language_for_name(language)
-                    .now_or_never()
-                    .and_then(Result::ok)
-                {
-                    markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
-                } else {
-                    text.push_str(&block.text);
-                }
+            project::HoverBlockKind::Code { language } => {
+                Cow::Owned(format!("```{}\n{}\n```", language, block.text.trim()))
             }
-        }
-    }
+        })
+        .join("\n\n");
+
+    let rendered_block = cx
+        .new_view(|cx| {
+            let settings = ThemeSettings::get_global(cx);
+            let buffer_font_family = settings.buffer_font.family.clone();
+            let mut base_style = cx.text_style();
+            base_style.refine(&TextStyleRefinement {
+                font_family: Some(buffer_font_family.clone()),
+                color: Some(cx.theme().colors().editor_foreground),
+                ..Default::default()
+            });
 
-    let leading_space = text.chars().take_while(|c| c.is_whitespace()).count();
-    if leading_space > 0 {
-        highlights = highlights
-            .into_iter()
-            .map(|(range, style)| {
-                (
-                    range.start.saturating_sub(leading_space)
-                        ..range.end.saturating_sub(leading_space),
-                    style,
-                )
-            })
-            .collect();
-        region_ranges = region_ranges
-            .into_iter()
-            .map(|range| {
-                range.start.saturating_sub(leading_space)..range.end.saturating_sub(leading_space)
-            })
-            .collect();
-    }
+            let markdown_style = MarkdownStyle {
+                base_text_style: base_style,
+                code_block: StyleRefinement::default().mt(rems(1.)).mb(rems(1.)),
+                inline_code: TextStyleRefinement {
+                    background_color: Some(cx.theme().colors().background),
+                    ..Default::default()
+                },
+                rule_color: Color::Muted.color(cx),
+                block_quote_border_color: Color::Muted.color(cx),
+                block_quote: TextStyleRefinement {
+                    color: Some(Color::Muted.color(cx)),
+                    ..Default::default()
+                },
+                link: TextStyleRefinement {
+                    color: Some(cx.theme().colors().editor_foreground),
+                    underline: Some(gpui::UnderlineStyle {
+                        thickness: px(1.),
+                        color: Some(cx.theme().colors().editor_foreground),
+                        wavy: false,
+                    }),
+                    ..Default::default()
+                },
+                syntax: cx.theme().syntax().clone(),
+                selection_background_color: { cx.theme().players().local().selection },
+                break_style: Default::default(),
+                heading: StyleRefinement::default()
+                    .font_weight(FontWeight::BOLD)
+                    .text_base()
+                    .mt(rems(1.))
+                    .mb_0(),
+            };
 
-    ParsedMarkdown {
-        text: text.trim().to_string(),
-        highlights,
-        region_ranges,
-        regions,
-    }
+            Markdown::new(
+                combined_text,
+                markdown_style.clone(),
+                Some(language_registry.clone()),
+                cx,
+                fallback_language_name,
+            )
+        })
+        .ok();
+
+    rendered_block
 }
 
 #[derive(Default, Debug)]
@@ -444,7 +475,7 @@ impl HoverState {
         style: &EditorStyle,
         visible_rows: Range<DisplayRow>,
         max_size: Size<Pixels>,
-        workspace: Option<WeakView<Workspace>>,
+        _workspace: Option<WeakView<Workspace>>,
         cx: &mut ViewContext<Editor>,
     ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
         // If there is a diagnostic, position the popovers based on that.
@@ -482,29 +513,39 @@ impl HoverState {
             elements.push(diagnostic_popover.render(style, max_size, cx));
         }
         for info_popover in &mut self.info_popovers {
-            elements.push(info_popover.render(style, max_size, workspace.clone(), cx));
+            elements.push(info_popover.render(max_size, cx));
         }
 
         Some((point, elements))
     }
+
+    pub fn focused(&self, cx: &mut ViewContext<Editor>) -> bool {
+        let mut hover_popover_is_focused = false;
+        for info_popover in &self.info_popovers {
+            for markdown_view in &info_popover.parsed_content {
+                if markdown_view.focus_handle(cx).is_focused(cx) {
+                    hover_popover_is_focused = true;
+                }
+            }
+        }
+        return hover_popover_is_focused;
+    }
 }
 
-#[derive(Clone, Debug)]
+#[derive(Debug, Clone)]
+
 pub struct InfoPopover {
     pub symbol_range: RangeInEditor,
-    pub parsed_content: ParsedMarkdown,
+    pub parsed_content: Option<View<Markdown>>,
     pub scroll_handle: ScrollHandle,
+    pub keyboard_grace: Rc<RefCell<bool>>,
+    pub anchor: Option<Anchor>,
 }
 
 impl InfoPopover {
-    pub fn render(
-        &mut self,
-        style: &EditorStyle,
-        max_size: Size<Pixels>,
-        workspace: Option<WeakView<Workspace>>,
-        cx: &mut ViewContext<Editor>,
-    ) -> AnyElement {
-        div()
+    pub fn render(&mut self, max_size: Size<Pixels>, cx: &mut ViewContext<Editor>) -> AnyElement {
+        let keyboard_grace = Rc::clone(&self.keyboard_grace);
+        let mut d = div()
             .id("info_popover")
             .elevation_2(cx)
             .overflow_y_scroll()
@@ -514,15 +555,17 @@ impl InfoPopover {
             // Prevent a mouse down/move on the popover from being propagated to the editor,
             // because that would dismiss the popover.
             .on_mouse_move(|_, cx| cx.stop_propagation())
-            .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
-            .child(div().p_2().child(crate::render_parsed_markdown(
-                "content",
-                &self.parsed_content,
-                style,
-                workspace,
-                cx,
-            )))
-            .into_any_element()
+            .on_mouse_down(MouseButton::Left, move |_, cx| {
+                let mut keyboard_grace = keyboard_grace.borrow_mut();
+                *keyboard_grace = false;
+                cx.stop_propagation();
+            })
+            .p_2();
+
+        if let Some(markdown) = &self.parsed_content {
+            d = d.child(markdown.clone());
+        }
+        d.into_any_element()
     }
 
     pub fn scroll(&self, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
@@ -642,17 +685,33 @@ mod tests {
         InlayId, PointForPosition,
     };
     use collections::BTreeSet;
-    use gpui::{FontWeight, HighlightStyle, UnderlineStyle};
     use indoc::indoc;
     use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
     use lsp::LanguageServerId;
-    use project::{HoverBlock, HoverBlockKind};
+    use markdown::parser::MarkdownEvent;
     use smol::stream::StreamExt;
     use std::sync::atomic;
     use std::sync::atomic::AtomicUsize;
     use text::Bias;
-    use unindent::Unindent;
-    use util::test::marked_text_ranges;
+
+    impl InfoPopover {
+        fn get_rendered_text(&self, cx: &gpui::AppContext) -> String {
+            let mut rendered_text = String::new();
+            if let Some(parsed_content) = self.parsed_content.clone() {
+                let markdown = parsed_content.read(cx);
+                let text = markdown.parsed_markdown().source().to_string();
+                let data = markdown.parsed_markdown().events();
+                let slice = data;
+
+                for (range, event) in slice.iter() {
+                    if [MarkdownEvent::Text, MarkdownEvent::Code].contains(event) {
+                        rendered_text.push_str(&text[range.clone()])
+                    }
+                }
+            }
+            rendered_text
+        }
+    }
 
     #[gpui::test]
     async fn test_mouse_hover_info_popover_with_autocomplete_popover(
@@ -736,7 +795,7 @@ mod tests {
             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
         requests.next().await;
 
-        cx.editor(|editor, _| {
+        cx.editor(|editor, cx| {
             assert!(editor.hover_state.visible());
             assert_eq!(
                 editor.hover_state.info_popovers.len(),
@@ -744,14 +803,13 @@ mod tests {
                 "Expected exactly one hover but got: {:?}",
                 editor.hover_state.info_popovers
             );
-            let rendered = editor
+            let rendered_text = editor
                 .hover_state
                 .info_popovers
                 .first()
-                .cloned()
                 .unwrap()
-                .parsed_content;
-            assert_eq!(rendered.text, "some basic docs".to_string())
+                .get_rendered_text(cx);
+            assert_eq!(rendered_text, "some basic docs".to_string())
         });
 
         // check that the completion menu is still visible and that there still has only been 1 completion request
@@ -777,7 +835,7 @@ mod tests {
         assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
 
         //verify the information popover is still visible and unchanged
-        cx.editor(|editor, _| {
+        cx.editor(|editor, cx| {
             assert!(editor.hover_state.visible());
             assert_eq!(
                 editor.hover_state.info_popovers.len(),
@@ -785,14 +843,14 @@ mod tests {
                 "Expected exactly one hover but got: {:?}",
                 editor.hover_state.info_popovers
             );
-            let rendered = editor
+            let rendered_text = editor
                 .hover_state
                 .info_popovers
                 .first()
-                .cloned()
                 .unwrap()
-                .parsed_content;
-            assert_eq!(rendered.text, "some basic docs".to_string())
+                .get_rendered_text(cx);
+
+            assert_eq!(rendered_text, "some basic docs".to_string())
         });
 
         // Mouse moved with no hover response dismisses
@@ -870,7 +928,7 @@ mod tests {
             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
         requests.next().await;
 
-        cx.editor(|editor, _| {
+        cx.editor(|editor, cx| {
             assert!(editor.hover_state.visible());
             assert_eq!(
                 editor.hover_state.info_popovers.len(),
@@ -878,14 +936,14 @@ mod tests {
                 "Expected exactly one hover but got: {:?}",
                 editor.hover_state.info_popovers
             );
-            let rendered = editor
+            let rendered_text = editor
                 .hover_state
                 .info_popovers
                 .first()
-                .cloned()
                 .unwrap()
-                .parsed_content;
-            assert_eq!(rendered.text, "some basic docs".to_string())
+                .get_rendered_text(cx);
+
+            assert_eq!(rendered_text, "some basic docs".to_string())
         });
 
         // Mouse moved with no hover response dismisses
@@ -931,34 +989,49 @@ mod tests {
         let symbol_range = cx.lsp_range(indoc! {"
             ยซfnยป test() { println!(); }
         "});
-        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
-            Ok(Some(lsp::Hover {
-                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
-                    kind: lsp::MarkupKind::Markdown,
-                    value: "some other basic docs".to_string(),
-                }),
-                range: Some(symbol_range),
-            }))
-        })
-        .next()
-        .await;
+
+        cx.editor(|editor, _cx| {
+            assert!(!editor.hover_state.visible());
+
+            assert_eq!(
+                editor.hover_state.info_popovers.len(),
+                0,
+                "Expected no hovers but got but got: {:?}",
+                editor.hover_state.info_popovers
+            );
+        });
+
+        let mut requests =
+            cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+                Ok(Some(lsp::Hover {
+                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                        kind: lsp::MarkupKind::Markdown,
+                        value: "some other basic docs".to_string(),
+                    }),
+                    range: Some(symbol_range),
+                }))
+            });
+
+        requests.next().await;
+        cx.dispatch_action(Hover);
 
         cx.condition(|editor, _| editor.hover_state.visible()).await;
-        cx.editor(|editor, _| {
+        cx.editor(|editor, cx| {
             assert_eq!(
                 editor.hover_state.info_popovers.len(),
                 1,
                 "Expected exactly one hover but got: {:?}",
                 editor.hover_state.info_popovers
             );
-            let rendered = editor
+
+            let rendered_text = editor
                 .hover_state
                 .info_popovers
                 .first()
-                .cloned()
                 .unwrap()
-                .parsed_content;
-            assert_eq!(rendered.text, "some other basic docs".to_string())
+                .get_rendered_text(cx);
+
+            assert_eq!(rendered_text, "some other basic docs".to_string())
         });
     }
 
@@ -998,24 +1071,25 @@ mod tests {
         })
         .next()
         .await;
+        cx.dispatch_action(Hover);
 
         cx.condition(|editor, _| editor.hover_state.visible()).await;
-        cx.editor(|editor, _| {
+        cx.editor(|editor, cx| {
             assert_eq!(
                 editor.hover_state.info_popovers.len(),
                 1,
                 "Expected exactly one hover but got: {:?}",
                 editor.hover_state.info_popovers
             );
-            let rendered = editor
+            let rendered_text = editor
                 .hover_state
                 .info_popovers
                 .first()
-                .cloned()
                 .unwrap()
-                .parsed_content;
+                .get_rendered_text(cx);
+
             assert_eq!(
-                rendered.text,
+                rendered_text,
                 "regular text for hover to show".to_string(),
                 "No empty string hovers should be shown"
             );
@@ -1063,24 +1137,25 @@ mod tests {
         .next()
         .await;
 
+        cx.dispatch_action(Hover);
+
         cx.condition(|editor, _| editor.hover_state.visible()).await;
-        cx.editor(|editor, _| {
+        cx.editor(|editor, cx| {
             assert_eq!(
                 editor.hover_state.info_popovers.len(),
                 1,
                 "Expected exactly one hover but got: {:?}",
                 editor.hover_state.info_popovers
             );
-            let rendered = editor
+            let rendered_text = editor
                 .hover_state
                 .info_popovers
                 .first()
-                .cloned()
                 .unwrap()
-                .parsed_content;
+                .get_rendered_text(cx);
+
             assert_eq!(
-                rendered.text,
-                code_str.trim(),
+                rendered_text, code_str,
                 "Should not have extra line breaks at end of rendered hover"
             );
         });
@@ -1156,153 +1231,6 @@ mod tests {
         });
     }
 
-    #[gpui::test]
-    fn test_render_blocks(cx: &mut gpui::TestAppContext) {
-        init_test(cx, |_| {});
-
-        let languages = Arc::new(LanguageRegistry::test(cx.executor()));
-        let editor = cx.add_window(|cx| Editor::single_line(cx));
-        editor
-            .update(cx, |editor, _cx| {
-                let style = editor.style.clone().unwrap();
-
-                struct Row {
-                    blocks: Vec<HoverBlock>,
-                    expected_marked_text: String,
-                    expected_styles: Vec<HighlightStyle>,
-                }
-
-                let rows = &[
-                    // Strong emphasis
-                    Row {
-                        blocks: vec![HoverBlock {
-                            text: "one **two** three".to_string(),
-                            kind: HoverBlockKind::Markdown,
-                        }],
-                        expected_marked_text: "one ยซtwoยป three".to_string(),
-                        expected_styles: vec![HighlightStyle {
-                            font_weight: Some(FontWeight::BOLD),
-                            ..Default::default()
-                        }],
-                    },
-                    // Links
-                    Row {
-                        blocks: vec![HoverBlock {
-                            text: "one [two](https://the-url) three".to_string(),
-                            kind: HoverBlockKind::Markdown,
-                        }],
-                        expected_marked_text: "one ยซtwoยป three".to_string(),
-                        expected_styles: vec![HighlightStyle {
-                            underline: Some(UnderlineStyle {
-                                thickness: 1.0.into(),
-                                ..Default::default()
-                            }),
-                            ..Default::default()
-                        }],
-                    },
-                    // Lists
-                    Row {
-                        blocks: vec![HoverBlock {
-                            text: "
-                            lists:
-                            * one
-                                - a
-                                - b
-                            * two
-                                - [c](https://the-url)
-                                - d"
-                            .unindent(),
-                            kind: HoverBlockKind::Markdown,
-                        }],
-                        expected_marked_text: "
-                        lists:
-                        - one
-                          - a
-                          - b
-                        - two
-                          - ยซcยป
-                          - d"
-                        .unindent(),
-                        expected_styles: vec![HighlightStyle {
-                            underline: Some(UnderlineStyle {
-                                thickness: 1.0.into(),
-                                ..Default::default()
-                            }),
-                            ..Default::default()
-                        }],
-                    },
-                    // Multi-paragraph list items
-                    Row {
-                        blocks: vec![HoverBlock {
-                            text: "
-                            * one two
-                              three
-
-                            * four five
-                                * six seven
-                                  eight
-
-                                  nine
-                                * ten
-                            * six"
-                                .unindent(),
-                            kind: HoverBlockKind::Markdown,
-                        }],
-                        expected_marked_text: "
-                        - one two three
-                        - four five
-                          - six seven eight
-
-                            nine
-                          - ten
-                        - six"
-                            .unindent(),
-                        expected_styles: vec![HighlightStyle {
-                            underline: Some(UnderlineStyle {
-                                thickness: 1.0.into(),
-                                ..Default::default()
-                            }),
-                            ..Default::default()
-                        }],
-                    },
-                ];
-
-                for Row {
-                    blocks,
-                    expected_marked_text,
-                    expected_styles,
-                } in &rows[0..]
-                {
-                    let rendered = smol::block_on(parse_blocks(&blocks, &languages, None));
-
-                    let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
-                    let expected_highlights = ranges
-                        .into_iter()
-                        .zip(expected_styles.iter().cloned())
-                        .collect::<Vec<_>>();
-                    assert_eq!(
-                        rendered.text, expected_text,
-                        "wrong text for input {blocks:?}"
-                    );
-
-                    let rendered_highlights: Vec<_> = rendered
-                        .highlights
-                        .iter()
-                        .filter_map(|(range, highlight)| {
-                            let highlight = highlight.to_highlight_style(&style.syntax)?;
-                            Some((range.clone(), highlight))
-                        })
-                        .collect();
-
-                    assert_eq!(
-                        rendered_highlights, expected_highlights,
-                        "wrong highlights for input {blocks:?}"
-                    );
-                }
-            })
-            .unwrap();
-    }
-
     #[gpui::test]
     async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
@@ -1546,9 +1474,8 @@ mod tests {
                 "Popover range should match the new type label part"
             );
             assert_eq!(
-                popover.parsed_content.text,
-                format!("A tooltip for `{new_type_label}`"),
-                "Rendered text should not anyhow alter backticks"
+                popover.get_rendered_text(cx),
+                format!("A tooltip for {new_type_label}"),
             );
         });
 
@@ -1602,7 +1529,7 @@ mod tests {
                 "Popover range should match the struct label part"
             );
             assert_eq!(
-                popover.parsed_content.text,
+                popover.get_rendered_text(cx),
                 format!("A tooltip for {struct_label}"),
                 "Rendered markdown element should remove backticks from text"
             );

crates/gpui/src/text_system/line.rs ๐Ÿ”—

@@ -109,7 +109,13 @@ fn paint_line(
     wrap_boundaries: &[WrapBoundary],
     cx: &mut WindowContext,
 ) -> Result<()> {
-    let line_bounds = Bounds::new(origin, size(layout.width, line_height));
+    let line_bounds = Bounds::new(
+        origin,
+        size(
+            layout.width,
+            line_height * (wrap_boundaries.len() as f32 + 1.),
+        ),
+    );
     cx.paint_layer(line_bounds, |cx| {
         let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
         let baseline_offset = point(px(0.), padding_top + layout.ascent);

crates/markdown/examples/markdown.rs ๐Ÿ”—

@@ -1,5 +1,5 @@
 use assets::Assets;
-use gpui::{prelude::*, App, KeyBinding, Task, View, WindowOptions};
+use gpui::{prelude::*, rgb, App, KeyBinding, StyleRefinement, Task, View, WindowOptions};
 use language::{language_settings::AllLanguageSettings, LanguageRegistry};
 use markdown::{Markdown, MarkdownStyle};
 use node_runtime::FakeNodeRuntime;
@@ -105,44 +105,49 @@ pub fn main() {
         cx.activate(true);
         cx.open_window(WindowOptions::default(), |cx| {
             cx.new_view(|cx| {
-                MarkdownExample::new(
-                    MARKDOWN_EXAMPLE.to_string(),
-                    MarkdownStyle {
-                        code_block: gpui::TextStyleRefinement {
-                            font_family: Some("Zed Plex Mono".into()),
-                            color: Some(cx.theme().colors().editor_foreground),
-                            background_color: Some(cx.theme().colors().editor_background),
-                            ..Default::default()
-                        },
-                        inline_code: gpui::TextStyleRefinement {
-                            font_family: Some("Zed Plex Mono".into()),
-                            // @nate: Could we add inline-code specific styles to the theme?
-                            color: Some(cx.theme().colors().editor_foreground),
-                            background_color: Some(cx.theme().colors().editor_background),
-                            ..Default::default()
-                        },
-                        rule_color: Color::Muted.color(cx),
-                        block_quote_border_color: Color::Muted.color(cx),
-                        block_quote: gpui::TextStyleRefinement {
-                            color: Some(Color::Muted.color(cx)),
-                            ..Default::default()
-                        },
-                        link: gpui::TextStyleRefinement {
+                let markdown_style = MarkdownStyle {
+                    base_text_style: gpui::TextStyle {
+                        font_family: "Zed Plex Sans".into(),
+                        color: cx.theme().colors().terminal_ansi_black,
+                        ..Default::default()
+                    },
+                    code_block: StyleRefinement::default()
+                        .font_family("Zed Plex Mono")
+                        .m(rems(1.))
+                        .bg(rgb(0xAAAAAAA)),
+                    inline_code: gpui::TextStyleRefinement {
+                        font_family: Some("Zed Mono".into()),
+                        color: Some(cx.theme().colors().editor_foreground),
+                        background_color: Some(cx.theme().colors().editor_background),
+                        ..Default::default()
+                    },
+                    rule_color: Color::Muted.color(cx),
+                    block_quote_border_color: Color::Muted.color(cx),
+                    block_quote: gpui::TextStyleRefinement {
+                        color: Some(Color::Muted.color(cx)),
+                        ..Default::default()
+                    },
+                    link: gpui::TextStyleRefinement {
+                        color: Some(Color::Accent.color(cx)),
+                        underline: Some(gpui::UnderlineStyle {
+                            thickness: px(1.),
                             color: Some(Color::Accent.color(cx)),
-                            underline: Some(gpui::UnderlineStyle {
-                                thickness: px(1.),
-                                color: Some(Color::Accent.color(cx)),
-                                wavy: false,
-                            }),
-                            ..Default::default()
-                        },
-                        syntax: cx.theme().syntax().clone(),
-                        selection_background_color: {
-                            let mut selection = cx.theme().players().local().selection;
-                            selection.fade_out(0.7);
-                            selection
-                        },
+                            wavy: false,
+                        }),
+                        ..Default::default()
                     },
+                    syntax: cx.theme().syntax().clone(),
+                    selection_background_color: {
+                        let mut selection = cx.theme().players().local().selection;
+                        selection.fade_out(0.7);
+                        selection
+                    },
+                    ..Default::default()
+                };
+
+                MarkdownExample::new(
+                    MARKDOWN_EXAMPLE.to_string(),
+                    markdown_style,
                     language_registry,
                     cx,
                 )
@@ -163,7 +168,8 @@ impl MarkdownExample {
         language_registry: Arc<LanguageRegistry>,
         cx: &mut WindowContext,
     ) -> Self {
-        let markdown = cx.new_view(|cx| Markdown::new(text, style, Some(language_registry), cx));
+        let markdown =
+            cx.new_view(|cx| Markdown::new(text, style, Some(language_registry), cx, None));
         Self { markdown }
     }
 }

crates/markdown/examples/markdown_as_child.rs ๐Ÿ”—

@@ -0,0 +1,120 @@
+use assets::Assets;
+use gpui::*;
+use language::{language_settings::AllLanguageSettings, LanguageRegistry};
+use markdown::{Markdown, MarkdownStyle};
+use node_runtime::FakeNodeRuntime;
+use settings::SettingsStore;
+use std::sync::Arc;
+use theme::LoadThemes;
+use ui::div;
+use ui::prelude::*;
+
+const MARKDOWN_EXAMPLE: &'static str = r#"
+this text should be selectable
+
+wow so cool
+
+## Heading 2
+"#;
+pub fn main() {
+    env_logger::init();
+
+    App::new().with_assets(Assets).run(|cx| {
+        let store = SettingsStore::test(cx);
+        cx.set_global(store);
+        language::init(cx);
+        SettingsStore::update(cx, |store, cx| {
+            store.update_user_settings::<AllLanguageSettings>(cx, |_| {});
+        });
+        cx.bind_keys([KeyBinding::new("cmd-c", markdown::Copy, None)]);
+
+        let node_runtime = FakeNodeRuntime::new();
+        let language_registry = Arc::new(LanguageRegistry::new(
+            Task::ready(()),
+            cx.background_executor().clone(),
+        ));
+        languages::init(language_registry.clone(), node_runtime, cx);
+        theme::init(LoadThemes::JustBase, cx);
+        Assets.load_fonts(cx).unwrap();
+
+        cx.activate(true);
+        let _ = cx.open_window(WindowOptions::default(), |cx| {
+            cx.new_view(|cx| {
+                let markdown_style = MarkdownStyle {
+                    base_text_style: gpui::TextStyle {
+                        font_family: "Zed Mono".into(),
+                        color: cx.theme().colors().text,
+                        ..Default::default()
+                    },
+                    code_block: StyleRefinement {
+                        text: Some(gpui::TextStyleRefinement {
+                            font_family: Some("Zed Mono".into()),
+                            background_color: Some(cx.theme().colors().editor_background),
+                            ..Default::default()
+                        }),
+                        margin: gpui::EdgesRefinement {
+                            top: Some(Length::Definite(rems(4.).into())),
+                            left: Some(Length::Definite(rems(4.).into())),
+                            right: Some(Length::Definite(rems(4.).into())),
+                            bottom: Some(Length::Definite(rems(4.).into())),
+                        },
+                        ..Default::default()
+                    },
+                    inline_code: gpui::TextStyleRefinement {
+                        font_family: Some("Zed Mono".into()),
+                        background_color: Some(cx.theme().colors().editor_background),
+                        ..Default::default()
+                    },
+                    rule_color: Color::Muted.color(cx),
+                    block_quote_border_color: Color::Muted.color(cx),
+                    block_quote: gpui::TextStyleRefinement {
+                        color: Some(Color::Muted.color(cx)),
+                        ..Default::default()
+                    },
+                    link: gpui::TextStyleRefinement {
+                        color: Some(Color::Accent.color(cx)),
+                        underline: Some(gpui::UnderlineStyle {
+                            thickness: px(1.),
+                            color: Some(Color::Accent.color(cx)),
+                            wavy: false,
+                        }),
+                        ..Default::default()
+                    },
+                    syntax: cx.theme().syntax().clone(),
+                    selection_background_color: {
+                        let mut selection = cx.theme().players().local().selection;
+                        selection.fade_out(0.7);
+                        selection
+                    },
+                    break_style: Default::default(),
+                    heading: Default::default(),
+                };
+                let markdown = cx.new_view(|cx| {
+                    Markdown::new(MARKDOWN_EXAMPLE.into(), markdown_style, None, cx, None)
+                });
+
+                HelloWorld { markdown }
+            })
+        });
+    });
+}
+struct HelloWorld {
+    markdown: View<Markdown>,
+}
+
+impl Render for HelloWorld {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+        div()
+            .flex()
+            .bg(rgb(0x2e7d32))
+            .size(Length::Definite(Pixels(700.0).into()))
+            .justify_center()
+            .items_center()
+            .shadow_lg()
+            .border_1()
+            .border_color(rgb(0x0000ff))
+            .text_xl()
+            .text_color(rgb(0xffffff))
+            .child(div().child(self.markdown.clone()).p_20())
+    }
+}

crates/markdown/src/markdown.rs ๐Ÿ”—

@@ -1,16 +1,17 @@
-mod parser;
+pub mod parser;
 
 use crate::parser::CodeBlockKind;
 use futures::FutureExt;
 use gpui::{
     actions, point, quad, AnyElement, AppContext, Bounds, ClipboardItem, CursorStyle,
     DispatchPhase, Edges, FocusHandle, FocusableView, FontStyle, FontWeight, GlobalElementId,
-    Hitbox, Hsla, KeyContext, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point,
-    Render, StrikethroughStyle, Style, StyledText, Task, TextLayout, TextRun, TextStyle,
-    TextStyleRefinement, View,
+    Hitbox, Hsla, KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent,
+    Point, Render, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun,
+    TextStyle, TextStyleRefinement, View,
 };
 use language::{Language, LanguageRegistry, Rope};
 use parser::{parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd};
+
 use std::{iter, mem, ops::Range, rc::Rc, sync::Arc};
 use theme::SyntaxTheme;
 use ui::prelude::*;
@@ -18,7 +19,8 @@ use util::{ResultExt, TryFutureExt};
 
 #[derive(Clone)]
 pub struct MarkdownStyle {
-    pub code_block: TextStyleRefinement,
+    pub base_text_style: TextStyle,
+    pub code_block: StyleRefinement,
     pub inline_code: TextStyleRefinement,
     pub block_quote: TextStyleRefinement,
     pub link: TextStyleRefinement,
@@ -26,8 +28,27 @@ pub struct MarkdownStyle {
     pub block_quote_border_color: Hsla,
     pub syntax: Arc<SyntaxTheme>,
     pub selection_background_color: Hsla,
+    pub break_style: StyleRefinement,
+    pub heading: StyleRefinement,
 }
 
+impl Default for MarkdownStyle {
+    fn default() -> Self {
+        Self {
+            base_text_style: Default::default(),
+            code_block: Default::default(),
+            inline_code: Default::default(),
+            block_quote: Default::default(),
+            link: Default::default(),
+            rule_color: Default::default(),
+            block_quote_border_color: Default::default(),
+            syntax: Arc::new(SyntaxTheme::default()),
+            selection_background_color: Default::default(),
+            break_style: Default::default(),
+            heading: Default::default(),
+        }
+    }
+}
 pub struct Markdown {
     source: String,
     selection: Selection,
@@ -39,6 +60,7 @@ pub struct Markdown {
     pending_parse: Option<Task<Option<()>>>,
     focus_handle: FocusHandle,
     language_registry: Option<Arc<LanguageRegistry>>,
+    fallback_code_block_language: Option<String>,
 }
 
 actions!(markdown, [Copy]);
@@ -49,6 +71,7 @@ impl Markdown {
         style: MarkdownStyle,
         language_registry: Option<Arc<LanguageRegistry>>,
         cx: &mut ViewContext<Self>,
+        fallback_code_block_language: Option<String>,
     ) -> Self {
         let focus_handle = cx.focus_handle();
         let mut this = Self {
@@ -62,6 +85,7 @@ impl Markdown {
             pending_parse: None,
             focus_handle,
             language_registry,
+            fallback_code_block_language,
         };
         this.parse(cx);
         this
@@ -89,7 +113,14 @@ impl Markdown {
         &self.source
     }
 
+    pub fn parsed_markdown(&self) -> &ParsedMarkdown {
+        &self.parsed_markdown
+    }
+
     fn copy(&self, text: &RenderedText, cx: &mut ViewContext<Self>) {
+        if self.selection.end <= self.selection.start {
+            return;
+        }
         let text = text.text_for_range(self.selection.start..self.selection.end);
         cx.write_to_clipboard(ClipboardItem::new(text));
     }
@@ -140,6 +171,7 @@ impl Render for Markdown {
             cx.view().clone(),
             self.style.clone(),
             self.language_registry.clone(),
+            self.fallback_code_block_language.clone(),
         )
     }
 }
@@ -185,11 +217,21 @@ impl Selection {
 }
 
 #[derive(Clone)]
-struct ParsedMarkdown {
+pub struct ParsedMarkdown {
     source: SharedString,
     events: Arc<[(Range<usize>, MarkdownEvent)]>,
 }
 
+impl ParsedMarkdown {
+    pub fn source(&self) -> &SharedString {
+        &self.source
+    }
+
+    pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> {
+        return &self.events;
+    }
+}
+
 impl Default for ParsedMarkdown {
     fn default() -> Self {
         Self {
@@ -203,6 +245,7 @@ pub struct MarkdownElement {
     markdown: View<Markdown>,
     style: MarkdownStyle,
     language_registry: Option<Arc<LanguageRegistry>>,
+    fallback_code_block_language: Option<String>,
 }
 
 impl MarkdownElement {
@@ -210,19 +253,31 @@ impl MarkdownElement {
         markdown: View<Markdown>,
         style: MarkdownStyle,
         language_registry: Option<Arc<LanguageRegistry>>,
+        fallback_code_block_language: Option<String>,
     ) -> Self {
         Self {
             markdown,
             style,
             language_registry,
+            fallback_code_block_language,
         }
     }
 
     fn load_language(&self, name: &str, cx: &mut WindowContext) -> Option<Arc<Language>> {
+        let language_test = self.language_registry.as_ref()?.language_for_name(name);
+
+        let language_name = match language_test.now_or_never() {
+            Some(Ok(_)) => String::from(name),
+            Some(Err(_)) if !name.is_empty() && self.fallback_code_block_language.is_some() => {
+                self.fallback_code_block_language.clone().unwrap()
+            }
+            _ => String::new(),
+        };
+
         let language = self
             .language_registry
             .as_ref()?
-            .language_for_name(name)
+            .language_for_name(language_name.as_str())
             .map(|language| language.ok())
             .shared();
 
@@ -417,7 +472,7 @@ impl MarkdownElement {
             .update(cx, |markdown, _| markdown.autoscroll_request.take())?;
         let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
 
-        let text_style = cx.text_style();
+        let text_style = self.style.base_text_style.clone();
         let font_id = cx.text_system().resolve_font(&text_style.font());
         let font_size = text_style.font_size.to_pixels(cx.rem_size());
         let em_width = cx
@@ -462,14 +517,26 @@ impl Element for MarkdownElement {
         _id: Option<&GlobalElementId>,
         cx: &mut WindowContext,
     ) -> (gpui::LayoutId, Self::RequestLayoutState) {
-        let mut builder = MarkdownElementBuilder::new(cx.text_style(), self.style.syntax.clone());
+        let mut builder = MarkdownElementBuilder::new(
+            self.style.base_text_style.clone(),
+            self.style.syntax.clone(),
+        );
         let parsed_markdown = self.markdown.read(cx).parsed_markdown.clone();
+        let markdown_end = if let Some(last) = parsed_markdown.events.last() {
+            last.0.end
+        } else {
+            0
+        };
         for (range, event) in parsed_markdown.events.iter() {
             match event {
                 MarkdownEvent::Start(tag) => {
                     match tag {
                         MarkdownTag::Paragraph => {
-                            builder.push_div(div().mb_2().line_height(rems(1.3)));
+                            builder.push_div(
+                                div().mb_2().line_height(rems(1.3)),
+                                range,
+                                markdown_end,
+                            );
                         }
                         MarkdownTag::Heading { level, .. } => {
                             let mut heading = div().mb_2();
@@ -480,7 +547,11 @@ impl Element for MarkdownElement {
                                 pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
                                 _ => heading,
                             };
-                            builder.push_div(heading);
+                            heading.style().refine(&self.style.heading);
+                            builder.push_text_style(
+                                self.style.heading.text_style().clone().unwrap_or_default(),
+                            );
+                            builder.push_div(heading, range, markdown_end);
                         }
                         MarkdownTag::BlockQuote => {
                             builder.push_text_style(self.style.block_quote.clone());
@@ -490,6 +561,8 @@ impl Element for MarkdownElement {
                                     .mb_2()
                                     .border_l_4()
                                     .border_color(self.style.block_quote_border_color),
+                                range,
+                                markdown_end,
                             );
                         }
                         MarkdownTag::CodeBlock(kind) => {
@@ -499,17 +572,18 @@ impl Element for MarkdownElement {
                                 None
                             };
 
+                            let mut d = div().w_full().rounded_lg();
+                            d.style().refine(&self.style.code_block);
+                            if let Some(code_block_text_style) = &self.style.code_block.text {
+                                builder.push_text_style(code_block_text_style.to_owned());
+                            }
                             builder.push_code_block(language);
-                            builder.push_text_style(self.style.code_block.clone());
-                            builder.push_div(div().rounded_lg().p_4().mb_2().w_full().when_some(
-                                self.style.code_block.background_color,
-                                |div, color| div.bg(color),
-                            ));
+                            builder.push_div(d, range, markdown_end);
                         }
-                        MarkdownTag::HtmlBlock => builder.push_div(div()),
+                        MarkdownTag::HtmlBlock => builder.push_div(div(), range, markdown_end),
                         MarkdownTag::List(bullet_index) => {
                             builder.push_list(*bullet_index);
-                            builder.push_div(div().pl_4());
+                            builder.push_div(div().pl_4(), range, markdown_end);
                         }
                         MarkdownTag::Item => {
                             let bullet = if let Some(bullet_index) = builder.next_bullet_index() {
@@ -525,9 +599,11 @@ impl Element for MarkdownElement {
                                     .items_start()
                                     .gap_1()
                                     .child(bullet),
+                                range,
+                                markdown_end,
                             );
                             // Without `w_0`, text doesn't wrap to the width of the container.
-                            builder.push_div(div().flex_1().w_0());
+                            builder.push_div(div().flex_1().w_0(), range, markdown_end);
                         }
                         MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
                             font_style: Some(FontStyle::Italic),
@@ -552,6 +628,7 @@ impl Element for MarkdownElement {
                                 builder.push_text_style(self.style.link.clone())
                             }
                         }
+                        MarkdownTag::MetadataBlock(_) => {}
                         _ => log::error!("unsupported markdown tag {:?}", tag),
                     }
                 }
@@ -559,7 +636,10 @@ impl Element for MarkdownElement {
                     MarkdownTagEnd::Paragraph => {
                         builder.pop_div();
                     }
-                    MarkdownTagEnd::Heading(_) => builder.pop_div(),
+                    MarkdownTagEnd::Heading(_) => {
+                        builder.pop_div();
+                        builder.pop_text_style()
+                    }
                     MarkdownTagEnd::BlockQuote => {
                         builder.pop_text_style();
                         builder.pop_div()
@@ -567,8 +647,10 @@ impl Element for MarkdownElement {
                     MarkdownTagEnd::CodeBlock => {
                         builder.trim_trailing_newline();
                         builder.pop_div();
-                        builder.pop_text_style();
                         builder.pop_code_block();
+                        if self.style.code_block.text.is_some() {
+                            builder.pop_text_style();
+                        }
                     }
                     MarkdownTagEnd::HtmlBlock => builder.pop_div(),
                     MarkdownTagEnd::List(_) => {
@@ -609,18 +691,24 @@ impl Element for MarkdownElement {
                             .border_b_1()
                             .my_2()
                             .border_color(self.style.rule_color),
+                        range,
+                        markdown_end,
                     );
                     builder.pop_div()
                 }
-                MarkdownEvent::SoftBreak => builder.push_text("\n", range.start),
-                MarkdownEvent::HardBreak => builder.push_text("\n", range.start),
+                MarkdownEvent::SoftBreak => builder.push_text(" ", range.start),
+                MarkdownEvent::HardBreak => {
+                    let mut d = div().py_3();
+                    d.style().refine(&self.style.break_style);
+                    builder.push_div(d, range, markdown_end);
+                    builder.pop_div()
+                }
                 _ => log::error!("unsupported markdown event {:?}", event),
             }
         }
-
         let mut rendered_markdown = builder.build();
         let child_layout_id = rendered_markdown.element.request_layout(cx);
-        let layout_id = cx.request_layout(Style::default(), [child_layout_id]);
+        let layout_id = cx.request_layout(gpui::Style::default(), [child_layout_id]);
         (layout_id, rendered_markdown)
     }
 
@@ -732,8 +820,32 @@ impl MarkdownElementBuilder {
         self.text_style_stack.pop();
     }
 
-    fn push_div(&mut self, div: Div) {
+    fn push_div(&mut self, mut div: Div, range: &Range<usize>, markdown_end: usize) {
         self.flush_text();
+
+        if range.start == 0 {
+            //first element, remove top margin
+            div.style().refine(&StyleRefinement {
+                margin: gpui::EdgesRefinement {
+                    top: Some(Length::Definite(px(0.).into())),
+                    left: None,
+                    right: None,
+                    bottom: None,
+                },
+                ..Default::default()
+            });
+        }
+        if range.end == markdown_end {
+            div.style().refine(&StyleRefinement {
+                margin: gpui::EdgesRefinement {
+                    top: None,
+                    left: None,
+                    right: None,
+                    bottom: Some(Length::Definite(rems(0.).into())),
+                },
+                ..Default::default()
+            });
+        }
         self.div_stack.push(div);
     }
 

crates/markdown/src/parser.rs ๐Ÿ”—

@@ -7,11 +7,22 @@ use std::ops::Range;
 pub fn parse_markdown(text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {
     let mut events = Vec::new();
     let mut within_link = false;
+    let mut within_metadata = false;
     for (pulldown_event, mut range) in Parser::new_ext(text, Options::all()).into_offset_iter() {
+        if within_metadata {
+            if let pulldown_cmark::Event::End(pulldown_cmark::TagEnd::MetadataBlock { .. }) =
+                pulldown_event
+            {
+                within_metadata = false;
+            }
+            continue;
+        }
         match pulldown_event {
             pulldown_cmark::Event::Start(tag) => {
-                if let pulldown_cmark::Tag::Link { .. } = tag {
-                    within_link = true;
+                match tag {
+                    pulldown_cmark::Tag::Link { .. } => within_link = true,
+                    pulldown_cmark::Tag::MetadataBlock { .. } => within_metadata = true,
+                    _ => {}
                 }
                 events.push((range, MarkdownEvent::Start(tag.into())))
             }

crates/project/src/project.rs ๐Ÿ”—

@@ -5801,7 +5801,7 @@ impl Project {
                     .await
                     .into_iter()
                     .filter_map(|hover| remove_empty_hover_blocks(hover?))
-                    .collect()
+                    .collect::<Vec<Hover>>()
             })
         } else if let Some(project_id) = self.remote_id() {
             let request_task = self.client().request(proto::MultiLspQuery {

crates/recent_projects/src/dev_servers.rs ๐Ÿ”—

@@ -114,25 +114,31 @@ impl DevServerProjects {
             cx.notify();
         });
 
+        let mut base_style = cx.text_style();
+        base_style.refine(&gpui::TextStyleRefinement {
+            color: Some(cx.theme().colors().editor_foreground),
+            ..Default::default()
+        });
+
         let markdown_style = MarkdownStyle {
-            code_block: gpui::TextStyleRefinement {
-                font_family: Some("Zed Plex Mono".into()),
-                color: Some(cx.theme().colors().editor_foreground),
-                background_color: Some(cx.theme().colors().editor_background),
+            base_text_style: base_style,
+            code_block: gpui::StyleRefinement {
+                text: Some(gpui::TextStyleRefinement {
+                    font_family: Some("Zed Plex Mono".into()),
+                    ..Default::default()
+                }),
                 ..Default::default()
             },
-            inline_code: Default::default(),
-            block_quote: Default::default(),
             link: gpui::TextStyleRefinement {
                 color: Some(Color::Accent.color(cx)),
                 ..Default::default()
             },
-            rule_color: Default::default(),
-            block_quote_border_color: Default::default(),
             syntax: cx.theme().syntax().clone(),
             selection_background_color: cx.theme().players().local().selection,
+            ..Default::default()
         };
-        let markdown = cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx));
+        let markdown =
+            cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx, None));
 
         Self {
             mode: Mode::Default(None),