diff --git a/crates/markdown_preview/src/markdown_elements.rs b/crates/markdown_preview/src/markdown_elements.rs index 865ae6fe6fcb78..748ace53f2e245 100644 --- a/crates/markdown_preview/src/markdown_elements.rs +++ b/crates/markdown_preview/src/markdown_elements.rs @@ -30,6 +30,7 @@ impl ParsedMarkdownElement { Self::Paragraph(text) => match text.get(0)? { MarkdownParagraphChunk::Text(t) => t.source_range.clone(), MarkdownParagraphChunk::Image(image) => image.source_range.clone(), + MarkdownParagraphChunk::KeyboardShortcut(shortcut) => shortcut.source_range.clone(), }, Self::HorizontalRule(range) => range.clone(), Self::Image(image) => image.source_range.clone(), @@ -48,6 +49,7 @@ pub type MarkdownParagraph = Vec; pub enum MarkdownParagraphChunk { Text(ParsedMarkdownText), Image(Image), + KeyboardShortcut(ParsedKeyboardShortcut), } #[derive(Debug)] @@ -292,9 +294,16 @@ impl Display for Link { } } -/// A Markdown Image #[derive(Debug, Clone)] #[cfg_attr(test, derive(PartialEq))] +pub struct ParsedKeyboardShortcut { + pub shortcut: SharedString, + pub source_range: Range, +} + +/// A Markdown Image +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct Image { pub link: Link, pub source_range: Range, diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index 117c06dffead6b..5a634fe1683052 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -938,6 +938,49 @@ impl<'a> MarkdownParser<'a> { })); self.consume_paragraph(source_range, node, paragraph, highlights, elements); + } else if local_name!("kbd") == name.local { + let mut child_paragraph = Vec::with_capacity(1); + self.consume_paragraph( + source_range.clone(), + node, + &mut child_paragraph, + highlights, + &mut Vec::new(), + ); + + for (index, child) in child_paragraph.iter().enumerate() { + match child { + MarkdownParagraphChunk::Text(text) => { + paragraph.push(MarkdownParagraphChunk::KeyboardShortcut( + ParsedKeyboardShortcut { + shortcut: text.contents.clone(), + source_range: source_range.clone(), + }, + )); + } + MarkdownParagraphChunk::KeyboardShortcut(shortcut) => { + paragraph.push(MarkdownParagraphChunk::KeyboardShortcut( + shortcut.clone(), + )); + + // supporting nested "kbd" elements, that should added " + " between each child + if let Some(MarkdownParagraphChunk::KeyboardShortcut(_)) = + child_paragraph.get(index + 1) + { + paragraph.push(MarkdownParagraphChunk::Text( + ParsedMarkdownText { + source_range: source_range.clone(), + contents: " + ".into(), + highlights: Vec::default(), + region_ranges: Vec::default(), + regions: Vec::default(), + }, + )); + } + } + _ => {} + } + } } else { self.consume_paragraph(source_range, node, paragraph, highlights, elements); @@ -1639,6 +1682,118 @@ mod tests { ); } + #[gpui::test] + async fn test_html_keyboard_shortcut() { + let parsed = parse( + "

Some text Ctrl + Shift + C some more text

", + ) + .await; + + assert_eq!( + ParsedMarkdown { + children: vec![ParsedMarkdownElement::Paragraph(vec![ + MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range: 0..81, + contents: "Some text ".into(), + highlights: Default::default(), + region_ranges: Default::default(), + regions: Default::default() + }), + MarkdownParagraphChunk::KeyboardShortcut(ParsedKeyboardShortcut { + source_range: 0..81, + shortcut: "Ctrl".into(), + }), + MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range: 0..81, + contents: " + ".into(), + highlights: Default::default(), + region_ranges: Default::default(), + regions: Default::default() + }), + MarkdownParagraphChunk::KeyboardShortcut(ParsedKeyboardShortcut { + source_range: 0..81, + shortcut: "Shift".into(), + }), + MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range: 0..81, + contents: " + ".into(), + highlights: Default::default(), + region_ranges: Default::default(), + regions: Default::default() + }), + MarkdownParagraphChunk::KeyboardShortcut(ParsedKeyboardShortcut { + source_range: 0..81, + shortcut: "C".into(), + }), + MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range: 0..81, + contents: " some more text".into(), + highlights: Default::default(), + region_ranges: Default::default(), + regions: Default::default() + }), + ])] + }, + parsed + ); + } + + #[gpui::test] + async fn test_html_nested_keyboard_shortcut() { + let parsed = parse( + "

Some text CtrlShiftC some more text

", + ) + .await; + + assert_eq!( + ParsedMarkdown { + children: vec![ParsedMarkdownElement::Paragraph(vec![ + MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range: 0..86, + contents: "Some text ".into(), + highlights: Default::default(), + region_ranges: Default::default(), + regions: Default::default() + }), + MarkdownParagraphChunk::KeyboardShortcut(ParsedKeyboardShortcut { + source_range: 0..86, + shortcut: "Ctrl".into(), + }), + MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range: 0..86, + contents: " + ".into(), + highlights: Default::default(), + region_ranges: Default::default(), + regions: Default::default() + }), + MarkdownParagraphChunk::KeyboardShortcut(ParsedKeyboardShortcut { + source_range: 0..86, + shortcut: "Shift".into(), + }), + MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range: 0..86, + contents: " + ".into(), + highlights: Default::default(), + region_ranges: Default::default(), + regions: Default::default() + }), + MarkdownParagraphChunk::KeyboardShortcut(ParsedKeyboardShortcut { + source_range: 0..86, + shortcut: "C".into(), + }), + MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range: 0..86, + contents: " some more text".into(), + highlights: Default::default(), + region_ranges: Default::default(), + regions: Default::default() + }), + ])] + }, + parsed + ); + } + #[gpui::test] async fn test_inline_html_image_tag() { let parsed = diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 781547b5dc9672..c7a728fa6f0cc1 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -1,6 +1,6 @@ use crate::markdown_elements::{ - HeadingLevel, Image, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown, - ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, ParsedMarkdownElement, + HeadingLevel, Image, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedKeyboardShortcut, + ParsedMarkdown, ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownTable, ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, }; @@ -451,6 +451,7 @@ fn paragraph_len(paragraphs: &MarkdownParagraph) -> usize { MarkdownParagraphChunk::Text(text) => text.contents.len(), // TODO: Scale column width based on image size MarkdownParagraphChunk::Image(_) => 1, + MarkdownParagraphChunk::KeyboardShortcut(shortcut) => shortcut.shortcut.len(), }) .sum() } @@ -624,10 +625,8 @@ fn render_markdown_code_block( } fn render_markdown_paragraph(parsed: &MarkdownParagraph, cx: &mut RenderContext) -> AnyElement { - cx.with_common_p(div()) + cx.with_common_p(h_flex().flex_wrap()) .children(render_markdown_text(parsed, cx)) - .flex() - .flex_col() .into_any_element() } @@ -721,10 +720,12 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext) .into_any(); any_element.push(element); } - MarkdownParagraphChunk::Image(image) => { any_element.push(render_markdown_image(image, cx)); } + MarkdownParagraphChunk::KeyboardShortcut(keyboard_shortcut) => { + any_element.push(render_markdown_keyboard_shortcut(keyboard_shortcut, cx)); + } } } @@ -800,6 +801,28 @@ fn render_markdown_image(image: &Image, cx: &mut RenderContext) -> AnyElement { .into_any() } +fn render_markdown_keyboard_shortcut( + keyboard_shortcut: &ParsedKeyboardShortcut, + cx: &mut RenderContext, +) -> AnyElement { + let element_id = cx.next_id(&keyboard_shortcut.source_range); + + div() + .id(element_id.clone()) + .border_1() + .rounded_md() + .bg(cx.code_block_background_color) + .px_2() + .text_xs() + .border_1() + .border_color(cx.border_color) + .child(InteractiveText::new( + element_id, + StyledText::new(keyboard_shortcut.shortcut.clone()), + )) + .into_any() +} + struct InteractiveMarkdownElementTooltip { tooltip_text: Option, action_text: SharedString,