Skip to content
hekla

Editor setup

Highlighting from the tree-sitter grammar, format on save from hek fmt, and what each editor needs.

heklang ships a tree-sitter grammar and a formatter that reads stdin, which between them cover most of what an editor needs. There is no language server yet, so there are no in-editor diagnostics: you get highlighting, indentation, text objects and a symbol picker from the grammar, and correctness from hekla check . on demand.

What you get

From
tree-sitter-hek highlighting, indentation, text objects, and a symbol outline
hek fmt - format on save
hekla check . diagnostics, on demand rather than as you type

Install the tool first

Every editor below shells out to hek, so install it once:

curl -fsSL https://hekla.tqwewe.com/install.sh | sh -s -- hek

or cargo install hek, or cargo binstall hek.

The grammar

The grammar lives in the heklang repository rather than its own, so one commit can change the lexer and the grammar together.

Repository https://git.tqwewe.com/tephra/heklang
Path within it tree-sitter-hek
Latest tag tree-sitter-hek-v0.4.0
Revision bc7f5171d2f17e14241566652f25477befde15f8
ABI 14
Queries highlights, indents, locals, tags, textobjects, rainbows

src/parser.c is committed, so nothing needs the tree-sitter CLI or Node to build it.

Helix

The best-supported editor, because the grammar was developed against it.

In languages.toml:

[[language]]
name = "hek"
scope = "source.hek"
injection-regex = "hek"
file-types = ["hk"]
roots = []
comment-token = "//"
indent = { tab-width = 2, unit = "  " }
formatter = { command = "hek", args = ["fmt", "-"] }
auto-format = true

[[grammar]]
name = "hek"
source = { git = "https://git.tqwewe.com/tephra/heklang", rev = "bc7f5171d2f17e14241566652f25477befde15f8", subpath = "tree-sitter-hek" }

Then build the grammar and put the queries where Helix looks for them:

hx --grammar fetch
hx --grammar build

git clone --depth 1 https://git.tqwewe.com/tephra/heklang /tmp/heklang
mkdir -p ~/.config/helix/runtime/queries/hek
cp /tmp/heklang/tree-sitter-hek/queries/*.scm ~/.config/helix/runtime/queries/hek/

hx --health hek should show ticks across the row. There is no block-comment-tokens entry because heklang has no block comment form.

With nix

The repository is a flake, so the binary and the grammar come from one commit and cannot drift apart:

inputs.heklang.url = "git+https://git.tqwewe.com/tephra/heklang";
hek = inputs.heklang.packages.${pkgs.system}.hek;
hek-grammar = inputs.heklang.packages.${pkgs.system}.tree-sitter-hek;

The grammar derivation carries its queries, so linking both into Helix’s runtime is:

"helix/runtime/grammars/hek.so".source = "${hek-grammar}/parser";
"helix/runtime/queries/hek".source = "${hek-grammar}/queries";

and the formatter is "${hek}/bin/hek" with [ "fmt" "-" ].

Neovim

Register the parser, then install it. The grammar is in a subdirectory, which is what location is for:

vim.filetype.add({ extension = { hk = "hek" } })

require("nvim-treesitter.parsers").get_parser_configs().hek = {
  install_info = {
    url = "https://git.tqwewe.com/tephra/heklang",
    location = "tree-sitter-hek",
    files = { "src/parser.c" },
    branch = "main",
    -- src/ is committed, so neither is needed.
    generate_requires_npm = false,
    requires_generate_from_grammar = false,
  },
  filetype = "hek",
}

Then :TSInstall hek.

nvim-treesitter does not fetch queries for a grammar it did not ship, so copy them in:

git clone --depth 1 https://git.tqwewe.com/tephra/heklang /tmp/heklang
mkdir -p ~/.config/nvim/queries/hek
cp /tmp/heklang/tree-sitter-hek/queries/*.scm ~/.config/nvim/queries/hek/

Format on save, with conform.nvim:

require("conform").setup({
  formatters = {
    hek = { command = "hek", args = { "fmt", "-" }, stdin = true },
  },
  formatters_by_ft = { hek = { "hek" } },
  format_on_save = { timeout_ms = 1000 },
})

Without conform, an autocmd running hek fmt over the file and reloading it works too, but conform handles the failure case properly, which matters here.

Zed

Zed loads a grammar from a git repository, so this is a small local extension. Create a directory with two files:

# extension.toml
id = "hek"
name = "hek"
version = "0.1.0"
schema_version = 1

[grammars.hek]
repository = "https://git.tqwewe.com/tephra/heklang"
rev = "bc7f5171d2f17e14241566652f25477befde15f8"
path = "tree-sitter-hek"
# languages/hek/config.toml
name = "hek"
grammar = "hek"
path_suffixes = ["hk"]
line_comments = ["// "]
tab_size = 2

Copy highlights.scm, indents.scm and textobjects.scm from the grammar’s queries/ into languages/hek/, then install it through the extensions panel with Install Dev Extension, pointing at that directory.

Format on save, in Zed’s settings.json:

{
  "languages": {
    "hek": {
      "format_on_save": "on",
      "formatter": {
        "external": { "command": "hek", "arguments": ["fmt", "-"] }
      }
    }
  }
}

VS Code, Sublime Text, and anything TextMate

None of them uses tree-sitter for syntax, so they need a TextMate grammar instead. This site serves the one it highlights its own code samples with:

curl -O https://hekla.tqwewe.com/hek.tmLanguage.json

It declares scope source.hek and file type hk, and it is the same file Shiki renders every .hk block on these pages with, so what you see here is what you get.

For VS Code, the smallest thing that works is a local extension: a directory under ~/.vscode/extensions/hek/ holding that grammar and a package.json:

{
  "name": "hek",
  "publisher": "local",
  "version": "0.1.0",
  "engines": { "vscode": "^1.80.0" },
  "contributes": {
    "languages": [
      { "id": "hek", "extensions": [".hk"], "configuration": "./language-configuration.json" }
    ],
    "grammars": [
      { "language": "hek", "scopeName": "source.hek", "path": "./hek.tmLanguage.json" }
    ]
  }
}

with a language-configuration.json of { "comments": { "lineComment": "//" } }.

Format on save needs a formatter, and there is no VS Code extension providing one yet. Any “run command on save” extension pointed at hek fmt covers it, or run hek fmt in a watch task.

Emacs

Emacs 29’s treesit can build and load the grammar:

(add-to-list 'treesit-language-source-alist
             '(hek "https://git.tqwewe.com/tephra/heklang" "main" "tree-sitter-hek/src"))
(treesit-install-language-grammar 'hek)

Being straight about the gap: there is no hek-ts-mode. treesit does not consume highlights.scm directly the way Helix and Neovim do; it wants font-lock rules written in elisp inside a major mode, and nobody has written one. Until then the grammar loads and there is nothing driving it.

Formatting works today without any of that:

(defun hek-format-buffer ()
  "Format the current buffer with hek fmt, leaving it alone if it does not parse."
  (interactive)
  (let ((tmp (make-temp-file "hek"))
        (point (point)))
    (if (zerop (call-process-region nil nil "hek" nil `((:file ,tmp) nil) nil "fmt" "-"))
        (progn (erase-buffer) (insert-file-contents tmp) (goto-char point))
      (message "hek fmt: the module does not parse"))
    (delete-file tmp)))

Pre-commit

Independent of any editor, and worth having whether or not format on save is set up:

hek fmt --check
hekla check .
hekla test .

hek fmt --check names what would change and writes nothing. Note the last two are hekla rather than hek: the two tools have different rule sets, and a program can pass hek check and fail hekla check.

What is missing

Stated plainly so nobody goes looking:

  • No language server. No in-editor diagnostics, no go-to-definition, no completion, no rename. hekla check . is the diagnostic surface, and its output is designed to be read.
  • No published editor extensions. Nothing on the VS Code Marketplace, the Zed registry, the Helix default language list or nvim-treesitter upstream. Everything above is a local install.
  • The grammar is deliberately wider than the language. It has no idea which declaration a body belongs to, so it parses put outside a projector and emit inside a fn. Nothing valid fails to parse, and some invalid programs parse anyway. The checker is what says no.
  • tree-sitter-hek is on crates.io but not npm, which matters only if your tooling installs grammars from npm. Nothing above does.