Skip to content

Tree-sitter Grammar

tree-sitter-scaf provides syntax highlighting and code navigation for scaf files.

Add the parser configuration:

local parser_config = require("nvim-treesitter.parsers").get_parser_configs()
parser_config.scaf = {
install_info = {
url = "https://github.com/rlch/tree-sitter-scaf",
files = { "src/parser.c" },
branch = "main",
},
filetype = "scaf",
}

Then run :TSInstall scaf.

Add to languages.toml:

[[language]]
name = "scaf"
scope = "source.scaf"
file-types = ["scaf"]
roots = [".scaf.yaml"]
[[grammar]]
name = "scaf"
source = { git = "https://github.com/rlch/tree-sitter-scaf", rev = "main" }

Then run hx --grammar fetch and hx --grammar build.

Tree-sitter support is built into Zed. Create an extension or wait for the grammar to be added to the official registry.

With tree-sitter support:

(add-to-list 'treesit-language-source-alist
'(scaf "https://github.com/rlch/tree-sitter-scaf"))
(treesit-install-language-grammar 'scaf)

The grammar defines these node types:

NodeDescription
source_fileRoot node
import_statementImport declaration
fn_definitionNamed fn
setup_blockSetup block
teardown_blockTeardown block
query_scopeQuery scope with tests
NodeDescription
groupTest group
testTest case
statementInput/output statement
assertionAssert block
NodeDescription
expressionAny expression
binary_expressionBinary operation
unary_expressionUnary operation
call_expressionFunction call
member_expressionProperty access
NodeDescription
stringQuoted string
raw_stringBacktick string
numberNumeric literal
booleantrue / false
nullnull
listArray literal
mapObject literal

Standard highlight groups used:

GroupUsage
@keywordquery, test, group, setup, etc.
@functionQuery names, function calls
@stringString literals
@string.specialRaw strings (query bodies)
@numberNumbers
@constant.builtintrue, false, null
@operatorOperators
@punctuation.bracket{}, [], ()
@punctuation.delimiter,, :, ;
@commentComments
@variable.parameter$param variables
@propertyProperty paths

The grammar includes query files for:

; Keywords
(["fn" "test" "group" "setup" "teardown" "assert" "import"] @keyword)
; Function names
(query_definition name: (identifier) @function)
(named_setup name: (identifier) @function)
; Strings
(string) @string
(raw_string) @string.special
; Numbers
(number) @number
; Booleans and null
(boolean) @constant.builtin
(null) @constant.builtin
; Parameters
((identifier) @variable.parameter
(#match? @variable.parameter "^\\$"))
; Comments
(comment) @comment

Injection queries for language highlighting in query bodies:

; Inject dialect language into raw strings
((raw_string) @injection.content
(#set! injection.language "cypher"))

For scope tracking and go-to-definition:

; Query definitions create scope
(query_definition
name: (identifier) @definition.function)
; Query scope references a query
(query_scope
query_name: (identifier) @reference)
Terminal window
git clone https://github.com/rlch/tree-sitter-scaf
cd tree-sitter-scaf
npm install
npm run build
Terminal window
npm test

After modifying grammar.js:

Terminal window
npx tree-sitter generate

Parse a scaf file programmatically:

const Parser = require('tree-sitter');
const Scaf = require('tree-sitter-scaf');
const parser = new Parser();
parser.setLanguage(Scaf);
const sourceCode = `
fn GetUser \`
MATCH (u:User {id: $userId})
RETURN u.name
\`
GetUser {
test "finds user" {
$userId: 1
u.name: "Alice"
}
}
`;
const tree = parser.parse(sourceCode);
console.log(tree.rootNode.toString());

Output:

(source_file
(query_definition name: (identifier) body: (raw_string))
(query_scope
query_name: (identifier)
(test
name: (string)
(statement key: (property_path (identifier)) value: (number))
(statement key: (property_path (identifier) (identifier)) value: (string)))))