markdown: Track code block metadata in parser (#28543)

This allows us to not scan the codeblock content for newlines on every
frame in `active_thread`

Release Notes:

- N/A
This commit is contained in:
Bennet Bo Fenner 2025-04-10 15:49:08 -06:00 committed by GitHub
parent 73305ce45e
commit 44cb8e582b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 247 additions and 203 deletions

View file

@ -18,6 +18,7 @@ use gpui::{
TextStyleRefinement, actions, point, quad,
};
use language::{Language, LanguageRegistry, Rope};
use parser::CodeBlockMetadata;
use parser::{MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown};
use pulldown_cmark::Alignment;
use sum_tree::TreeMap;
@ -99,10 +100,19 @@ pub enum CodeBlockRenderer {
},
}
pub type CodeBlockRenderFn =
Arc<dyn Fn(&CodeBlockKind, &ParsedMarkdown, Range<usize>, &mut Window, &App) -> Div>;
pub type CodeBlockRenderFn = Arc<
dyn Fn(
&CodeBlockKind,
&ParsedMarkdown,
Range<usize>,
CodeBlockMetadata,
&mut Window,
&App,
) -> Div,
>;
pub type CodeBlockTransformFn = Arc<dyn Fn(AnyDiv, Range<usize>, &mut Window, &App) -> AnyDiv>;
pub type CodeBlockTransformFn =
Arc<dyn Fn(AnyDiv, Range<usize>, CodeBlockMetadata, &mut Window, &App) -> AnyDiv>;
actions!(markdown, [Copy, CopyAsMarkdown]);
@ -603,6 +613,8 @@ impl Element for MarkdownElement {
0
};
let mut current_code_block_metadata = None;
for (range, event) in parsed_markdown.events.iter() {
match event {
MarkdownEvent::Start(tag) => {
@ -641,7 +653,7 @@ impl Element for MarkdownElement {
markdown_end,
);
}
MarkdownTag::CodeBlock(kind) => {
MarkdownTag::CodeBlock { kind, metadata } => {
let language = match kind {
CodeBlockKind::Fenced => None,
CodeBlockKind::FencedLang(language) => {
@ -654,6 +666,8 @@ impl Element for MarkdownElement {
_ => None,
};
current_code_block_metadata = Some(metadata.clone());
let is_indented = matches!(kind, CodeBlockKind::Indented);
match (&self.code_block_renderer, is_indented) {
@ -686,8 +700,14 @@ impl Element for MarkdownElement {
builder.push_div(code_block, range, markdown_end);
}
(CodeBlockRenderer::Custom { render, .. }, _) => {
let parent_container =
render(kind, &parsed_markdown, range.clone(), window, cx);
let parent_container = render(
kind,
&parsed_markdown,
range.clone(),
metadata.clone(),
window,
cx,
);
builder.push_div(parent_container, range, markdown_end);
@ -852,12 +872,22 @@ impl Element for MarkdownElement {
builder.pop_text_style();
}
let metadata = current_code_block_metadata.take();
if let CodeBlockRenderer::Custom {
transform: Some(modify),
transform: Some(transform),
..
} = &self.code_block_renderer
{
builder.modify_current_div(|el| modify(el, range.clone(), window, cx));
builder.modify_current_div(|el| {
transform(
el,
range.clone(),
metadata.clone().unwrap_or_default(),
window,
cx,
)
});
}
if matches!(
@ -866,9 +896,13 @@ impl Element for MarkdownElement {
) {
builder.flush_text();
builder.modify_current_div(|el| {
let code =
without_fences(parsed_markdown.source()[range.clone()].trim())
.to_string();
let content_range = parser::extract_code_block_content_range(
parsed_markdown.source()[range.clone()].trim(),
);
let content_range = content_range.start + range.start
..content_range.end + range.start;
let code = parsed_markdown.source()[content_range].to_string();
let codeblock = render_copy_code_block_button(
range.end,
code,
@ -1507,43 +1541,3 @@ impl RenderedText {
.find(|link| link.source_range.contains(&source_index))
}
}
/// Some markdown blocks are indented, and others have e.g. ```rust … ``` around them.
/// If this block is fenced with backticks, strip them off (and the language name).
/// We use this when copying code blocks to the clipboard.
pub fn without_fences(mut markdown: &str) -> &str {
if let Some(opening_backticks) = markdown.find("```") {
markdown = &markdown[opening_backticks..];
// Trim off the next newline. This also trims off a language name if it's there.
if let Some(newline) = markdown.find('\n') {
markdown = &markdown[newline + 1..];
}
};
if let Some(closing_backticks) = markdown.rfind("```") {
markdown = &markdown[..closing_backticks];
};
markdown
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_without_fences() {
let input = "```rust\nlet x = 5;\n```";
assert_eq!(without_fences(input), "let x = 5;\n");
let input = " ```\nno language\n``` ";
assert_eq!(without_fences(input), "no language\n");
let input = "plain text";
assert_eq!(without_fences(input), "plain text");
let input = "```python\nprint('hello')\nprint('world')\n```";
assert_eq!(without_fences(input), "print('hello')\nprint('world')\n");
}
}