From a7e23df67bfcfb5440b5db2752c3ef4544a09236 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 21 Aug 2026 16:00:50 +0000 Subject: [PATCH] markdown: Restore fallback language highlighting for untagged code blocks (#63023) Restores fallback highlights lost in https://github.com/zed-industries/zed/pull/28217 Release Notes: - N/A --- Cargo.lock | 1 - README.md | 1 - crates/markdown/Cargo.toml | 1 - crates/markdown/src/markdown.rs | 100 ++++++++++++++++++++++++++++---- crates/markdown/src/parser.rs | 4 ++ 5 files changed, 93 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a899ba16f6..172dfd5b78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10460,7 +10460,6 @@ dependencies = [ "collections", "env_logger 0.11.8", "fs", - "futures 0.3.32", "gpui", "gpui_platform", "html5ever 0.27.0", diff --git a/README.md b/README.md index 84f536569a..9f641fb384 100644 --- a/README.md +++ b/README.md @@ -46,4 +46,3 @@ Zed is developed by **Zed Industries, Inc.**, a for-profit company. If you’d like to financially support the project, you can do so via GitHub Sponsors. Sponsorships go directly to Zed Industries and are used as general company revenue. There are no perks or entitlements associated with sponsorship. - diff --git a/crates/markdown/Cargo.toml b/crates/markdown/Cargo.toml index f123920536..ebc64a2b44 100644 --- a/crates/markdown/Cargo.toml +++ b/crates/markdown/Cargo.toml @@ -22,7 +22,6 @@ test-support = [ anyhow.workspace = true base64.workspace = true collections.workspace = true -futures.workspace = true gpui.workspace = true html5ever.workspace = true language.workspace = true diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 458485733c..e048c26425 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -5,7 +5,7 @@ mod path_range; mod selection; use base64::Engine as _; -use futures::FutureExt as _; + use gpui::EdgesRefinement; use gpui::HitboxBehavior; use gpui::UnderlineStyle; @@ -958,7 +958,8 @@ impl Markdown { .languages_by_path .get(&path_range.path) .cloned(), - CodeBlockKind::Fenced | CodeBlockKind::Indented => None, + CodeBlockKind::Fenced => self.parsed_markdown.fallback_code_block_language.clone(), + CodeBlockKind::Indented => None, } }) } @@ -1226,6 +1227,7 @@ impl Markdown { heading_slugs: HashMap::default(), footnote_definitions: HashMap::default(), link_definition_spans: Arc::default(), + fallback_code_block_language: None, }, Default::default(), ); @@ -1246,6 +1248,7 @@ impl Markdown { let heading_slugs = parsed.heading_slugs; let footnote_definitions = parsed.footnote_definitions; let link_definition_spans = parsed.link_definition_spans; + let has_untagged_code_block = parsed.has_untagged_code_block; let mermaid_diagrams = if should_render_mermaid_diagrams { extract_mermaid_diagrams(&source, &events) } else { @@ -1254,16 +1257,10 @@ impl Markdown { let mut images_by_source_offset = HashMap::default(); let mut languages_by_name = TreeMap::default(); let mut languages_by_path = TreeMap::default(); + let mut fallback_code_block_language = None; if let Some(registry) = language_registry.as_ref() { for name in language_names { - let language = if !name.is_empty() { - registry.language_for_name_or_extension(&name).left_future() - } else if let Some(fallback) = &fallback { - registry.language_for_name(fallback.as_ref()).right_future() - } else { - continue; - }; - if let Ok(language) = language.await { + if let Ok(language) = registry.language_for_name_or_extension(&name).await { languages_by_name.insert(name, language); } } @@ -1276,6 +1273,11 @@ impl Markdown { languages_by_path.insert(path, language); } } + + if has_untagged_code_block && let Some(fallback) = &fallback { + fallback_code_block_language = + registry.language_for_name(fallback.as_ref()).await.ok(); + } } for (range, event) in &events { @@ -1316,6 +1318,7 @@ impl Markdown { heading_slugs, footnote_definitions, link_definition_spans: Arc::from(link_definition_spans), + fallback_code_block_language, }, images_by_source_offset, ) @@ -1463,6 +1466,7 @@ pub struct ParsedMarkdown { pub heading_slugs: HashMap, pub footnote_definitions: HashMap, pub(crate) link_definition_spans: Arc<[Range]>, + pub(crate) fallback_code_block_language: Option>, } impl ParsedMarkdown { @@ -2622,7 +2626,9 @@ impl Element for MarkdownElement { } let language = match kind { - CodeBlockKind::Fenced => None, + CodeBlockKind::Fenced => { + parsed_markdown.fallback_code_block_language.clone() + } CodeBlockKind::FencedLang(language) => { parsed_markdown.languages_by_name.get(language).cloned() } @@ -5014,6 +5020,78 @@ mod tests { }); } + #[gpui::test] + fn test_fallback_language_highlights_untagged_code_blocks(cx: &mut TestAppContext) { + let language = language::rust_lang(); + let language_registry = Arc::new(LanguageRegistry::test(cx.executor())); + language_registry.add(language.clone()); + + let source = indoc::indoc! {" + ``` + fn main() {} + ``` + "}; + let markdown = cx.new(|cx| { + Markdown::new( + source.into(), + Some(language_registry), + Some(language.name()), + cx, + ) + }); + cx.run_until_parked(); + + markdown.read_with(cx, |markdown, _| { + let fallback = markdown + .parsed_markdown() + .fallback_code_block_language + .as_ref() + .expect("the fallback language must be resolved for untagged code blocks"); + assert_eq!(fallback.name(), language.name()); + assert_eq!( + markdown + .first_code_block_language() + .map(|language| language.name()), + Some(language.name()), + "untagged code blocks must resolve to the fallback language" + ); + }); + } + + #[gpui::test] + fn test_no_fallback_language_without_untagged_code_blocks(cx: &mut TestAppContext) { + let language = language::rust_lang(); + let language_registry = Arc::new(LanguageRegistry::test(cx.executor())); + language_registry.add(language.clone()); + + let source = indoc::indoc! {" + ```rust + fn main() {} + ``` + "}; + let markdown = cx.new(|cx| { + Markdown::new( + source.into(), + Some(language_registry), + Some(language.name()), + cx, + ) + }); + cx.run_until_parked(); + + markdown.read_with(cx, |markdown, _| { + assert_eq!( + markdown + .parsed_markdown() + .fallback_code_block_language + .as_ref() + .map(|language| language.name()), + None, + "The fallback language must not be loaded when no code block needs it" + ); + }); + } + #[gpui::test] fn test_frontmatter_renders_without_delimiters(cx: &mut TestAppContext) { let rendered = render_markdown_with_options( diff --git a/crates/markdown/src/parser.rs b/crates/markdown/src/parser.rs index a711722808..0a47fb7333 100644 --- a/crates/markdown/src/parser.rs +++ b/crates/markdown/src/parser.rs @@ -43,6 +43,7 @@ pub(crate) struct ParsedMarkdownData { /// Source spans of link reference definitions (`[id]: https://example.com`), which are /// consumed by the parser and never appear in any event range. pub link_definition_spans: Vec>, + pub has_untagged_code_block: bool, } #[derive(Clone, Debug, PartialEq)] @@ -240,6 +241,7 @@ pub(crate) fn parse_markdown_with_options( let mut state = ParseState::default(); let mut language_names = HashSet::default(); let mut language_paths = HashSet::default(); + let mut has_untagged_code_block = false; let mut html_blocks = BTreeMap::default(); let mut metadata_blocks = BTreeMap::default(); let mut within_link = false; @@ -359,6 +361,7 @@ pub(crate) fn parse_markdown_with_options( let info = info.trim(); let kind = if info.is_empty() { + has_untagged_code_block = true; CodeBlockKind::Fenced // Languages should never contain a slash, and PathRanges always should. // (Models are told to specify them relative to a workspace root.) @@ -686,6 +689,7 @@ pub(crate) fn parse_markdown_with_options( heading_slugs, footnote_definitions, link_definition_spans, + has_untagged_code_block, } }