Markdown pictures: freed with their elements, glyphs whole, failures keep the source, file URLs parsed, Mermaid labels in Ely's ink
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 23s
CI / crate (push) Failing after 27m31s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-11 23:59:56 -04:00
1 parent d5ecc5a69e
commit ab9a14c692
12 files changed
+417 -126

No files matched your search

+1 -1
View File
@@ -159,7 +159,7 @@ impl BlockEditor {
}
BlockKind::Math => {
let tex = block.fields[0].read(cx).text().to_string();
let shown = match Latex::parse(&tex) {
let shown = match Latex::parse((id.clone(), format!("math-{key}")), &tex) {
_ if tex.trim().is_empty() => div().into_any_element(),
Ok(math) => div()
.flex()
+25 -11
View File
@@ -4,22 +4,27 @@ use std::{
};
use gpui::{
App, Asset, Hsla, IntoElement, ParentElement, Pixels, RenderImage, RenderOnce, SharedString,
Size, Styled, Window, div, img, px, size,
App, Asset, ElementId, Hsla, IntoElement, ParentElement, Pixels, RenderImage, RenderOnce,
SharedString, Size, Styled, Window, div, img, px, size,
};
use mermaid_rs_renderer::{LayoutConfig, RenderOptions, Theme, render_strict};
use crate::theme::{ActiveTheme, Radius, TextSize};
use crate::{
primitives::hold,
theme::{ActiveTheme, Radius, TextSize},
};
/// A Mermaid diagram; a failure shows why and the source.
#[derive(IntoElement)]
pub struct Diagram {
id: ElementId,
source: SharedString,
}
impl Diagram {
pub fn new(source: impl Into<SharedString>) -> Self {
pub fn new(id: impl Into<ElementId>, source: impl Into<SharedString>) -> Self {
Self {
id: id.into(),
source: source.into(),
}
}
@@ -66,9 +71,14 @@ fn svg(chart: &Chart) -> Result<String, SharedString> {
true => Theme::dark(),
false => Theme::modern(),
};
theme.background = chart.ground.to_string();
theme.edge_label_background = chart.ground.to_string();
theme.text_color = chart.ink.to_string();
let (ground, ink) = (chart.ground.to_string(), chart.ink.to_string());
theme.background = ground.clone();
theme.edge_label_background = ground;
theme.primary_text_color = ink.clone();
theme.text_color = ink.clone();
theme.pie_title_text_color = ink.clone();
theme.pie_section_text_color = ink.clone();
theme.pie_legend_text_color = ink;
let options = RenderOptions {
theme,
layout: LayoutConfig::default(),
@@ -114,7 +124,8 @@ impl RenderOnce for Diagram {
theme.text_size(TextSize::Sm),
theme.mono_family.clone(),
);
match window.use_asset::<Drawer>(&chart, cx) {
let picture = |drawn: &Drawn| drawn.as_ref().ok().map(|(image, _)| image.clone());
match hold::<Drawer>(self.id, &chart, picture, window, cx) {
Some(Ok((picture, shape))) => div()
.flex()
.justify_center()
@@ -157,9 +168,12 @@ mod tests {
#[test]
fn a_flowchart_draws_with_the_ground_and_ink() {
let svg = svg(&chart("flowchart LR\n A --> B", true)).expect("draws");
assert!(svg.contains("<svg"), "{svg}");
assert!(svg.to_lowercase().contains("#2c2c2c"), "the ground: {svg}");
let svg = svg(&chart("flowchart LR\n A[Start] --> B", true)).expect("draws");
let svg = svg.to_lowercase();
assert!(svg.contains("#2c2c2c"), "the ground: {svg}");
let label = svg.find(">start<").expect("the label");
let tag = &svg[svg[..label].rfind("<text").expect("its tag")..label];
assert!(tag.contains("fill=\"#eeeeee\""), "the label's ink: {tag}");
}
#[test]
+59 -30
View File
@@ -5,14 +5,14 @@ use std::{
use base64::Engine as _;
use gpui::{
AnyElement, App, Asset, Hsla, Image, ImageAssetLoader, ImageFormat, InteractiveElement,
IntoElement, ParentElement, RenderImage, RenderOnce, Resource, SMOOTH_SVG_SCALE_FACTOR,
SharedString, Styled, Window, div, img, px,
AnyElement, App, Asset, ElementId, Hsla, Image, ImageAssetLoader, ImageFormat,
InteractiveElement, IntoElement, ParentElement, RenderImage, RenderOnce, Resource,
SMOOTH_SVG_SCALE_FACTOR, SharedString, Styled, Window, div, img, px,
};
use crate::{
i18n,
primitives::{Icon, IconName},
primitives::{Icon, IconName, hold},
theme::{ActiveTheme, IconSize, Radius},
};
@@ -62,14 +62,8 @@ pub(crate) fn origin(url: &str, base: Option<&Path>) -> Origin {
if lower.starts_with("data:") {
return inline(url);
}
if let Some(rest) = url.strip_prefix("file://") {
let rest = rest.strip_prefix("localhost").unwrap_or(rest);
// Windows writes file:///C:/…
let rest = match rest.as_bytes() {
[b'/', _, b':', ..] if cfg!(windows) => &rest[1..],
_ => rest,
};
return path(rest).map_or_else(Origin::Bad, Origin::File);
if lower.starts_with("file:") {
return file(&url[5..]);
}
let scheme = url.split_once(':').is_some_and(|(scheme, _)| {
scheme.len() > 1
@@ -88,6 +82,24 @@ pub(crate) fn origin(url: &str, base: Option<&Path>) -> Origin {
}
}
/// A `file:` address after its scheme: host, path, no query.
fn file(rest: &str) -> Origin {
let rest = rest.split(['?', '#']).next().unwrap_or_default();
let (host, written) = match rest.strip_prefix("//") {
Some(after) => after.split_at(after.find('/').unwrap_or(after.len())),
None => ("", rest),
};
let local = host.is_empty() || host.eq_ignore_ascii_case("localhost");
let written = match (local, written.as_bytes()) {
// Windows writes file:///C:/…
(true, [b'/', _, b':', ..]) if cfg!(windows) => written[1..].to_string(),
(true, _) => written.to_string(),
(false, _) if cfg!(windows) => format!("//{host}{written}"),
(false, _) => return Origin::Bad(format!("files on {host} are not read").into()),
};
path(&written).map_or_else(Origin::Bad, Origin::File)
}
/// A `data:` address: base64 bytes of an image type.
fn inline(url: &str) -> Origin {
let Some((head, payload)) = url[5..].split_once(',') else {
@@ -128,6 +140,7 @@ impl Asset for Unpacker {
/// The picture at `url`, natural size up to the column.
#[derive(IntoElement)]
pub(crate) struct Picture {
pub id: ElementId,
pub alt: String,
pub url: String,
/// The folder relative addresses start from.
@@ -136,32 +149,32 @@ pub(crate) struct Picture {
impl RenderOnce for Picture {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
picture(&self.alt, &self.url, self.base.as_deref(), window, cx)
picture(self, window, cx)
}
}
fn picture(
alt: &str,
url: &str,
base: Option<&Path>,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
fn picture(shown: Picture, window: &mut Window, cx: &mut App) -> AnyElement {
let Picture { id, alt, url, base } = shown;
let (alt, url) = (alt.as_str(), url.as_str());
let colors = cx.theme().colors.clone();
let (loaded, svg) = match origin(url, base) {
let (loaded, svg) = match origin(url, base.as_deref()) {
Origin::File(path) => {
let svg = path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("svg"));
let resource = Resource::Path(path.into());
let loaded = window
.use_asset::<ImageAssetLoader>(&resource, cx)
let image = |loaded: &Result<Arc<RenderImage>, _>| loaded.as_ref().ok().cloned();
let loaded = hold::<ImageAssetLoader>(id, &resource, image, window, cx)
.map(|loaded| loaded.map_err(|error| SharedString::from(error.to_string())));
(loaded, svg)
}
Origin::Data(format, bytes) => {
let svg = format == ImageFormat::Svg;
(window.use_asset::<Unpacker>(&(format, bytes), cx), svg)
let image = |loaded: &Result<Arc<RenderImage>, _>| loaded.as_ref().ok().cloned();
(
hold::<Unpacker>(id, &(format, bytes), image, window, cx),
svg,
)
}
Origin::Web => {
let said = i18n::text(cx, "markdown.picture.web", &[]);
@@ -240,19 +253,35 @@ mod tests {
}
#[test]
fn file_addresses_decode() {
assert_eq!(
origin("file:///tmp/a%23b.png", None),
Origin::File(PathBuf::from("/tmp/a#b.png"))
);
fn file_addresses_decode_without_query_or_fragment() {
for url in [
"file:///tmp/a%23b.png",
"file://localhost/tmp/a%23b.png#x",
"FILE:/tmp/a%23b.png?y",
] {
assert_eq!(
origin(url, None),
Origin::File(PathBuf::from("/tmp/a#b.png")),
"{url}"
);
}
if cfg!(windows) {
assert_eq!(
origin("file:///C:/notes/a.png", None),
origin("file:///C:/notes/a.png#preview", None),
Origin::File(PathBuf::from("C:/notes/a.png"))
);
}
}
#[test]
fn a_file_on_another_host_is_its_share_or_refused() {
let found = origin("file://server/share/a.png", Some(Path::new("/notes")));
match cfg!(windows) {
true => assert_eq!(found, Origin::File(PathBuf::from("//server/share/a.png"))),
false => assert!(matches!(found, Origin::Bad(_)), "{found:?}"),
}
}
#[test]
fn data_addresses_carry_their_bytes() {
assert_eq!(
+5 -7
View File
@@ -17,7 +17,7 @@ use crate::{
navigation::{Anchor, Sections},
primitives::{FocusRing as _, Icon, IconName},
theme::{ActiveTheme, IconSize, Radius, TextSize},
typography::Latex,
typography::{Latex, failed},
};
type OnLink = Rc<dyn Fn(&str, &mut Window, &mut App)>;
@@ -214,7 +214,7 @@ impl Draw<'_> {
.into_any_element(),
Node::Code { language, text } => {
if language.as_deref() == Some("mermaid") {
return Diagram::new(text.clone()).into_any_element();
return Diagram::new(self.key("diagram"), text.clone()).into_any_element();
}
if let Some(draw) = self.code {
return draw(
@@ -351,16 +351,14 @@ impl Draw<'_> {
.flex()
.justify_center()
.py_2()
.child(match Latex::parse(tex) {
.child(match Latex::parse(self.key("math"), tex) {
Ok(math) => math.display().into_any_element(),
Err(error) => div()
.text_color(colors.danger)
.child(error.to_string())
.into_any_element(),
Err(error) => failed(&error, tex, cx),
})
.into_any_element(),
Node::Image { alt, url, link } => {
let picture = div().id(self.key("image")).child(Picture {
id: self.key("picture"),
alt: alt.clone(),
url: url.clone(),
base: self.base.clone(),
+12
View File
@@ -358,3 +358,15 @@ fn a_picture_beside_the_file_draws_at_its_ratio_within_the_column(cx: &mut TestA
fn a_picture_narrower_than_the_column_keeps_its_size(cx: &mut TestAppContext) {
assert_eq!(drawn_picture(400.0, cx), size(px(200.0), px(100.0)));
}
#[gpui::test]
fn a_formula_that_fails_shows_why_and_its_source(cx: &mut TestAppContext) {
cx.update(Theme::init);
let (_, cx) = cx.add_window_view(|_, _| Page {
source: "$$x^2^3$$\n",
base: std::env::temp_dir(),
width: 300.0,
});
settle(cx);
assert!(cx.debug_bounds("latex-error").is_some(), "the failure drew");
}