Merge e606e2e239
into 0e575b2809
This commit is contained in:
commit
7f545cb1b6
5 changed files with 456 additions and 78 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -9916,9 +9916,11 @@ dependencies = [
|
|||
"editor",
|
||||
"fs",
|
||||
"gpui",
|
||||
"html5ever 0.27.0",
|
||||
"language",
|
||||
"linkify",
|
||||
"log",
|
||||
"markup5ever_rcdom",
|
||||
"pretty_assertions",
|
||||
"pulldown-cmark 0.12.2",
|
||||
"settings",
|
||||
|
|
|
@ -19,19 +19,21 @@ anyhow.workspace = true
|
|||
async-recursion.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
html5ever.workspace = true
|
||||
language.workspace = true
|
||||
linkify.workspace = true
|
||||
log.workspace = true
|
||||
markup5ever_rcdom.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
pulldown-cmark.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
fs.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use gpui::{
|
||||
FontStyle, FontWeight, HighlightStyle, SharedString, StrikethroughStyle, UnderlineStyle, px,
|
||||
DefiniteLength, FontStyle, FontWeight, HighlightStyle, SharedString, StrikethroughStyle,
|
||||
UnderlineStyle, px,
|
||||
};
|
||||
use language::HighlightId;
|
||||
use std::{fmt::Display, ops::Range, path::PathBuf};
|
||||
|
@ -15,6 +16,7 @@ pub enum ParsedMarkdownElement {
|
|||
/// A paragraph of text and other inline elements.
|
||||
Paragraph(MarkdownParagraph),
|
||||
HorizontalRule(Range<usize>),
|
||||
Image(Image),
|
||||
}
|
||||
|
||||
impl ParsedMarkdownElement {
|
||||
|
@ -30,6 +32,7 @@ impl ParsedMarkdownElement {
|
|||
MarkdownParagraphChunk::Image(image) => image.source_range.clone(),
|
||||
},
|
||||
Self::HorizontalRule(range) => range.clone(),
|
||||
Self::Image(image) => image.source_range.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -290,6 +293,8 @@ pub struct Image {
|
|||
pub link: Link,
|
||||
pub source_range: Range<usize>,
|
||||
pub alt_text: Option<SharedString>,
|
||||
pub width: Option<DefiniteLength>,
|
||||
pub height: Option<DefiniteLength>,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
|
@ -303,10 +308,20 @@ impl Image {
|
|||
source_range,
|
||||
link,
|
||||
alt_text: None,
|
||||
width: None,
|
||||
height: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_alt_text(&mut self, alt_text: SharedString) {
|
||||
self.alt_text = Some(alt_text);
|
||||
}
|
||||
|
||||
pub fn set_width(&mut self, width: DefiniteLength) {
|
||||
self.width = Some(width);
|
||||
}
|
||||
|
||||
pub fn set_height(&mut self, height: DefiniteLength) {
|
||||
self.height = Some(height);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
use crate::markdown_elements::*;
|
||||
use async_recursion::async_recursion;
|
||||
use collections::FxHashMap;
|
||||
use gpui::FontWeight;
|
||||
use gpui::{DefiniteLength, FontWeight, px, relative};
|
||||
use html5ever::{ParseOpts, local_name, parse_document, tendril::TendrilSink};
|
||||
use language::LanguageRegistry;
|
||||
use markup5ever_rcdom::RcDom;
|
||||
use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd};
|
||||
use std::{ops::Range, path::PathBuf, sync::Arc, vec};
|
||||
use std::{cell::RefCell, collections::HashMap, ops::Range, path::PathBuf, rc::Rc, sync::Arc, vec};
|
||||
|
||||
pub async fn parse_markdown(
|
||||
markdown_input: &str,
|
||||
|
@ -172,9 +174,14 @@ impl<'a> MarkdownParser<'a> {
|
|||
|
||||
self.cursor += 1;
|
||||
|
||||
let code_block = self.parse_code_block(language).await;
|
||||
let code_block = self.parse_code_block(language).await?;
|
||||
Some(vec![ParsedMarkdownElement::CodeBlock(code_block)])
|
||||
}
|
||||
Tag::HtmlBlock => {
|
||||
self.cursor += 1;
|
||||
|
||||
Some(self.parse_html_block().await)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
Event::Rule => {
|
||||
|
@ -378,7 +385,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
TagEnd::Image => {
|
||||
if let Some(mut image) = image.take() {
|
||||
if !text.is_empty() {
|
||||
image.alt_text = Some(std::mem::take(&mut text).into());
|
||||
image.set_alt_text(std::mem::take(&mut text).into());
|
||||
}
|
||||
markdown_text_like.push(MarkdownParagraphChunk::Image(image));
|
||||
}
|
||||
|
@ -695,13 +702,22 @@ impl<'a> MarkdownParser<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
async fn parse_code_block(&mut self, language: Option<String>) -> ParsedMarkdownCodeBlock {
|
||||
let (_event, source_range) = self.previous().unwrap();
|
||||
async fn parse_code_block(
|
||||
&mut self,
|
||||
language: Option<String>,
|
||||
) -> Option<ParsedMarkdownCodeBlock> {
|
||||
let Some((_event, source_range)) = self.previous() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let source_range = source_range.clone();
|
||||
let mut code = String::new();
|
||||
|
||||
while !self.eof() {
|
||||
let (current, _source_range) = self.current().unwrap();
|
||||
let Some((current, _source_range)) = self.current() else {
|
||||
break;
|
||||
};
|
||||
|
||||
match current {
|
||||
Event::Text(text) => {
|
||||
code.push_str(text);
|
||||
|
@ -734,23 +750,190 @@ impl<'a> MarkdownParser<'a> {
|
|||
None
|
||||
};
|
||||
|
||||
ParsedMarkdownCodeBlock {
|
||||
Some(ParsedMarkdownCodeBlock {
|
||||
source_range,
|
||||
contents: code.into(),
|
||||
language,
|
||||
highlights,
|
||||
})
|
||||
}
|
||||
|
||||
async fn parse_html_block(&mut self) -> Vec<ParsedMarkdownElement> {
|
||||
let mut elements = Vec::new();
|
||||
let Some((_event, _source_range)) = self.previous() else {
|
||||
return elements;
|
||||
};
|
||||
|
||||
while !self.eof() {
|
||||
let Some((current, source_range)) = self.current() else {
|
||||
break;
|
||||
};
|
||||
let source_range = source_range.clone();
|
||||
match current {
|
||||
Event::Html(html) => {
|
||||
let mut cursor = std::io::Cursor::new(html.as_bytes());
|
||||
let Some(dom) = parse_document(RcDom::default(), ParseOpts::default())
|
||||
.from_utf8()
|
||||
.read_from(&mut cursor)
|
||||
.ok()
|
||||
else {
|
||||
self.cursor += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
self.cursor += 1;
|
||||
|
||||
self.parse_html_node(source_range, &dom.document, &mut elements);
|
||||
}
|
||||
Event::End(TagEnd::CodeBlock) => {
|
||||
self.cursor += 1;
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
elements
|
||||
}
|
||||
|
||||
fn parse_html_node(
|
||||
&self,
|
||||
source_range: Range<usize>,
|
||||
node: &Rc<markup5ever_rcdom::Node>,
|
||||
elements: &mut Vec<ParsedMarkdownElement>,
|
||||
) {
|
||||
match &node.data {
|
||||
markup5ever_rcdom::NodeData::Document => {
|
||||
self.consume_children(source_range, node, elements);
|
||||
}
|
||||
markup5ever_rcdom::NodeData::Doctype { .. } => {}
|
||||
markup5ever_rcdom::NodeData::Text { contents } => {
|
||||
elements.push(ParsedMarkdownElement::Paragraph(vec![
|
||||
MarkdownParagraphChunk::Text(ParsedMarkdownText {
|
||||
source_range,
|
||||
contents: contents.borrow().to_string(),
|
||||
highlights: Vec::default(),
|
||||
region_ranges: Vec::default(),
|
||||
regions: Vec::default(),
|
||||
}),
|
||||
]));
|
||||
}
|
||||
markup5ever_rcdom::NodeData::Comment { .. } => {}
|
||||
markup5ever_rcdom::NodeData::Element { name, attrs, .. } => {
|
||||
if local_name!("img") == name.local {
|
||||
if let Some(image) = self.extract_image(source_range, attrs) {
|
||||
elements.push(ParsedMarkdownElement::Image(image));
|
||||
}
|
||||
} else {
|
||||
self.consume_children(source_range, node, elements);
|
||||
}
|
||||
}
|
||||
markup5ever_rcdom::NodeData::ProcessingInstruction { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_children(
|
||||
&self,
|
||||
source_range: Range<usize>,
|
||||
node: &Rc<markup5ever_rcdom::Node>,
|
||||
elements: &mut Vec<ParsedMarkdownElement>,
|
||||
) {
|
||||
for node in node.children.borrow().iter() {
|
||||
self.parse_html_node(source_range.clone(), node, elements);
|
||||
}
|
||||
}
|
||||
|
||||
fn attr_value(
|
||||
attrs: &RefCell<Vec<html5ever::Attribute>>,
|
||||
name: html5ever::LocalName,
|
||||
) -> Option<String> {
|
||||
attrs.borrow().iter().find_map(|attr| {
|
||||
if attr.name.local == name {
|
||||
Some(attr.value.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_styles_from_attributes(
|
||||
attrs: &RefCell<Vec<html5ever::Attribute>>,
|
||||
) -> HashMap<String, String> {
|
||||
let mut styles = HashMap::new();
|
||||
|
||||
if let Some(style) = Self::attr_value(attrs, local_name!("style")) {
|
||||
for decl in style.split(';') {
|
||||
let mut parts = decl.splitn(2, ':');
|
||||
if let Some((key, value)) = parts.next().zip(parts.next()) {
|
||||
styles.insert(
|
||||
key.trim().to_lowercase().to_string(),
|
||||
value.trim().to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
styles
|
||||
}
|
||||
|
||||
fn extract_image(
|
||||
&self,
|
||||
source_range: Range<usize>,
|
||||
attrs: &RefCell<Vec<html5ever::Attribute>>,
|
||||
) -> Option<Image> {
|
||||
let src = Self::attr_value(attrs, local_name!("src"))?;
|
||||
|
||||
let mut image = Image::identify(src, source_range, self.file_location_directory.clone())?;
|
||||
|
||||
if let Some(alt) = Self::attr_value(attrs, local_name!("alt")) {
|
||||
image.set_alt_text(alt.into());
|
||||
}
|
||||
|
||||
let styles = Self::extract_styles_from_attributes(attrs);
|
||||
|
||||
if let Some(width) = Self::attr_value(attrs, local_name!("width"))
|
||||
.or_else(|| styles.get("width").cloned())
|
||||
.and_then(|width| Self::parse_length(&width))
|
||||
{
|
||||
image.set_width(width);
|
||||
}
|
||||
|
||||
if let Some(height) = Self::attr_value(attrs, local_name!("height"))
|
||||
.or_else(|| styles.get("height").cloned())
|
||||
.and_then(|height| Self::parse_length(&height))
|
||||
{
|
||||
image.set_height(height);
|
||||
}
|
||||
|
||||
Some(image)
|
||||
}
|
||||
|
||||
/// Parses the width/height attribute value of an html element (e.g. img element)
|
||||
fn parse_length(value: &str) -> Option<DefiniteLength> {
|
||||
if value.ends_with("%") {
|
||||
value
|
||||
.trim_end_matches("%")
|
||||
.parse::<f32>()
|
||||
.ok()
|
||||
.map(|value| relative(value / 100.))
|
||||
} else {
|
||||
value
|
||||
.trim_end_matches("px")
|
||||
.parse()
|
||||
.ok()
|
||||
.map(|value| px(value).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use core::panic;
|
||||
|
||||
use super::*;
|
||||
|
||||
use ParsedMarkdownListItemType::*;
|
||||
use gpui::BackgroundExecutor;
|
||||
use core::panic;
|
||||
use gpui::{AbsoluteLength, BackgroundExecutor, DefiniteLength};
|
||||
use language::{
|
||||
HighlightId, Language, LanguageConfig, LanguageMatcher, LanguageRegistry, tree_sitter_rust,
|
||||
};
|
||||
|
@ -925,6 +1108,8 @@ mod tests {
|
|||
url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(),
|
||||
},
|
||||
alt_text: Some("test".into()),
|
||||
height: None,
|
||||
width: None,
|
||||
},)
|
||||
);
|
||||
}
|
||||
|
@ -946,6 +1131,8 @@ mod tests {
|
|||
url: "http://example.com/foo.png".to_string(),
|
||||
},
|
||||
alt_text: None,
|
||||
height: None,
|
||||
width: None,
|
||||
},)
|
||||
);
|
||||
}
|
||||
|
@ -965,6 +1152,8 @@ mod tests {
|
|||
url: "http://example.com/foo.png".to_string(),
|
||||
},
|
||||
alt_text: Some("foo bar baz".into()),
|
||||
height: None,
|
||||
width: None,
|
||||
}),],
|
||||
);
|
||||
}
|
||||
|
@ -990,6 +1179,8 @@ mod tests {
|
|||
url: "http://example.com/foo.png".to_string(),
|
||||
},
|
||||
alt_text: Some("foo".into()),
|
||||
height: None,
|
||||
width: None,
|
||||
}),
|
||||
MarkdownParagraphChunk::Text(ParsedMarkdownText {
|
||||
source_range: 0..81,
|
||||
|
@ -1004,11 +1195,168 @@ mod tests {
|
|||
url: "http://example.com/bar.png".to_string(),
|
||||
},
|
||||
alt_text: Some("bar".into()),
|
||||
height: None,
|
||||
width: None,
|
||||
})
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_length() {
|
||||
// Test percentage values
|
||||
assert_eq!(
|
||||
MarkdownParser::parse_length("50%"),
|
||||
Some(DefiniteLength::Fraction(0.5))
|
||||
);
|
||||
assert_eq!(
|
||||
MarkdownParser::parse_length("100%"),
|
||||
Some(DefiniteLength::Fraction(1.0))
|
||||
);
|
||||
assert_eq!(
|
||||
MarkdownParser::parse_length("25%"),
|
||||
Some(DefiniteLength::Fraction(0.25))
|
||||
);
|
||||
assert_eq!(
|
||||
MarkdownParser::parse_length("0%"),
|
||||
Some(DefiniteLength::Fraction(0.0))
|
||||
);
|
||||
|
||||
// Test pixel values
|
||||
assert_eq!(
|
||||
MarkdownParser::parse_length("100px"),
|
||||
Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0))))
|
||||
);
|
||||
assert_eq!(
|
||||
MarkdownParser::parse_length("50px"),
|
||||
Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(50.0))))
|
||||
);
|
||||
assert_eq!(
|
||||
MarkdownParser::parse_length("0px"),
|
||||
Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(0.0))))
|
||||
);
|
||||
|
||||
// Test values without units (should be treated as pixels)
|
||||
assert_eq!(
|
||||
MarkdownParser::parse_length("100"),
|
||||
Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0))))
|
||||
);
|
||||
assert_eq!(
|
||||
MarkdownParser::parse_length("42"),
|
||||
Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))))
|
||||
);
|
||||
|
||||
// Test invalid values
|
||||
assert_eq!(MarkdownParser::parse_length("invalid"), None);
|
||||
assert_eq!(MarkdownParser::parse_length("px"), None);
|
||||
assert_eq!(MarkdownParser::parse_length("%"), None);
|
||||
assert_eq!(MarkdownParser::parse_length(""), None);
|
||||
assert_eq!(MarkdownParser::parse_length("abc%"), None);
|
||||
assert_eq!(MarkdownParser::parse_length("abcpx"), None);
|
||||
|
||||
// Test decimal values
|
||||
assert_eq!(
|
||||
MarkdownParser::parse_length("50.5%"),
|
||||
Some(DefiniteLength::Fraction(0.505))
|
||||
);
|
||||
assert_eq!(
|
||||
MarkdownParser::parse_length("100.25px"),
|
||||
Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.25))))
|
||||
);
|
||||
assert_eq!(
|
||||
MarkdownParser::parse_length("42.0"),
|
||||
Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))))
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_html_image_tag() {
|
||||
let parsed = parse("<img src=\"http://example.com/foo.png\" />").await;
|
||||
|
||||
let ParsedMarkdownElement::Image(image) = &parsed.children[0] else {
|
||||
panic!("Expected a image element");
|
||||
};
|
||||
assert_eq!(
|
||||
image.clone(),
|
||||
Image {
|
||||
source_range: 0..40,
|
||||
link: Link::Web {
|
||||
url: "http://example.com/foo.png".to_string(),
|
||||
},
|
||||
alt_text: None,
|
||||
height: None,
|
||||
width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_html_image_tag_with_alt_text() {
|
||||
let parsed = parse("<img src=\"http://example.com/foo.png\" alt=\"Foo\" />").await;
|
||||
|
||||
let ParsedMarkdownElement::Image(image) = &parsed.children[0] else {
|
||||
panic!("Expected a image element");
|
||||
};
|
||||
assert_eq!(
|
||||
image.clone(),
|
||||
Image {
|
||||
source_range: 0..50,
|
||||
link: Link::Web {
|
||||
url: "http://example.com/foo.png".to_string(),
|
||||
},
|
||||
alt_text: Some("Foo".into()),
|
||||
height: None,
|
||||
width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_html_image_tag_with_height_and_width() {
|
||||
let parsed =
|
||||
parse("<img src=\"http://example.com/foo.png\" height=\"100\" width=\"200\" />").await;
|
||||
|
||||
let ParsedMarkdownElement::Image(image) = &parsed.children[0] else {
|
||||
panic!("Expected a image element");
|
||||
};
|
||||
assert_eq!(
|
||||
image.clone(),
|
||||
Image {
|
||||
source_range: 0..65,
|
||||
link: Link::Web {
|
||||
url: "http://example.com/foo.png".to_string(),
|
||||
},
|
||||
alt_text: None,
|
||||
height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))),
|
||||
width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_html_image_style_tag_with_height_and_width() {
|
||||
let parsed = parse(
|
||||
"<img src=\"http://example.com/foo.png\" style=\"height:100px; width:200px;\" />",
|
||||
)
|
||||
.await;
|
||||
|
||||
let ParsedMarkdownElement::Image(image) = &parsed.children[0] else {
|
||||
panic!("Expected a image element");
|
||||
};
|
||||
assert_eq!(
|
||||
image.clone(),
|
||||
Image {
|
||||
source_range: 0..75,
|
||||
link: Link::Web {
|
||||
url: "http://example.com/foo.png".to_string(),
|
||||
},
|
||||
alt_text: None,
|
||||
height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))),
|
||||
width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_header_only_table() {
|
||||
let markdown = "\
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::markdown_elements::{
|
||||
HeadingLevel, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown,
|
||||
HeadingLevel, Image, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown,
|
||||
ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, ParsedMarkdownElement,
|
||||
ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownTable,
|
||||
ParsedMarkdownTableAlignment, ParsedMarkdownTableRow,
|
||||
|
@ -164,6 +164,7 @@ pub fn render_markdown_block(block: &ParsedMarkdownElement, cx: &mut RenderConte
|
|||
BlockQuote(block_quote) => render_markdown_block_quote(block_quote, cx),
|
||||
CodeBlock(code_block) => render_markdown_code_block(code_block, cx),
|
||||
HorizontalRule(_) => render_markdown_rule(cx),
|
||||
Image(image) => render_markdown_image(image, cx),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -722,65 +723,7 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext)
|
|||
}
|
||||
|
||||
MarkdownParagraphChunk::Image(image) => {
|
||||
let image_resource = match image.link.clone() {
|
||||
Link::Web { url } => Resource::Uri(url.into()),
|
||||
Link::Path { path, .. } => Resource::Path(Arc::from(path)),
|
||||
};
|
||||
|
||||
let element_id = cx.next_id(&image.source_range);
|
||||
|
||||
let image_element = div()
|
||||
.id(element_id)
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
img(ImageSource::Resource(image_resource))
|
||||
.max_w_full()
|
||||
.with_fallback({
|
||||
let alt_text = image.alt_text.clone();
|
||||
move || div().children(alt_text.clone()).into_any_element()
|
||||
}),
|
||||
)
|
||||
.tooltip({
|
||||
let link = image.link.clone();
|
||||
move |_, cx| {
|
||||
InteractiveMarkdownElementTooltip::new(
|
||||
Some(link.to_string()),
|
||||
"open image",
|
||||
cx,
|
||||
)
|
||||
.into()
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let workspace = workspace_clone.clone();
|
||||
let link = image.link.clone();
|
||||
move |_, window, cx| {
|
||||
if window.modifiers().secondary() {
|
||||
match &link {
|
||||
Link::Web { url } => cx.open_url(url),
|
||||
Link::Path { path, .. } => {
|
||||
if let Some(workspace) = &workspace {
|
||||
_ = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.open_abs_path(
|
||||
path.clone(),
|
||||
OpenOptions {
|
||||
visible: Some(OpenVisible::None),
|
||||
..Default::default()
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.into_any();
|
||||
any_element.push(image_element);
|
||||
any_element.push(render_markdown_image(image, cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -793,18 +736,86 @@ fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement {
|
|||
div().py(cx.scaled_rems(0.5)).child(rule).into_any()
|
||||
}
|
||||
|
||||
fn render_markdown_image(image: &Image, cx: &mut RenderContext) -> AnyElement {
|
||||
let image_resource = match image.link.clone() {
|
||||
Link::Web { url } => Resource::Uri(url.into()),
|
||||
Link::Path { path, .. } => Resource::Path(Arc::from(path)),
|
||||
};
|
||||
|
||||
let element_id = cx.next_id(&image.source_range);
|
||||
let workspace = cx.workspace.clone();
|
||||
|
||||
div()
|
||||
.id(element_id)
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
img(ImageSource::Resource(image_resource))
|
||||
.max_w_full()
|
||||
.with_fallback({
|
||||
let alt_text = image.alt_text.clone();
|
||||
move || div().children(alt_text.clone()).into_any_element()
|
||||
})
|
||||
.when_some(image.height, |this, height| this.h(height))
|
||||
.when_some(image.width, |this, width| this.w(width)),
|
||||
)
|
||||
.tooltip({
|
||||
let link = image.link.clone();
|
||||
let alt_text = image.alt_text.clone();
|
||||
move |_, cx| {
|
||||
InteractiveMarkdownElementTooltip::new(
|
||||
Some(alt_text.clone().unwrap_or(link.to_string().into())),
|
||||
"open image",
|
||||
cx,
|
||||
)
|
||||
.into()
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let link = image.link.clone();
|
||||
move |_, window, cx| {
|
||||
if window.modifiers().secondary() {
|
||||
match &link {
|
||||
Link::Web { url } => cx.open_url(url),
|
||||
Link::Path { path, .. } => {
|
||||
if let Some(workspace) = &workspace {
|
||||
_ = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.open_abs_path(
|
||||
path.clone(),
|
||||
OpenOptions {
|
||||
visible: Some(OpenVisible::None),
|
||||
..Default::default()
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
struct InteractiveMarkdownElementTooltip {
|
||||
tooltip_text: Option<SharedString>,
|
||||
action_text: String,
|
||||
action_text: SharedString,
|
||||
}
|
||||
|
||||
impl InteractiveMarkdownElementTooltip {
|
||||
pub fn new(tooltip_text: Option<String>, action_text: &str, cx: &mut App) -> Entity<Self> {
|
||||
pub fn new(
|
||||
tooltip_text: Option<SharedString>,
|
||||
action_text: impl Into<SharedString>,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let tooltip_text = tooltip_text.map(|t| util::truncate_and_trailoff(&t, 50).into());
|
||||
|
||||
cx.new(|_cx| Self {
|
||||
tooltip_text,
|
||||
action_text: action_text.to_string(),
|
||||
action_text: action_text.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue