From 446227cf6d3ffdaf78d0ac2acf584c816c7f42d4 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Wed, 28 Jan 2026 09:42:07 -0800 Subject: [PATCH] repl: Streamline Markdown output usage (#47713) 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. image Release Notes: - N/A --- 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(-) diff --git a/Cargo.lock b/Cargo.lock index 431865335480c136f9e8aedff40b827a9d5db891..662703a1beddbd9536b68fe893501578f0d4459d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13764,7 +13764,7 @@ dependencies = [ "language", "languages", "log", - "markdown_preview", + "markdown", "menu", "multi_buffer", "nbformat", diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 8ef92266ea28a60426df2a200c5542b415aadf0c..0ec6fd6b36f28fa9621017b261fc0d169fa08974 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/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 { diff --git a/crates/markdown/Cargo.toml b/crates/markdown/Cargo.toml index 3f9b4ea366eca21a25ca422f4b1c5681d4ae9765..f58df4da86784d626b7428b60133475dc952aa8e 100644 --- a/crates/markdown/Cargo.toml +++ b/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 diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 950fc4a2eaf86739752c4ec3139fe07520f9985b..f5e3ee1366cbae0a91c24efde1f034d2a2d5ffb1 100644 --- a/crates/markdown/src/markdown.rs +++ b/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, diff --git a/crates/repl/Cargo.toml b/crates/repl/Cargo.toml index ca263d4edf14bb84407cf47dd3c0b17c60255a6f..1047dd68f2b10181765916612fd330b27fed66ad 100644 --- a/crates/repl/Cargo.toml +++ b/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 diff --git a/crates/repl/src/notebook/cell.rs b/crates/repl/src/notebook/cell.rs index 495285f86e44266d22455d56ff202e69f5f4c2fe..a62c6cd4b7baedff929442e93f90a9c8e3f736a8 100644 --- a/crates/repl/src/notebook/cell.rs +++ b/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, source: String, editor: Entity, - parsed_markdown: Option, - markdown_parsing_task: Task<()>, + markdown: Entity, editing: bool, selected: bool, cell_position: Option, @@ -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)) diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index 8a48868ab31a20795b166ab63aae014fa8e14ac0..d1942a67d8ceaafd2ad207397a6677682e2e1e05 100644 --- a/crates/repl/src/outputs.rs +++ b/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) diff --git a/crates/repl/src/outputs/markdown.rs b/crates/repl/src/outputs/markdown.rs index bd88f4e159f7dbd472ad52a9fa424741400394d7..445932987c7dc27c6ae992739aa9c4fbeb8b87fb 100644 --- a/crates/repl/src/outputs/markdown.rs +++ b/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, - contents: Option, - parsing_markdown_task: Option>>, + markdown: Entity, } impl MarkdownView { pub fn from(text: String, cx: &mut Context) -> 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 { - Some(ClipboardItem::new_string(self.raw_text.clone())) + fn clipboard_content(&self, _window: &Window, cx: &App) -> Option { + 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> { + 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) -> 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) +} diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index ef6757df07be310913b83eda2cc9c4a3c8d0ff09..57703c305badcf7f09d09e65e8d712c0ef3bef7a 100644 --- a/crates/repl/src/session.rs +++ b/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()