This one's ours — I built it at floscom for working with HelixDB every day without going blind reading monochrome .hx files in Zed. Reviewing it with the same honesty I'd apply to anything else.
What it does
zed-helixql is a Zed editor extension that adds syntax highlighting for HelixQL — the query language used by HelixDB, a combined graph + vector database written in Rust. It activates on .hx and .hql files. No language server, no autocomplete — highlights only.
Why I starred it
HelixDB's query language has a distinct surface area. Schema definitions use a N::User / E::Follows / V::Embedding sigil system. Traversals are chained with ::. Operators like GT, LTE, EQ are keywords, not symbols. Without highlighting, reading a complex traversal like:
followers <- N<User>(userId)::InE<Follows>::FromN::WHERE(_::{age}::GT(18))
is genuinely difficult — everything looks like an identifier.
The alternative was to use a generic language mode, which makes the :: chains completely unreadable. So: write the extension.
How it works
Zed extensions consist of an extension.toml manifest, a languages/ directory with tree-sitter query files, and optionally a Rust WASM binary for language server support. This extension has no binary — it's all tree-sitter queries.
The manifest in extension.toml pins a specific upstream grammar commit:
[grammars.helixql]
repository = "https://github.com/benwoodward/tree-sitter-helixql"
commit = "259d2b68c56ae40ef7d190919ed49e0f17d1f3ee"
Zed fetches and compiles this WASM grammar at install time. The extension itself never ships compiled grammar — that's the right call. Pinning to a commit hash means the extension is reproducible and won't silently break when the upstream grammar changes.
The meat is in languages/helixql/highlights.scm. It's 130 lines of tree-sitter capture patterns, and the ordering matters. Tree-sitter query files are evaluated top-to-bottom, with later rules overriding earlier ones only when both match the same node. The file uses that intentionally:
; Generic identifiers (fallback)
(identifier) @variable
(identifier_upper) @type
; ... later, more specific overrides ...
; Query name
(query_def name: (identifier) @function)
; Schema names
(node_def name: (identifier_upper) @type)
The generic (identifier) @variable catch-all comes first. Then specific field capture patterns using tree-sitter's named child syntax (name:, key:, field:) override the fallback for identifiers in known positions. This means any new syntax the grammar introduces will silently fall back to @variable rather than being unstyled — defensively correct.
One early commit had a bug worth noting: brackets.scm originally tried to match the inner quote characters of string literals as tree-sitter nodes. That fails because the upstream grammar wraps string literals in token(...), which makes the internal characters opaque to the query engine. Zed's CI would catch this at package time, but you wouldn't know until you pushed. That's what led to scripts/check.sh.
The check script reads the pinned commit hash directly from extension.toml:
GRAMMAR_COMMIT="$(awk -F\" '/^commit *=/ {print $2}' extension.toml)"
Then clones the grammar, generates it locally, and runs every .scm file against examples/sample.hx using the tree-sitter CLI. Same check as Zed's CI, runnable locally in under 10 seconds. If a query references a node type the grammar doesn't emit, it fails loudly before you push.
The outline.scm is minimal but complete — four patterns that give Zed's outline panel entries for every QUERY, N::, E::, and V:: definition:
(query_def
"QUERY" @context
name: (identifier) @name) @item
That's enough to get a working symbol list for navigation without needing a language server.
Using it
Install during development:
# In Zed: cmd-shift-p → "zed: install dev extension" → pick repo directory
# Then open any .hx file
Validate your query files before pushing:
npm i -g tree-sitter-cli # one-time
./scripts/check.sh
Output when everything is clean:
brackets.scm: ok
highlights.scm: ok
indents.scm: ok
outline.scm: ok
A sample .hx file that exercises every highlighted construct lives in examples/sample.hx — useful both as a test fixture and as a quick syntax reference.
Rough edges
The extension is highlight-only, and that's the right scope for a v0.0.1. But the real gap is that HelixDB has no LSP yet. Until it does, there's no hover types, no error underlining, no go-to-definition. Highlighting is the floor, not the ceiling.
The config.toml sets line_comments = ["// "] which is correct, but there's no block comment defined. HelixQL doesn't appear to have block comment syntax in the current grammar, so that's accurate — just worth noting if you're used to /* */ escapes.
The PREFILTER keyword was intentionally omitted — the upstream grammar defines a pre_filter rule but nothing references it, so the tree-sitter node is never emitted. Including it in highlights.scm would fail the query check. That's a grammar upstream issue, not something fixable here.
Bottom line
If you're writing HelixQL in Zed, this is the only option and it does what it needs to. The local CI parity via check.sh is the genuinely useful engineering decision here — it catches broken query references before they hit the upstream extension CI and lets you iterate without the push-wait-fail loop.
