No description
  • Zig 52.5%
  • Shell 30.3%
  • Lua 16.8%
  • Scheme 0.4%
Find a file
Oliver Nutter c2f96fb3b3 fix: disown persistent ZLE worker
Co-Authored-By: GPT-6 Luna <noreply@pi.dev>
Generated-By: pi 0.99.2
2026-10-05 21:02:29 +00:00
docs/superpowers docs: plan asynchronous ZLE responses 2026-09-29 12:27:11 -05:00
grammars feat: add optional grammar modules 2026-09-28 20:58:01 -05:00
lib fix: disown persistent ZLE worker 2026-10-05 21:02:29 +00:00
queries fix: avoid Python query predicates 2026-09-28 21:18:59 -05:00
src fix: resolve final Tree-sitter review findings 2026-09-29 15:48:51 -05:00
tests fix: disown persistent ZLE worker 2026-10-05 21:02:29 +00:00
themes feat: queue asynchronous ZLE responses 2026-09-29 13:32:58 -05:00
tools feat: add optional grammar modules 2026-09-28 20:58:01 -05:00
vendor fix: align scanner create prototypes 2026-10-02 18:22:00 +00:00
build.zig fix: enforce worker resource caps 2026-09-29 15:10:31 -05:00
build.zig.zon feat: add optional grammar modules 2026-09-28 20:58:01 -05:00
README.md docs: document asynchronous Tree-sitter ZLE 2026-09-29 15:10:55 -05:00
THIRD_PARTY_NOTICES.md fix: resolve final Tree-sitter review findings 2026-09-29 15:48:51 -05:00
treesitter4zsh.plugin.zsh fix: enforce async client limits and stale IDs 2026-09-29 13:48:35 -05:00
zig-version.txt build: add Zig Tree-sitter parser bindings 2026-09-28 14:48:47 -05:00

treesitter4zsh

A Tree-sitter syntax highlighter for the Zsh line editor (ZLE). It parses the current input as source text; it does not run, evaluate, source, expand, or probe commands or paths to decide how to highlight them.

Build and enable

Requires Zsh 5.9 or newer, Zig 0.16.0 (zig-version.txt), and a C compiler for the vendored Tree-sitter runtime and parsers. From this checkout:

zig build -Doptimize=ReleaseFast
zig build test --summary all

The installed helper is zig-out/bin/treesitter4zsh-worker. Build it before loading the plugin; loading never compiles or downloads anything. In an interactive Zsh session (for example, from .zshrc):

source /absolute/path/to/treesitter4zsh/treesitter4zsh.plugin.zsh

Sourcing from a noninteractive shell does nothing. The helper starts on the first eligible nonempty input change, not at source time. There is one persistent worker per interactive shell, not one process per edit. The line-pre-redraw hook sends requests without waiting for a response; a private zle -F -w callback applies a complete response to the still-current PREBUFFER/BUFFER and refreshes the display. Rapid edits coalesce to one in-flight request plus the newest pending snapshot.

Configuration

Set styles before sourcing (all keys use :treesitter4zsh:config):

zstyle ':treesitter4zsh:config' theme default
zstyle ':treesitter4zsh:config' worker-path /absolute/path/to/treesitter4zsh-worker
zstyle ':treesitter4zsh:config' response-timeout-ms 250
zstyle ':treesitter4zsh:config' max-buffer-length 4096
zstyle ':treesitter4zsh:config' max-injection-count 256
zstyle ':treesitter4zsh:config' max-embedded-bytes 65536
Setting Default Supported values
theme default Name of a local themes/<name>.ini (alphanumeric, _, -)
worker-path <plugin-dir>/zig-out/bin/treesitter4zsh-worker Executable helper path; quote spaces in Zsh
response-timeout-ms 250 1–60000 ms; maximum asynchronous request age, not a synchronous redraw wait
max-buffer-length 4096 1–4096 total Unicode scalars in PREBUFFER + BUFFER
max-injection-count 256 1–256 injected regions, not total returned highlight spans
max-embedded-bytes 65536 1–65536 cumulative parsed child-source bytes

Invalid numeric settings fall back to their defaults; the worker also rejects startup overrides above the hard caps. Theme files map semantic capture names to ZLE region_highlight styles, using themes/default.ini as the base. A missing helper reports one diagnostic and leaves ZLE usable.

The age limit is checked on redraw and response events, without a polling timer: an idle silent worker can remain until the next event or unload, but it never blocks a redraw. A response past the limit is discarded. On malformed output, EOF, or timeout, plugin-owned spans are cleared, the worker is stopped, and at most one automatic restart is permitted; the second failure disables highlighting until reload. Empty, pending, oversized, or changed input clears only plugin-owned spans; other plugins' region_highlight entries are retained.

Grammar and injection coverage

The default binary includes a Zsh host grammar plus these command-string injectors, selected only for statically identifiable script operands:

Command Selected grammar Qualification
perl -e / perl -E Perl Separated and attached script operands
sh -c, dash -c POSIX sh Supplemental POSIX grammar, not Dash-specific
bash -c Bash No POSIX mode
ksh -c Bash Approximate fallback, not Ksh validation
zsh -c Zsh Recursive injections supported within budgets

For example, sh -c 'zsh -c "perl -e print"' is parsed through the POSIX, Zsh, and Perl layers. Host quote delimiters retain their host styles; decoded static child spans are mapped back to visible input offsets, excluding PREBUFFER. Runtime substitutions, ambiguous expansions, and unsafe source maps are not evaluated or guessed: highlighting of that injection is omitted. Incomplete input and Tree-sitter ERROR nodes are normal while editing. Highlighting is not shell execution, syntax validation, command/path checking, or a guarantee that a script is valid for another shell.

The pinned grammars/catalog.json is metadata, not a set of automatically installed parsers. An optional Python grammar module can be installed only with an explicit operation:

zig build install-grammar -Dlanguage=python

This writes its library, query, and manifest under ${XDG_DATA_HOME:-$HOME/.local/share}/treesitter4zsh/grammars/python/. The helper validates metadata/ABI when an optional grammar is requested; missing modules do not prevent host highlighting. The shipped command-string injectors above do not select Python, so installing that module alone does not add Python command-string highlighting. No grammar build, installation, network fetch, or generated file creation happens when the plugin is passively sourced.

Verification and unload

zig build test --summary all
zig build --summary all
for file in treesitter4zsh.plugin.zsh lib/*.zsh; do zsh -f -n "$file" || exit; done
for test in tests/integration/test-*.zsh; do zsh -f -i "$test" || exit; done

The passive-load syscall test uses Linux strace; complete corpus fixtures in tests/fixtures/{zsh,injections}/ are checked with zsh -f -n. For the 1,000-edit ReleaseFast callback/response benchmark, run zsh -f -i tests/integration/benchmark-async.zsh after a ReleaseFast build.

Call treesitter4zsh_unload to clear the plugin's spans, remove its hook and owned fd widget/registration, stop and reap its worker, and release plugin functions. Shell exit also stops the worker. Caller-owned widgets, hooks, regions, and replacement fd handlers are not deleted: if a caller takes over the worker fd, a later restart fails closed rather than overwriting that handler. A caller handler retained on an fd closed by the plugin can receive an invalid-fd event; its owner is responsible for removing that handler.