repl: Streamline Markdown output usage (#47713)

Kyle Kelley created

Brought the Markdown output up to date with how Markdown is used in the
Agent panel. This fixed an issue with outputs that were too large for
the execution view as well as made sure that markdown would wrap.

<img width="3222" height="2334" alt="image"
src="https://github.com/user-attachments/assets/c65efa53-b792-4529-909a-9117053e30be"
/>

Release Notes:

- N/A

Change summary

Cargo.lock                             |   2 
crates/agent_ui/src/acp/thread_view.rs | 245 +++++++--------------------
crates/markdown/Cargo.toml             |   1 
crates/markdown/src/markdown.rs        | 131 ++++++++++++++
crates/repl/Cargo.toml                 |   2 
crates/repl/src/notebook/cell.rs       |  87 +--------
crates/repl/src/outputs.rs             |  12 +
crates/repl/src/outputs/markdown.rs    |  75 ++------
crates/repl/src/session.rs             |   2 
9 files changed, 242 insertions(+), 315 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -13764,7 +13764,7 @@ dependencies = [
  "language",
  "languages",
  "log",
- "markdown_preview",
+ "markdown",
  "menu",
  "multi_buffer",
  "nbformat",

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -28,16 +28,15 @@ use file_icons::FileIcons;
 use fs::Fs;
 use futures::FutureExt as _;
 use gpui::{
-    Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
-    CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
-    ListOffset, ListState, ObjectFit, PlatformDisplay, ScrollHandle, SharedString, StyleRefinement,
-    Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window,
+    Action, Animation, AnimationExt, AnyView, App, ClickEvent, ClipboardItem, CursorStyle,
+    ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, ListOffset, ListState, ObjectFit,
+    PlatformDisplay, ScrollHandle, SharedString, Subscription, Task, TextStyle, WeakEntity, Window,
     WindowHandle, div, ease_in_out, img, linear_color_stop, linear_gradient, list, point,
     pulsating_between,
 };
 use language::Buffer;
 use language_model::LanguageModelRegistry;
-use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
+use markdown::{Markdown, MarkdownElement, MarkdownFont, MarkdownStyle};
 use project::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId};
 use prompt_store::{PromptId, PromptStore};
 use rope::Point;
@@ -49,7 +48,7 @@ use std::time::Instant;
 use std::{collections::BTreeMap, rc::Rc, time::Duration};
 use terminal_view::terminal_panel::TerminalPanel;
 use text::{Anchor, ToPoint as _};
-use theme::{AgentFontSize, ThemeSettings};
+use theme::AgentFontSize;
 use ui::{
     Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton, DecoratedIcon,
     DiffStat, Disclosure, Divider, DividerColor, IconButtonShape, IconDecoration,
@@ -2787,7 +2786,7 @@ impl AcpThreadView {
                 let mut is_blank = true;
                 let is_last = entry_ix + 1 == total_entries;
 
-                let style = default_markdown_style(false, false, window, cx);
+                let style = MarkdownStyle::themed(MarkdownFont::Agent, window, cx);
                 let message_body = v_flex()
                     .w_full()
                     .gap_3()
@@ -3072,9 +3071,10 @@ impl AcpThreadView {
                 })
                 .text_ui_sm(cx)
                 .overflow_hidden()
-                .child(
-                    self.render_markdown(chunk, default_markdown_style(false, false, window, cx)),
-                )
+                .child(self.render_markdown(
+                    chunk,
+                    MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
+                ))
         };
 
         v_flex()
@@ -3277,7 +3277,11 @@ impl AcpThreadView {
                                         |input| {
                                             self.render_markdown(
                                                 input,
-                                                default_markdown_style(false, false, window, cx),
+                                                MarkdownStyle::themed(
+                                                    MarkdownFont::Agent,
+                                                    window,
+                                                    cx,
+                                                ),
                                             )
                                         },
                                     ))
@@ -3302,31 +3306,34 @@ impl AcpThreadView {
                 | ToolCallStatus::InProgress
                 | ToolCallStatus::Completed
                 | ToolCallStatus::Failed
-                | ToolCallStatus::Canceled => {
-                    v_flex()
-                        .when(should_show_raw_input, |this| {
-                            this.mt_1p5().w_full().child(
-                                v_flex()
-                                    .ml(rems(0.4))
-                                    .px_3p5()
-                                    .pb_1()
-                                    .gap_1()
-                                    .border_l_1()
-                                    .border_color(self.tool_card_border_color(cx))
-                                    .child(input_output_header("Raw Input:".into()))
-                                    .children(tool_call.raw_input_markdown.clone().map(|input| {
-                                        div().id(("tool-call-raw-input-markdown", entry_ix)).child(
-                                            self.render_markdown(
-                                                input,
-                                                default_markdown_style(false, false, window, cx),
-                                            ),
-                                        )
-                                    }))
-                                    .child(input_output_header("Output:".into())),
-                            )
-                        })
-                        .children(tool_call.content.iter().enumerate().map(
-                            |(content_ix, content)| {
+                | ToolCallStatus::Canceled => v_flex()
+                    .when(should_show_raw_input, |this| {
+                        this.mt_1p5().w_full().child(
+                            v_flex()
+                                .ml(rems(0.4))
+                                .px_3p5()
+                                .pb_1()
+                                .gap_1()
+                                .border_l_1()
+                                .border_color(self.tool_card_border_color(cx))
+                                .child(input_output_header("Raw Input:".into()))
+                                .children(tool_call.raw_input_markdown.clone().map(|input| {
+                                    div().id(("tool-call-raw-input-markdown", entry_ix)).child(
+                                        self.render_markdown(
+                                            input,
+                                            MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
+                                        ),
+                                    )
+                                }))
+                                .child(input_output_header("Output:".into())),
+                        )
+                    })
+                    .children(
+                        tool_call
+                            .content
+                            .iter()
+                            .enumerate()
+                            .map(|(content_ix, content)| {
                                 div().id(("tool-call-output", entry_ix)).child(
                                     self.render_tool_call_content(
                                         entry_ix,
@@ -3340,10 +3347,9 @@ impl AcpThreadView {
                                         cx,
                                     ),
                                 )
-                            },
-                        ))
-                        .into_any()
-                }
+                            }),
+                    )
+                    .into_any(),
                 ToolCallStatus::Rejected => Empty.into_any(),
             }
             .into()
@@ -3613,13 +3619,16 @@ impl AcpThreadView {
                             this.text_color(cx.theme().colors().text_muted)
                         }
                     })
-                    .child(self.render_markdown(
-                        tool_call.label.clone(),
-                        MarkdownStyle {
-                            prevent_mouse_interaction: true,
-                            ..default_markdown_style(false, true, window, cx)
-                        },
-                    ))
+                    .child(
+                        self.render_markdown(
+                            tool_call.label.clone(),
+                            MarkdownStyle {
+                                prevent_mouse_interaction: true,
+                                ..MarkdownStyle::themed(MarkdownFont::Agent, window, cx)
+                                    .with_muted_text(cx)
+                            },
+                        ),
+                    )
                     .tooltip(Tooltip::text("Go to File"))
                     .on_click(cx.listener(move |this, _, window, cx| {
                         this.open_tool_call_location(entry_ix, 0, window, cx);
@@ -3630,7 +3639,7 @@ impl AcpThreadView {
                     .w_full()
                     .child(self.render_markdown(
                         tool_call.label.clone(),
-                        default_markdown_style(false, true, window, cx),
+                        MarkdownStyle::themed(MarkdownFont::Agent, window, cx).with_muted_text(cx),
                     ))
                     .into_any()
             })
@@ -3962,7 +3971,7 @@ impl AcpThreadView {
                     .when_some(last_assistant_markdown, |this, markdown| {
                         this.child(self.render_markdown(
                             markdown,
-                            default_markdown_style(false, false, window, cx),
+                            MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
                         ))
                     }),
             )
@@ -3998,7 +4007,10 @@ impl AcpThreadView {
             })
             .text_xs()
             .text_color(cx.theme().colors().text_muted)
-            .child(self.render_markdown(markdown, default_markdown_style(false, false, window, cx)))
+            .child(self.render_markdown(
+                markdown,
+                MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
+            ))
             .when(!card_layout, |this| {
                 this.child(
                     IconButton::new(button_id, IconName::ChevronUp)
@@ -5173,7 +5185,7 @@ impl AcpThreadView {
                             .children(description.map(|desc| {
                                 self.render_markdown(
                                     desc.clone(),
-                                    default_markdown_style(false, false, window, cx),
+                                    MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
                                 )
                             }))
                         }
@@ -8240,7 +8252,8 @@ impl AcpThreadView {
             markdown
         };
 
-        let markdown_style = default_markdown_style(false, true, window, cx);
+        let markdown_style =
+            MarkdownStyle::themed(MarkdownFont::Agent, window, cx).with_muted_text(cx);
         let description = self
             .render_markdown(markdown, markdown_style)
             .into_any_element();
@@ -8721,138 +8734,12 @@ impl Render for AcpThreadView {
     }
 }
 
-fn default_markdown_style(
-    buffer_font: bool,
-    muted_text: bool,
-    window: &Window,
-    cx: &App,
-) -> MarkdownStyle {
-    let theme_settings = ThemeSettings::get_global(cx);
-    let colors = cx.theme().colors();
-
-    let buffer_font_size = theme_settings.agent_buffer_font_size(cx);
-
-    let mut text_style = window.text_style();
-    let line_height = buffer_font_size * 1.75;
-
-    let font_family = if buffer_font {
-        theme_settings.buffer_font.family.clone()
-    } else {
-        theme_settings.ui_font.family.clone()
-    };
-
-    let font_size = if buffer_font {
-        theme_settings.agent_buffer_font_size(cx)
-    } else {
-        theme_settings.agent_ui_font_size(cx)
-    };
-
-    let text_color = if muted_text {
-        colors.text_muted
-    } else {
-        colors.text
-    };
-
-    text_style.refine(&TextStyleRefinement {
-        font_family: Some(font_family),
-        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
-        font_features: Some(theme_settings.ui_font.features.clone()),
-        font_size: Some(font_size.into()),
-        line_height: Some(line_height.into()),
-        color: Some(text_color),
-        ..Default::default()
-    });
-
-    MarkdownStyle {
-        base_text_style: text_style.clone(),
-        syntax: cx.theme().syntax().clone(),
-        selection_background_color: colors.element_selection_background,
-        code_block_overflow_x_scroll: true,
-        heading_level_styles: Some(HeadingLevelStyles {
-            h1: Some(TextStyleRefinement {
-                font_size: Some(rems(1.15).into()),
-                ..Default::default()
-            }),
-            h2: Some(TextStyleRefinement {
-                font_size: Some(rems(1.1).into()),
-                ..Default::default()
-            }),
-            h3: Some(TextStyleRefinement {
-                font_size: Some(rems(1.05).into()),
-                ..Default::default()
-            }),
-            h4: Some(TextStyleRefinement {
-                font_size: Some(rems(1.).into()),
-                ..Default::default()
-            }),
-            h5: Some(TextStyleRefinement {
-                font_size: Some(rems(0.95).into()),
-                ..Default::default()
-            }),
-            h6: Some(TextStyleRefinement {
-                font_size: Some(rems(0.875).into()),
-                ..Default::default()
-            }),
-        }),
-        code_block: StyleRefinement {
-            padding: EdgesRefinement {
-                top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
-                left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
-                right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
-                bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
-            },
-            margin: EdgesRefinement {
-                top: Some(Length::Definite(px(8.).into())),
-                left: Some(Length::Definite(px(0.).into())),
-                right: Some(Length::Definite(px(0.).into())),
-                bottom: Some(Length::Definite(px(12.).into())),
-            },
-            border_style: Some(BorderStyle::Solid),
-            border_widths: EdgesRefinement {
-                top: Some(AbsoluteLength::Pixels(px(1.))),
-                left: Some(AbsoluteLength::Pixels(px(1.))),
-                right: Some(AbsoluteLength::Pixels(px(1.))),
-                bottom: Some(AbsoluteLength::Pixels(px(1.))),
-            },
-            border_color: Some(colors.border_variant),
-            background: Some(colors.editor_background.into()),
-            text: TextStyleRefinement {
-                font_family: Some(theme_settings.buffer_font.family.clone()),
-                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
-                font_features: Some(theme_settings.buffer_font.features.clone()),
-                font_size: Some(buffer_font_size.into()),
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        inline_code: TextStyleRefinement {
-            font_family: Some(theme_settings.buffer_font.family.clone()),
-            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
-            font_features: Some(theme_settings.buffer_font.features.clone()),
-            font_size: Some(buffer_font_size.into()),
-            background_color: Some(colors.editor_foreground.opacity(0.08)),
-            ..Default::default()
-        },
-        link: TextStyleRefinement {
-            background_color: Some(colors.editor_foreground.opacity(0.025)),
-            color: Some(colors.text_accent),
-            underline: Some(UnderlineStyle {
-                color: Some(colors.text_accent.opacity(0.5)),
-                thickness: px(1.),
-                ..Default::default()
-            }),
-            ..Default::default()
-        },
-        ..Default::default()
-    }
-}
-
 fn plan_label_markdown_style(
     status: &acp::PlanEntryStatus,
     window: &Window,
     cx: &App,
 ) -> MarkdownStyle {
-    let default_md_style = default_markdown_style(false, false, window, cx);
+    let default_md_style = MarkdownStyle::themed(MarkdownFont::Agent, window, cx);
 
     MarkdownStyle {
         base_text_style: TextStyle {

crates/markdown/Cargo.toml 🔗

@@ -27,6 +27,7 @@ language.workspace = true
 linkify.workspace = true
 log.workspace = true
 pulldown-cmark.workspace = true
+settings.workspace = true
 sum_tree.workspace = true
 theme.workspace = true
 ui.workspace = true

crates/markdown/src/markdown.rs 🔗

@@ -3,10 +3,14 @@ mod path_range;
 
 use base64::Engine as _;
 use futures::FutureExt as _;
+use gpui::EdgesRefinement;
 use gpui::HitboxBehavior;
+use gpui::UnderlineStyle;
 use language::LanguageName;
 use log::Level;
 pub use path_range::{LineCol, PathWithRange};
+use settings::Settings as _;
+use theme::ThemeSettings;
 use ui::Checkbox;
 use ui::CopyButton;
 
@@ -98,6 +102,133 @@ impl Default for MarkdownStyle {
     }
 }
 
+pub enum MarkdownFont {
+    Agent,
+    Editor,
+}
+
+impl MarkdownStyle {
+    pub fn themed(font: MarkdownFont, window: &Window, cx: &App) -> Self {
+        let theme_settings = ThemeSettings::get_global(cx);
+        let colors = cx.theme().colors();
+
+        let (buffer_font_size, ui_font_size) = match font {
+            MarkdownFont::Agent => (
+                theme_settings.agent_buffer_font_size(cx),
+                theme_settings.agent_ui_font_size(cx),
+            ),
+            MarkdownFont::Editor => (
+                theme_settings.buffer_font_size(cx),
+                theme_settings.ui_font_size(cx),
+            ),
+        };
+
+        let text_color = colors.text;
+
+        let mut text_style = window.text_style();
+        let line_height = buffer_font_size * 1.75;
+
+        text_style.refine(&TextStyleRefinement {
+            font_family: Some(theme_settings.ui_font.family.clone()),
+            font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
+            font_features: Some(theme_settings.ui_font.features.clone()),
+            font_size: Some(ui_font_size.into()),
+            line_height: Some(line_height.into()),
+            color: Some(text_color),
+            ..Default::default()
+        });
+
+        MarkdownStyle {
+            base_text_style: text_style.clone(),
+            syntax: cx.theme().syntax().clone(),
+            selection_background_color: colors.element_selection_background,
+            code_block_overflow_x_scroll: true,
+            heading_level_styles: Some(HeadingLevelStyles {
+                h1: Some(TextStyleRefinement {
+                    font_size: Some(rems(1.15).into()),
+                    ..Default::default()
+                }),
+                h2: Some(TextStyleRefinement {
+                    font_size: Some(rems(1.1).into()),
+                    ..Default::default()
+                }),
+                h3: Some(TextStyleRefinement {
+                    font_size: Some(rems(1.05).into()),
+                    ..Default::default()
+                }),
+                h4: Some(TextStyleRefinement {
+                    font_size: Some(rems(1.).into()),
+                    ..Default::default()
+                }),
+                h5: Some(TextStyleRefinement {
+                    font_size: Some(rems(0.95).into()),
+                    ..Default::default()
+                }),
+                h6: Some(TextStyleRefinement {
+                    font_size: Some(rems(0.875).into()),
+                    ..Default::default()
+                }),
+            }),
+            code_block: StyleRefinement {
+                padding: EdgesRefinement {
+                    top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
+                    left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
+                    right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
+                    bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
+                },
+                margin: EdgesRefinement {
+                    top: Some(Length::Definite(px(8.).into())),
+                    left: Some(Length::Definite(px(0.).into())),
+                    right: Some(Length::Definite(px(0.).into())),
+                    bottom: Some(Length::Definite(px(12.).into())),
+                },
+                border_style: Some(BorderStyle::Solid),
+                border_widths: EdgesRefinement {
+                    top: Some(AbsoluteLength::Pixels(px(1.))),
+                    left: Some(AbsoluteLength::Pixels(px(1.))),
+                    right: Some(AbsoluteLength::Pixels(px(1.))),
+                    bottom: Some(AbsoluteLength::Pixels(px(1.))),
+                },
+                border_color: Some(colors.border_variant),
+                background: Some(colors.editor_background.into()),
+                text: TextStyleRefinement {
+                    font_family: Some(theme_settings.buffer_font.family.clone()),
+                    font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
+                    font_features: Some(theme_settings.buffer_font.features.clone()),
+                    font_size: Some(buffer_font_size.into()),
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            inline_code: TextStyleRefinement {
+                font_family: Some(theme_settings.buffer_font.family.clone()),
+                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
+                font_features: Some(theme_settings.buffer_font.features.clone()),
+                font_size: Some(buffer_font_size.into()),
+                background_color: Some(colors.editor_foreground.opacity(0.08)),
+                ..Default::default()
+            },
+            link: TextStyleRefinement {
+                background_color: Some(colors.editor_foreground.opacity(0.025)),
+                color: Some(colors.text_accent),
+                underline: Some(UnderlineStyle {
+                    color: Some(colors.text_accent.opacity(0.5)),
+                    thickness: px(1.),
+                    ..Default::default()
+                }),
+                ..Default::default()
+            },
+            ..Default::default()
+        }
+    }
+
+    pub fn with_muted_text(mut self, cx: &App) -> Self {
+        let colors = cx.theme().colors();
+        self.base_text_style.color = colors.text_muted;
+        self
+    }
+}
+
 pub struct Markdown {
     source: SharedString,
     selection: Selection,

crates/repl/Cargo.toml 🔗

@@ -36,7 +36,7 @@ jupyter-websocket-client.workspace = true
 jupyter-protocol.workspace = true
 language.workspace = true
 log.workspace = true
-markdown_preview.workspace = true
+markdown.workspace = true
 menu.workspace = true
 multi_buffer.workspace = true
 nbformat.workspace = true

crates/repl/src/notebook/cell.rs 🔗

@@ -9,7 +9,7 @@ use gpui::{
     StatefulInteractiveElement, Task, TextStyleRefinement, image_cache, prelude::*,
 };
 use language::{Buffer, Language, LanguageRegistry};
-use markdown_preview::{markdown_parser::parse_markdown, markdown_renderer::render_markdown_block};
+use markdown::{Markdown, MarkdownElement, MarkdownStyle};
 use nbformat::v4::{CellId, CellMetadata, CellType};
 use runtimelib::{JupyterMessage, JupyterMessageContent};
 use settings::Settings as _;
@@ -322,8 +322,7 @@ pub struct MarkdownCell {
     image_cache: Entity<RetainAllImageCache>,
     source: String,
     editor: Entity<Editor>,
-    parsed_markdown: Option<markdown_preview::markdown_elements::ParsedMarkdown>,
-    markdown_parsing_task: Task<()>,
+    markdown: Entity<Markdown>,
     editing: bool,
     selected: bool,
     cell_position: Option<CellPosition>,
@@ -381,23 +380,7 @@ impl MarkdownCell {
             editor
         });
 
-        let markdown_parsing_task = {
-            let languages = languages.clone();
-            let source = source.clone();
-
-            cx.spawn_in(window, async move |this, cx| {
-                let parsed_markdown = cx
-                    .background_spawn(async move {
-                        parse_markdown(&source, None, Some(languages)).await
-                    })
-                    .await;
-
-                this.update(cx, |cell: &mut MarkdownCell, _| {
-                    cell.parsed_markdown = Some(parsed_markdown);
-                })
-                .log_err();
-            })
-        };
+        let markdown = cx.new(|cx| Markdown::new(source.clone().into(), None, None, cx));
 
         let cell_id = id.clone();
         let editor_subscription =
@@ -419,9 +402,8 @@ impl MarkdownCell {
             image_cache: RetainAllImageCache::new(cx),
             source,
             editor,
-            parsed_markdown: None,
-            markdown_parsing_task,
-            editing: start_editing, // Start in edit mode if empty
+            markdown,
+            editing: start_editing,
             selected: false,
             cell_position: None,
             languages,
@@ -477,18 +459,8 @@ impl MarkdownCell {
         self.source = source.clone();
         let languages = self.languages.clone();
 
-        self.markdown_parsing_task = cx.spawn(async move |this, cx| {
-            let parsed_markdown = cx
-                .background_spawn(
-                    async move { parse_markdown(&source, None, Some(languages)).await },
-                )
-                .await;
-
-            this.update(cx, |cell: &mut MarkdownCell, cx| {
-                cell.parsed_markdown = Some(parsed_markdown);
-                cx.notify();
-            })
-            .log_err();
+        self.markdown.update(cx, |markdown, cx| {
+            markdown.reset(source.into(), cx);
         });
     }
 
@@ -581,42 +553,11 @@ impl Render for MarkdownCell {
         }
 
         // Preview mode - show rendered markdown
-        let Some(parsed) = self.parsed_markdown.as_ref() else {
-            // No parsed content yet, show placeholder that can be clicked to edit
-            let focus_handle = self.editor.focus_handle(cx);
-            return v_flex()
-                .size_full()
-                .children(self.cell_position_spacer(true, window, cx))
-                .child(
-                    h_flex()
-                        .w_full()
-                        .pr_6()
-                        .rounded_xs()
-                        .items_start()
-                        .gap(DynamicSpacing::Base08.rems(cx))
-                        .bg(self.selected_bg_color(window, cx))
-                        .child(self.gutter(window, cx))
-                        .child(
-                            div()
-                                .id("markdown-placeholder")
-                                .flex_1()
-                                .p_3()
-                                .italic()
-                                .text_color(cx.theme().colors().text_muted)
-                                .child("Click to edit markdown...")
-                                .cursor_pointer()
-                                .on_click(cx.listener(move |this, _event, window, cx| {
-                                    this.editing = true;
-                                    window.focus(&this.editor.focus_handle(cx), cx);
-                                    cx.notify();
-                                })),
-                        ),
-                )
-                .children(self.cell_position_spacer(false, window, cx));
-        };
 
-        let mut markdown_render_context =
-            markdown_preview::markdown_renderer::RenderContext::new(None, window, cx);
+        let style = MarkdownStyle {
+            base_text_style: window.text_style(),
+            ..Default::default()
+        };
 
         v_flex()
             .size_full()
@@ -645,11 +586,7 @@ impl Render for MarkdownCell {
                                 window.focus(&this.editor.focus_handle(cx), cx);
                                 cx.notify();
                             }))
-                            .children(parsed.children.iter().map(|child| {
-                                div().relative().child(div().relative().child(
-                                    render_markdown_block(child, &mut markdown_render_context),
-                                ))
-                            })),
+                            .child(MarkdownElement::new(self.markdown.clone(), style)),
                     ),
             )
             .children(self.cell_position_spacer(false, window, cx))

crates/repl/src/outputs.rs 🔗

@@ -254,12 +254,20 @@ impl Output {
             Self::ClearOutputWaitMarker => None,
         };
 
+        let needs_horizontal_scroll = matches!(self, Self::Table { .. } | Self::Image { .. });
+
         h_flex()
             .id("output-content")
             .w_full()
-            .overflow_x_scroll()
+            .when(needs_horizontal_scroll, |el| el.overflow_x_scroll())
             .items_start()
-            .child(div().flex_1().children(content))
+            .child(
+                div()
+                    .when(!needs_horizontal_scroll, |el| {
+                        el.flex_1().w_full().overflow_x_hidden()
+                    })
+                    .children(content),
+            )
             .children(match self {
                 Self::Plain { content, .. } => {
                     Self::render_output_controls(content.clone(), workspace, window, cx)

crates/repl/src/outputs/markdown.rs 🔗

@@ -1,51 +1,25 @@
-use anyhow::Result;
-use gpui::{
-    App, ClipboardItem, Context, Entity, RetainAllImageCache, Task, Window, div, prelude::*,
-};
+use gpui::{App, AppContext, ClipboardItem, Context, Entity, Window, div, prelude::*};
 use language::Buffer;
-use markdown_preview::{
-    markdown_elements::ParsedMarkdown, markdown_parser::parse_markdown,
-    markdown_renderer::render_markdown_block,
-};
-use ui::v_flex;
+use markdown::{Markdown, MarkdownElement, MarkdownFont, MarkdownStyle};
 
 use crate::outputs::OutputContent;
 
 pub struct MarkdownView {
-    raw_text: String,
-    image_cache: Entity<RetainAllImageCache>,
-    contents: Option<ParsedMarkdown>,
-    parsing_markdown_task: Option<Task<Result<()>>>,
+    markdown: Entity<Markdown>,
 }
 
 impl MarkdownView {
     pub fn from(text: String, cx: &mut Context<Self>) -> Self {
-        let parsed = {
-            let text = text.clone();
-            cx.background_spawn(async move { parse_markdown(&text.clone(), None, None).await })
-        };
-        let task = cx.spawn(async move |markdown_view, cx| {
-            let content = parsed.await;
+        let markdown = cx.new(|cx| Markdown::new(text.clone().into(), None, None, cx));
 
-            markdown_view.update(cx, |markdown, cx| {
-                markdown.parsing_markdown_task.take();
-                markdown.contents = Some(content);
-                cx.notify();
-            })
-        });
-
-        Self {
-            raw_text: text,
-            image_cache: RetainAllImageCache::new(cx),
-            contents: None,
-            parsing_markdown_task: Some(task),
-        }
+        Self { markdown }
     }
 }
 
 impl OutputContent for MarkdownView {
-    fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option<ClipboardItem> {
-        Some(ClipboardItem::new_string(self.raw_text.clone()))
+    fn clipboard_content(&self, _window: &Window, cx: &App) -> Option<ClipboardItem> {
+        let source = self.markdown.read(cx).source().to_string();
+        Some(ClipboardItem::new_string(source))
     }
 
     fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool {
@@ -57,10 +31,10 @@ impl OutputContent for MarkdownView {
     }
 
     fn buffer_content(&mut self, _: &mut Window, cx: &mut App) -> Option<Entity<Buffer>> {
+        let source = self.markdown.read(cx).source().to_string();
         let buffer = cx.new(|cx| {
-            // TODO: Bring in the language registry so we can set the language to markdown
-            let mut buffer = Buffer::local(self.raw_text.clone(), cx)
-                .with_language(language::PLAIN_TEXT.clone(), cx);
+            let mut buffer =
+                Buffer::local(source.clone(), cx).with_language(language::PLAIN_TEXT.clone(), cx);
             buffer.set_capability(language::Capability::ReadOnly, cx);
             buffer
         });
@@ -70,24 +44,13 @@ impl OutputContent for MarkdownView {
 
 impl Render for MarkdownView {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let Some(parsed) = self.contents.as_ref() else {
-            return div().into_any_element();
-        };
-
-        let mut markdown_render_context =
-            markdown_preview::markdown_renderer::RenderContext::new(None, window, cx);
-
-        v_flex()
-            .image_cache(self.image_cache.clone())
-            .gap_3()
-            .py_4()
-            .children(parsed.children.iter().map(|child| {
-                div().relative().child(
-                    div()
-                        .relative()
-                        .child(render_markdown_block(child, &mut markdown_render_context)),
-                )
-            }))
-            .into_any_element()
+        let style = markdown_style(window, cx);
+        div()
+            .w_full()
+            .child(MarkdownElement::new(self.markdown.clone(), style))
     }
 }
+
+fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
+    MarkdownStyle::themed(MarkdownFont::Editor, window, cx)
+}

crates/repl/src/session.rs 🔗

@@ -191,7 +191,7 @@ impl EditorBlock {
                 .child(
                     div()
                         .flex_1()
-                        .size_full()
+                        .overflow_x_hidden()
                         .py(text_line_height / 2.)
                         .mr(editor_margins.right)
                         .pr_2()