Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions crates/language/src/syntax_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 => {}
Expand Down Expand Up @@ -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(),
},
})
Expand Down
69 changes: 67 additions & 2 deletions crates/language/src/syntax_map/syntax_map_tests.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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()));
Expand Down
Loading