language: Respect combined injection sub-ranges for language queries (#48522)
Follow-up to https://github.com/zed-industries/zed/pull/41111. PR #41111 introduced combined injection handling, but cursor language queries still relied on layer range selection alone. For combined injections this can surface a language from the outer combined layer even when the cursor is outside that language's actual included sub-range. So, we just need to add sub-range filtering based on anchor-aware boundary checks in the same way we did in the previous PR. That means: apply it in `Buffer::language_at`, `Buffer::languages_at`, and `BufferSnapshot::language_scope_at`. All places that rely on the described behavior. I also added some additional test cases for the `HTML+ERB` lang to verify language resolution for HTML and Ruby positions. Thank you! Closes https://github.com/zed-industries/zed/issues/48358 Release Notes: - Respect combined injection sub-ranges for language queries
This commit is contained in:
1 parent
c35110461e
commit
82f3eca559
2 files changed
+148
-40
No files matched your search
@@ -1716,28 +1716,14 @@ impl Buffer {
|
||||
/// Returns the [`Language`] at the given location.
|
||||
pub fn language_at<D: ToOffset>(&self, position: D) -> Option<Arc<Language>> {
|
||||
let offset = position.to_offset(self);
|
||||
let mut is_first = true;
|
||||
let start_anchor = self.anchor_before(offset);
|
||||
let end_anchor = self.anchor_after(offset);
|
||||
let text: &TextBufferSnapshot = &self.text;
|
||||
self.syntax_map
|
||||
.lock()
|
||||
.layers_for_range(offset..offset, &self.text, false)
|
||||
.layers_for_range(offset..offset, text, false)
|
||||
.filter(|layer| {
|
||||
if is_first {
|
||||
is_first = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
layer
|
||||
.included_sub_ranges
|
||||
.map(|sub_ranges| {
|
||||
sub_ranges.iter().any(|sub_range| {
|
||||
let is_before_start = sub_range.end.cmp(&start_anchor, self).is_lt();
|
||||
let is_after_end = sub_range.start.cmp(&end_anchor, self).is_gt();
|
||||
!is_before_start && !is_after_end
|
||||
})
|
||||
})
|
||||
.unwrap_or(true)
|
||||
.is_none_or(|ranges| offset_in_sub_ranges(ranges, offset, text))
|
||||
})
|
||||
.last()
|
||||
.map(|info| info.language.clone())
|
||||
@@ -1747,10 +1733,17 @@ impl Buffer {
|
||||
/// Returns each [`Language`] for the active syntax layers at the given location.
|
||||
pub fn languages_at<D: ToOffset>(&self, position: D) -> Vec<Arc<Language>> {
|
||||
let offset = position.to_offset(self);
|
||||
let text: &TextBufferSnapshot = &self.text;
|
||||
let mut languages: Vec<Arc<Language>> = self
|
||||
.syntax_map
|
||||
.lock()
|
||||
.layers_for_range(offset..offset, &self.text, false)
|
||||
.layers_for_range(offset..offset, text, false)
|
||||
.filter(|layer| {
|
||||
// For combined injections, check if offset is within the actual sub-ranges.
|
||||
layer
|
||||
.included_sub_ranges
|
||||
.is_none_or(|ranges| offset_in_sub_ranges(ranges, offset, text))
|
||||
})
|
||||
.map(|info| info.language.clone())
|
||||
.collect();
|
||||
|
||||
@@ -3340,6 +3333,21 @@ impl Buffer {
|
||||
|
||||
impl EventEmitter<BufferEvent> for Buffer {}
|
||||
|
||||
fn offset_in_sub_ranges(
|
||||
sub_ranges: &[Range<Anchor>],
|
||||
offset: usize,
|
||||
snapshot: &TextBufferSnapshot,
|
||||
) -> bool {
|
||||
let start_anchor = snapshot.anchor_before(offset);
|
||||
let end_anchor = snapshot.anchor_after(offset);
|
||||
|
||||
sub_ranges.iter().any(|sub_range| {
|
||||
let is_before_start = sub_range.end.cmp(&start_anchor, snapshot).is_lt();
|
||||
let is_after_end = sub_range.start.cmp(&end_anchor, snapshot).is_gt();
|
||||
!is_before_start && !is_after_end
|
||||
})
|
||||
}
|
||||
|
||||
impl Deref for Buffer {
|
||||
type Target = TextBuffer;
|
||||
|
||||
@@ -3854,12 +3862,19 @@ impl BufferSnapshot {
|
||||
let offset = position.to_offset(self);
|
||||
let mut scope = None;
|
||||
let mut smallest_range_and_depth: Option<(Range<usize>, usize)> = None;
|
||||
let text: &TextBufferSnapshot = self;
|
||||
|
||||
// Use the layer that has the smallest node intersecting the given point.
|
||||
for layer in self
|
||||
.syntax
|
||||
.layers_for_range(offset..offset, &self.text, false)
|
||||
{
|
||||
if let Some(ranges) = layer.included_sub_ranges
|
||||
&& !offset_in_sub_ranges(ranges, offset, text)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut cursor = layer.node().walk();
|
||||
|
||||
let mut range = None;
|
||||
|
||||
@@ -2771,14 +2771,11 @@ fn test_language_scope_at_with_combined_injections(cx: &mut App) {
|
||||
|
||||
let mut buffer = Buffer::local(text, cx);
|
||||
buffer.set_language_registry(language_registry.clone());
|
||||
buffer.set_language(
|
||||
language_registry
|
||||
.language_for_name("HTML+ERB")
|
||||
.now_or_never()
|
||||
.unwrap()
|
||||
.ok(),
|
||||
cx,
|
||||
);
|
||||
let language = language_registry
|
||||
.language_for_name("HTML+ERB")
|
||||
.now_or_never()
|
||||
.and_then(Result::ok);
|
||||
buffer.set_language(language, cx);
|
||||
|
||||
let snapshot = buffer.snapshot();
|
||||
let html_config = snapshot.language_scope_at(Point::new(2, 4)).unwrap();
|
||||
@@ -2894,15 +2891,80 @@ fn test_language_at_for_markdown_code_block(cx: &mut App) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_syntax_layer_at_for_injected_languages(cx: &mut App) {
|
||||
fn test_syntax_layer_at_for_combined_injections(cx: &mut App) {
|
||||
init_settings(cx, |_| {});
|
||||
|
||||
cx.new(|cx| {
|
||||
// ERB template with HTML and Ruby content
|
||||
let text = r#"
|
||||
```html+erb
|
||||
<div>Hello</div>
|
||||
<%= link_to "Some", "https://zed.dev" %>
|
||||
```
|
||||
<div>Hello</div>
|
||||
<%= link_to "Click", url %>
|
||||
<p>World</p>
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
language_registry.add(Arc::new(erb_lang()));
|
||||
language_registry.add(Arc::new(html_lang()));
|
||||
language_registry.add(Arc::new(ruby_lang()));
|
||||
|
||||
let mut buffer = Buffer::local(text, cx);
|
||||
buffer.set_language_registry(language_registry.clone());
|
||||
let language = language_registry
|
||||
.language_for_name("HTML+ERB")
|
||||
.now_or_never()
|
||||
.and_then(Result::ok);
|
||||
buffer.set_language(language, cx);
|
||||
|
||||
let snapshot = buffer.snapshot();
|
||||
|
||||
// Test language_at for HTML content (line 0: "<div>Hello</div>")
|
||||
let html_point = Point::new(0, 4);
|
||||
let language = snapshot.language_at(html_point).unwrap();
|
||||
assert_eq!(
|
||||
language.name().as_ref(),
|
||||
"HTML",
|
||||
"Expected HTML at {:?}, got {}",
|
||||
html_point,
|
||||
language.name()
|
||||
);
|
||||
|
||||
// Test language_at for Ruby code (line 1: "<%= link_to ... %>")
|
||||
let ruby_point = Point::new(1, 6);
|
||||
let language = snapshot.language_at(ruby_point).unwrap();
|
||||
assert_eq!(
|
||||
language.name().as_ref(),
|
||||
"Ruby",
|
||||
"Expected Ruby at {:?}, got {}",
|
||||
ruby_point,
|
||||
language.name()
|
||||
);
|
||||
|
||||
// Test language_at for HTML after Ruby (line 2: "<p>World</p>")
|
||||
let html_after_ruby = Point::new(2, 2);
|
||||
let language = snapshot.language_at(html_after_ruby).unwrap();
|
||||
assert_eq!(
|
||||
language.name().as_ref(),
|
||||
"HTML",
|
||||
"Expected HTML at {:?}, got {}",
|
||||
html_after_ruby,
|
||||
language.name()
|
||||
);
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_languages_at_for_combined_injections(cx: &mut App) {
|
||||
init_settings(cx, |_| {});
|
||||
|
||||
cx.new(|cx| {
|
||||
// ERB template with HTML and Ruby content
|
||||
let text = r#"
|
||||
<div>Hello</div>
|
||||
<%= yield %>
|
||||
<p>World</p>
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
@@ -2922,16 +2984,47 @@ fn test_syntax_layer_at_for_injected_languages(cx: &mut App) {
|
||||
cx,
|
||||
);
|
||||
|
||||
let snapshot = buffer.snapshot();
|
||||
// Test languages_at for HTML content - should NOT include Ruby
|
||||
let html_point = Point::new(0, 4);
|
||||
let languages = buffer.languages_at(html_point);
|
||||
let language_names: Vec<_> = languages.iter().map(|language| language.name()).collect();
|
||||
assert!(
|
||||
language_names
|
||||
.iter()
|
||||
.any(|language_name| language_name.as_ref() == "HTML"),
|
||||
"Expected HTML in languages at {:?}, got {:?}",
|
||||
html_point,
|
||||
language_names
|
||||
);
|
||||
assert!(
|
||||
!language_names
|
||||
.iter()
|
||||
.any(|language_name| language_name.as_ref() == "Ruby"),
|
||||
"Did not expect Ruby in languages at {:?}, got {:?}",
|
||||
html_point,
|
||||
language_names
|
||||
);
|
||||
|
||||
// Test points in the code line
|
||||
let html_point = Point::new(1, 4);
|
||||
let language = snapshot.language_at(html_point).unwrap();
|
||||
assert_eq!(language.name().as_ref(), "HTML");
|
||||
|
||||
let ruby_point = Point::new(2, 6);
|
||||
let language = snapshot.language_at(ruby_point).unwrap();
|
||||
assert_eq!(language.name().as_ref(), "Ruby");
|
||||
// Test languages_at for Ruby code - should NOT include HTML
|
||||
let ruby_point = Point::new(1, 6);
|
||||
let languages = buffer.languages_at(ruby_point);
|
||||
let language_names: Vec<_> = languages.iter().map(|language| language.name()).collect();
|
||||
assert!(
|
||||
language_names
|
||||
.iter()
|
||||
.any(|language_name| language_name.as_ref() == "Ruby"),
|
||||
"Expected Ruby in languages at {:?}, got {:?}",
|
||||
ruby_point,
|
||||
language_names
|
||||
);
|
||||
assert!(
|
||||
!language_names
|
||||
.iter()
|
||||
.any(|language_name| language_name.as_ref() == "HTML"),
|
||||
"Did not expect HTML in languages at {:?}, got {:?}",
|
||||
ruby_point,
|
||||
language_names
|
||||
);
|
||||
|
||||
buffer
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user