# Testscript highlighting in Neovim


[This is a good article](https://blog.windpul.eu/posts/testscript/) on how to use _testscript_ in Go(lang). It
also goes as far as [adding a syntax
file](https://blog.windpul.eu/posts/testscript/#bonus-neovim-syntax-highlighting). But I'm 100% into Neovim,
so this needs to be tree-sitter grammar on my system.

Well... this was a journey and I needed claude.ai for a lot of stuff to pull it off.
I _also_ wanted to insert Go syntax highlighting for Go blocks, like in this file.

(Note: this is working on Fedora 43, not on Ubuntu with a (custom) Neovim 0.12.1.)

See [this post](https://mastodon.nl/@miekg/116376160008133210) for a screen shot.

```testscript
# test power.go against a golden file
exec go run power.go
cmp stdout golden
!stdout 'hello world'

-- power.go --
package main

import "fmt"

func main() {
        for i := 0; i < 10; i++ {
                fmt.Printf("2^%d = %d\n", i, 1<<i)
        }
}

-- golden --
2^0 = 1
2^1 = 2
2^2 = 4
2^3 = 8
2^4 = 16
2^5 = 32
2^6 = 64
2^7 = 128
2^8 = 256
2^9 = 512
```

The grammar I got (and which works) is:

```js
module.exports = grammar({
  name: "testscript",

  extras: ($) => [/\s/],

  rules: {
    source_file: ($) => repeat($._line),

    _line: ($) => choice($.comment, $.heredoc, $.command_line),

    comment: ($) => /#[^\n]*/,

    heredoc: ($) => seq($.heredoc_marker, $.heredoc_body),

    heredoc_marker: ($) => /-- [^-\n]+ --/,

    heredoc_body: ($) => repeat1($.file_content),

    file_content: ($) => /[^\t\n ][^\n]*/,

    command_line: ($) =>
      seq(
        optional($.negation),
        optional($.condition),
        $.command,
        repeat(choice($.variable, $.string, $.redirection, $.argument)),
      ),

    negation: ($) => /!\s*/,

    condition: ($) => seq("[", /[^\]]*/, "]"),

    command: ($) =>
      choice(
        "cd",
        "chmod",
        "cmp",
        "cmpenv",
        "cp",
        "env",
        "exec",
        "exists",
        "grep",
        "kill",
        "mkdir",
        "mv",
        "rm",
        "skip",
        "stderr",
        "stdin",
        "stdout",
        "ttyin",
        "ttyout",
        "stop",
        "symlink",
        "unquote",
        "wait",
      ),

    variable: ($) => choice(/\$\w+/, /\$\{[^}]+\}/),

    string: ($) =>
      choice(
        seq('"', repeat(choice(/[^"\\]/, /\\./)), '"'),
        seq("'", /[^']*/, "'"),
      ),

    redirection: ($) => choice(">>", ">"),

    argument: ($) => /[^\s#>'"$\n]+/,
  },
});
```

For making this works you need, `.../queries/testscript`

a `highlights.scm` with:

```scheme
; Comments
(comment) @comment

; Negation
(negation) @operator

; Commands
(command) @keyword

; Conditions
(condition) @punctuation.special

; Variables
(variable) @variable

; Strings
(string) @string

; Redirection
(redirection) @operator

; Heredoc markers
(heredoc_marker) @type

; File content between heredoc markers
(file_content) @constant
```

_and_ an `injections.scm` to inject Go syntax:

```scheme
;; extends

((heredoc
  (heredoc_marker) @_marker
  (heredoc_body) @injection.content)
  (#match? @_marker "\.go")
  (#set! injection.language "go")
  (#set! injection.include-children true))
```

Then you _also_ need a callback to register the parser with Neovim:

```lua
vim.api.nvim_create_autocmd("User", {
        pattern = "TSUpdate",
        callback = function()
                require("nvim-treesitter.parsers").testscript = {
                        install_info = {
                                path = vim.fn.expand("~/.dotfiles/nvim/grammar/testscript"),
                                files = { "src/parser.c" },
                                generate_requires_npm = false,
                                requires_generate_from_grammar = true,
                        },
                }
        end,
})
```

If this is all done and done correctly - I'm not sure if a `tree-sitter generate` in the grammar directory is
actually needed - and the stars are aligned you can run `:TSUpdate` followed with a `:TSInstall testscript`.

