Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions app/src/ai/blocklist/block/view_impl/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1514,7 +1514,10 @@ fn lightbox_image_for_mermaid_diagram(
return None;
}

let asset_source = mermaid_asset_source(&diagram.source);
let asset_source = mermaid_asset_source(
&diagram.source,
Appearance::as_ref(app).theme().inferred_color_scheme(),
);
let asset_state = AssetCache::as_ref(app).load_asset::<ImageType>(asset_source.clone());
if matches!(asset_state, AssetState::FailedToLoad(_)) {
return None;
Expand Down Expand Up @@ -2154,7 +2157,10 @@ fn render_mermaid_diagram_section<A: Action>(
return render_visual_markdown_fallback(&diagram.markdown_source, text_color, app);
}

let asset_source = mermaid_asset_source(&diagram.source);
let asset_source = mermaid_asset_source(
&diagram.source,
Appearance::as_ref(app).theme().inferred_color_scheme(),
);
let asset_state = AssetCache::as_ref(app).load_asset::<ImageType>(asset_source.clone());
if matches!(asset_state, AssetState::FailedToLoad(_)) {
return render_visual_markdown_fallback(&diagram.markdown_source, text_color, app);
Expand Down Expand Up @@ -2187,7 +2193,7 @@ fn render_mermaid_diagram_section<A: Action>(
app,
);
let mermaid_canvas = Container::new(mermaid_block)
.with_background(theme.foreground())
.with_background(theme.background())
.with_uniform_padding(MERMAID_CANVAS_PADDING)
.finish();

Expand Down
7 changes: 6 additions & 1 deletion app/src/notebooks/editor/notebook_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,12 @@ impl RunnableCommandModel for NotebookCommand {
ctx.dispatch_typed_action(WorkspaceAction::OpenLightbox {
images: vec![LightboxImage {
source: LightboxImageSource::Resolved {
asset_source: mermaid_asset_source(&source),
asset_source: mermaid_asset_source(
&source,
Appearance::as_ref(app)
.theme()
.inferred_color_scheme(),
),
},
description: None,
}],
Expand Down
3 changes: 2 additions & 1 deletion app/src/notebooks/editor/view_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use async_channel::TryRecvError;
use parking_lot::Mutex;
use string_offset::CharOffset;
use tempfile::tempdir;
use warp_core::ui::theme::ColorScheme;
use warp_editor::content::mermaid_diagram::mermaid_asset_source;
use warp_editor::render::element::RichTextAction;
use warp_editor::render::model::{
Expand Down Expand Up @@ -190,7 +191,7 @@ fn test_loaded_mermaid_diagram_with_placeholder_height_needs_relayout() {
App::test((), |app| async move {
let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true);
let contents = "graph TD\nA[Start] --> B[Finish]\n";
let asset_source = mermaid_asset_source(contents);
let asset_source = mermaid_asset_source(contents, ColorScheme::LightOnDark);

let pending = app.read(|ctx| {
let asset_cache = AssetCache::as_ref(ctx);
Expand Down
24 changes: 18 additions & 6 deletions crates/editor/src/content/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use string_offset::{ByteOffset, CharOffset};
use urlocator::{UrlLocation, UrlLocator};
use vec1::Vec1;
use warp_core::features::FeatureFlag;
use warp_core::ui::appearance::Appearance;
use warp_core::ui::theme::Fill as ThemeFill;
use warp_errors::report_error;
use warpui_core::assets::asset_cache::{AssetCache, AssetSource, AssetState};
Expand Down Expand Up @@ -782,7 +783,8 @@ impl LayoutTask {
};
}

let asset_source = mermaid_asset_source(&source);
let color_scheme = Appearance::as_ref(app).theme().inferred_color_scheme();
let asset_source = mermaid_asset_source(&source, color_scheme);
let asset_cache = AssetCache::as_ref(app);
match asset_cache.load_asset::<ImageType>(asset_source.clone()) {
AssetState::Loaded { .. } => {
Expand All @@ -791,7 +793,7 @@ impl LayoutTask {
.block_spacings
.from_block_style(&text_block.style);
let (asset_source, config) =
mermaid_diagram_layout(&source, layout, spacing, app);
mermaid_diagram_layout(&source, layout, spacing, color_scheme, app);
Self::MermaidDiagram {
text_block,
asset_source,
Expand All @@ -806,8 +808,13 @@ impl LayoutTask {
.rich_text_styles()
.block_spacings
.from_block_style(&text_block.style);
let (asset_source, config) =
mermaid_diagram_layout(&source, layout, spacing, app);
let (asset_source, config) = mermaid_diagram_layout(
&source,
layout,
spacing,
color_scheme,
app,
);
Self::MermaidDiagram {
text_block,
asset_source,
Expand All @@ -828,8 +835,13 @@ impl LayoutTask {
.rich_text_styles()
.block_spacings
.from_block_style(&text_block.style);
let (asset_source, config) =
mermaid_diagram_layout(&source, layout, spacing, app);
let (asset_source, config) = mermaid_diagram_layout(
&source,
layout,
spacing,
color_scheme,
app,
);
Self::MermaidDiagram {
text_block,
asset_source,
Expand Down
28 changes: 22 additions & 6 deletions crates/editor/src/content/edit_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use std::time::{SystemTime, UNIX_EPOCH};

use string_offset::CharOffset;
use warp_core::features::FeatureFlag;
use warp_core::ui::appearance::Appearance;
use warp_core::ui::theme::ColorScheme;
use warpui_core::assets::asset_cache::{AssetCache, AssetSource, AssetState};
use warpui_core::fonts::{Properties, Style, Weight};
use warpui_core::image_cache::ImageType;
Expand Down Expand Up @@ -297,7 +299,7 @@ fn test_layout_mermaid_block_uses_loaded_svg_aspect_ratio() {
App::test((), |app| async move {
let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true);
let content = "graph TD\nA[Start] --> B[Finish]\n";
let asset_source = mermaid_asset_source(content);
let asset_source = mermaid_asset_source(content, ColorScheme::LightOnDark);

let mermaid_load = app.read(|ctx| {
let asset_cache = AssetCache::as_ref(ctx);
Expand Down Expand Up @@ -335,7 +337,13 @@ fn test_layout_mermaid_block_uses_loaded_svg_aspect_ratio() {
content_length: CharOffset::from(content.chars().count()),
};
let spacing = TEST_STYLES.block_spacings.from_block_style(&block_style);
let mermaid_diagram = mermaid_diagram_layout(content, &text_layout, spacing, ctx);
let mermaid_diagram = mermaid_diagram_layout(
content,
&text_layout,
spacing,
ColorScheme::LightOnDark,
ctx,
);

let (item, _has_trailing_newline) = layout_mermaid_diagram_block(
block,
Expand Down Expand Up @@ -404,8 +412,13 @@ fn test_unloaded_mermaid_diagram_uses_stable_full_width_placeholder_height() {
code_block_type: CodeBlockType::Mermaid,
};
let spacing = TEST_STYLES.block_spacings.from_block_style(&block_style);
let (_asset_source, config) =
mermaid_diagram_layout(contents, &text_layout, spacing, ctx);
let (_asset_source, config) = mermaid_diagram_layout(
contents,
&text_layout,
spacing,
ColorScheme::LightOnDark,
ctx,
);
let expected_width = 800. - spacing.x_axis_offset().as_f32();
let expected_height = TEST_STYLES.base_line_height().as_f32() * 10.;

Expand Down Expand Up @@ -486,6 +499,7 @@ fn test_empty_mermaid_block_lays_out_as_code_block() {
fn test_non_parseable_mermaid_block_lays_out_as_code_block() {
App::test((), |app| async move {
let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true);
app.add_singleton_model(|_| Appearance::mock());
app.read(|ctx| {
let layout_cache = LayoutCache::new();
let text_layout = TextLayout::new(
Expand Down Expand Up @@ -524,8 +538,9 @@ fn test_non_parseable_mermaid_block_lays_out_as_code_block() {
fn test_invalid_mermaid_block_stays_as_code_block_after_load_fails() {
App::test((), |app| async move {
let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true);
app.add_singleton_model(|_| Appearance::mock());
let contents = "echo hi\n";
let asset_source = mermaid_asset_source(contents);
let asset_source = mermaid_asset_source(contents, ColorScheme::LightOnDark);

// Drive the asset load to completion (it should fail, since `echo hi` isn't
// valid Mermaid).
Expand Down Expand Up @@ -586,8 +601,9 @@ fn test_invalid_mermaid_block_stays_as_code_block_after_load_fails() {
fn test_valid_mermaid_block_lays_out_as_diagram_after_load() {
App::test((), |app| async move {
let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true);
app.add_singleton_model(|_| Appearance::mock());
let contents = "graph TD\nA[Start] --> B[Finish]\n";
let asset_source = mermaid_asset_source(contents);
let asset_source = mermaid_asset_source(contents, ColorScheme::LightOnDark);

// Drive the async Mermaid render to completion.
let pending = app.read(|ctx| {
Expand Down
23 changes: 20 additions & 3 deletions crates/editor/src/content/mermaid_diagram.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use std::hash::{DefaultHasher, Hash, Hasher};
use std::sync::Arc;

use bytes::Bytes;
use mermaid_to_svg::{MermaidTheme, parse_mermaid_frontmatter};
use warp_core::ui::theme::ColorScheme;
use warpui_core::assets::asset_cache::{
AssetCache, AssetSource, AssetState, AsyncAssetId, AsyncAssetType,
};
Expand All @@ -19,19 +21,22 @@ struct MermaidDiagramAsset;

impl AsyncAssetType for MermaidDiagramAsset {}

pub fn mermaid_asset_source(source: &str) -> AssetSource {
pub fn mermaid_asset_source(source: &str, color_scheme: ColorScheme) -> AssetSource {
let source = source.to_string();
let theme = mermaid_theme(&source, color_scheme);
let mut hasher = DefaultHasher::new();
source.hash(&mut hasher);
theme.hash(&mut hasher);
let id = format!("configured:{:x}", hasher.finish());
let fetch_source = source.clone();

AssetSource::Async {
id: AsyncAssetId::new::<MermaidDiagramAsset>(id),
fetch: Arc::new(move || {
let source = fetch_source.clone();
let theme = theme.clone();
Box::pin(async move {
mermaid_to_svg::render_mermaid_to_svg(&source, None)
mermaid_to_svg::render_mermaid_to_svg(&source, Some(&theme))
.map(|svg| Bytes::from(svg.into_bytes()))
.map_err(Into::into)
})
Expand All @@ -43,14 +48,26 @@ pub fn mermaid_diagram_layout(
source: &str,
layout: &TextLayout,
spacing: BlockSpacing,
color_scheme: ColorScheme,
app: &AppContext,
) -> (AssetSource, ImageBlockConfig) {
let asset_source = mermaid_asset_source(source);
let asset_source = mermaid_asset_source(source, color_scheme);
let config = mermaid_diagram_config(&asset_source, layout, spacing, app);

(asset_source, config)
}

fn mermaid_theme(source: &str, color_scheme: ColorScheme) -> MermaidTheme {
if let Some(configured_theme) = parse_mermaid_frontmatter(source).config.to_mermaid_theme() {
return configured_theme;
}

match color_scheme {
ColorScheme::LightOnDark => MermaidTheme::dark(),
ColorScheme::DarkOnLight => MermaidTheme::light(),
}
}

fn mermaid_diagram_config(
asset_source: &AssetSource,
layout: &TextLayout,
Expand Down
52 changes: 49 additions & 3 deletions crates/editor/src/content/mermaid_diagram_tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use warp_core::ui::theme::ColorScheme;
use warpui_core::assets::asset_cache::{AssetCache, AssetSource, AssetState};
use warpui_core::image_cache::ImageType;
use warpui_core::text_layout::LayoutCache;
Expand Down Expand Up @@ -27,8 +28,13 @@ fn loading_mermaid_layout_uses_default_height() {
&TEST_STYLES,
800.,
);
let (_asset_source, config) =
mermaid_diagram_layout(source, &text_layout, mermaid_block_spacing(), ctx);
let (_asset_source, config) = mermaid_diagram_layout(
source,
&text_layout,
mermaid_block_spacing(),
ColorScheme::LightOnDark,
ctx,
);
let expected_height = TEST_STYLES.base_line_height()
* DEFAULT_MERMAID_HEIGHT_LINE_MULTIPLIER.into_pixels();

Expand All @@ -54,7 +60,8 @@ flowchart TD
A[Start] --> B[Done]
"##;

let AssetSource::Async { fetch, .. } = mermaid_asset_source(source) else {
let AssetSource::Async { fetch, .. } = mermaid_asset_source(source, ColorScheme::LightOnDark)
else {
panic!("expected Mermaid diagrams to be async assets");
};
let bytes = match futures_lite::future::block_on(fetch()) {
Expand All @@ -67,10 +74,49 @@ flowchart TD
};

assert!(svg.contains("<svg "));
assert!(svg.contains("background-color: #ffffff"));
assert!(svg.contains(r##"fill="#ff0000""##));
assert!(svg.contains(r#"font-family="Inter""#));
}

#[test]
fn mermaid_asset_source_uses_active_color_scheme() {
let source = "graph TD\nA[Start] --> B[Finish]\n";
let AssetSource::Async {
fetch: fetch_dark, ..
} = mermaid_asset_source(source, ColorScheme::LightOnDark)
else {
panic!("expected Mermaid diagrams to be async assets");
};
let AssetSource::Async {
fetch: fetch_light, ..
} = mermaid_asset_source(source, ColorScheme::DarkOnLight)
else {
panic!("expected Mermaid diagrams to be async assets");
};

let dark_svg = match futures_lite::future::block_on(fetch_dark()) {
Ok(bytes) => String::from_utf8(bytes.to_vec()).expect("expected valid UTF-8"),
Err(error) => panic!("expected dark Mermaid SVG to render: {error:#}"),
};
let light_svg = match futures_lite::future::block_on(fetch_light()) {
Ok(bytes) => String::from_utf8(bytes.to_vec()).expect("expected valid UTF-8"),
Err(error) => panic!("expected light Mermaid SVG to render: {error:#}"),
};

assert!(dark_svg.contains("background-color: #1e1e1e"));
assert!(light_svg.contains("background-color: #ffffff"));
}

#[test]
fn mermaid_asset_source_cache_key_includes_color_scheme() {
let source = "graph TD\nA[Start] --> B[Finish]\n";
let dark_asset = mermaid_asset_source(source, ColorScheme::LightOnDark);
let light_asset = mermaid_asset_source(source, ColorScheme::DarkOnLight);

assert_ne!(dark_asset, light_asset);
}

#[test]
fn failed_mermaid_layout_uses_compact_height() {
App::test((), |app| async move {
Expand Down