Use standard injection.language and injection.content captures (#22268)

uncenter and Finn Evers created

Closes #9656. Continuation of #9654, but with the addition of backwards
compatibility for the existing captures.

Release Notes:

- Improved Tree-sitter support with added compatibility for standard
injections captures

---------

Co-authored-by: Finn Evers <finn.evers@outlook.de>

Change summary

crates/editor/src/editor_tests.rs                            |  8 
crates/language/src/buffer_tests.rs                          | 24 +-
crates/language/src/language.rs                              | 26 +++
crates/language/src/syntax_map/syntax_map_tests.rs           | 26 +-
crates/languages/src/c/injections.scm                        |  8 
crates/languages/src/cpp/injections.scm                      | 12 
crates/languages/src/go/injections.scm                       |  4 
crates/languages/src/javascript/injections.scm               | 44 ++--
crates/languages/src/markdown/injections.scm                 | 16 +-
crates/languages/src/rust/injections.scm                     |  8 
crates/languages/src/tsx/injections.scm                      | 44 ++--
crates/languages/src/typescript/injections.scm               | 48 +++---
crates/outline_panel/src/outline_panel.rs                    |  4 
docs/src/extensions/languages.md                             | 14 
extensions/elixir/languages/elixir/injections.scm            |  4 
extensions/elixir/languages/heex/injections.scm              | 10 
extensions/html/languages/html/injections.scm                |  8 
extensions/php/languages/php/injections.scm                  | 14 
extensions/terraform/languages/hcl/injections.scm            |  6 
extensions/terraform/languages/terraform-vars/injections.scm |  6 
extensions/terraform/languages/terraform/injections.scm      |  6 
21 files changed, 181 insertions(+), 159 deletions(-)

Detailed changes

crates/editor/src/editor_tests.rs 🔗

@@ -5962,8 +5962,8 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
         .with_injection_query(
             r#"
             (script_element
-                (raw_text) @content
-                (#set! "language" "javascript"))
+                (raw_text) @injection.content
+                (#set! injection.language "javascript"))
             "#,
         )
         .unwrap(),
@@ -9068,8 +9068,8 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
         .with_injection_query(
             r#"
             (script_element
-                (raw_text) @content
-                (#set! "language" "javascript"))
+                (raw_text) @injection.content
+                (#set! injection.language "javascript"))
             "#,
         )
         .unwrap(),

crates/language/src/buffer_tests.rs 🔗

@@ -3115,8 +3115,8 @@ fn html_lang() -> Language {
     .with_injection_query(
         r#"
         (script_element
-            (raw_text) @content
-            (#set! "language" "javascript"))
+            (raw_text) @injection.content
+            (#set! injection.language "javascript"))
         "#,
     )
     .unwrap()
@@ -3138,15 +3138,15 @@ fn erb_lang() -> Language {
     .with_injection_query(
         r#"
             (
-                (code) @content
-                (#set! "language" "ruby")
-                (#set! "combined")
+                (code) @injection.content
+                (#set! injection.language "ruby")
+                (#set! injection.combined)
             )
 
             (
-                (content) @content
-                (#set! "language" "html")
-                (#set! "combined")
+                (content) @injection.content
+                (#set! injection.language "html")
+                (#set! injection.combined)
             )
         "#,
     )
@@ -3278,11 +3278,11 @@ pub fn markdown_lang() -> Language {
         r#"
             (fenced_code_block
                 (info_string
-                    (language) @language)
-                (code_fence_content) @content)
+                    (language) @injection.language)
+                (code_fence_content) @injection.content)
 
-            ((inline) @content
-                (#set! "language" "markdown-inline"))
+                ((inline) @injection.content
+                (#set! injection.language "markdown-inline"))
         "#,
     )
     .unwrap()

crates/language/src/language.rs 🔗

@@ -1273,23 +1273,45 @@ impl Language {
             .ok_or_else(|| anyhow!("cannot mutate grammar"))?;
         let query = Query::new(&grammar.ts_language, source)?;
         let mut language_capture_ix = None;
+        let mut injection_language_capture_ix = None;
         let mut content_capture_ix = None;
+        let mut injection_content_capture_ix = None;
         get_capture_indices(
             &query,
             &mut [
                 ("language", &mut language_capture_ix),
+                ("injection.language", &mut injection_language_capture_ix),
                 ("content", &mut content_capture_ix),
+                ("injection.content", &mut injection_content_capture_ix),
             ],
         );
+        language_capture_ix = match (language_capture_ix, injection_language_capture_ix) {
+            (None, Some(ix)) => Some(ix),
+            (Some(_), Some(_)) => {
+                return Err(anyhow!(
+                    "both language and injection.language captures are present"
+                ));
+            }
+            _ => language_capture_ix,
+        };
+        content_capture_ix = match (content_capture_ix, injection_content_capture_ix) {
+            (None, Some(ix)) => Some(ix),
+            (Some(_), Some(_)) => {
+                return Err(anyhow!(
+                    "both content and injection.content captures are present"
+                ));
+            }
+            _ => content_capture_ix,
+        };
         let patterns = (0..query.pattern_count())
             .map(|ix| {
                 let mut config = InjectionPatternConfig::default();
                 for setting in query.property_settings(ix) {
                     match setting.key.as_ref() {
-                        "language" => {
+                        "language" | "injection.language" => {
                             config.language.clone_from(&setting.value);
                         }
-                        "combined" => {
+                        "combined" | "injection.combined" => {
                             config.combined = true;
                         }
                         _ => {}

crates/language/src/syntax_map/syntax_map_tests.rs 🔗

@@ -1193,15 +1193,15 @@ fn erb_lang() -> Language {
     .with_injection_query(
         r#"
             (
-                (code) @content
-                (#set! "language" "ruby")
-                (#set! "combined")
+                (code) @injection.content
+                (#set! injection.language "ruby")
+                (#set! injection.combined)
             )
 
             (
-                (content) @content
-                (#set! "language" "html")
-                (#set! "combined")
+                (content) @injection.content
+                (#set! injection.language "html")
+                (#set! injection.combined)
             )
         "#,
     )
@@ -1230,8 +1230,8 @@ fn rust_lang() -> Language {
     .with_injection_query(
         r#"
             (macro_invocation
-                (token_tree) @content
-                (#set! "language" "rust"))
+                (token_tree) @injection.content
+                (#set! injection.language "rust"))
         "#,
     )
     .unwrap()
@@ -1277,13 +1277,13 @@ fn heex_lang() -> Language {
               (partial_expression_value)
               (expression_value)
               (ending_expression_value)
-            ] @content)
-          (#set! language "elixir")
-          (#set! combined)
+            ] @injection.content)
+          (#set! injection.language "elixir")
+          (#set! injection.combined)
         )
 
-        ((expression (expression_value) @content)
-         (#set! language "elixir"))
+        ((expression (expression_value) @injection.content)
+         (#set! injection.language "elixir"))
         "#,
     )
     .unwrap()

crates/languages/src/c/injections.scm 🔗

@@ -1,7 +1,7 @@
 (preproc_def
-    value: (preproc_arg) @content
-    (#set! "language" "c"))
+    value: (preproc_arg) @injection.content
+    (#set! injection.language "c"))
 
 (preproc_function_def
-    value: (preproc_arg) @content
-    (#set! "language" "c"))
+    value: (preproc_arg) @injection.content
+    (#set! injection.language "c"))

crates/languages/src/cpp/injections.scm 🔗

@@ -1,11 +1,11 @@
 (preproc_def
-    value: (preproc_arg) @content
-    (#set! "language" "c++"))
+    value: (preproc_arg) @injection.content
+    (#set! injection.language "c++"))
 
 (preproc_function_def
-    value: (preproc_arg) @content
-    (#set! "language" "c++"))
+    value: (preproc_arg) @injection.content
+    (#set! injection.language "c++"))
 
 (raw_string_literal
-  delimiter: (raw_string_delimiter) @language
-  (raw_string_content) @content)
+  delimiter: (raw_string_delimiter) @injection.language
+  (raw_string_content) @injection.content)

crates/languages/src/go/injections.scm 🔗

@@ -9,5 +9,5 @@
     [
       (raw_string_literal)
       (interpreted_string_literal)
-    ] @content
-    (#set! "language" "regex")))
+    ] @injection.content
+    (#set! injection.language "regex")))

crates/languages/src/javascript/injections.scm 🔗

@@ -1,60 +1,60 @@
 (((comment) @_jsdoc_comment
-  (#match? @_jsdoc_comment "(?s)^/[*][*][^*].*[*]/$")) @content
-  (#set! "language" "jsdoc"))
+  (#match? @_jsdoc_comment "(?s)^/[*][*][^*].*[*]/$")) @injection.content
+  (#set! injection.language "jsdoc"))
 
-((regex) @content
-  (#set! "language" "regex"))
+((regex) @injection.content
+  (#set! injection.language "regex"))
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "css")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "css"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "css"))
 )
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "html")
-  arguments: (template_string) @content
-                              (#set! "language" "html")
+  arguments: (template_string) @injection.content
+                              (#set! injection.language "html")
 )
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "js")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "javascript"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "javascript"))
 )
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "json")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "json"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "json"))
 )
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "sql")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "sql"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "sql"))
 )
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "ts")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "typescript"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "typescript"))
 )
 
 (call_expression
   function: (identifier) @_name (#match? @_name "^ya?ml$")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "yaml"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "yaml"))
 )
 
 (call_expression
   function: (identifier) @_name (#match? @_name "^g(raph)?ql$")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "graphql"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "graphql"))
 )
 
 (call_expression
   function: (identifier) @_name (#match? @_name "^g(raph)?ql$")
-  arguments: (arguments (template_string (string_fragment) @content
-                              (#set! "language" "graphql")))
+  arguments: (arguments (template_string (string_fragment) @injection.content
+                              (#set! injection.language "graphql")))
 )

crates/languages/src/markdown/injections.scm 🔗

@@ -1,14 +1,14 @@
 (fenced_code_block
   (info_string
-    (language) @language)
-  (code_fence_content) @content)
+    (language) @injection.language)
+  (code_fence_content) @injection.content)
 
-((inline) @content
- (#set! "language" "markdown-inline"))
+((inline) @injection.content
+ (#set! injection.language "markdown-inline"))
 
-((html_block) @content
-  (#set! "language" "html"))
+((html_block) @injection.content
+  (#set! injection.language "html"))
 
-((minus_metadata) @content (#set! "language" "yaml"))
+((minus_metadata) @injection.content (#set! injection.language "yaml"))
 
-((plus_metadata) @content (#set! "language" "toml"))
+((plus_metadata) @injection.content (#set! injection.language "toml"))

crates/languages/src/rust/injections.scm 🔗

@@ -1,7 +1,7 @@
 (macro_invocation
-  (token_tree) @content
-  (#set! "language" "rust"))
+  (token_tree) @injection.content
+  (#set! injection.language "rust"))
 
 (macro_rule
-  (token_tree) @content
-  (#set! "language" "rust"))
+  (token_tree) @injection.content
+  (#set! injection.language "rust"))

crates/languages/src/tsx/injections.scm 🔗

@@ -1,60 +1,60 @@
 (((comment) @_jsdoc_comment
-  (#match? @_jsdoc_comment "(?s)^/[*][*][^*].*[*]/$")) @content
-  (#set! "language" "jsdoc"))
+  (#match? @_jsdoc_comment "(?s)^/[*][*][^*].*[*]/$")) @injection.content
+  (#set! injection.language "jsdoc"))
 
-((regex) @content
-  (#set! "language" "regex"))
+((regex) @injection.content
+  (#set! injection.language "regex"))
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "css")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "css"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "css"))
 )
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "html")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "html"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "html"))
 )
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "js")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "javascript"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "javascript"))
 )
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "json")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "json"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "json"))
 )
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "sql")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "sql"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "sql"))
 )
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "ts")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "typescript"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "typescript"))
 )
 
 (call_expression
   function: (identifier) @_name (#match? @_name "^ya?ml$")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "yaml"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "yaml"))
 )
 
 (call_expression
   function: (identifier) @_name (#match? @_name "^g(raph)?ql$")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "graphql"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "graphql"))
 )
 
 (call_expression
   function: (identifier) @_name (#match? @_name "^g(raph)?ql$")
-  arguments: (arguments (template_string (string_fragment) @content
-                              (#set! "language" "graphql")))
+  arguments: (arguments (template_string (string_fragment) @injection.content
+                              (#set! injection.language "graphql")))
 )

crates/languages/src/typescript/injections.scm 🔗

@@ -1,64 +1,64 @@
 (((comment) @_jsdoc_comment
-  (#match? @_jsdoc_comment "(?s)^/[*][*][^*].*[*]/$")) @content
-  (#set! "language" "jsdoc"))
+  (#match? @_jsdoc_comment "(?s)^/[*][*][^*].*[*]/$")) @injection.content
+  (#set! injection.language "jsdoc"))
 
 (((comment) @reference
-  (#match? @reference "^///\\s+<reference\\s+types=\"\\S+\"\\s*/>\\s*$")) @content
-  (#set! "language" "html"))
+  (#match? @reference "^///\\s+<reference\\s+types=\"\\S+\"\\s*/>\\s*$")) @injection.content
+  (#set! injection.language "html"))
 
-((regex) @content
-  (#set! "language" "regex"))
+((regex) @injection.content
+  (#set! injection.language "regex"))
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "css")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "css"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "css"))
 )
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "html")
-  arguments: (template_string) @content
-                              (#set! "language" "html")
+  arguments: (template_string) @injection.content
+                              (#set! injection.language "html")
 )
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "js")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "javascript"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "javascript"))
 )
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "json")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "json"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "json"))
 )
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "sql")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "sql"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "sql"))
 )
 
 (call_expression
   function: (identifier) @_name (#eq? @_name "ts")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "typescript"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "typescript"))
 )
 
 (call_expression
   function: (identifier) @_name (#match? @_name "^ya?ml$")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "yaml"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "yaml"))
 )
 
 (call_expression
   function: (identifier) @_name (#match? @_name "^g(raph)?ql$")
-  arguments: (template_string (string_fragment) @content
-                              (#set! "language" "graphql"))
+  arguments: (template_string (string_fragment) @injection.content
+                              (#set! injection.language "graphql"))
 )
 
 (call_expression
   function: (identifier) @_name (#match? @_name "^g(raph)?ql$")
-  arguments: (arguments (template_string (string_fragment) @content
-                              (#set! "language" "graphql")))
+  arguments: (arguments (template_string (string_fragment) @injection.content
+                              (#set! injection.language "graphql")))
 )

crates/outline_panel/src/outline_panel.rs 🔗

@@ -6043,8 +6043,8 @@ mod tests {
         .with_injection_query(
             r#"
                 (macro_invocation
-                    (token_tree) @content
-                    (#set! "language" "rust"))
+                    (token_tree) @injection.content
+                    (#set! injection.language "rust"))
             "#,
         )
         .unwrap()

docs/src/extensions/languages.md 🔗

@@ -203,19 +203,19 @@ Here's an example from an `injections.scm` file for Markdown:
 ```scheme
 (fenced_code_block
   (info_string
-    (language) @language)
-  (code_fence_content) @content)
+    (language) @injection.language)
+  (code_fence_content) @injection.content)
 
 ((inline) @content
- (#set! "language" "markdown-inline"))
+ (#set! injection.language "markdown-inline"))
 ```
 
 This query identifies fenced code blocks, capturing the language specified in the info string and the content within the block. It also captures inline content and sets its language to "markdown-inline".
 
-| Capture   | Description                                                |
-| --------- | ---------------------------------------------------------- |
-| @language | Captures the language identifier for a code block          |
-| @content  | Captures the content to be treated as a different language |
+| Capture             | Description                                                |
+| ------------------- | ---------------------------------------------------------- |
+| @injection.language | Captures the language identifier for a code block          |
+| @injection.content  | Captures the content to be treated as a different language |
 
 Note that we couldn't use JSON as an example here because it doesn't support language injections.
 

extensions/elixir/languages/heex/injections.scm 🔗

@@ -4,10 +4,10 @@
       (partial_expression_value)
       (expression_value)
       (ending_expression_value)
-    ] @content)
-  (#set! language "elixir")
-  (#set! combined)
+    ] @injection.content)
+  (#set! injection.language "elixir")
+  (#set! injection.combined)
 )
 
-((expression (expression_value) @content)
- (#set! language "elixir"))
+((expression (expression_value) @injection.content)
+ (#set! injection.language "elixir"))

extensions/html/languages/html/injections.scm 🔗

@@ -1,7 +1,7 @@
 (script_element
-  (raw_text) @content
-  (#set! "language" "javascript"))
+  (raw_text) @injection.content
+  (#set! injection.language "javascript"))
 
 (style_element
-  (raw_text) @content
-  (#set! "language" "css"))
+  (raw_text) @injection.content
+  (#set! injection.language "css"))

extensions/php/languages/php/injections.scm 🔗

@@ -1,9 +1,9 @@
-((text) @content
- (#set! "language" "html")
- (#set! "combined"))
+((text) @injection.content
+ (#set! injection.language "html")
+ (#set! injection.combined))
 
-((comment) @content
-  (#match? @content "^/\\*\\*[^*]")
-  (#set! "language" "phpdoc"))
+((comment) @injection.content
+  (#match? @injection.content "^/\\*\\*[^*]")
+  (#set! injection.language "phpdoc"))
 
-((heredoc_body) (heredoc_end) @language) @content
+((heredoc_body) (heredoc_end) @injection.language) @injection.content

extensions/terraform/languages/hcl/injections.scm 🔗

@@ -1,6 +1,6 @@
 ; https://github.com/nvim-treesitter/nvim-treesitter/blob/ce4adf11cfe36fc5b0e5bcdce0c7c6e8fbc9798a/queries/hcl/injections.scm
 
 (heredoc_template
-  (template_literal) @content
-  (heredoc_identifier) @language
-  (#downcase! @language))
+  (template_literal) @injection.content
+  (heredoc_identifier) @injection.language
+  (#downcase! @injection.language))

extensions/terraform/languages/terraform-vars/injections.scm 🔗

@@ -1,9 +1,9 @@
 ; https://github.com/nvim-treesitter/nvim-treesitter/blob/ce4adf11cfe36fc5b0e5bcdce0c7c6e8fbc9798a/queries/hcl/injections.scm
 
 (heredoc_template
-  (template_literal) @content
-  (heredoc_identifier) @language
-  (#downcase! @language))
+  (template_literal) @injection.content
+  (heredoc_identifier) @injection.language
+  (#downcase! @injection.language))
 
 ; https://github.com/nvim-treesitter/nvim-treesitter/blob/ce4adf11cfe36fc5b0e5bcdce0c7c6e8fbc9798a/queries/terraform/injections.scm
 ; inherits: hcl

extensions/terraform/languages/terraform/injections.scm 🔗

@@ -1,9 +1,9 @@
 ; https://github.com/nvim-treesitter/nvim-treesitter/blob/ce4adf11cfe36fc5b0e5bcdce0c7c6e8fbc9798a/queries/hcl/injections.scm
 
 (heredoc_template
-  (template_literal) @content
-  (heredoc_identifier) @language
-  (#downcase! @language))
+  (template_literal) @injection.content
+  (heredoc_identifier) @injection.language
+  (#downcase! @injection.language))
 
 ; https://github.com/nvim-treesitter/nvim-treesitter/blob/ce4adf11cfe36fc5b0e5bcdce0c7c6e8fbc9798a/queries/terraform/injections.scm
 ; inherits: hcl