repl: Render markdown output from kernels (#15742)
<img width="1268" alt="image" src="https://github.com/user-attachments/assets/73e03a28-f5e3-4395-a58c-cabd07f57889"> Release Notes: - Added markdown rendering for Jupyter/REPL outputs. Push Markdown from Deno/Typescript with `Deno.jupyter.md` and in IPython use `IPython.display.Markdown`.
This commit is contained in:
parent
36b61a8b87
commit
b7eae7fbd9
3 changed files with 67 additions and 21 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -8749,6 +8749,7 @@ dependencies = [
|
|||
"language",
|
||||
"languages",
|
||||
"log",
|
||||
"markdown_preview",
|
||||
"multi_buffer",
|
||||
"project",
|
||||
"runtimelib",
|
||||
|
|
|
@ -26,6 +26,7 @@ gpui.workspace = true
|
|||
image.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
markdown_preview.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
project.workspace = true
|
||||
runtimelib.workspace = true
|
||||
|
|
|
@ -5,8 +5,8 @@ use crate::stdio::TerminalOutput;
|
|||
use anyhow::Result;
|
||||
use base64::prelude::*;
|
||||
use gpui::{
|
||||
img, percentage, Animation, AnimationExt, AnyElement, FontWeight, ImageData, Render, TextRun,
|
||||
Transformation,
|
||||
img, percentage, Animation, AnimationExt, AnyElement, FontWeight, ImageData, Render, Task,
|
||||
TextRun, Transformation, View,
|
||||
};
|
||||
use runtimelib::datatable::TableSchema;
|
||||
use runtimelib::media::datatable::TabularDataResource;
|
||||
|
@ -16,6 +16,11 @@ use settings::Settings;
|
|||
use theme::ThemeSettings;
|
||||
use ui::{div, prelude::*, v_flex, IntoElement, Styled, ViewContext};
|
||||
|
||||
use markdown_preview::{
|
||||
markdown_elements::ParsedMarkdown, markdown_parser::parse_markdown,
|
||||
markdown_renderer::render_markdown_block,
|
||||
};
|
||||
|
||||
/// When deciding what to render from a collection of mediatypes, we need to rank them in order of importance
|
||||
fn rank_mime_type(mimetype: &MimeType) -> usize {
|
||||
match mimetype {
|
||||
|
@ -268,6 +273,58 @@ impl ErrorView {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct MarkdownView {
|
||||
contents: Option<ParsedMarkdown>,
|
||||
parsing_markdown_task: Option<Task<Result<()>>>,
|
||||
}
|
||||
|
||||
impl MarkdownView {
|
||||
pub fn from(text: String, cx: &mut ViewContext<Self>) -> Self {
|
||||
let task = cx.spawn(|markdown, mut cx| async move {
|
||||
let text = text.clone();
|
||||
let parsed = cx
|
||||
.background_executor()
|
||||
.spawn(async move { parse_markdown(&text, None, None).await });
|
||||
|
||||
let content = parsed.await;
|
||||
|
||||
markdown.update(&mut cx, |markdown, cx| {
|
||||
markdown.parsing_markdown_task.take();
|
||||
markdown.contents = Some(content);
|
||||
cx.notify();
|
||||
})
|
||||
});
|
||||
|
||||
Self {
|
||||
contents: None,
|
||||
parsing_markdown_task: Some(task),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for MarkdownView {
|
||||
fn render(&mut self, cx: &mut ViewContext<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, cx);
|
||||
|
||||
v_flex()
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Output {
|
||||
content: OutputContent,
|
||||
display_id: Option<String>,
|
||||
|
@ -296,33 +353,17 @@ pub enum OutputContent {
|
|||
ErrorOutput(ErrorView),
|
||||
Message(String),
|
||||
Table(TableView),
|
||||
Markdown(View<MarkdownView>),
|
||||
ClearOutputWaitMarker,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for OutputContent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
OutputContent::Plain(_) => f.debug_struct("OutputContent(Plain)"),
|
||||
OutputContent::Stream(_) => f.debug_struct("OutputContent(Stream)"),
|
||||
OutputContent::Image(_) => f.debug_struct("OutputContent(Image)"),
|
||||
OutputContent::ErrorOutput(_) => f.debug_struct("OutputContent(ErrorOutput)"),
|
||||
OutputContent::Message(_) => f.debug_struct("OutputContent(Message)"),
|
||||
OutputContent::Table(_) => f.debug_struct("OutputContent(Table)"),
|
||||
OutputContent::ClearOutputWaitMarker => {
|
||||
f.debug_struct("OutputContent(ClearOutputWaitMarker)")
|
||||
}
|
||||
}
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputContent {
|
||||
fn render(&self, cx: &ViewContext<ExecutionView>) -> Option<AnyElement> {
|
||||
let el = match self {
|
||||
// Note: in typical frontends we would show the execute_result.execution_count
|
||||
// Here we can just handle either
|
||||
Self::Plain(stdio) => Some(stdio.render(cx)),
|
||||
// Self::Markdown(markdown) => Some(markdown.render(theme)),
|
||||
Self::Markdown(markdown) => Some(markdown.clone().into_any_element()),
|
||||
Self::Stream(stdio) => Some(stdio.render(cx)),
|
||||
Self::Image(image) => Some(image.render(cx)),
|
||||
Self::Message(message) => Some(div().child(message.clone()).into_any_element()),
|
||||
|
@ -337,7 +378,10 @@ impl OutputContent {
|
|||
pub fn new(data: &MimeBundle, cx: &mut WindowContext) -> Self {
|
||||
match data.richest(rank_mime_type) {
|
||||
Some(MimeType::Plain(text)) => OutputContent::Plain(TerminalOutput::from(text, cx)),
|
||||
Some(MimeType::Markdown(text)) => OutputContent::Plain(TerminalOutput::from(text, cx)),
|
||||
Some(MimeType::Markdown(text)) => {
|
||||
let view = cx.new_view(|cx| MarkdownView::from(text.clone(), cx));
|
||||
OutputContent::Markdown(view)
|
||||
}
|
||||
Some(MimeType::Png(data)) | Some(MimeType::Jpeg(data)) => match ImageView::from(data) {
|
||||
Ok(view) => OutputContent::Image(view),
|
||||
Err(error) => OutputContent::Message(format!("Failed to load image: {}", error)),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue