diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 5158e3ece98cbacf102c3e6362772c8579faed0b..2721c1fc552ad8293dbe72c34b42159788948164 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1716,28 +1716,14 @@ impl Buffer { /// Returns the [`Language`] at the given location. pub fn language_at(&self, position: D) -> Option> { 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(&self, position: D) -> Vec> { let offset = position.to_offset(self); + let text: &TextBufferSnapshot = &self.text; let mut languages: Vec> = 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 for Buffer {} +fn offset_in_sub_ranges( + sub_ranges: &[Range], + 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)> = 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; diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 80af08a53512d1d5624b20d0dad2c231f1b70a7f..39af3142a461cb034f66c92526feff07a3730180 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -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 -
Hello
- <%= link_to "Some", "https://zed.dev" %> - ``` +
Hello
+<%= link_to "Click", url %> +

World

+ "# + .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: "
Hello
") + 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: "

World

") + 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#" +
Hello
+<%= yield %> +

World

"# .unindent(); @@ -2922,16 +2984,47 @@ fn test_syntax_layer_at_for_injected_languages(cx: &mut App) { cx, ); - let snapshot = buffer.snapshot(); - - // 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"); + // 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 + ); - 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 });