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 <dovakin0007@gmail.com> Co-authored-by: dovakin0007 <73059450+dovakin0007@users.noreply.github.com>
This commit is contained in:
parent
becc36380f
commit
96854c68ea
7 changed files with 538 additions and 181 deletions
|
@ -239,12 +239,7 @@ pub async fn parse_markdown_block(
|
||||||
Event::Start(tag) => match tag {
|
Event::Start(tag) => match tag {
|
||||||
Tag::Paragraph => new_paragraph(text, &mut list_stack),
|
Tag::Paragraph => new_paragraph(text, &mut list_stack),
|
||||||
|
|
||||||
Tag::Heading {
|
Tag::Heading { .. } => {
|
||||||
level: _,
|
|
||||||
id: _,
|
|
||||||
classes: _,
|
|
||||||
attrs: _,
|
|
||||||
} => {
|
|
||||||
new_paragraph(text, &mut list_stack);
|
new_paragraph(text, &mut list_stack);
|
||||||
bold_depth += 1;
|
bold_depth += 1;
|
||||||
}
|
}
|
||||||
|
@ -267,12 +262,7 @@ pub async fn parse_markdown_block(
|
||||||
|
|
||||||
Tag::Strikethrough => strikethrough_depth += 1,
|
Tag::Strikethrough => strikethrough_depth += 1,
|
||||||
|
|
||||||
Tag::Link {
|
Tag::Link { dest_url, .. } => link_url = Some(dest_url.to_string()),
|
||||||
link_type: _,
|
|
||||||
dest_url,
|
|
||||||
title: _,
|
|
||||||
id: _,
|
|
||||||
} => link_url = Some(dest_url.to_string()),
|
|
||||||
|
|
||||||
Tag::List(number) => {
|
Tag::List(number) => {
|
||||||
list_stack.push((number, false));
|
list_stack.push((number, false));
|
||||||
|
|
|
@ -13,7 +13,7 @@ pub enum ParsedMarkdownElement {
|
||||||
BlockQuote(ParsedMarkdownBlockQuote),
|
BlockQuote(ParsedMarkdownBlockQuote),
|
||||||
CodeBlock(ParsedMarkdownCodeBlock),
|
CodeBlock(ParsedMarkdownCodeBlock),
|
||||||
/// A paragraph of text and other inline elements.
|
/// A paragraph of text and other inline elements.
|
||||||
Paragraph(ParsedMarkdownText),
|
Paragraph(MarkdownParagraph),
|
||||||
HorizontalRule(Range<usize>),
|
HorizontalRule(Range<usize>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,13 @@ impl ParsedMarkdownElement {
|
||||||
Self::Table(table) => table.source_range.clone(),
|
Self::Table(table) => table.source_range.clone(),
|
||||||
Self::BlockQuote(block_quote) => block_quote.source_range.clone(),
|
Self::BlockQuote(block_quote) => block_quote.source_range.clone(),
|
||||||
Self::CodeBlock(code_block) => code_block.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(),
|
Self::HorizontalRule(range) => range.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +41,15 @@ impl ParsedMarkdownElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type MarkdownParagraph = Vec<MarkdownParagraphChunk>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
|
pub enum MarkdownParagraphChunk {
|
||||||
|
Text(ParsedMarkdownText),
|
||||||
|
Image(Image),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
pub struct ParsedMarkdown {
|
pub struct ParsedMarkdown {
|
||||||
|
@ -73,7 +88,7 @@ pub struct ParsedMarkdownCodeBlock {
|
||||||
pub struct ParsedMarkdownHeading {
|
pub struct ParsedMarkdownHeading {
|
||||||
pub source_range: Range<usize>,
|
pub source_range: Range<usize>,
|
||||||
pub level: HeadingLevel,
|
pub level: HeadingLevel,
|
||||||
pub contents: ParsedMarkdownText,
|
pub contents: MarkdownParagraph,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
|
@ -107,7 +122,7 @@ pub enum ParsedMarkdownTableAlignment {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
pub struct ParsedMarkdownTableRow {
|
pub struct ParsedMarkdownTableRow {
|
||||||
pub children: Vec<ParsedMarkdownText>,
|
pub children: Vec<MarkdownParagraph>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ParsedMarkdownTableRow {
|
impl Default for ParsedMarkdownTableRow {
|
||||||
|
@ -123,7 +138,7 @@ impl ParsedMarkdownTableRow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_children(children: Vec<ParsedMarkdownText>) -> Self {
|
pub fn with_children(children: Vec<MarkdownParagraph>) -> Self {
|
||||||
Self { children }
|
Self { children }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,7 +150,7 @@ pub struct ParsedMarkdownBlockQuote {
|
||||||
pub children: Vec<ParsedMarkdownElement>,
|
pub children: Vec<ParsedMarkdownElement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ParsedMarkdownText {
|
pub struct ParsedMarkdownText {
|
||||||
/// Where the text is located in the source Markdown document.
|
/// Where the text is located in the source Markdown document.
|
||||||
pub source_range: Range<usize>,
|
pub source_range: Range<usize>,
|
||||||
|
@ -266,10 +281,112 @@ impl Display for Link {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Link::Web { url } => write!(f, "{}", url),
|
Link::Web { url } => write!(f, "{}", url),
|
||||||
Link::Path {
|
Link::Path { display_path, .. } => write!(f, "{}", display_path.display()),
|
||||||
display_path,
|
}
|
||||||
path: _,
|
}
|
||||||
} => write!(f, "{}", display_path.display()),
|
}
|
||||||
|
|
||||||
|
/// 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>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Image {
|
||||||
|
pub fn identify(
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ use collections::FxHashMap;
|
||||||
use gpui::FontWeight;
|
use gpui::FontWeight;
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd};
|
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(
|
pub async fn parse_markdown(
|
||||||
markdown_input: &str,
|
markdown_input: &str,
|
||||||
|
@ -101,11 +101,11 @@ impl<'a> MarkdownParser<'a> {
|
||||||
| Event::Code(_)
|
| Event::Code(_)
|
||||||
| Event::Html(_)
|
| Event::Html(_)
|
||||||
| Event::FootnoteReference(_)
|
| Event::FootnoteReference(_)
|
||||||
| Event::Start(Tag::Link { link_type: _, dest_url: _, title: _, id: _ })
|
| Event::Start(Tag::Link { .. })
|
||||||
| Event::Start(Tag::Emphasis)
|
| Event::Start(Tag::Emphasis)
|
||||||
| Event::Start(Tag::Strong)
|
| Event::Start(Tag::Strong)
|
||||||
| Event::Start(Tag::Strikethrough)
|
| Event::Start(Tag::Strikethrough)
|
||||||
| Event::Start(Tag::Image { link_type: _, dest_url: _, title: _, id: _ }) => {
|
| Event::Start(Tag::Image { .. }) => {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
|
@ -134,12 +134,7 @@ impl<'a> MarkdownParser<'a> {
|
||||||
let text = self.parse_text(false, Some(source_range));
|
let text = self.parse_text(false, Some(source_range));
|
||||||
Some(vec![ParsedMarkdownElement::Paragraph(text)])
|
Some(vec![ParsedMarkdownElement::Paragraph(text)])
|
||||||
}
|
}
|
||||||
Tag::Heading {
|
Tag::Heading { level, .. } => {
|
||||||
level,
|
|
||||||
id: _,
|
|
||||||
classes: _,
|
|
||||||
attrs: _,
|
|
||||||
} => {
|
|
||||||
let level = *level;
|
let level = *level;
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
let heading = self.parse_heading(level);
|
let heading = self.parse_heading(level);
|
||||||
|
@ -194,22 +189,23 @@ impl<'a> MarkdownParser<'a> {
|
||||||
&mut self,
|
&mut self,
|
||||||
should_complete_on_soft_break: bool,
|
should_complete_on_soft_break: bool,
|
||||||
source_range: Option<Range<usize>>,
|
source_range: Option<Range<usize>>,
|
||||||
) -> ParsedMarkdownText {
|
) -> MarkdownParagraph {
|
||||||
let source_range = source_range.unwrap_or_else(|| {
|
let source_range = source_range.unwrap_or_else(|| {
|
||||||
self.current()
|
self.current()
|
||||||
.map(|(_, range)| range.clone())
|
.map(|(_, range)| range.clone())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut markdown_text_like = Vec::new();
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
let mut bold_depth = 0;
|
let mut bold_depth = 0;
|
||||||
let mut italic_depth = 0;
|
let mut italic_depth = 0;
|
||||||
let mut strikethrough_depth = 0;
|
let mut strikethrough_depth = 0;
|
||||||
let mut link: Option<Link> = None;
|
let mut link: Option<Link> = None;
|
||||||
|
let mut image: Option<Image> = None;
|
||||||
let mut region_ranges: Vec<Range<usize>> = vec![];
|
let mut region_ranges: Vec<Range<usize>> = vec![];
|
||||||
let mut regions: Vec<ParsedRegion> = vec![];
|
let mut regions: Vec<ParsedRegion> = vec![];
|
||||||
let mut highlights: Vec<(Range<usize>, MarkdownHighlight)> = vec![];
|
let mut highlights: Vec<(Range<usize>, MarkdownHighlight)> = vec![];
|
||||||
|
|
||||||
let mut link_urls: Vec<String> = vec![];
|
let mut link_urls: Vec<String> = vec![];
|
||||||
let mut link_ranges: Vec<Range<usize>> = vec![];
|
let mut link_ranges: Vec<Range<usize>> = vec![];
|
||||||
|
|
||||||
|
@ -225,8 +221,6 @@ impl<'a> MarkdownParser<'a> {
|
||||||
if should_complete_on_soft_break {
|
if should_complete_on_soft_break {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// `Some text\nSome more text` should be treated as a single line.
|
|
||||||
text.push(' ');
|
text.push(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,7 +234,6 @@ impl<'a> MarkdownParser<'a> {
|
||||||
|
|
||||||
Event::Text(t) => {
|
Event::Text(t) => {
|
||||||
text.push_str(t.as_ref());
|
text.push_str(t.as_ref());
|
||||||
|
|
||||||
let mut style = MarkdownHighlightStyle::default();
|
let mut style = MarkdownHighlightStyle::default();
|
||||||
|
|
||||||
if bold_depth > 0 {
|
if bold_depth > 0 {
|
||||||
|
@ -299,7 +292,6 @@ impl<'a> MarkdownParser<'a> {
|
||||||
url: link.as_str().to_string(),
|
url: link.as_str().to_string(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
last_link_len = end;
|
last_link_len = end;
|
||||||
}
|
}
|
||||||
last_link_len
|
last_link_len
|
||||||
|
@ -316,13 +308,63 @@ impl<'a> MarkdownParser<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if new_highlight {
|
if new_highlight {
|
||||||
highlights
|
highlights.push((
|
||||||
.push((last_run_len..text.len(), MarkdownHighlight::Style(style)));
|
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) => {
|
Event::Code(t) => {
|
||||||
text.push_str(t.as_ref());
|
text.push_str(t.as_ref());
|
||||||
region_ranges.push(prev_len..text.len());
|
region_ranges.push(prev_len..text.len());
|
||||||
|
@ -336,46 +378,44 @@ impl<'a> MarkdownParser<'a> {
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
regions.push(ParsedRegion {
|
regions.push(ParsedRegion {
|
||||||
code: true,
|
code: true,
|
||||||
link: link.clone(),
|
link: link.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Event::Start(tag) => match tag {
|
Event::Start(tag) => match tag {
|
||||||
Tag::Emphasis => italic_depth += 1,
|
Tag::Emphasis => italic_depth += 1,
|
||||||
Tag::Strong => bold_depth += 1,
|
Tag::Strong => bold_depth += 1,
|
||||||
Tag::Strikethrough => strikethrough_depth += 1,
|
Tag::Strikethrough => strikethrough_depth += 1,
|
||||||
Tag::Link {
|
Tag::Link { dest_url, .. } => {
|
||||||
link_type: _,
|
|
||||||
dest_url,
|
|
||||||
title: _,
|
|
||||||
id: _,
|
|
||||||
} => {
|
|
||||||
link = Link::identify(
|
link = Link::identify(
|
||||||
self.file_location_directory.clone(),
|
self.file_location_directory.clone(),
|
||||||
dest_url.to_string(),
|
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;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
Event::End(tag) => match tag {
|
Event::End(tag) => match tag {
|
||||||
TagEnd::Emphasis => {
|
TagEnd::Emphasis => italic_depth -= 1,
|
||||||
italic_depth -= 1;
|
TagEnd::Strong => bold_depth -= 1,
|
||||||
}
|
TagEnd::Strikethrough => strikethrough_depth -= 1,
|
||||||
TagEnd::Strong => {
|
|
||||||
bold_depth -= 1;
|
|
||||||
}
|
|
||||||
TagEnd::Strikethrough => {
|
|
||||||
strikethrough_depth -= 1;
|
|
||||||
}
|
|
||||||
TagEnd::Link => {
|
TagEnd::Link => {
|
||||||
link = None;
|
link = None;
|
||||||
}
|
}
|
||||||
|
TagEnd::Image => {
|
||||||
|
image = None;
|
||||||
|
}
|
||||||
TagEnd::Paragraph => {
|
TagEnd::Paragraph => {
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
break;
|
break;
|
||||||
|
@ -384,7 +424,6 @@ impl<'a> MarkdownParser<'a> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -392,14 +431,16 @@ impl<'a> MarkdownParser<'a> {
|
||||||
|
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
}
|
}
|
||||||
|
if !text.is_empty() {
|
||||||
ParsedMarkdownText {
|
markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText {
|
||||||
source_range,
|
source_range: source_range.clone(),
|
||||||
contents: text,
|
contents: text,
|
||||||
highlights,
|
highlights,
|
||||||
regions,
|
regions,
|
||||||
region_ranges,
|
region_ranges,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
markdown_text_like
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading {
|
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 {
|
let highlights = if let Some(language) = &language {
|
||||||
if let Some(registry) = &self.language_registry {
|
if let Some(registry) = &self.language_registry {
|
||||||
let rope: language::Rope = code.as_str().into();
|
let rope: language::Rope = code.as_str().into();
|
||||||
|
@ -735,10 +775,14 @@ impl<'a> MarkdownParser<'a> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use core::panic;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use gpui::BackgroundExecutor;
|
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 pretty_assertions::assert_eq;
|
||||||
use ParsedMarkdownListItemType::*;
|
use ParsedMarkdownListItemType::*;
|
||||||
|
|
||||||
|
@ -810,20 +854,29 @@ mod tests {
|
||||||
assert_eq!(parsed.children.len(), 1);
|
assert_eq!(parsed.children.len(), 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parsed.children[0],
|
parsed.children[0],
|
||||||
ParsedMarkdownElement::Paragraph(ParsedMarkdownText {
|
ParsedMarkdownElement::Paragraph(vec![MarkdownParagraphChunk::Text(
|
||||||
source_range: 0..35,
|
ParsedMarkdownText {
|
||||||
contents: "Some bostrikethroughld text".to_string(),
|
source_range: 0..35,
|
||||||
highlights: Vec::new(),
|
contents: "Some bostrikethroughld text".to_string(),
|
||||||
region_ranges: Vec::new(),
|
highlights: Vec::new(),
|
||||||
regions: 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
|
text
|
||||||
} else {
|
} else {
|
||||||
panic!("Expected a paragraph");
|
panic!("Expected a paragraph");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let paragraph = if let MarkdownParagraphChunk::Text(text) = &new_text[0] {
|
||||||
|
text
|
||||||
|
} else {
|
||||||
|
panic!("Expected a text");
|
||||||
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
paragraph.highlights,
|
paragraph.highlights,
|
||||||
vec![
|
vec![
|
||||||
|
@ -871,6 +924,11 @@ mod tests {
|
||||||
parsed.children,
|
parsed.children,
|
||||||
vec![p("Checkout this https://zed.dev link", 0..34)]
|
vec![p("Checkout this https://zed.dev link", 0..34)]
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_image_links_detection() {
|
||||||
|
let parsed = parse("").await;
|
||||||
|
|
||||||
let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
|
let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
|
||||||
text
|
text
|
||||||
|
@ -878,25 +936,22 @@ mod tests {
|
||||||
panic!("Expected a paragraph");
|
panic!("Expected a paragraph");
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
paragraph.highlights,
|
paragraph[0],
|
||||||
vec![(
|
MarkdownParagraphChunk::Image(Image::Web {
|
||||||
14..29,
|
source_range: 0..111,
|
||||||
MarkdownHighlight::Style(MarkdownHighlightStyle {
|
url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(),
|
||||||
underline: true,
|
link: None,
|
||||||
..Default::default()
|
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]
|
#[gpui::test]
|
||||||
|
@ -1169,7 +1224,7 @@ Some other content
|
||||||
vec![
|
vec![
|
||||||
list_item(0..8, 1, Unordered, vec![p("code", 2..8)]),
|
list_item(0..8, 1, Unordered, vec![p("code", 2..8)]),
|
||||||
list_item(9..19, 1, Unordered, vec![p("bold", 11..19)]),
|
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<usize>) -> ParsedMarkdownElement {
|
fn h1(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
|
||||||
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
|
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
|
||||||
source_range,
|
source_range,
|
||||||
level: HeadingLevel::H1,
|
level: HeadingLevel::H1,
|
||||||
|
@ -1320,7 +1375,7 @@ fn main() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn h2(contents: ParsedMarkdownText, source_range: Range<usize>) -> ParsedMarkdownElement {
|
fn h2(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
|
||||||
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
|
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
|
||||||
source_range,
|
source_range,
|
||||||
level: HeadingLevel::H2,
|
level: HeadingLevel::H2,
|
||||||
|
@ -1328,7 +1383,7 @@ fn main() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn h3(contents: ParsedMarkdownText, source_range: Range<usize>) -> ParsedMarkdownElement {
|
fn h3(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
|
||||||
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
|
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
|
||||||
source_range,
|
source_range,
|
||||||
level: HeadingLevel::H3,
|
level: HeadingLevel::H3,
|
||||||
|
@ -1340,14 +1395,14 @@ fn main() {
|
||||||
ParsedMarkdownElement::Paragraph(text(contents, source_range))
|
ParsedMarkdownElement::Paragraph(text(contents, source_range))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text(contents: &str, source_range: Range<usize>) -> ParsedMarkdownText {
|
fn text(contents: &str, source_range: Range<usize>) -> MarkdownParagraph {
|
||||||
ParsedMarkdownText {
|
vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
|
||||||
highlights: Vec::new(),
|
highlights: Vec::new(),
|
||||||
region_ranges: Vec::new(),
|
region_ranges: Vec::new(),
|
||||||
regions: Vec::new(),
|
regions: Vec::new(),
|
||||||
source_range,
|
source_range,
|
||||||
contents: contents.to_string(),
|
contents: contents.to_string(),
|
||||||
}
|
})]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn block_quote(
|
fn block_quote(
|
||||||
|
@ -1401,7 +1456,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn row(children: Vec<ParsedMarkdownText>) -> ParsedMarkdownTableRow {
|
fn row(children: Vec<MarkdownParagraph>) -> ParsedMarkdownTableRow {
|
||||||
ParsedMarkdownTableRow { children }
|
ParsedMarkdownTableRow { children }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,33 @@
|
||||||
use crate::markdown_elements::{
|
use crate::markdown_elements::{
|
||||||
HeadingLevel, Link, ParsedMarkdown, ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock,
|
HeadingLevel, Image, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown,
|
||||||
ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownListItem,
|
ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, ParsedMarkdownElement,
|
||||||
ParsedMarkdownListItemType, ParsedMarkdownTable, ParsedMarkdownTableAlignment,
|
ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownTable,
|
||||||
ParsedMarkdownTableRow, ParsedMarkdownText,
|
ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, ParsedMarkdownText,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, rems, AbsoluteLength, AnyElement, ClipboardItem, DefiniteLength, Div, Element,
|
div, img, px, rems, AbsoluteLength, AnyElement, ClipboardItem, DefiniteLength, Div, Element,
|
||||||
ElementId, HighlightStyle, Hsla, InteractiveText, IntoElement, Keystroke, Length, Modifiers,
|
ElementId, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement, Keystroke, Length,
|
||||||
ParentElement, SharedString, Styled, StyledText, TextStyle, WeakView, WindowContext,
|
Modifiers, ParentElement, Resource, SharedString, Styled, StyledText, TextStyle, WeakView,
|
||||||
|
WindowContext,
|
||||||
};
|
};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{
|
use std::{
|
||||||
ops::{Mul, Range},
|
ops::{Mul, Range},
|
||||||
|
path::Path,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
vec,
|
||||||
};
|
};
|
||||||
use theme::{ActiveTheme, SyntaxTheme, ThemeSettings};
|
use theme::{ActiveTheme, SyntaxTheme, ThemeSettings};
|
||||||
use ui::{
|
use ui::{
|
||||||
h_flex, relative, v_flex, Checkbox, Clickable, FluentBuilder, IconButton, IconName, IconSize,
|
h_flex, relative, v_flex, Checkbox, Clickable, FluentBuilder, IconButton, IconName, IconSize,
|
||||||
InteractiveElement, LinkPreview, Selection, StatefulInteractiveElement, StyledExt, Tooltip,
|
InteractiveElement, LinkPreview, Selection, StatefulInteractiveElement, StyledExt, StyledImage,
|
||||||
VisibleOnHover,
|
Tooltip, VisibleOnHover,
|
||||||
};
|
};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
type CheckboxClickedCallback = Arc<Box<dyn Fn(bool, Range<usize>, &mut WindowContext)>>;
|
type CheckboxClickedCallback = Arc<Box<dyn Fn(bool, Range<usize>, &mut WindowContext)>>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct RenderContext {
|
pub struct RenderContext {
|
||||||
workspace: Option<WeakView<Workspace>>,
|
workspace: Option<WeakView<Workspace>>,
|
||||||
next_id: usize,
|
next_id: usize,
|
||||||
|
@ -153,7 +157,7 @@ fn render_markdown_heading(parsed: &ParsedMarkdownHeading, cx: &mut RenderContex
|
||||||
.text_color(color)
|
.text_color(color)
|
||||||
.pt(rems(0.15))
|
.pt(rems(0.15))
|
||||||
.pb_1()
|
.pb_1()
|
||||||
.child(render_markdown_text(&parsed.contents, cx))
|
.children(render_markdown_text(&parsed.contents, cx))
|
||||||
.whitespace_normal()
|
.whitespace_normal()
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
|
@ -231,17 +235,29 @@ fn render_markdown_list_item(
|
||||||
cx.with_common_p(item).into_any()
|
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 {
|
fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement {
|
||||||
let mut max_lengths: Vec<usize> = vec![0; parsed.header.children.len()];
|
let mut max_lengths: Vec<usize> = vec![0; parsed.header.children.len()];
|
||||||
|
|
||||||
for (index, cell) in parsed.header.children.iter().enumerate() {
|
for (index, cell) in parsed.header.children.iter().enumerate() {
|
||||||
let length = cell.contents.len();
|
let length = paragraph_len(&cell);
|
||||||
max_lengths[index] = length;
|
max_lengths[index] = length;
|
||||||
}
|
}
|
||||||
|
|
||||||
for row in &parsed.body {
|
for row in &parsed.body {
|
||||||
for (index, cell) in row.children.iter().enumerate() {
|
for (index, cell) in row.children.iter().enumerate() {
|
||||||
let length = cell.contents.len();
|
let length = paragraph_len(&cell);
|
||||||
|
|
||||||
if length > max_lengths[index] {
|
if length > max_lengths[index] {
|
||||||
max_lengths[index] = length;
|
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 max_width = max_column_widths.get(index).unwrap_or(&0.0);
|
||||||
|
|
||||||
let mut cell = container
|
let mut cell = container
|
||||||
.w(Length::Definite(relative(*max_width)))
|
.w(Length::Definite(relative(*max_width)))
|
||||||
.h_full()
|
.h_full()
|
||||||
.child(contents)
|
.children(contents)
|
||||||
.px_2()
|
.px_2()
|
||||||
.py_1()
|
.py_1()
|
||||||
.border_color(cx.border_color);
|
.border_color(cx.border_color);
|
||||||
|
@ -398,18 +413,219 @@ fn render_markdown_code_block(
|
||||||
.into_any()
|
.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())
|
cx.with_common_p(div())
|
||||||
.child(render_markdown_text(parsed, cx))
|
.children(render_markdown_text(parsed, cx))
|
||||||
|
.flex()
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> AnyElement {
|
fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext) -> Vec<AnyElement> {
|
||||||
let element_id = cx.next_id(&parsed.source_range);
|
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<WeakView<Workspace>>,
|
||||||
|
text_style: &TextStyle,
|
||||||
|
) -> AnyElement {
|
||||||
|
let element_id = source_range;
|
||||||
|
|
||||||
let highlights = gpui::combine_highlights(
|
let highlights = gpui::combine_highlights(
|
||||||
parsed.highlights.iter().filter_map(|(range, highlight)| {
|
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))
|
Some((range.clone(), highlight))
|
||||||
}),
|
}),
|
||||||
parsed
|
parsed
|
||||||
|
@ -421,7 +637,7 @@ fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) ->
|
||||||
Some((
|
Some((
|
||||||
range.clone(),
|
range.clone(),
|
||||||
HighlightStyle {
|
HighlightStyle {
|
||||||
background_color: Some(cx.code_span_background_color),
|
background_color: Some(code_span_bg_color),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
|
@ -430,7 +646,6 @@ fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) ->
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut links = Vec::new();
|
let mut links = Vec::new();
|
||||||
let mut link_ranges = Vec::new();
|
let mut link_ranges = Vec::new();
|
||||||
for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) {
|
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());
|
link_ranges.push(range.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let element = div()
|
||||||
let workspace = cx.workspace.clone();
|
.child(
|
||||||
|
InteractiveText::new(
|
||||||
InteractiveText::new(
|
element_id,
|
||||||
element_id,
|
StyledText::new(parsed.contents.clone()).with_highlights(text_style, highlights),
|
||||||
StyledText::new(parsed.contents.clone()).with_highlights(&cx.text_style, highlights),
|
)
|
||||||
)
|
.tooltip({
|
||||||
.tooltip({
|
let links = links.clone();
|
||||||
let links = links.clone();
|
let link_ranges = link_ranges.clone();
|
||||||
let link_ranges = link_ranges.clone();
|
move |idx, cx| {
|
||||||
move |idx, cx| {
|
for (ix, range) in link_ranges.iter().enumerate() {
|
||||||
for (ix, range) in link_ranges.iter().enumerate() {
|
if range.contains(&idx) {
|
||||||
if range.contains(&idx) {
|
return Some(LinkPreview::new(&links[ix].to_string(), cx));
|
||||||
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] {
|
||||||
.on_click(
|
Link::Web { url } => window_cx.open_url(url),
|
||||||
link_ranges,
|
Link::Path { path, .. } => {
|
||||||
move |clicked_range_ix, window_cx| match &links[clicked_range_ix] {
|
if let Some(workspace) = &workspace {
|
||||||
Link::Web { url } => window_cx.open_url(url),
|
_ = workspace.update(window_cx, |workspace, cx| {
|
||||||
Link::Path {
|
workspace.open_abs_path(path.clone(), false, cx).detach();
|
||||||
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();
|
||||||
}
|
return element;
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.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()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -238,11 +238,8 @@ impl NotificationStore {
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
if let Some(notification) = envelope.payload.notification {
|
if let Some(notification) = envelope.payload.notification {
|
||||||
if let Some(rpc::Notification::ChannelMessageMention {
|
if let Some(rpc::Notification::ChannelMessageMention { message_id, .. }) =
|
||||||
message_id,
|
Notification::from_proto(¬ification)
|
||||||
sender_id: _,
|
|
||||||
channel_id: _,
|
|
||||||
}) = Notification::from_proto(¬ification)
|
|
||||||
{
|
{
|
||||||
let fetch_message_task = this.channel_store.update(cx, |this, cx| {
|
let fetch_message_task = this.channel_store.update(cx, |this, cx| {
|
||||||
this.fetch_channel_messages(vec![message_id], cx)
|
this.fetch_channel_messages(vec![message_id], cx)
|
||||||
|
|
|
@ -114,7 +114,7 @@ impl Cell {
|
||||||
id,
|
id,
|
||||||
metadata,
|
metadata,
|
||||||
source,
|
source,
|
||||||
attachments: _,
|
..
|
||||||
} => {
|
} => {
|
||||||
let source = source.join("");
|
let source = source.join("");
|
||||||
|
|
||||||
|
|
|
@ -310,12 +310,7 @@ pub fn render_markdown_mut(
|
||||||
}
|
}
|
||||||
Event::Start(tag) => match tag {
|
Event::Start(tag) => match tag {
|
||||||
Tag::Paragraph => new_paragraph(text, &mut list_stack),
|
Tag::Paragraph => new_paragraph(text, &mut list_stack),
|
||||||
Tag::Heading {
|
Tag::Heading { .. } => {
|
||||||
level: _,
|
|
||||||
id: _,
|
|
||||||
classes: _,
|
|
||||||
attrs: _,
|
|
||||||
} => {
|
|
||||||
new_paragraph(text, &mut list_stack);
|
new_paragraph(text, &mut list_stack);
|
||||||
bold_depth += 1;
|
bold_depth += 1;
|
||||||
}
|
}
|
||||||
|
@ -333,12 +328,7 @@ pub fn render_markdown_mut(
|
||||||
Tag::Emphasis => italic_depth += 1,
|
Tag::Emphasis => italic_depth += 1,
|
||||||
Tag::Strong => bold_depth += 1,
|
Tag::Strong => bold_depth += 1,
|
||||||
Tag::Strikethrough => strikethrough_depth += 1,
|
Tag::Strikethrough => strikethrough_depth += 1,
|
||||||
Tag::Link {
|
Tag::Link { dest_url, .. } => link_url = Some(dest_url.to_string()),
|
||||||
link_type: _,
|
|
||||||
dest_url,
|
|
||||||
title: _,
|
|
||||||
id: _,
|
|
||||||
} => link_url = Some(dest_url.to_string()),
|
|
||||||
Tag::List(number) => {
|
Tag::List(number) => {
|
||||||
list_stack.push((number, false));
|
list_stack.push((number, false));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue