From 96854c68eadd1dd72aa379368fde3cea791498a4 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 22 Nov 2024 14:49:26 -0800 Subject: [PATCH] Markdown preview image rendering (#21082) Closes https://github.com/zed-industries/zed/issues/13246 Supersedes: https://github.com/zed-industries/zed/pull/16192 I couldn't push to the git fork this user was using, so here's the exact same PR but with some style nits implemented. Release Notes: - Added image rendering to the Markdown preview --------- Co-authored-by: dovakin0007 Co-authored-by: dovakin0007 <73059450+dovakin0007@users.noreply.github.com> --- crates/language/src/markdown.rs | 14 +- .../markdown_preview/src/markdown_elements.rs | 137 +++++++- .../markdown_preview/src/markdown_parser.rs | 215 +++++++----- .../markdown_preview/src/markdown_renderer.rs | 330 ++++++++++++++---- .../notifications/src/notification_store.rs | 7 +- crates/repl/src/notebook/cell.rs | 2 +- crates/rich_text/src/rich_text.rs | 14 +- 7 files changed, 538 insertions(+), 181 deletions(-) diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs index b9393a16ab..0221f0f431 100644 --- a/crates/language/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -239,12 +239,7 @@ pub async fn parse_markdown_block( Event::Start(tag) => match tag { Tag::Paragraph => new_paragraph(text, &mut list_stack), - Tag::Heading { - level: _, - id: _, - classes: _, - attrs: _, - } => { + Tag::Heading { .. } => { new_paragraph(text, &mut list_stack); bold_depth += 1; } @@ -267,12 +262,7 @@ pub async fn parse_markdown_block( Tag::Strikethrough => strikethrough_depth += 1, - Tag::Link { - link_type: _, - dest_url, - title: _, - id: _, - } => link_url = Some(dest_url.to_string()), + Tag::Link { dest_url, .. } => link_url = Some(dest_url.to_string()), Tag::List(number) => { list_stack.push((number, false)); diff --git a/crates/markdown_preview/src/markdown_elements.rs b/crates/markdown_preview/src/markdown_elements.rs index 8423e4ec82..ff43fab08a 100644 --- a/crates/markdown_preview/src/markdown_elements.rs +++ b/crates/markdown_preview/src/markdown_elements.rs @@ -13,7 +13,7 @@ pub enum ParsedMarkdownElement { BlockQuote(ParsedMarkdownBlockQuote), CodeBlock(ParsedMarkdownCodeBlock), /// A paragraph of text and other inline elements. - Paragraph(ParsedMarkdownText), + Paragraph(MarkdownParagraph), HorizontalRule(Range), } @@ -25,7 +25,13 @@ impl ParsedMarkdownElement { 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) => text.source_range.clone(), + Self::Paragraph(text) => match &text[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(), + }, + }, Self::HorizontalRule(range) => range.clone(), } } @@ -35,6 +41,15 @@ impl ParsedMarkdownElement { } } +pub type MarkdownParagraph = Vec; + +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] +pub enum MarkdownParagraphChunk { + Text(ParsedMarkdownText), + Image(Image), +} + #[derive(Debug)] #[cfg_attr(test, derive(PartialEq))] pub struct ParsedMarkdown { @@ -73,7 +88,7 @@ pub struct ParsedMarkdownCodeBlock { pub struct ParsedMarkdownHeading { pub source_range: Range, pub level: HeadingLevel, - pub contents: ParsedMarkdownText, + pub contents: MarkdownParagraph, } #[derive(Debug, PartialEq)] @@ -107,7 +122,7 @@ pub enum ParsedMarkdownTableAlignment { #[derive(Debug)] #[cfg_attr(test, derive(PartialEq))] pub struct ParsedMarkdownTableRow { - pub children: Vec, + pub children: Vec, } impl Default for ParsedMarkdownTableRow { @@ -123,7 +138,7 @@ impl ParsedMarkdownTableRow { } } - pub fn with_children(children: Vec) -> Self { + pub fn with_children(children: Vec) -> Self { Self { children } } } @@ -135,7 +150,7 @@ pub struct ParsedMarkdownBlockQuote { pub children: Vec, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ParsedMarkdownText { /// Where the text is located in the source Markdown document. pub source_range: Range, @@ -266,10 +281,112 @@ impl Display for Link { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Link::Web { url } => write!(f, "{}", url), - Link::Path { - display_path, - path: _, - } => write!(f, "{}", display_path.display()), + Link::Path { display_path, .. } => write!(f, "{}", display_path.display()), + } + } +} + +/// A Markdown Image +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq))] +pub enum Image { + Web { + source_range: Range, + /// The URL of the Image. + url: String, + /// Link URL if exists. + link: Option, + /// alt text if it exists + alt_text: Option, + }, + /// Image path on the filesystem. + Path { + source_range: Range, + /// 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, + /// alt text if it exists + alt_text: Option, + }, +} + +impl Image { + pub fn identify( + source_range: Range, + file_location_directory: Option, + text: String, + link: Option, + ) -> Option { + 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 + } + + 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()), } } } diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index d514b89e52..211cca2494 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -4,7 +4,7 @@ use collections::FxHashMap; use gpui::FontWeight; use language::LanguageRegistry; use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd}; -use std::{ops::Range, path::PathBuf, sync::Arc}; +use std::{ops::Range, path::PathBuf, sync::Arc, vec}; pub async fn parse_markdown( markdown_input: &str, @@ -101,11 +101,11 @@ impl<'a> MarkdownParser<'a> { | Event::Code(_) | Event::Html(_) | Event::FootnoteReference(_) - | Event::Start(Tag::Link { link_type: _, dest_url: _, title: _, id: _ }) + | Event::Start(Tag::Link { .. }) | Event::Start(Tag::Emphasis) | Event::Start(Tag::Strong) | Event::Start(Tag::Strikethrough) - | Event::Start(Tag::Image { link_type: _, dest_url: _, title: _, id: _ }) => { + | Event::Start(Tag::Image { .. }) => { true } _ => false, @@ -134,12 +134,7 @@ impl<'a> MarkdownParser<'a> { let text = self.parse_text(false, Some(source_range)); Some(vec![ParsedMarkdownElement::Paragraph(text)]) } - Tag::Heading { - level, - id: _, - classes: _, - attrs: _, - } => { + Tag::Heading { level, .. } => { let level = *level; self.cursor += 1; let heading = self.parse_heading(level); @@ -194,22 +189,23 @@ impl<'a> MarkdownParser<'a> { &mut self, should_complete_on_soft_break: bool, source_range: Option>, - ) -> ParsedMarkdownText { + ) -> MarkdownParagraph { let source_range = source_range.unwrap_or_else(|| { self.current() .map(|(_, range)| range.clone()) .unwrap_or_default() }); + let mut markdown_text_like = Vec::new(); let mut text = String::new(); let mut bold_depth = 0; let mut italic_depth = 0; let mut strikethrough_depth = 0; let mut link: Option = None; + let mut image: Option = None; let mut region_ranges: Vec> = vec![]; let mut regions: Vec = vec![]; let mut highlights: Vec<(Range, MarkdownHighlight)> = vec![]; - let mut link_urls: Vec = vec![]; let mut link_ranges: Vec> = vec![]; @@ -225,8 +221,6 @@ impl<'a> MarkdownParser<'a> { if should_complete_on_soft_break { break; } - - // `Some text\nSome more text` should be treated as a single line. text.push(' '); } @@ -240,7 +234,6 @@ impl<'a> MarkdownParser<'a> { Event::Text(t) => { text.push_str(t.as_ref()); - let mut style = MarkdownHighlightStyle::default(); if bold_depth > 0 { @@ -299,7 +292,6 @@ impl<'a> MarkdownParser<'a> { url: link.as_str().to_string(), }), }); - last_link_len = end; } last_link_len @@ -316,13 +308,63 @@ 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(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); + } - // Note: This event means "inline code" and not "code block" + 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()); region_ranges.push(prev_len..text.len()); @@ -336,46 +378,44 @@ impl<'a> MarkdownParser<'a> { }), )); } - regions.push(ParsedRegion { code: true, link: link.clone(), }); } - Event::Start(tag) => match tag { Tag::Emphasis => italic_depth += 1, Tag::Strong => bold_depth += 1, Tag::Strikethrough => strikethrough_depth += 1, - Tag::Link { - link_type: _, - dest_url, - title: _, - id: _, - } => { + Tag::Link { dest_url, .. } => { link = Link::identify( self.file_location_directory.clone(), dest_url.to_string(), ); } + Tag::Image { dest_url, .. } => { + image = Image::identify( + source_range.clone(), + self.file_location_directory.clone(), + dest_url.to_string(), + link.clone(), + ); + } _ => { break; } }, Event::End(tag) => match tag { - TagEnd::Emphasis => { - italic_depth -= 1; - } - TagEnd::Strong => { - bold_depth -= 1; - } - TagEnd::Strikethrough => { - strikethrough_depth -= 1; - } + TagEnd::Emphasis => italic_depth -= 1, + TagEnd::Strong => bold_depth -= 1, + TagEnd::Strikethrough => strikethrough_depth -= 1, TagEnd::Link => { link = None; } + TagEnd::Image => { + image = None; + } TagEnd::Paragraph => { self.cursor += 1; break; @@ -384,7 +424,6 @@ impl<'a> MarkdownParser<'a> { break; } }, - _ => { break; } @@ -392,14 +431,16 @@ impl<'a> MarkdownParser<'a> { self.cursor += 1; } - - ParsedMarkdownText { - source_range, - contents: text, - highlights, - regions, - region_ranges, + if !text.is_empty() { + markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range: source_range.clone(), + contents: text, + highlights, + regions, + region_ranges, + })); } + markdown_text_like } fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading { @@ -708,7 +749,6 @@ impl<'a> MarkdownParser<'a> { } } } - let highlights = if let Some(language) = &language { if let Some(registry) = &self.language_registry { let rope: language::Rope = code.as_str().into(); @@ -735,10 +775,14 @@ impl<'a> MarkdownParser<'a> { #[cfg(test)] mod tests { + use core::panic; + use super::*; use gpui::BackgroundExecutor; - use language::{tree_sitter_rust, HighlightId, Language, LanguageConfig, LanguageMatcher}; + use language::{ + tree_sitter_rust, HighlightId, Language, LanguageConfig, LanguageMatcher, LanguageRegistry, + }; use pretty_assertions::assert_eq; use ParsedMarkdownListItemType::*; @@ -810,20 +854,29 @@ mod tests { assert_eq!(parsed.children.len(), 1); assert_eq!( parsed.children[0], - ParsedMarkdownElement::Paragraph(ParsedMarkdownText { - source_range: 0..35, - contents: "Some bostrikethroughld text".to_string(), - highlights: Vec::new(), - region_ranges: Vec::new(), - regions: Vec::new(), - }) + ParsedMarkdownElement::Paragraph(vec![MarkdownParagraphChunk::Text( + ParsedMarkdownText { + source_range: 0..35, + contents: "Some bostrikethroughld text".to_string(), + highlights: Vec::new(), + region_ranges: Vec::new(), + regions: Vec::new(), + } + )]) ); - let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { + let new_text = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { text } else { panic!("Expected a paragraph"); }; + + let paragraph = if let MarkdownParagraphChunk::Text(text) = &new_text[0] { + text + } else { + panic!("Expected a text"); + }; + assert_eq!( paragraph.highlights, vec![ @@ -871,6 +924,11 @@ mod tests { parsed.children, vec![p("Checkout this https://zed.dev link", 0..34)] ); + } + + #[gpui::test] + async fn test_image_links_detection() { + let parsed = parse("![test](https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png)").await; let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { text @@ -878,25 +936,22 @@ mod tests { panic!("Expected a paragraph"); }; assert_eq!( - paragraph.highlights, - vec![( - 14..29, - MarkdownHighlight::Style(MarkdownHighlightStyle { - underline: true, - ..Default::default() - }), - )] + paragraph[0], + MarkdownParagraphChunk::Image(Image::Web { + 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![], + }, + ), + },) ); - assert_eq!( - paragraph.regions, - vec![ParsedRegion { - code: false, - link: Some(Link::Web { - url: "https://zed.dev".to_string() - }), - }] - ); - assert_eq!(paragraph.region_ranges, vec![14..29]); } #[gpui::test] @@ -1169,7 +1224,7 @@ 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)],), ], ); } @@ -1312,7 +1367,7 @@ fn main() { )) } - fn h1(contents: ParsedMarkdownText, source_range: Range) -> ParsedMarkdownElement { + fn h1(contents: MarkdownParagraph, source_range: Range) -> ParsedMarkdownElement { ParsedMarkdownElement::Heading(ParsedMarkdownHeading { source_range, level: HeadingLevel::H1, @@ -1320,7 +1375,7 @@ fn main() { }) } - fn h2(contents: ParsedMarkdownText, source_range: Range) -> ParsedMarkdownElement { + fn h2(contents: MarkdownParagraph, source_range: Range) -> ParsedMarkdownElement { ParsedMarkdownElement::Heading(ParsedMarkdownHeading { source_range, level: HeadingLevel::H2, @@ -1328,7 +1383,7 @@ fn main() { }) } - fn h3(contents: ParsedMarkdownText, source_range: Range) -> ParsedMarkdownElement { + fn h3(contents: MarkdownParagraph, source_range: Range) -> ParsedMarkdownElement { ParsedMarkdownElement::Heading(ParsedMarkdownHeading { source_range, level: HeadingLevel::H3, @@ -1340,14 +1395,14 @@ fn main() { ParsedMarkdownElement::Paragraph(text(contents, source_range)) } - fn text(contents: &str, source_range: Range) -> ParsedMarkdownText { - ParsedMarkdownText { + fn text(contents: &str, source_range: Range) -> MarkdownParagraph { + vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { highlights: Vec::new(), region_ranges: Vec::new(), regions: Vec::new(), source_range, contents: contents.to_string(), - } + })] } fn block_quote( @@ -1401,7 +1456,7 @@ fn main() { } } - fn row(children: Vec) -> ParsedMarkdownTableRow { + fn row(children: Vec) -> ParsedMarkdownTableRow { ParsedMarkdownTableRow { children } } diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 37ca5636a6..6140372e0b 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -1,29 +1,33 @@ use crate::markdown_elements::{ - HeadingLevel, Link, ParsedMarkdown, ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, - ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownListItem, - ParsedMarkdownListItemType, ParsedMarkdownTable, ParsedMarkdownTableAlignment, - ParsedMarkdownTableRow, ParsedMarkdownText, + HeadingLevel, Image, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown, + ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, ParsedMarkdownElement, + ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownTable, + ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, ParsedMarkdownText, }; use gpui::{ - div, px, rems, AbsoluteLength, AnyElement, ClipboardItem, DefiniteLength, Div, Element, - ElementId, HighlightStyle, Hsla, InteractiveText, IntoElement, Keystroke, Length, Modifiers, - ParentElement, SharedString, Styled, StyledText, TextStyle, WeakView, WindowContext, + div, img, px, rems, AbsoluteLength, AnyElement, ClipboardItem, DefiniteLength, Div, Element, + ElementId, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement, Keystroke, Length, + Modifiers, ParentElement, Resource, SharedString, Styled, StyledText, TextStyle, WeakView, + WindowContext, }; use settings::Settings; use std::{ ops::{Mul, Range}, + path::Path, sync::Arc, + vec, }; use theme::{ActiveTheme, SyntaxTheme, ThemeSettings}; use ui::{ h_flex, relative, v_flex, Checkbox, Clickable, FluentBuilder, IconButton, IconName, IconSize, - InteractiveElement, LinkPreview, Selection, StatefulInteractiveElement, StyledExt, Tooltip, - VisibleOnHover, + InteractiveElement, LinkPreview, Selection, StatefulInteractiveElement, StyledExt, StyledImage, + Tooltip, VisibleOnHover, }; use workspace::Workspace; type CheckboxClickedCallback = Arc, &mut WindowContext)>>; +#[derive(Clone)] pub struct RenderContext { workspace: Option>, next_id: usize, @@ -153,7 +157,7 @@ fn render_markdown_heading(parsed: &ParsedMarkdownHeading, cx: &mut RenderContex .text_color(color) .pt(rems(0.15)) .pb_1() - .child(render_markdown_text(&parsed.contents, cx)) + .children(render_markdown_text(&parsed.contents, cx)) .whitespace_normal() .into_any() } @@ -231,17 +235,29 @@ fn render_markdown_list_item( cx.with_common_p(item).into_any() } +fn paragraph_len(paragraphs: &MarkdownParagraph) -> usize { + paragraphs + .iter() + .map(|paragraph| match paragraph { + MarkdownParagraphChunk::Text(text) => text.contents.len(), + // TODO: Scale column width based on image size + MarkdownParagraphChunk::Image(_) => 1, + }) + .sum() +} + fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement { let mut max_lengths: Vec = vec![0; parsed.header.children.len()]; for (index, cell) in parsed.header.children.iter().enumerate() { - let length = cell.contents.len(); + let length = paragraph_len(&cell); max_lengths[index] = length; } for row in &parsed.body { for (index, cell) in row.children.iter().enumerate() { - let length = cell.contents.len(); + let length = paragraph_len(&cell); + if length > max_lengths[index] { max_lengths[index] = length; } @@ -307,11 +323,10 @@ fn render_markdown_table_row( }; let max_width = max_column_widths.get(index).unwrap_or(&0.0); - let mut cell = container .w(Length::Definite(relative(*max_width))) .h_full() - .child(contents) + .children(contents) .px_2() .py_1() .border_color(cx.border_color); @@ -398,18 +413,219 @@ fn render_markdown_code_block( .into_any() } -fn render_markdown_paragraph(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> AnyElement { +fn render_markdown_paragraph(parsed: &MarkdownParagraph, cx: &mut RenderContext) -> AnyElement { cx.with_common_p(div()) - .child(render_markdown_text(parsed, cx)) + .children(render_markdown_text(parsed, cx)) + .flex() .into_any_element() } -fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> AnyElement { - let element_id = cx.next_id(&parsed.source_range); +fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext) -> Vec { + let mut any_element = vec![]; + // these values are cloned in-order satisfy borrow checker + let syntax_theme = cx.syntax_theme.clone(); + let workspace_clone = cx.workspace.clone(); + let code_span_bg_color = cx.code_span_background_color; + let text_style = cx.text_style.clone(); + + for parsed_region in parsed_new { + match parsed_region { + MarkdownParagraphChunk::Text(parsed) => { + let element_id = cx.next_id(&parsed.source_range); + + let highlights = gpui::combine_highlights( + parsed.highlights.iter().filter_map(|(range, highlight)| { + highlight + .to_highlight_style(&syntax_theme) + .map(|style| (range.clone(), style)) + }), + 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 workspace = workspace_clone.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(); + any_element.push(element); + } + + 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 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(); + }); + } + } + } + }) + .into_any(); + any_element.push(image_element); + } + } + } + } + } + + any_element +} + +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>, + 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(&cx.syntax_theme)?; + let highlight = highlight.to_highlight_style(syntax_theme)?; Some((range.clone(), highlight)) }), parsed @@ -421,7 +637,7 @@ fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> Some(( range.clone(), HighlightStyle { - background_color: Some(cx.code_span_background_color), + background_color: Some(code_span_bg_color), ..Default::default() }, )) @@ -430,7 +646,6 @@ fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> } }), ); - let mut links = Vec::new(); let mut link_ranges = Vec::new(); for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) { @@ -439,45 +654,38 @@ fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> link_ranges.push(range.clone()); } } - - let workspace = cx.workspace.clone(); - - InteractiveText::new( - element_id, - StyledText::new(parsed.contents.clone()).with_highlights(&cx.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)); + 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 } - } - 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, - display_path: _, - } => { - if let Some(workspace) = &workspace { - _ = workspace.update(window_cx, |workspace, cx| { - workspace.open_abs_path(path.clone(), false, cx).detach(); - }); - } - } - }, - ) - .into_any_element() -} - -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() + }) + .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; } diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index 5c3de53ee1..a61f1da1c4 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -238,11 +238,8 @@ impl NotificationStore { ) -> Result<()> { this.update(&mut cx, |this, cx| { if let Some(notification) = envelope.payload.notification { - if let Some(rpc::Notification::ChannelMessageMention { - message_id, - sender_id: _, - channel_id: _, - }) = Notification::from_proto(¬ification) + if let Some(rpc::Notification::ChannelMessageMention { message_id, .. }) = + Notification::from_proto(¬ification) { let fetch_message_task = this.channel_store.update(cx, |this, cx| { this.fetch_channel_messages(vec![message_id], cx) diff --git a/crates/repl/src/notebook/cell.rs b/crates/repl/src/notebook/cell.rs index 055e4c09f8..12d11853fb 100644 --- a/crates/repl/src/notebook/cell.rs +++ b/crates/repl/src/notebook/cell.rs @@ -114,7 +114,7 @@ impl Cell { id, metadata, source, - attachments: _, + .. } => { let source = source.join(""); diff --git a/crates/rich_text/src/rich_text.rs b/crates/rich_text/src/rich_text.rs index 80b7786c24..df830419d3 100644 --- a/crates/rich_text/src/rich_text.rs +++ b/crates/rich_text/src/rich_text.rs @@ -310,12 +310,7 @@ pub fn render_markdown_mut( } Event::Start(tag) => match tag { Tag::Paragraph => new_paragraph(text, &mut list_stack), - Tag::Heading { - level: _, - id: _, - classes: _, - attrs: _, - } => { + Tag::Heading { .. } => { new_paragraph(text, &mut list_stack); bold_depth += 1; } @@ -333,12 +328,7 @@ pub fn render_markdown_mut( Tag::Emphasis => italic_depth += 1, Tag::Strong => bold_depth += 1, Tag::Strikethrough => strikethrough_depth += 1, - Tag::Link { - link_type: _, - dest_url, - title: _, - id: _, - } => link_url = Some(dest_url.to_string()), + Tag::Link { dest_url, .. } => link_url = Some(dest_url.to_string()), Tag::List(number) => { list_stack.push((number, false)); }