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",
|
"language",
|
||||||
"languages",
|
"languages",
|
||||||
"log",
|
"log",
|
||||||
|
"markdown_preview",
|
||||||
"multi_buffer",
|
"multi_buffer",
|
||||||
"project",
|
"project",
|
||||||
"runtimelib",
|
"runtimelib",
|
||||||
|
|
|
@ -26,6 +26,7 @@ gpui.workspace = true
|
||||||
image.workspace = true
|
image.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
markdown_preview.workspace = true
|
||||||
multi_buffer.workspace = true
|
multi_buffer.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
runtimelib.workspace = true
|
runtimelib.workspace = true
|
||||||
|
|
|
@ -5,8 +5,8 @@ use crate::stdio::TerminalOutput;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use base64::prelude::*;
|
use base64::prelude::*;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
img, percentage, Animation, AnimationExt, AnyElement, FontWeight, ImageData, Render, TextRun,
|
img, percentage, Animation, AnimationExt, AnyElement, FontWeight, ImageData, Render, Task,
|
||||||
Transformation,
|
TextRun, Transformation, View,
|
||||||
};
|
};
|
||||||
use runtimelib::datatable::TableSchema;
|
use runtimelib::datatable::TableSchema;
|
||||||
use runtimelib::media::datatable::TabularDataResource;
|
use runtimelib::media::datatable::TabularDataResource;
|
||||||
|
@ -16,6 +16,11 @@ use settings::Settings;
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{div, prelude::*, v_flex, IntoElement, Styled, ViewContext};
|
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
|
/// 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 {
|
fn rank_mime_type(mimetype: &MimeType) -> usize {
|
||||||
match mimetype {
|
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 {
|
pub struct Output {
|
||||||
content: OutputContent,
|
content: OutputContent,
|
||||||
display_id: Option<String>,
|
display_id: Option<String>,
|
||||||
|
@ -296,33 +353,17 @@ pub enum OutputContent {
|
||||||
ErrorOutput(ErrorView),
|
ErrorOutput(ErrorView),
|
||||||
Message(String),
|
Message(String),
|
||||||
Table(TableView),
|
Table(TableView),
|
||||||
|
Markdown(View<MarkdownView>),
|
||||||
ClearOutputWaitMarker,
|
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 {
|
impl OutputContent {
|
||||||
fn render(&self, cx: &ViewContext<ExecutionView>) -> Option<AnyElement> {
|
fn render(&self, cx: &ViewContext<ExecutionView>) -> Option<AnyElement> {
|
||||||
let el = match self {
|
let el = match self {
|
||||||
// Note: in typical frontends we would show the execute_result.execution_count
|
// Note: in typical frontends we would show the execute_result.execution_count
|
||||||
// Here we can just handle either
|
// Here we can just handle either
|
||||||
Self::Plain(stdio) => Some(stdio.render(cx)),
|
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::Stream(stdio) => Some(stdio.render(cx)),
|
||||||
Self::Image(image) => Some(image.render(cx)),
|
Self::Image(image) => Some(image.render(cx)),
|
||||||
Self::Message(message) => Some(div().child(message.clone()).into_any_element()),
|
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 {
|
pub fn new(data: &MimeBundle, cx: &mut WindowContext) -> Self {
|
||||||
match data.richest(rank_mime_type) {
|
match data.richest(rank_mime_type) {
|
||||||
Some(MimeType::Plain(text)) => OutputContent::Plain(TerminalOutput::from(text, cx)),
|
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) {
|
Some(MimeType::Png(data)) | Some(MimeType::Jpeg(data)) => match ImageView::from(data) {
|
||||||
Ok(view) => OutputContent::Image(view),
|
Ok(view) => OutputContent::Image(view),
|
||||||
Err(error) => OutputContent::Message(format!("Failed to load image: {}", error)),
|
Err(error) => OutputContent::Message(format!("Failed to load image: {}", error)),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue