From 8525a76ca16dc904e4d283f5a30ebff4b7538dc1 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 4 Sep 2025 16:36:49 +0200 Subject: [PATCH 1/4] Implement keyboard shortcuts #14830 Example code: ```markdown

Some text Ctrl + Shift + C some more text

Enter

Ctrl + Z

``` --- .../markdown_preview/src/markdown_elements.rs | 11 ++- .../markdown_preview/src/markdown_parser.rs | 79 +++++++++++++++++++ .../markdown_preview/src/markdown_renderer.rs | 28 ++++++- 3 files changed, 114 insertions(+), 4 deletions(-) diff --git a/crates/markdown_preview/src/markdown_elements.rs b/crates/markdown_preview/src/markdown_elements.rs index 865ae6fe6fcb78..5ed7ec23652c8a 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,8 +294,15 @@ impl Display for Link { } } +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] +pub struct ParsedKeyboardShortcut { + pub shortcut: SharedString, + pub source_range: Range, +} + /// A Markdown Image -#[derive(Debug, Clone)] +#[derive(Debug)] #[cfg_attr(test, derive(PartialEq))] pub struct Image { pub link: Link, diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index 117c06dffead6b..f0111eec6c8600 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -938,6 +938,29 @@ 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(), + ); + + let shortcut = child_paragraph.iter().find_map(|child| match child { + MarkdownParagraphChunk::Text(text) => Some(text.contents.clone()), + _ => None, + }); + + if let Some(shortcut) = shortcut { + paragraph.push(MarkdownParagraphChunk::KeyboardShortcut( + ParsedKeyboardShortcut { + shortcut, + source_range, + }, + )); + } } else { self.consume_paragraph(source_range, node, paragraph, highlights, elements); @@ -1639,6 +1662,62 @@ 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_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 1e679b42cbf8a0..8e73505f28d950 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() } @@ -719,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)); + } } } @@ -798,6 +801,25 @@ 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) + .p_1() + .child(InteractiveText::new( + element_id, + StyledText::new(keyboard_shortcut.shortcut.clone()), + )) + .into_any() +} + struct InteractiveMarkdownElementTooltip { tooltip_text: Option, action_text: SharedString, From a7932e1dae3bd11307106c26a44bbff68673bb44 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 4 Sep 2025 17:07:01 +0200 Subject: [PATCH 2/4] Add support for nested "kbd" elements --- .../markdown_preview/src/markdown_elements.rs | 2 +- .../markdown_preview/src/markdown_parser.rs | 98 ++++++++++++++++--- 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/crates/markdown_preview/src/markdown_elements.rs b/crates/markdown_preview/src/markdown_elements.rs index 5ed7ec23652c8a..748ace53f2e245 100644 --- a/crates/markdown_preview/src/markdown_elements.rs +++ b/crates/markdown_preview/src/markdown_elements.rs @@ -294,7 +294,7 @@ impl Display for Link { } } -#[derive(Debug)] +#[derive(Debug, Clone)] #[cfg_attr(test, derive(PartialEq))] pub struct ParsedKeyboardShortcut { pub shortcut: SharedString, diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index f0111eec6c8600..5a634fe1683052 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -948,18 +948,38 @@ impl<'a> MarkdownParser<'a> { &mut Vec::new(), ); - let shortcut = child_paragraph.iter().find_map(|child| match child { - MarkdownParagraphChunk::Text(text) => Some(text.contents.clone()), - _ => None, - }); + 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(), + )); - if let Some(shortcut) = shortcut { - paragraph.push(MarkdownParagraphChunk::KeyboardShortcut( - ParsedKeyboardShortcut { - shortcut, - source_range, - }, - )); + // 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); @@ -1718,6 +1738,62 @@ mod tests { ); } + #[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 = From 3163088b9a59a66c43afc06f3de15e732e172054 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 4 Sep 2025 17:15:35 +0200 Subject: [PATCH 3/4] Revert "Undo flex wrap logic" Reverting this because see: https://github.com/zed-industries/zed/pull/37264/commits/8b8186ac7aa701868ae212bff5fc2a5407300419#r2322518759 This reverts commit 8b8186ac7aa701868ae212bff5fc2a5407300419. --- crates/markdown_preview/src/markdown_renderer.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 781547b5dc9672..1e679b42cbf8a0 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -624,10 +624,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() } From e68a9fe4ee14dc4dfc346f261265ac76d5a1618c Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 4 Sep 2025 17:29:56 +0200 Subject: [PATCH 4/4] Make it look better --- crates/markdown_preview/src/markdown_renderer.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 8e73505f28d950..c7a728fa6f0cc1 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -812,7 +812,10 @@ fn render_markdown_keyboard_shortcut( .border_1() .rounded_md() .bg(cx.code_block_background_color) - .p_1() + .px_2() + .text_xs() + .border_1() + .border_color(cx.border_color) .child(InteractiveText::new( element_id, StyledText::new(keyboard_shortcut.shortcut.clone()),