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:
1 parent
a884bd5438
commit
589739b816
2 files changed
+96
-9
No files matched your search
@@ -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()));
|
||||
|
||||
Reference in new issue
Block a user