DEV Community

Riel Joseph
Riel Joseph

Posted on

Neovim Treesitter highlighting with sql generic types (Typescript)

If you’ve ever written raw SQL queries inside TypeScript template literals, you probably love Treesitter’s syntax highlighting for embedded languages like:

const result = await sql`SELECT * FROM users`;
Enter fullscreen mode Exit fullscreen mode

But what happens when your code starts getting fancy — say, using generic calls, non-null assertions, or different helper functions like tx.sql or sql<User>?

By default, Treesitter doesn’t inject SQL highlighting in these more complex patterns.
In this post, we’ll fix that by writing a custom Treesitter injection query to handle generic TypeScript expressions with sql and tx.


The Problem

Neovim’s Treesitter injection for SQL works great in the simplest case:

const users = await sql`SELECT * FROM users`;
Enter fullscreen mode Exit fullscreen mode

…but once you introduce TypeScript generics or non-null assertions, highlighting breaks:

// Generic instantiation
const result = await sql<User>`SELECT * FROM users`;

// Non-null expression
const result = (await sql<User>!)`SELECT * FROM users`;
Enter fullscreen mode Exit fullscreen mode

The reason?
Treesitter doesn’t recognize that sql<User> (an instantiation_expression) still represents a function call for the purpose of language injection.


The Fix: Custom Treesitter Injection Query

To solve this, we extend Neovim’s built-in SQL injection rules using a custom .scm query file.

Create a file at:

~/.config/nvim/queries/typescript/injections.scm
Enter fullscreen mode Exit fullscreen mode

Paste this code:

;; Extend built-in injections
(call_expression
  function: (non_null_expression
    (instantiation_expression
      (await_expression
        (identifier) @_name)))
  arguments: (template_string) @injection.content
  (#eq? @_name "sql")
  (#set! injection.language "sql")
  (#set! injection.include-children))

;; Handle simpler variants like sql`...` and await sql`...`
(call_expression
  function: [
    (identifier) @_name
    (await_expression (identifier) @_name)
    (instantiation_expression function: (identifier) @_name)
  ]
  arguments: (template_string) @injection.content
  (#eq? @_name "sql")
  (#set! injection.language "sql")
  (#set! injection.include-children))

;; Support tx`...` and tx<User>`...`
(call_expression
  function: (non_null_expression
    (instantiation_expression
      (await_expression
        (identifier) @_name)))
  arguments: (template_string) @injection.content
  (#eq? @_name "tx")
  (#set! injection.language "sql")
  (#set! injection.include-children))

;; Handle simpler variants for tx as well
(call_expression
  function: [
    (identifier) @_name
    (await_expression (identifier) @_name)
    (instantiation_expression function: (identifier) @_name)
  ]
  arguments: (template_string) @injection.content
  (#eq? @_name "tx")
  (#set! injection.language "sql")
  (#set! injection.include-children))
Enter fullscreen mode Exit fullscreen mode

What This Query Does

Let’s break it down.

Treesitter parses TypeScript code into syntax nodes like:

  • call_expression — a function call (sql\...``)
  • instantiation_expression — a generic function call (sql<User>)
  • await_expression — for await sql\...``
  • template_string — your backticked SQL string

We’re telling Treesitter:

“Whenever you see a call_expression whose function name is sql or tx, treat the argument (a template string) as SQL content.”

The #set! injection.language "sql" command tells Neovim to apply SQL highlighting within that string.

We also include multiple variations to cover:

  • sql\...``
  • await sql\...``
  • sql<User>\...``
  • await sql<User>\...``
  • tx\...``
  • await tx<User>\...``

Testing It

Once you’ve saved the query, restart Neovim and open a TypeScript file containing SQL template strings.

Try all these patterns:

const a = sql`SELECT * FROM users`;
const b = await sql`SELECT * FROM posts`;
const c = await sql<User>`SELECT * FROM users`;
const d = await tx<Post>`SELECT * FROM posts`;
const e = tx`SELECT * FROM comments`;
Enter fullscreen mode Exit fullscreen mode

If everything worked, your SQL inside backticks should now be properly highlighted in all cases.


Bonus Tip: Reload Queries Without Restarting Neovim

You can quickly reload queries after editing them using:

:TSBufReload
Enter fullscreen mode Exit fullscreen mode

or restart Treesitter highlighting manually with:

:edit
Enter fullscreen mode Exit fullscreen mode

Conclusion

By extending Treesitter’s injection rules, we’ve made Neovim smart enough to recognize complex TypeScript SQL calls — even with generics, awaits, or alternate function names.

This small tweak can massively improve readability and developer experience when working with query-heavy TypeScript codebases.


TL;DR

  • Create queries/typescript/injections.scm
  • Paste the SQL injection query above
  • Enjoy perfect SQL highlighting for all your sql and tx template strings

Would you like me to include a short section on how to debug Treesitter queries (e.g., using :InspectTree or :TSPlaygroundToggle) before you publish it? It’s a good addition for readers who want to tweak it further.

Top comments (1)

Collapse
 
thomas_anderson_officiall profile image
Thomas Anderson

Neovim’s default Treesitter SQL highlighting breaks with TypeScript generics or non-null assertions like sql or await sql!. Fix it by creating queries/typescript/injections.scm with a custom injection query that tells Treesitter to treat sql or tx template strings as SQL, covering all variants including generics and await calls. After saving, reload with :TSBufReload to get full SQL highlighting.