From 36d9fff9c874eb77ab7639bab652b36518ac0426 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 1 Jul 2026 01:04:21 +0300 Subject: [PATCH] Fix the syntax layer panic --- crates/language/src/syntax_map.rs | 36 ++++++++-- .../src/syntax_map/syntax_map_tests.rs | 69 ++++++++++++++++++- 2 files changed, 96 insertions(+), 9 deletions(-) diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index fbd81e3ab3367f..d22a12879e1cbb 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -152,6 +152,16 @@ impl SyntaxLayerContent { SyntaxLayerContent::Pending { .. } => None, } } + + #[cfg(debug_assertions)] + fn language_name(&self) -> SharedString { + match self { + SyntaxLayerContent::Parsed { language, .. } => language.name().0, + SyntaxLayerContent::Pending { language_name } => { + SharedString::from(language_name.clone()) + } + } + } } /// A layer of syntax highlighting, corresponding to a single syntax @@ -901,22 +911,34 @@ impl SyntaxSnapshot { #[cfg(debug_assertions)] fn check_invariants(&self, text: &BufferSnapshot) { + let out_of_order = |reason: &str| -> ! { + let mut dump = format!("layers out of order: {reason}\nlayers:\n"); + for layer in self.layers.iter() { + dump.push_str(&format!( + " depth={} range={:?} language={} id={:?}\n", + layer.depth, + layer.range.to_offset(text), + layer.content.language_name(), + layer.content.language_id(), + )); + } + panic!("{dump}"); + }; + let mut max_depth = 0; let mut prev_layer: Option<(Range, Option)> = None; for layer in self.layers.iter() { match Ord::cmp(&layer.depth, &max_depth) { - Ordering::Less => { - panic!("layers out of order") - } + Ordering::Less => out_of_order("depth decreased"), Ordering::Equal => { if let Some((prev_range, prev_language_id)) = prev_layer { match layer.range.start.cmp(&prev_range.start, text) { - Ordering::Less => panic!("layers out of order"), + Ordering::Less => out_of_order("start decreased"), Ordering::Equal => match layer.range.end.cmp(&prev_range.end, text) { - Ordering::Less => panic!("layers out of order"), + Ordering::Less => out_of_order("end decreased at equal start"), Ordering::Equal => { if layer.content.language_id() < prev_language_id { - panic!("layers out of order") + out_of_order("language id decreased at equal range") } } Ordering::Greater => {} @@ -1677,7 +1699,7 @@ fn get_injections( range: outer_range.clone(), included_ranges, mode: ParseMode::Combined { - parent_layer_range: node.start_byte()..node.end_byte(), + parent_layer_range: outer_range.to_offset(text), parent_layer_changed_ranges: changed_ranges.to_vec(), }, }) diff --git a/crates/language/src/syntax_map/syntax_map_tests.rs b/crates/language/src/syntax_map/syntax_map_tests.rs index 11b286764ce2a1..f54182902e47d9 100644 --- a/crates/language/src/syntax_map/syntax_map_tests.rs +++ b/crates/language/src/syntax_map/syntax_map_tests.rs @@ -1,7 +1,7 @@ use super::*; use crate::{ - LanguageConfig, LanguageMatcher, LanguageQueries, buffer_tests::markdown_inline_lang, - markdown_lang, rust_lang, + LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, + buffer_tests::markdown_inline_lang, markdown_lang, rust_lang, }; use gpui::App; use pretty_assertions::assert_eq; @@ -929,6 +929,71 @@ fn test_empty_combined_injections_inside_injections(cx: &mut App) { ); } +#[gpui::test] +fn test_combined_injection_with_leading_content_layer_ordering(cx: &mut App) { + // Regression test for "layers out of order". + // + // A combined injection stores its layer `range` as the parent's full + // `outer_range`, but the parse queue orders steps by `ParseStep::range()`, + // which for a combined injection is the parsed node span. When the parent + // layer has content before its first injected range (here: leading HEEx + // markup before the first `<% %>` directive), those two ranges start at + // different offsets. A nested combined injection then inherits the wide + // `outer_range` (starting at 0) but is ordered by the narrow node span, + // landing after a sibling injection and breaking the sorted-by-start + // invariant. + let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); + let heex = Arc::new(heex_lang()); + let elixir = Arc::new( + Language::new( + LanguageConfig { + name: LanguageName::new_static("Elixir"), + matcher: Arc::new(LanguageMatcher { + path_suffixes: vec![String::from("ex")], + ..Default::default() + }), + ..LanguageConfig::default() + }, + Some(tree_sitter::Language::new(tree_sitter_elixir::LANGUAGE)), + ) + .with_injection_query( + r#" + ((string (quoted_content) @injection.content) + (#set! injection.language "html") + (#set! injection.combined)) + ((string (quoted_content) @injection.content) + (#set! injection.language "Markdown") + (#set! injection.combined)) + "#, + ) + .unwrap(), + ); + registry.add(heex.clone()); + registry.add(elixir); + registry.add(Arc::new(html_lang())); + registry.add(markdown_lang()); + + let buffer = Buffer::new( + ReplicaId::LOCAL, + BufferId::new(1).unwrap(), + r#" +
leading markup before any directive
+x +<%= "mid" %> +<% y = "code" %> +z +<%= "tail" %> +"# + .unindent(), + ); + + let mut syntax_map = SyntaxMap::new(&buffer); + syntax_map.set_language_registry(registry); + // In debug builds, `reparse` runs `check_invariants`, which panics with + // "layers out of order" if the produced layers are not correctly sorted. + syntax_map.reparse(heex, &buffer); +} + #[gpui::test] fn test_comment_triggered_injection_toggle(cx: &mut App) { let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));