Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for images in Markdown previews #16192

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a008b62
add markdown image preview
dovakin0007 Aug 14, 2024
e89f63f
fmt fix
dovakin0007 Aug 17, 2024
5e1befb
fix image not rendering side by side
dovakin0007 Aug 23, 2024
94b4efe
Merge branch 'zed-industries:main' into main
dovakin0007 Aug 29, 2024
425cd03
use Image Source to check if image is valid
dovakin0007 Aug 29, 2024
e2e71bb
test fixes
dovakin0007 Aug 31, 2024
3686ce7
fix image not rendering in table
dovakin0007 Sep 1, 2024
e1122d6
fix image not rendering together with text
dovakin0007 Sep 7, 2024
1097dab
fix
dovakin0007 Sep 7, 2024
ba105e1
fix
dovakin0007 Sep 7, 2024
4bc4741
fix local images not rendering
dovakin0007 Sep 8, 2024
067a906
init
dovakin0007 Sep 8, 2024
302b917
fix
dovakin0007 Sep 10, 2024
100b09d
Merge branch 'zed-industries:main' into main
dovakin0007 Sep 16, 2024
43697e2
removed unwanted code
dovakin0007 Sep 17, 2024
4f0ad6a
Merge branch 'zed-industries:main' into main
dovakin0007 Sep 17, 2024
128c555
fix
dovakin0007 Sep 17, 2024
2963ae1
Merge branch 'zed-industries:main' into main
dovakin0007 Sep 17, 2024
005956f
fmt fix
dovakin0007 Sep 17, 2024
cb16b35
Update markdown_renderer.rs
dovakin0007 Oct 1, 2024
33c7b89
Update markdown_renderer.rs
dovakin0007 Oct 1, 2024
c9401fc
renamed x
dovakin0007 Oct 1, 2024
27b46a2
fix
dovakin0007 Oct 1, 2024
65189ce
merge conflict fix
dovakin0007 Oct 3, 2024
ec36f15
Merge branch 'main' into main
dovakin0007 Oct 15, 2024
e500677
Update markdown_renderer.rs
dovakin0007 Oct 15, 2024
e149b39
nameing changes
dovakin0007 Oct 15, 2024
e45171f
Merge branch 'main' into main
dovakin0007 Oct 28, 2024
2c1eeee
test fix
dovakin0007 Oct 28, 2024
1de2b6e
Merge branch 'main' into main
dovakin0007 Nov 18, 2024
5a91d67
wip image fallback
dovakin0007 Nov 19, 2024
d985a6a
added fallback text
dovakin0007 Nov 21, 2024
4b0fceb
fmt fix
dovakin0007 Nov 21, 2024
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/markdown_preview/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pulldown-cmark.workspace = true
theme.workspace = true
ui.workspace = true
workspace.workspace = true
isahc.workspace = true

[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }
68 changes: 67 additions & 1 deletion crates/markdown_preview/src/markdown_elements.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use gpui::{
px, FontStyle, FontWeight, HighlightStyle, SharedString, StrikethroughStyle, UnderlineStyle,
img, px, FontStyle, FontWeight, HighlightStyle, ImageSource, RenderOnce, SharedString,
StrikethroughStyle, Styled, UnderlineStyle,
};
use language::HighlightId;
use std::{fmt::Display, ops::Range, path::PathBuf};
use ui::{div, IntoElement, ParentElement};

#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
Expand Down Expand Up @@ -210,6 +212,8 @@ pub struct ParsedRegion {
pub code: bool,
/// The link contained in this region, if it has one.
pub link: Option<Link>,
/// The image contained in this region, if it has one.
pub image: Option<Image>,
}

/// A Markdown link.
Expand Down Expand Up @@ -267,3 +271,65 @@ impl Display for Link {
}
}
}

/// A Markdown Image
#[derive(Debug, Clone, IntoElement)]
#[cfg_attr(test, derive(PartialEq))]
pub enum Image {
Web {
/// The URL of the Image.
url: String,
},
/// Image path on the filesystem.
Path {
/// The path as provided in the Markdown document.
display_path: PathBuf,
/// The absolute path to the item.
path: PathBuf,
},
}

impl Image {
pub fn identify(file_location_directory: Option<PathBuf>, text: String) -> Option<Image> {
if text.starts_with("http") {
return Some(Image::Web { url: text });
}
let path = PathBuf::from(&text);
if path.is_absolute() && path.exists() {
dovakin0007 marked this conversation as resolved.
Show resolved Hide resolved
return Some(Image::Path {
display_path: path.clone(),
path,
});
}
if let Some(file_location_directory) = file_location_directory {
let display_path = path;
let path = file_location_directory.join(text);
if path.exists() {
dovakin0007 marked this conversation as resolved.
Show resolved Hide resolved
return Some(Image::Path { display_path, path });
}
}
None
}
}

impl Display for Image {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Image::Web { url } => write!(f, "{}", url),
Image::Path {
display_path,
path: _,
} => write!(f, "{}", display_path.display()),
}
}
}

impl RenderOnce for Image {
fn render(self, _: &mut ui::WindowContext) -> impl ui::IntoElement {
let image_src = match self {
Image::Web { url } => ImageSource::Uri(url.into()),
Image::Path { path, .. } => ImageSource::File(path.into()),
};
div().child(div().flex_row().size_full().child(img(image_src)))
}
}
82 changes: 77 additions & 5 deletions crates/markdown_preview/src/markdown_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ impl<'a> MarkdownParser<'a> {
let mut italic_depth = 0;
let mut strikethrough_depth = 0;
let mut link: Option<Link> = None;
let mut image: Option<Image> = None;
let mut region_ranges: Vec<Range<usize>> = vec![];
let mut regions: Vec<ParsedRegion> = vec![];
let mut highlights: Vec<(Range<usize>, MarkdownHighlight)> = vec![];
Expand Down Expand Up @@ -244,6 +245,7 @@ impl<'a> MarkdownParser<'a> {
regions.push(ParsedRegion {
code: false,
link: Some(link),
image: None,
});
style.underline = true;
prev_len
Expand Down Expand Up @@ -282,6 +284,7 @@ impl<'a> MarkdownParser<'a> {
link: Some(Link::Web {
url: link.as_str().to_string(),
}),
image: None,
});

last_link_len = end;
Expand All @@ -300,10 +303,39 @@ impl<'a> MarkdownParser<'a> {
}
}
if new_highlight {
highlights
.push((last_run_len..text.len(), MarkdownHighlight::Style(style)));
highlights.push((
last_run_len..text.len(),
MarkdownHighlight::Style(style.clone()),
));
}
}
if let Some(image) = image.clone() {
let is_valid_image = match image.clone() {
Image::Path {
display_path,
path: _,
} => display_path.is_file(),
Image::Web { url } => match isahc::get(url) {
dovakin0007 marked this conversation as resolved.
Show resolved Hide resolved
Ok(response) => match response.status().as_u16() {
200 => true,
404 => false,
_ => false,
},
Err(_) => false,
},
};

if is_valid_image {
text.truncate(text.len() - t.len());
}
region_ranges.push(prev_len..text.len());
regions.push(ParsedRegion {
code: false,
link: None,
image: Some(image),
});
style.underline = true;
};
}

// Note: This event means "inline code" and not "code block"
Expand All @@ -324,6 +356,7 @@ impl<'a> MarkdownParser<'a> {
regions.push(ParsedRegion {
code: true,
link: link.clone(),
image: image.clone(),
});
}

Expand All @@ -342,6 +375,17 @@ impl<'a> MarkdownParser<'a> {
dest_url.to_string(),
);
}
Tag::Image {
link_type: _,
dest_url,
title: _,
id: _,
} => {
image = Image::identify(
self.file_location_directory.clone(),
dest_url.to_string(),
);
}
_ => {
break;
}
Expand All @@ -360,6 +404,9 @@ impl<'a> MarkdownParser<'a> {
TagEnd::Link => {
link = None;
}
TagEnd::Image => {
image = None;
}
TagEnd::Paragraph => {
self.cursor += 1;
break;
Expand All @@ -368,7 +415,6 @@ impl<'a> MarkdownParser<'a> {
break;
}
},

_ => {
break;
}
Expand Down Expand Up @@ -844,7 +890,6 @@ mod tests {
#[gpui::test]
async fn test_raw_links_detection() {
let parsed = parse("Checkout this https://zed.dev link").await;

assert_eq!(
parsed.children,
vec![p("Checkout this https://zed.dev link", 0..34)]
Expand All @@ -855,6 +900,7 @@ mod tests {
} else {
panic!("Expected a paragraph");
};
println!("{:?}", paragraph);
mikayla-maki marked this conversation as resolved.
Show resolved Hide resolved
assert_eq!(
paragraph.highlights,
vec![(
Expand All @@ -872,11 +918,35 @@ mod tests {
link: Some(Link::Web {
url: "https://zed.dev".to_string()
}),
image: None
}]
);
assert_eq!(paragraph.region_ranges, vec![14..29]);
}

#[gpui::test]
async fn test_image_links_detection() {
let parsed = parse("Check out this![Zed logo](https://zed.dev/logo.png) link").await;

let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
text
} else {
panic!("Expected a paragraph");
};

assert_eq!(
paragraph.regions,
vec![ParsedRegion {
code: false,
link: None,
image: Some(Image::Web {
url: "https://zed.dev/logo.png".to_string()
}),
}]
);
assert_eq!(paragraph.region_ranges, vec![14..14]);
}

#[gpui::test]
async fn test_header_only_table() {
let markdown = "\
Expand Down Expand Up @@ -1087,6 +1157,7 @@ Some other content
* `code`
* **bold**
* [link](https://example.com)
* ![image]()
",
)
.await;
Expand All @@ -1096,7 +1167,8 @@ Some other content
vec![
list_item(0..8, 1, Unordered, vec![p("code", 2..8)]),
list_item(9..19, 1, Unordered, vec![p("bold", 11..19)]),
list_item(20..49, 1, Unordered, vec![p("link", 22..49)],)
list_item(20..49, 1, Unordered, vec![p("link", 22..49)]),
list_item(50..63, 1, Unordered, vec![p("image", 52..61)]),
],
);
}
Expand Down
13 changes: 13 additions & 0 deletions crates/markdown_preview/src/markdown_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ fn render_markdown_code_block(
fn render_markdown_paragraph(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> AnyElement {
cx.with_common_p(div())
.child(render_markdown_text(parsed, cx))
.child(render_markdown_text_image(parsed, cx))
.into_any_element()
}

Expand Down Expand Up @@ -407,6 +408,18 @@ fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) ->
.into_any_element()
}

fn render_markdown_text_image(parsed: &ParsedMarkdownText, _: &mut RenderContext) -> AnyElement {
let mut images = Vec::new();
let mut image_ranges = Vec::new();
for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) {
if let Some(image) = region.image.clone() {
images.push(image);
image_ranges.push(range.clone());
}
}
div().flex().children(images).size_full().into_any()
}

fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement {
let rule = div().w_full().h(px(2.)).bg(cx.border_color);
div().pt_3().pb_3().child(rule).into_any()
Expand Down
Loading