Detailed changes
@@ -18,22 +18,19 @@ pub enum ParsedMarkdownElement {
}
impl ParsedMarkdownElement {
- pub fn source_range(&self) -> Range<usize> {
- match self {
+ pub fn source_range(&self) -> Option<Range<usize>> {
+ Some(match self {
Self::Heading(heading) => heading.source_range.clone(),
Self::ListItem(list_item) => list_item.source_range.clone(),
Self::Table(table) => table.source_range.clone(),
Self::BlockQuote(block_quote) => block_quote.source_range.clone(),
Self::CodeBlock(code_block) => code_block.source_range.clone(),
- Self::Paragraph(text) => match &text[0] {
+ Self::Paragraph(text) => match text.get(0)? {
MarkdownParagraphChunk::Text(t) => t.source_range.clone(),
- MarkdownParagraphChunk::Image(image) => match image {
- Image::Web { source_range, .. } => source_range.clone(),
- Image::Path { source_range, .. } => source_range.clone(),
- },
+ MarkdownParagraphChunk::Image(image) => image.source_range.clone(),
},
Self::HorizontalRule(range) => range.clone(),
- }
+ })
}
pub fn is_list_item(&self) -> bool {
@@ -289,104 +286,27 @@ impl Display for Link {
/// A Markdown Image
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
-pub enum Image {
- Web {
- source_range: Range<usize>,
- /// The URL of the Image.
- url: String,
- /// Link URL if exists.
- link: Option<Link>,
- /// alt text if it exists
- alt_text: Option<ParsedMarkdownText>,
- },
- /// Image path on the filesystem.
- Path {
- source_range: Range<usize>,
- /// The path as provided in the Markdown document.
- display_path: PathBuf,
- /// The absolute path to the item.
- path: PathBuf,
- /// Link URL if exists.
- link: Option<Link>,
- /// alt text if it exists
- alt_text: Option<ParsedMarkdownText>,
- },
+pub struct Image {
+ pub link: Link,
+ pub source_range: Range<usize>,
+ pub alt_text: Option<SharedString>,
}
impl Image {
pub fn identify(
+ text: String,
source_range: Range<usize>,
file_location_directory: Option<PathBuf>,
- text: String,
- link: Option<Link>,
- ) -> Option<Image> {
- if text.starts_with("http") {
- return Some(Image::Web {
- source_range,
- url: text,
- link,
- alt_text: None,
- });
- }
- let path = PathBuf::from(&text);
- if path.is_absolute() {
- return Some(Image::Path {
- source_range,
- display_path: path.clone(),
- path,
- link,
- alt_text: None,
- });
- }
- if let Some(file_location_directory) = file_location_directory {
- let display_path = path;
- let path = file_location_directory.join(text);
- return Some(Image::Path {
- source_range,
- display_path,
- path,
- link,
- alt_text: None,
- });
- }
- None
+ ) -> Option<Self> {
+ let link = Link::identify(file_location_directory, text)?;
+ Some(Self {
+ source_range,
+ link,
+ alt_text: None,
+ })
}
- pub fn with_alt_text(&self, alt_text: ParsedMarkdownText) -> Self {
- match self {
- Image::Web {
- ref source_range,
- ref url,
- ref link,
- ..
- } => Image::Web {
- source_range: source_range.clone(),
- url: url.clone(),
- link: link.clone(),
- alt_text: Some(alt_text),
- },
- Image::Path {
- ref source_range,
- ref display_path,
- ref path,
- ref link,
- ..
- } => Image::Path {
- source_range: source_range.clone(),
- display_path: display_path.clone(),
- path: path.clone(),
- link: link.clone(),
- alt_text: Some(alt_text),
- },
- }
- }
-}
-
-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, .. } => write!(f, "{}", display_path.display()),
- }
+ pub fn set_alt_text(&mut self, alt_text: SharedString) {
+ self.alt_text = Some(alt_text);
}
}
@@ -214,7 +214,7 @@ impl<'a> MarkdownParser<'a> {
break;
}
- let (current, _source_range) = self.current().unwrap();
+ let (current, _) = self.current().unwrap();
let prev_len = text.len();
match current {
Event::SoftBreak => {
@@ -314,56 +314,29 @@ impl<'a> MarkdownParser<'a> {
));
}
}
- if let Some(mut image) = image.clone() {
- let is_valid_image = match image.clone() {
- Image::Path { display_path, .. } => {
- gpui::ImageSource::try_from(display_path).is_ok()
- }
- Image::Web { url, .. } => gpui::ImageSource::try_from(url).is_ok(),
- };
- if is_valid_image {
- text.truncate(text.len() - t.len());
- if !t.is_empty() {
- let alt_text = ParsedMarkdownText {
- source_range: source_range.clone(),
- contents: t.to_string(),
- highlights: highlights.clone(),
- region_ranges: region_ranges.clone(),
- regions: regions.clone(),
- };
- image = image.with_alt_text(alt_text);
- } else {
- let alt_text = ParsedMarkdownText {
- source_range: source_range.clone(),
- contents: "img".to_string(),
- highlights: highlights.clone(),
- region_ranges: region_ranges.clone(),
- regions: regions.clone(),
- };
- image = image.with_alt_text(alt_text);
- }
- if !text.is_empty() {
- let parsed_regions =
- MarkdownParagraphChunk::Text(ParsedMarkdownText {
- source_range: source_range.clone(),
- contents: text.clone(),
- highlights: highlights.clone(),
- region_ranges: region_ranges.clone(),
- regions: regions.clone(),
- });
- text = String::new();
- highlights = vec![];
- region_ranges = vec![];
- regions = vec![];
- markdown_text_like.push(parsed_regions);
- }
-
- let parsed_image = MarkdownParagraphChunk::Image(image.clone());
- markdown_text_like.push(parsed_image);
- style = MarkdownHighlightStyle::default();
+ if let Some(image) = image.as_mut() {
+ text.truncate(text.len() - t.len());
+ image.set_alt_text(t.to_string().into());
+ if !text.is_empty() {
+ let parsed_regions = MarkdownParagraphChunk::Text(ParsedMarkdownText {
+ source_range: source_range.clone(),
+ contents: text.clone(),
+ highlights: highlights.clone(),
+ region_ranges: region_ranges.clone(),
+ regions: regions.clone(),
+ });
+ text = String::new();
+ highlights = vec![];
+ region_ranges = vec![];
+ regions = vec![];
+ markdown_text_like.push(parsed_regions);
}
+
+ let parsed_image = MarkdownParagraphChunk::Image(image.clone());
+ markdown_text_like.push(parsed_image);
+ style = MarkdownHighlightStyle::default();
style.underline = true;
- };
+ }
}
Event::Code(t) => {
text.push_str(t.as_ref());
@@ -395,10 +368,9 @@ impl<'a> MarkdownParser<'a> {
}
Tag::Image { dest_url, .. } => {
image = Image::identify(
+ dest_url.to_string(),
source_range.clone(),
self.file_location_directory.clone(),
- dest_url.to_string(),
- link.clone(),
);
}
_ => {
@@ -926,6 +898,18 @@ mod tests {
);
}
+ #[gpui::test]
+ async fn test_empty_image() {
+ let parsed = parse("![]()").await;
+
+ let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
+ text
+ } else {
+ panic!("Expected a paragraph");
+ };
+ assert_eq!(paragraph.len(), 0);
+ }
+
#[gpui::test]
async fn test_image_links_detection() {
let parsed = parse("").await;
@@ -937,19 +921,12 @@ mod tests {
};
assert_eq!(
paragraph[0],
- MarkdownParagraphChunk::Image(Image::Web {
+ MarkdownParagraphChunk::Image(Image {
source_range: 0..111,
- url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(),
- link: None,
- alt_text: Some(
- ParsedMarkdownText {
- source_range: 0..111,
- contents: "test".to_string(),
- highlights: vec![],
- region_ranges: vec![],
- regions: vec![],
- },
- ),
+ link: Link::Web {
+ 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()),
},)
);
}
@@ -192,11 +192,16 @@ impl MarkdownPreviewView {
.group("markdown-block")
.on_click(cx.listener(move |this, event: &ClickEvent, cx| {
if event.down.click_count == 2 {
- if let Some(block) =
- this.contents.as_ref().and_then(|c| c.children.get(ix))
+ if let Some(source_range) = this
+ .contents
+ .as_ref()
+ .and_then(|c| c.children.get(ix))
+ .and_then(|block| block.source_range())
{
- let start = block.source_range().start;
- this.move_cursor_to_block(cx, start..start);
+ this.move_cursor_to_block(
+ cx,
+ source_range.start..source_range.start,
+ );
}
}
}))
@@ -410,7 +415,9 @@ impl MarkdownPreviewView {
let mut last_end = 0;
if let Some(content) = &self.contents {
for (i, block) in content.children.iter().enumerate() {
- let Range { start, end } = block.source_range();
+ let Some(Range { start, end }) = block.source_range() else {
+ continue;
+ };
// Check if the cursor is between the last block and the current block
if last_end <= cursor && cursor < start {
@@ -1,8 +1,8 @@
use crate::markdown_elements::{
- HeadingLevel, Image, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown,
+ HeadingLevel, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown,
ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, ParsedMarkdownElement,
ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownTable,
- ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, ParsedMarkdownText,
+ ParsedMarkdownTableAlignment, ParsedMarkdownTableRow,
};
use gpui::{
div, img, px, rems, AbsoluteLength, AnyElement, ClipboardItem, DefiniteLength, Div, Element,
@@ -13,7 +13,6 @@ use gpui::{
use settings::Settings;
use std::{
ops::{Mul, Range},
- path::Path,
sync::Arc,
vec,
};
@@ -505,103 +504,41 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext)
}
MarkdownParagraphChunk::Image(image) => {
- let (link, source_range, image_source, alt_text) = match image {
- Image::Web {
- link,
- source_range,
- url,
- alt_text,
- } => (
- link,
- source_range,
- Resource::Uri(url.clone().into()),
- alt_text,
- ),
- Image::Path {
- link,
- source_range,
- path,
- alt_text,
- ..
- } => {
- let image_path = Path::new(path.to_str().unwrap());
- (
- link,
- source_range,
- Resource::Path(Arc::from(image_path)),
- alt_text,
- )
- }
+ 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(source_range);
-
- match link {
- None => {
- let fallback_workspace = workspace_clone.clone();
- let fallback_syntax_theme = syntax_theme.clone();
- let fallback_text_style = text_style.clone();
- let fallback_alt_text = alt_text.clone();
- let element_id_new = element_id.clone();
- let element = div()
- .child(img(ImageSource::Resource(image_source)).with_fallback({
- move || {
- fallback_text(
- fallback_alt_text.clone().unwrap(),
- element_id.clone(),
- &fallback_syntax_theme,
- code_span_bg_color,
- fallback_workspace.clone(),
- &fallback_text_style,
- )
- }
- }))
- .id(element_id_new)
- .into_any();
- any_element.push(element);
- }
- Some(link) => {
- let link_click = link.clone();
- let link_tooltip = link.clone();
- let fallback_workspace = workspace_clone.clone();
- let fallback_syntax_theme = syntax_theme.clone();
- let fallback_text_style = text_style.clone();
- let fallback_alt_text = alt_text.clone();
- let element_id_new = element_id.clone();
- let image_element = div()
- .child(img(ImageSource::Resource(image_source)).with_fallback({
- move || {
- fallback_text(
- fallback_alt_text.clone().unwrap(),
- element_id.clone(),
- &fallback_syntax_theme,
- code_span_bg_color,
- fallback_workspace.clone(),
- &fallback_text_style,
- )
- }
- }))
- .id(element_id_new)
- .tooltip(move |cx| LinkPreview::new(&link_tooltip.to_string(), cx))
- .on_click({
- let workspace = workspace_clone.clone();
- move |_event, window_cx| match &link_click {
- Link::Web { url } => window_cx.open_url(url),
- Link::Path { path, .. } => {
- if let Some(workspace) = &workspace {
- _ = workspace.update(window_cx, |workspace, cx| {
- workspace
- .open_abs_path(path.clone(), false, cx)
- .detach();
- });
- }
- }
+ let element_id = cx.next_id(&image.source_range);
+
+ let image_element = div()
+ .id(element_id)
+ .child(img(ImageSource::Resource(image_resource)).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| LinkPreview::new(&link.to_string(), cx)
+ })
+ .on_click({
+ let workspace = workspace_clone.clone();
+ let link = image.link.clone();
+ move |_event, window_cx| match &link {
+ Link::Web { url } => window_cx.open_url(url),
+ Link::Path { path, .. } => {
+ if let Some(workspace) = &workspace {
+ _ = workspace.update(window_cx, |workspace, cx| {
+ workspace.open_abs_path(path.clone(), false, cx).detach();
+ });
}
- })
- .into_any();
- any_element.push(image_element);
- }
- }
+ }
+ }
+ })
+ .into_any();
+ any_element.push(image_element);
}
}
}
@@ -613,80 +550,3 @@ 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()
}
-
-fn fallback_text(
- parsed: ParsedMarkdownText,
- source_range: ElementId,
- syntax_theme: &theme::SyntaxTheme,
- code_span_bg_color: Hsla,
- workspace: Option<WeakView<Workspace>>,
- text_style: &TextStyle,
-) -> AnyElement {
- let element_id = source_range;
-
- let highlights = gpui::combine_highlights(
- parsed.highlights.iter().filter_map(|(range, highlight)| {
- let highlight = highlight.to_highlight_style(syntax_theme)?;
- Some((range.clone(), highlight))
- }),
- parsed
- .regions
- .iter()
- .zip(&parsed.region_ranges)
- .filter_map(|(region, range)| {
- if region.code {
- Some((
- range.clone(),
- HighlightStyle {
- background_color: Some(code_span_bg_color),
- ..Default::default()
- },
- ))
- } else {
- None
- }
- }),
- );
- let mut links = Vec::new();
- let mut link_ranges = Vec::new();
- for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) {
- if let Some(link) = region.link.clone() {
- links.push(link);
- link_ranges.push(range.clone());
- }
- }
- let element = div()
- .child(
- InteractiveText::new(
- element_id,
- StyledText::new(parsed.contents.clone()).with_highlights(text_style, highlights),
- )
- .tooltip({
- let links = links.clone();
- let link_ranges = link_ranges.clone();
- move |idx, cx| {
- for (ix, range) in link_ranges.iter().enumerate() {
- if range.contains(&idx) {
- return Some(LinkPreview::new(&links[ix].to_string(), cx));
- }
- }
- None
- }
- })
- .on_click(
- link_ranges,
- move |clicked_range_ix, window_cx| match &links[clicked_range_ix] {
- Link::Web { url } => window_cx.open_url(url),
- Link::Path { path, .. } => {
- if let Some(workspace) = &workspace {
- _ = workspace.update(window_cx, |workspace, cx| {
- workspace.open_abs_path(path.clone(), false, cx).detach();
- });
- }
- }
- },
- ),
- )
- .into_any();
- return element;
-}