Skip to content
Open
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
20 changes: 20 additions & 0 deletions crates/language/src/syntax_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ enum ParseMode {
struct ChangedRegion {
depth: usize,
range: Range<Anchor>,
/// The range of the layer whose injections are being invalidated. Only layers nested
/// inside of it can be invalidated, because only that layer will recreate them.
owner_range: Range<Anchor>,
}

#[derive(Default)]
Expand Down Expand Up @@ -625,6 +628,7 @@ impl SyntaxSnapshot {
ChangedRegion {
depth: layer.depth + 1,
range: layer.range.clone(),
owner_range: layer.range.clone(),
},
text,
);
Expand Down Expand Up @@ -835,6 +839,7 @@ impl SyntaxSnapshot {
depth: step.depth + 1,
range: text.anchor_before(range.start)
..text.anchor_after(range.end),
owner_range: step.range.clone(),
},
text,
);
Expand Down Expand Up @@ -1950,6 +1955,10 @@ impl ChangedRegion {
Ord::cmp(&self.depth, &other.depth)
.then_with(|| range_a.start.cmp(&range_b.start, buffer))
.then_with(|| range_b.end.cmp(&range_a.end, buffer))
// Regions that differ only in their owner must both be kept, as they
// invalidate different layers.
.then_with(|| self.owner_range.start.cmp(&other.owner_range.start, buffer))
.then_with(|| other.owner_range.end.cmp(&self.owner_range.end, buffer))
}
}

Expand Down Expand Up @@ -1981,6 +1990,17 @@ impl ChangeRegionSet {
if region.range.start.cmp(&layer.range.end, text).is_ge() {
break;
}
// Only the layer that owns an injection will recreate it, so a layer that
// merely abuts or overlaps the owner must be left alone.
if region
.owner_range
.start
.cmp(&layer.range.start, text)
.is_gt()
|| region.owner_range.end.cmp(&layer.range.end, text).is_lt()
{
continue;
}
return true;
}
false
Expand Down
137 changes: 137 additions & 0 deletions crates/language/src/syntax_map/syntax_map_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,93 @@ fn test_comment_triggered_injection_toggle(cx: &mut App) {
);
}

#[gpui::test]
fn test_injections_are_preserved_when_a_sibling_layer_is_reparsed(cx: &mut App) {
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let markdown = markdown_lang();
registry.add(markdown.clone());
registry.add(Arc::new(markdown_inline_lang_with_html_injections()));
registry.add(Arc::new(html_lang_with_injections()));

// Each list item is a separate inline layer, so the inline HTML of each item lives
// in its own injection layer.
let mut buffer = Buffer::new(
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
"- one <!--first-->\n- two <!--second-->\n- three <b>3</b>".to_string(),
);

let mut syntax_map = SyntaxMap::new(&buffer);
syntax_map.set_language_registry(registry);
syntax_map.reparse(markdown.clone(), &buffer);

assert_capture_ranges(
&syntax_map,
&buffer,
&["comment", "tag"],
"- one «<!--first-->»\n- two «<!--second-->»\n- three <«b»>3</«b»>",
);

// Editing one list item must not invalidate the injections of the adjacent items,
// whose own layers are not reparsed and would therefore never be restored.
let second_item_end = buffer.as_rope().to_string().rfind('\n').unwrap();
buffer.edit([(second_item_end..second_item_end, " ")]);
syntax_map.interpolate(&buffer);
syntax_map.reparse(markdown, &buffer);

assert_capture_ranges(
&syntax_map,
&buffer,
&["comment", "tag"],
"- one «<!--first-->»\n- two «<!--second-->» \n- three <«b»>3</«b»>",
);
}

#[gpui::test]
fn test_injections_are_preserved_when_an_abutting_layer_is_reparsed(cx: &mut App) {
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let markdown = markdown_lang();
registry.add(markdown.clone());
registry.add(Arc::new(markdown_inline_lang_with_html_injections()));
registry.add(Arc::new(html_lang_with_injections()));

// A line starting with a comment is an HTML block, so the first two lines become HTML
// layers of their own, while the last two lines are one paragraph whose inline layer
// has HTML injected into it. The second HTML block ends exactly where that paragraph
// begins.
let text = "<!--first--> a\n<!--second--> b\n<b>c</b> d\n<i>e</i> f";
let mut buffer = Buffer::new(
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
text.to_string(),
);

let mut syntax_map = SyntaxMap::new(&buffer);
syntax_map.set_language_registry(registry);
syntax_map.reparse(markdown.clone(), &buffer);

assert_capture_ranges(
&syntax_map,
&buffer,
&["comment", "tag"],
"«<!--first-->» a\n«<!--second-->» b\n<«b»>c</«b»> d\n<«i»>e</«i»> f",
);

// Reparsing the second HTML block must not invalidate the HTML injected into the
// paragraph that starts at its end offset.
let second_block_end = text.find("\n<b>").unwrap();
buffer.edit([(second_block_end..second_block_end, " ")]);
syntax_map.interpolate(&buffer);
syntax_map.reparse(markdown, &buffer);

assert_capture_ranges(
&syntax_map,
&buffer,
&["comment", "tag"],
"«<!--first-->» a\n«<!--second-->» b \n<«b»>c</«b»> d\n<«i»>e</«i»> f",
);
}

#[gpui::test]
fn test_syntax_map_languages_loading_with_erb(cx: &mut App) {
let text = r#"
Expand Down Expand Up @@ -1416,6 +1503,56 @@ fn html_lang() -> Language {
.unwrap()
}

/// Like [`markdown_inline_lang`], but injecting HTML into inline HTML tags, the way
/// the real Markdown-Inline queries do.
fn markdown_inline_lang_with_html_injections() -> Language {
Language::new(
LanguageConfig {
name: "Markdown-Inline".into(),
hidden: true,
..LanguageConfig::default()
},
Some(tree_sitter_md::INLINE_LANGUAGE.into()),
)
.with_highlights_query("(emphasis) @emphasis")
.unwrap()
.with_injection_query(
r#"
((html_tag) @injection.content
(#set! injection.language "html")
(#set! injection.combined))
"#,
)
.unwrap()
}

/// Like [`html_lang`], but with an injection query, so that reparsing an HTML layer
/// invalidates the layers injected into it.
fn html_lang_with_injections() -> Language {
Language::new(
LanguageConfig {
name: "HTML".into(),
matcher: (LanguageMatcher {
path_suffixes: vec!["html".to_string()],
..Default::default()
})
.into(),
..Default::default()
},
Some(tree_sitter_html::LANGUAGE.into()),
)
.with_highlights_query("(comment) @comment (tag_name) @tag")
.unwrap()
.with_injection_query(
r#"
(script_element
(raw_text) @injection.content
(#set! injection.language "javascript"))
"#,
)
.unwrap()
}

fn ruby_lang() -> Language {
Language::new(
LanguageConfig {
Expand Down
Loading