From 605ba7428f7aa1f30f7dca338e1d6e3114e52cda Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Sat, 28 Mar 2026 22:04:12 +0300 Subject: [PATCH 1/2] fix(injections): use `string_fragment` for template literal injections Template literal injection queries previously captured the entire `template_string` node with `injection.include-children`, which caused template substitutions (e.g. `${expr}`) to be included in the injected language's parse input. This broke highlighting in cases like: html`

` where the HTML parser's attribute_value node would span across the template substitution gap, and the lit-html `${` injection query (in html_tags) would match the buffer text at that range, injecting JS at the wrong offset. Two fixes: 1. ecma/injections.scm: capture `string_fragment` nodes instead of `template_string`, and use `injection.combined` to merge them. This aligns with upstream tree-sitter-javascript's approach. The `#offset!` directives are removed since `string_fragment` already excludes the backticks. 2. html_tags/injections.scm: anchor the lit-html `${` pattern with `^` so it only matches attribute values that *start* with `${`, not attribute values whose buffer text happens to contain `${` in the middle (which occurs when the attribute_value node spans a template substitution gap). Assisted-By: Claude Opus 4.6 (1M context) --- runtime/queries/ecma/injections.scm | 110 ++++++++++++----------- runtime/queries/html_tags/injections.scm | 4 +- 2 files changed, 59 insertions(+), 55 deletions(-) diff --git a/runtime/queries/ecma/injections.scm b/runtime/queries/ecma/injections.scm index 69afb6d41..2aa7c2d21 100644 --- a/runtime/queries/ecma/injections.scm +++ b/runtime/queries/ecma/injections.scm @@ -10,12 +10,13 @@ function: (identifier) @injection.language arguments: [ (arguments - (template_string) @injection.content) - (template_string) @injection.content + (template_string + (string_fragment) @injection.content)) + (template_string + (string_fragment) @injection.content) ] (#lua-match? @injection.language "^[a-zA-Z][a-zA-Z0-9]*$") - (#offset! @injection.content 0 1 0 -1) - (#set! injection.include-children) + (#set! injection.combined) ; Languages excluded from auto-injection due to special rules ; - svg uses the html parser ; - css uses the styled parser @@ -27,11 +28,12 @@ (#eq? @_name "svg") arguments: [ (arguments - (template_string) @injection.content) - (template_string) @injection.content + (template_string + (string_fragment) @injection.content)) + (template_string + (string_fragment) @injection.content) ] - (#offset! @injection.content 0 1 0 -1) - (#set! injection.include-children) + (#set! injection.combined) (#set! injection.language "html")) ; Vercel PostgreSQL @@ -41,12 +43,13 @@ property: (property_identifier) @injection.language) arguments: [ (arguments - (template_string) @injection.content) - (template_string) @injection.content + (template_string + (string_fragment) @injection.content)) + (template_string + (string_fragment) @injection.content) ] (#eq? @injection.language "sql") - (#offset! @injection.content 0 1 0 -1) - (#set! injection.include-children)) + (#set! injection.combined)) ; Sanity CMS GROQ query ; defineQuery(`...`) @@ -54,9 +57,9 @@ function: (identifier) @_name (#eq? @_name "defineQuery") arguments: (arguments - (template_string) @injection.content) - (#offset! @injection.content 0 1 0 -1) - (#set! injection.include-children) + (template_string + (string_fragment) @injection.content)) + (#set! injection.combined) (#set! injection.language "groq")) ; gql`...` or gql(`...`) @@ -65,28 +68,29 @@ (#eq? @_name "gql") arguments: [ (arguments - (template_string) @injection.content) - (template_string) @injection.content + (template_string + (string_fragment) @injection.content)) + (template_string + (string_fragment) @injection.content) ] - (#offset! @injection.content 0 1 0 -1) - (#set! injection.include-children) + (#set! injection.combined) (#set! injection.language "graphql")) (call_expression function: (identifier) @_name (#eq? @_name "hbs") - arguments: (template_string) @injection.content - (#offset! @injection.content 0 1 0 -1) - (#set! injection.include-children) + arguments: (template_string + (string_fragment) @injection.content) + (#set! injection.combined) (#set! injection.language "glimmer")) ; css``, keyframes`` (call_expression function: (identifier) @_name (#any-of? @_name "css" "keyframes") - arguments: (template_string) @injection.content - (#offset! @injection.content 0 1 0 -1) - (#set! injection.include-children) + arguments: (template_string + (string_fragment) @injection.content) + (#set! injection.combined) (#set! injection.language "styled")) ; styled.div`` @@ -94,9 +98,9 @@ function: (member_expression object: (identifier) @_name (#eq? @_name "styled")) - arguments: ((template_string) @injection.content - (#offset! @injection.content 0 1 0 -1) - (#set! injection.include-children) + arguments: ((template_string + (string_fragment) @injection.content) + (#set! injection.combined) (#set! injection.language "styled"))) ; styled(Component)`` @@ -104,9 +108,9 @@ function: (call_expression function: (identifier) @_name (#eq? @_name "styled")) - arguments: ((template_string) @injection.content - (#offset! @injection.content 0 1 0 -1) - (#set! injection.include-children) + arguments: ((template_string + (string_fragment) @injection.content) + (#set! injection.combined) (#set! injection.language "styled"))) ; styled.div.attrs({ prop: "foo" })`` @@ -116,9 +120,9 @@ object: (member_expression object: (identifier) @_name (#eq? @_name "styled")))) - arguments: ((template_string) @injection.content - (#offset! @injection.content 0 1 0 -1) - (#set! injection.include-children) + arguments: ((template_string + (string_fragment) @injection.content) + (#set! injection.combined) (#set! injection.language "styled"))) ; styled(Component).attrs({ prop: "foo" })`` @@ -128,9 +132,9 @@ object: (call_expression function: (identifier) @_name (#eq? @_name "styled")))) - arguments: ((template_string) @injection.content - (#offset! @injection.content 0 1 0 -1) - (#set! injection.include-children) + arguments: ((template_string + (string_fragment) @injection.content) + (#set! injection.combined) (#set! injection.language "styled"))) ((regex_pattern) @injection.content @@ -140,10 +144,10 @@ ; (#eq? @_gql_comment "/* GraphQL */") ; (template_string) @injection.content ; (#set! injection.language "graphql")) -((template_string) @injection.content - (#lua-match? @injection.content "^`#graphql") - (#offset! @injection.content 0 1 0 -1) - (#set! injection.include-children) +((template_string + (string_fragment) @injection.content) + (#lua-match? @injection.content "^#graphql") + (#set! injection.combined) (#set! injection.language "graphql")) ; el.innerHTML = `` @@ -151,9 +155,9 @@ left: (member_expression property: (property_identifier) @_prop (#any-of? @_prop "outerHTML" "innerHTML")) - right: (template_string) @injection.content - (#offset! @injection.content 0 1 0 -1) - (#set! injection.include-children) + right: (template_string + (string_fragment) @injection.content) + (#set! injection.combined) (#set! injection.language "html")) ; el.innerHTML = '' @@ -178,9 +182,9 @@ (pair key: ((property_identifier) @_prop (#eq? @_prop "template")) - value: ((template_string) @injection.content - (#offset! @injection.content 0 1 0 -1) - (#set! injection.include-children) + value: ((template_string + (string_fragment) @injection.content) + (#set! injection.combined) (#set! injection.language "angular"))))))) ; @Component({ @@ -196,9 +200,9 @@ key: ((property_identifier) @_prop (#eq? @_prop "styles")) value: (array - ((template_string) @injection.content - (#offset! @injection.content 0 1 0 -1) - (#set! injection.include-children) + ((template_string + (string_fragment) @injection.content) + (#set! injection.combined) (#set! injection.language "css")))))))) ; @Component({ @@ -213,7 +217,7 @@ (pair key: ((property_identifier) @_prop (#eq? @_prop "styles")) - value: ((template_string) @injection.content - (#set! injection.include-children) - (#offset! @injection.content 0 1 0 -1) + value: ((template_string + (string_fragment) @injection.content) + (#set! injection.combined) (#set! injection.language "css"))))))) diff --git a/runtime/queries/html_tags/injections.scm b/runtime/queries/html_tags/injections.scm index 1c8ce012b..eb0ecd8e1 100644 --- a/runtime/queries/html_tags/injections.scm +++ b/runtime/queries/html_tags/injections.scm @@ -81,13 +81,13 @@ ((attribute (quoted_attribute_value (attribute_value) @injection.content)) - (#lua-match? @injection.content "%${") + (#lua-match? @injection.content "^%${") (#offset! @injection.content 0 2 0 -1) (#set! injection.language "javascript")) ((attribute (attribute_value) @injection.content) - (#lua-match? @injection.content "%${") + (#lua-match? @injection.content "^%${") (#offset! @injection.content 0 2 0 -2) (#set! injection.language "javascript")) From f4f9a36fa4ec8c7c0772a9d4bf9123a4aceae8bc Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Fri, 3 Apr 2026 10:08:44 +0300 Subject: [PATCH 2/2] fix(ecma): use scoped injections instead of combined Use `[string_fragment template_substitution]+` pattern for scoped injections per maintainer feedback, avoiding the cost of combined injections. This requires splitting alternations into separate patterns for the `fn()` and tagged template literal forms. Assisted-By: Claude Opus 4.6 (1M context) --- runtime/queries/ecma/injections.scm | 168 +++++++++++++++++++--------- 1 file changed, 113 insertions(+), 55 deletions(-) diff --git a/runtime/queries/ecma/injections.scm b/runtime/queries/ecma/injections.scm index 2aa7c2d21..d3b3a9340 100644 --- a/runtime/queries/ecma/injections.scm +++ b/runtime/queries/ecma/injections.scm @@ -5,51 +5,78 @@ ((comment) @injection.content (#set! injection.language "comment")) -; html(`...`), html`...`, sql(`...`), etc. +; html(`...`), sql(`...`), etc. (call_expression function: (identifier) @injection.language - arguments: [ - (arguments - (template_string - (string_fragment) @injection.content)) + arguments: (arguments (template_string - (string_fragment) @injection.content) - ] + [ + (string_fragment) @injection.content + (template_substitution) + ]+)) (#lua-match? @injection.language "^[a-zA-Z][a-zA-Z0-9]*$") - (#set! injection.combined) ; Languages excluded from auto-injection due to special rules ; - svg uses the html parser ; - css uses the styled parser (#not-any-of? @injection.language "svg" "css")) -; svg`...` or svg(`...`) +; html`...`, sql`...`, etc. +(call_expression + function: (identifier) @injection.language + arguments: (template_string + [ + (string_fragment) @injection.content + (template_substitution) + ]+) + (#lua-match? @injection.language "^[a-zA-Z][a-zA-Z0-9]*$") + (#not-any-of? @injection.language "svg" "css")) + +; svg(`...`) (call_expression function: (identifier) @_name (#eq? @_name "svg") - arguments: [ - (arguments - (template_string - (string_fragment) @injection.content)) + arguments: (arguments (template_string - (string_fragment) @injection.content) - ] - (#set! injection.combined) + [ + (string_fragment) @injection.content + (template_substitution) + ]+)) + (#set! injection.language "html")) + +; svg`...` +(call_expression + function: (identifier) @_name + (#eq? @_name "svg") + arguments: (template_string + [ + (string_fragment) @injection.content + (template_substitution) + ]+) (#set! injection.language "html")) ; Vercel PostgreSQL -; foo.sql`...` or foo.sql(`...`) +; foo.sql(`...`) (call_expression function: (member_expression property: (property_identifier) @injection.language) - arguments: [ - (arguments - (template_string - (string_fragment) @injection.content)) + arguments: (arguments (template_string - (string_fragment) @injection.content) - ] - (#eq? @injection.language "sql") - (#set! injection.combined)) + [ + (string_fragment) @injection.content + (template_substitution) + ]+)) + (#eq? @injection.language "sql")) + +; foo.sql`...` +(call_expression + function: (member_expression + property: (property_identifier) @injection.language) + arguments: (template_string + [ + (string_fragment) @injection.content + (template_substitution) + ]+) + (#eq? @injection.language "sql")) ; Sanity CMS GROQ query ; defineQuery(`...`) @@ -58,30 +85,43 @@ (#eq? @_name "defineQuery") arguments: (arguments (template_string - (string_fragment) @injection.content)) - (#set! injection.combined) + [ + (string_fragment) @injection.content + (template_substitution) + ]+)) (#set! injection.language "groq")) -; gql`...` or gql(`...`) +; gql(`...`) (call_expression function: (identifier) @_name (#eq? @_name "gql") - arguments: [ - (arguments - (template_string - (string_fragment) @injection.content)) + arguments: (arguments (template_string - (string_fragment) @injection.content) - ] - (#set! injection.combined) + [ + (string_fragment) @injection.content + (template_substitution) + ]+)) + (#set! injection.language "graphql")) + +; gql`...` +(call_expression + function: (identifier) @_name + (#eq? @_name "gql") + arguments: (template_string + [ + (string_fragment) @injection.content + (template_substitution) + ]+) (#set! injection.language "graphql")) (call_expression function: (identifier) @_name (#eq? @_name "hbs") arguments: (template_string - (string_fragment) @injection.content) - (#set! injection.combined) + [ + (string_fragment) @injection.content + (template_substitution) + ]+) (#set! injection.language "glimmer")) ; css``, keyframes`` @@ -89,8 +129,10 @@ function: (identifier) @_name (#any-of? @_name "css" "keyframes") arguments: (template_string - (string_fragment) @injection.content) - (#set! injection.combined) + [ + (string_fragment) @injection.content + (template_substitution) + ]+) (#set! injection.language "styled")) ; styled.div`` @@ -99,8 +141,10 @@ object: (identifier) @_name (#eq? @_name "styled")) arguments: ((template_string - (string_fragment) @injection.content) - (#set! injection.combined) + [ + (string_fragment) @injection.content + (template_substitution) + ]+) (#set! injection.language "styled"))) ; styled(Component)`` @@ -109,8 +153,10 @@ function: (identifier) @_name (#eq? @_name "styled")) arguments: ((template_string - (string_fragment) @injection.content) - (#set! injection.combined) + [ + (string_fragment) @injection.content + (template_substitution) + ]+) (#set! injection.language "styled"))) ; styled.div.attrs({ prop: "foo" })`` @@ -121,8 +167,10 @@ object: (identifier) @_name (#eq? @_name "styled")))) arguments: ((template_string - (string_fragment) @injection.content) - (#set! injection.combined) + [ + (string_fragment) @injection.content + (template_substitution) + ]+) (#set! injection.language "styled"))) ; styled(Component).attrs({ prop: "foo" })`` @@ -133,8 +181,10 @@ function: (identifier) @_name (#eq? @_name "styled")))) arguments: ((template_string - (string_fragment) @injection.content) - (#set! injection.combined) + [ + (string_fragment) @injection.content + (template_substitution) + ]+) (#set! injection.language "styled"))) ((regex_pattern) @injection.content @@ -156,8 +206,10 @@ property: (property_identifier) @_prop (#any-of? @_prop "outerHTML" "innerHTML")) right: (template_string - (string_fragment) @injection.content) - (#set! injection.combined) + [ + (string_fragment) @injection.content + (template_substitution) + ]+) (#set! injection.language "html")) ; el.innerHTML = '' @@ -183,8 +235,10 @@ key: ((property_identifier) @_prop (#eq? @_prop "template")) value: ((template_string - (string_fragment) @injection.content) - (#set! injection.combined) + [ + (string_fragment) @injection.content + (template_substitution) + ]+) (#set! injection.language "angular"))))))) ; @Component({ @@ -201,8 +255,10 @@ (#eq? @_prop "styles")) value: (array ((template_string - (string_fragment) @injection.content) - (#set! injection.combined) + [ + (string_fragment) @injection.content + (template_substitution) + ]+) (#set! injection.language "css")))))))) ; @Component({ @@ -218,6 +274,8 @@ key: ((property_identifier) @_prop (#eq? @_prop "styles")) value: ((template_string - (string_fragment) @injection.content) - (#set! injection.combined) + [ + (string_fragment) @injection.content + (template_substitution) + ]+) (#set! injection.language "css")))))))