A tree-sitter grammar for the Emet configuration language, together with the Neovim plugin that registers it with nvim-treesitter.
This one repository is both:
- the grammar (
grammar.js, generatedsrc/,queries/, corpus tests), and - the nvim plugin (
plugin/emet.lua,queries/emet/), so the repo root is a valid Neovimruntimepathentry.
It covers Emet's Elm-like surface syntax: line comments, string literals with
escapes and ${…} interpolation, integer/float numbers, lower/upper
identifiers, dotted module names and qualified names (List.map,
Limesurvey.Database.Config), keywords (let in if then else case of), the
reserved glyph constructors (aptPackage systemdService file lineInFile scroll), the full operator table, lambdas, let … in, case … of,
if … then … else, records and field access, lists, function application, type
signatures, and type expressions including record types { r | field : a }.
Extracted from the golem monorepo
(libs/tree-sitter-emet).
Add the plugin — one line — and let nvim-treesitter compile the parser:
{ "dull-ca/emet.nvim" }Then, inside Neovim:
:TSInstall emetThe plugin (plugin/emet.lua) registers the emet parser (pointing
install_info at this repository), maps the .emet file type, and ships the
highlight queries under queries/emet/ on the runtimepath — so no manual query
symlinks are needed.
:TSInstall emet compiles src/parser.c and the external scanner
src/scanner.c, so a C compiler must be on PATH. Tree-sitter highlighting
needs Neovim 0.9+; classic Vim has no tree-sitter support.
Emet is layout-sensitive (offside rule). This grammar does not reimplement
the compiler's full layout algorithm — it only needs a robust, error-tolerant
tree for highlighting. A tiny external scanner (src/scanner.c) emits two
synthetic tokens:
- a declaration boundary when a new line starts at column 0, which ends the previous top-level declaration; and
- a line boundary inside
case … of, which separates one arm from the next (Emet lays out case arms one per line).
Everything else relies on tree-sitter's error recovery, which is sufficient for highlighting.
A dot binds an upper-case segment to whatever sits on its left, in every position such a name may appear — module headers, imports, type annotations, expressions, and patterns:
module Limesurvey.Database exposing (Config(..), config)
import Limesurvey.Survey as Survey
describe : Limesurvey.Survey.Kind -> Str
describe kind =
case kind of
Survey.Poll -> Survey.label kind
Survey.Quiz -> "quiz"
The segments have to touch. A.B is one name and A . B is two, which the
grammar reports as an error — the compiler compares spans and rejects it too.
A spaced dot before a lower-case name stays legal, because that is field
access: shapes . area.
A string node is a sequence of string_content, escape_sequence, and
interpolation nodes. An interpolation wraps a full Emet expression between
interpolation_start (${) and interpolation_end (}) nodes, so the
embedded expression is parsed and highlighted like any other code — including
nested strings, applications, and parenthesised sub-expressions.
Two copies of the query files exist and must be kept in sync:
queries/*.scm— the canonical location (tree-sitter convention, listed intree-sitter.json/package.json).queries/emet/*.scm— a copy (not a symlink; portable across platforms) so that nvim-treesitter, which looks forqueries/<lang>/on the runtimepath, finds them when this repo root is on the runtimepath.
Edit queries/*.scm, then copy over queries/emet/*.scm.
queries/highlights.scm— maps nodes to standard capture names.queries/injections.scm— treats--comments as comment text.queries/locals.scm— scopes and definitions for local variable resolution.
tree-sitter generate # regenerate src/parser.c from grammar.js
tree-sitter test # run test/corpus
tree-sitter parse FILE.emet # inspect a parse treeThe golem documentation site (Astro +
Starlight + Expressive Code / Shiki) highlights Emet code blocks with a
TextMate grammar, not tree-sitter. That grammar is generated from this
repository's grammar.js:
node scripts/generate-textmate.mjs [output.json]With no argument it writes emet.tmLanguage.json at the repo root; pass a path
to write elsewhere. The golem site consumes the committed generated JSON
(vendored at sites/website/src/grammars/emet.tmLanguage.json) — it no longer
runs the generator at build time. When the grammar changes, regenerate here and
copy the resulting JSON into golem.
MIT — see LICENSE.