Find the layer with the smallest enclosing node in language_scope_at

Max Brunsfeld created

Change summary

crates/language/src/buffer.rs             |  34 +++--
crates/language/src/buffer_tests.rs       | 154 ++++++++++++++++++------
crates/language/src/syntax_map.rs         |  27 ++-
crates/project/src/project.rs             |   2 
crates/project/src/worktree.rs            |   1 
crates/zed/src/languages/heex/config.toml |   2 
6 files changed, 151 insertions(+), 69 deletions(-)

Detailed changes

crates/language/src/buffer.rs 🔗

@@ -2145,23 +2145,27 @@ impl BufferSnapshot {
 
     pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> {
         let offset = position.to_offset(self);
+        let mut range = 0..self.len();
+        let mut scope = self.language.clone().map(|language| LanguageScope {
+            language,
+            override_id: None,
+        });
 
-        if let Some(layer_info) = self
-            .syntax
-            .layers_for_range(offset..offset, &self.text)
-            .filter(|l| l.node().end_byte() > offset)
-            .last()
-        {
-            Some(LanguageScope {
-                language: layer_info.language.clone(),
-                override_id: layer_info.override_id(offset, &self.text),
-            })
-        } else {
-            self.language.clone().map(|language| LanguageScope {
-                language,
-                override_id: None,
-            })
+        // Use the layer that has the smallest node intersecting the given point.
+        for layer in self.syntax.layers_for_range(offset..offset, &self.text) {
+            let mut cursor = layer.node().walk();
+            while cursor.goto_first_child_for_byte(offset).is_some() {}
+            let node_range = cursor.node().byte_range();
+            if node_range.to_inclusive().contains(&offset) && node_range.len() < range.len() {
+                range = node_range;
+                scope = Some(LanguageScope {
+                    language: layer.language.clone(),
+                    override_id: layer.override_id(offset, &self.text),
+                });
+            }
         }
+
+        scope
     }
 
     pub fn surrounding_word<T: ToOffset>(&self, start: T) -> (Range<usize>, Option<CharKind>) {

crates/language/src/buffer_tests.rs 🔗

@@ -1533,47 +1533,9 @@ fn test_autoindent_with_injected_languages(cx: &mut AppContext) {
         ])
     });
 
-    let html_language = Arc::new(
-        Language::new(
-            LanguageConfig {
-                name: "HTML".into(),
-                ..Default::default()
-            },
-            Some(tree_sitter_html::language()),
-        )
-        .with_indents_query(
-            "
-            (element
-              (start_tag) @start
-              (end_tag)? @end) @indent
-            ",
-        )
-        .unwrap()
-        .with_injection_query(
-            r#"
-            (script_element
-                (raw_text) @content
-                (#set! "language" "javascript"))
-            "#,
-        )
-        .unwrap(),
-    );
+    let html_language = Arc::new(html_lang());
 
-    let javascript_language = Arc::new(
-        Language::new(
-            LanguageConfig {
-                name: "JavaScript".into(),
-                ..Default::default()
-            },
-            Some(tree_sitter_javascript::language()),
-        )
-        .with_indents_query(
-            r#"
-            (object "}" @end) @indent
-            "#,
-        )
-        .unwrap(),
-    );
+    let javascript_language = Arc::new(javascript_lang());
 
     let language_registry = Arc::new(LanguageRegistry::test());
     language_registry.add(html_language.clone());
@@ -1669,7 +1631,7 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) {
 }
 
 #[gpui::test]
-fn test_language_config_at(cx: &mut AppContext) {
+fn test_language_scope_at(cx: &mut AppContext) {
     init_settings(cx, |_| {});
 
     cx.add_model(|cx| {
@@ -1756,6 +1718,54 @@ fn test_language_config_at(cx: &mut AppContext) {
     });
 }
 
+#[gpui::test]
+fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) {
+    init_settings(cx, |_| {});
+
+    cx.add_model(|cx| {
+        let text = r#"
+            <ol>
+            <% people.each do |person| %>
+                <li>
+                    <%= person.name %>
+                </li>
+            <% end %>
+            </ol>
+        "#
+        .unindent();
+
+        let language_registry = Arc::new(LanguageRegistry::test());
+        language_registry.add(Arc::new(ruby_lang()));
+        language_registry.add(Arc::new(html_lang()));
+        language_registry.add(Arc::new(erb_lang()));
+
+        let mut buffer = Buffer::new(0, text, cx);
+        buffer.set_language_registry(language_registry.clone());
+        buffer.set_language(
+            language_registry
+                .language_for_name("ERB")
+                .now_or_never()
+                .unwrap()
+                .ok(),
+            cx,
+        );
+
+        let snapshot = buffer.snapshot();
+        let html_config = snapshot.language_scope_at(Point::new(2, 4)).unwrap();
+        assert_eq!(html_config.line_comment_prefix(), None);
+        assert_eq!(
+            html_config.block_comment_delimiters(),
+            Some((&"<!--".into(), &"-->".into()))
+        );
+
+        let ruby_config = snapshot.language_scope_at(Point::new(3, 12)).unwrap();
+        assert_eq!(ruby_config.line_comment_prefix().unwrap().as_ref(), "# ");
+        assert_eq!(ruby_config.block_comment_delimiters(), None);
+
+        buffer
+    });
+}
+
 #[gpui::test]
 fn test_serialization(cx: &mut gpui::AppContext) {
     let mut now = Instant::now();
@@ -2143,6 +2153,7 @@ fn ruby_lang() -> Language {
         LanguageConfig {
             name: "Ruby".into(),
             path_suffixes: vec!["rb".to_string()],
+            line_comment: Some("# ".into()),
             ..Default::default()
         },
         Some(tree_sitter_ruby::language()),
@@ -2158,6 +2169,61 @@ fn ruby_lang() -> Language {
     .unwrap()
 }
 
+fn html_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "HTML".into(),
+            block_comment: Some(("<!--".into(), "-->".into())),
+            ..Default::default()
+        },
+        Some(tree_sitter_html::language()),
+    )
+    .with_indents_query(
+        "
+        (element
+          (start_tag) @start
+          (end_tag)? @end) @indent
+        ",
+    )
+    .unwrap()
+    .with_injection_query(
+        r#"
+        (script_element
+            (raw_text) @content
+            (#set! "language" "javascript"))
+        "#,
+    )
+    .unwrap()
+}
+
+fn erb_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "ERB".into(),
+            path_suffixes: vec!["erb".to_string()],
+            block_comment: Some(("<%#".into(), "%>".into())),
+            ..Default::default()
+        },
+        Some(tree_sitter_embedded_template::language()),
+    )
+    .with_injection_query(
+        r#"
+            (
+                (code) @content
+                (#set! "language" "ruby")
+                (#set! "combined")
+            )
+
+            (
+                (content) @content
+                (#set! "language" "html")
+                (#set! "combined")
+            )
+        "#,
+    )
+    .unwrap()
+}
+
 fn rust_lang() -> Language {
     Language::new(
         LanguageConfig {
@@ -2236,6 +2302,12 @@ fn javascript_lang() -> Language {
         "#,
     )
     .unwrap()
+    .with_indents_query(
+        r#"
+        (object "}" @end) @indent
+        "#,
+    )
+    .unwrap()
 }
 
 fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> String {

crates/language/src/syntax_map.rs 🔗

@@ -771,8 +771,10 @@ impl SyntaxSnapshot {
         range: Range<T>,
         buffer: &'a BufferSnapshot,
     ) -> impl 'a + Iterator<Item = SyntaxLayerInfo> {
-        let start = buffer.anchor_before(range.start.to_offset(buffer));
-        let end = buffer.anchor_after(range.end.to_offset(buffer));
+        let start_offset = range.start.to_offset(buffer);
+        let end_offset = range.end.to_offset(buffer);
+        let start = buffer.anchor_before(start_offset);
+        let end = buffer.anchor_after(end_offset);
 
         let mut cursor = self.layers.filter::<_, ()>(move |summary| {
             if summary.max_depth > summary.min_depth {
@@ -787,20 +789,21 @@ impl SyntaxSnapshot {
         cursor.next(buffer);
         iter::from_fn(move || {
             while let Some(layer) = cursor.item() {
+                let mut info = None;
                 if let SyntaxLayerContent::Parsed { tree, language } = &layer.content {
-                    let info = SyntaxLayerInfo {
+                    let layer_start_offset = layer.range.start.to_offset(buffer);
+                    let layer_start_point = layer.range.start.to_point(buffer).to_ts_point();
+
+                    info = Some(SyntaxLayerInfo {
                         tree,
                         language,
                         depth: layer.depth,
-                        offset: (
-                            layer.range.start.to_offset(buffer),
-                            layer.range.start.to_point(buffer).to_ts_point(),
-                        ),
-                    };
-                    cursor.next(buffer);
-                    return Some(info);
-                } else {
-                    cursor.next(buffer);
+                        offset: (layer_start_offset, layer_start_point),
+                    });
+                }
+                cursor.next(buffer);
+                if info.is_some() {
+                    return info;
                 }
             }
             None

crates/project/src/project.rs 🔗

@@ -3045,6 +3045,8 @@ impl Project {
     ) -> Task<(Option<PathBuf>, Vec<WorktreeId>)> {
         let key = (worktree_id, adapter_name);
         if let Some(server_id) = self.language_server_ids.remove(&key) {
+            log::info!("stopping language server {}", key.1 .0);
+
             // Remove other entries for this language server as well
             let mut orphaned_worktrees = vec![worktree_id];
             let other_keys = self.language_server_ids.keys().cloned().collect::<Vec<_>>();

crates/zed/src/languages/heex/config.toml 🔗

@@ -4,4 +4,4 @@ autoclose_before = ">})"
 brackets = [
     { start = "<", end = ">", close = true, newline = true },
 ]
-block_comment = ["<%#", "%>"]
+block_comment = ["<%!--", "--%>"]