Fix the syntax layer panic (#62366)

While working on the project search on type PR and testing it, uncovered
this bug and split off into a separate commit

Release Notes:

- N/A
This commit is contained in:
Kirill Bulatov authored and GitHub committed 2026-08-10 16:58:49 +00:00
1 parent a884bd5438
commit 589739b816
2 files changed
+96 -9

No files matched your search

+29 -7
View File
@@ -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<Anchor>, Option<LanguageId>)> = 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(),
},
})
@@ -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#"
<div>leading markup before any directive</div>
<a href={"early-attr"}>x</a>
<%= "mid" %>
<% y = "code" %>
<b class={"late-attr"}>z</b>
<%= "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()));