Files
zed-p8/README.md
T
kistaro c8ad7e74e7 Document line-significance limitations in the Pico-8 Lua grammar
PICO-8's shorthand `if (cond) stmt [else stmt]` is line-bounded, but
tree-sitter has no built-in newline awareness. Without an external
scanner ( the same mechanism tree-sitter-python uses for INDENT /
DEDENT / NEWLINE ), the grammar greedily binds `else` to the nearest
`if` and takes only one consequence statement for the shorthand body.
Token classification is unaffected, so syntax highlighting renders
identically to a correct parse; only auto-indent and semantic
selection are subtly off, in a code pattern that is uncommon in real
PICO-8 code.

New `grammars/pico-8-lua/KNOWN_LIMITATIONS.md` walks through both
incorrect cases ( the dangling-else mis-bind and the multi-statement
shorthand body ), tabulates which Zed features are and aren't
affected, and sketches the fix. README cross-links it from the
"Known limitations" block and adds it as a prerequisite to the v0.3
LSP work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:23:50 -07:00

9.6 KiB

zed-p8

A Zed extension for the PICO-8 fantasy console. Reasonable editor support for the entire .p8 cartridge file format and for PICO-8's Lua dialect — including the parts where PICO-8 deviates from standard Lua 5.2 (compound assignments, ? print shorthand, single-line if (cond) ..., !=, 0b... binary literals, integer divide \, peek operators @/%/$, the rotate/logical-shift family <<> / >>< / >>>, and #include).

Status — v0.2

Working today:

  • tree-sitter-p8-cart ( grammars/p8-cart/ ) — parses the .p8 cartridge container: the magic header line, version line, and the named sections __lua__, __gfx__, __gff__, __label__, __map__, __sfx__, __music__. Unknown __name__ markers fall through to unknown_section.
  • tree-sitter-pico8-lua ( grammars/pico-8-lua/ ) — fork of tree-sitter-grammars/tree-sitter-lua with the PICO-8 dialect added. Handles every dialect form the manual documents: see Dialect coverage below. Upstream attribution is preserved in grammars/pico-8-lua/UPSTREAM-LICENSE.md.
  • Pico-8 Cartridge language ( languages/pico-8-cart/, suffix .p8 ) — config, marker highlights, outline view, and an injection that hands the __lua__ body to the Pico-8 Lua grammar.
  • Pico-8 Lua language ( languages/pico-8-lua/, suffix .p8lua ) — config, dialect-aware highlights with PICO-8 builtins recognized, brackets, indents, outline.

The .p8lua suffix is the convention for bare Pico-8 Lua source files — the ones pulled into a cart via #include. Plain .lua files are intentionally not claimed by this extension, so users who keep stock Lua files alongside their PICO-8 work continue to get standard Lua treatment.

Dialect coverage

Feature Status
!= ( alias for ~= )
Compound assignment: += -= *= /= %= \= ^= ..= &= |= ^^= <<= >>= >>>= <<>= >><=
Integer divide \ and modulo % ( binary )
Bitwise XOR ^^ ( binary, in addition to upstream's ~ )
Logical shift right >>>, rotate left <<>, rotate right >><
Hex literals with fractional part: 0x11.4000
Binary literals: 0b1010
Memory peek prefix unary: @addr, %addr, $addr
Single-line if (cond) stmt [else stmt] ( no then/end )
Single-line while (cond) stmt ( no do/end )
? print shorthand statement
#include path directive
_init / _update / _update60 / _draw highlighted as builtins

Known limitations

  • Line-significant dialect features are not modeled. PICO-8's shorthand if (cond) stmt [else stmt] is line-bounded — a later-line else belongs to an enclosing standard if, not the shorthand. Without an external scanner, the grammar can't see newlines, so it greedily binds else to the nearest if ( the C / Java convention ) and treats a multi-statement single-line shorthand body as one statement plus a sequence of unconditional follow-ups. The parse is structurally wrong but tokens still classify correctly, so syntax highlighting renders identically to a correct parse; only auto-indent and semantic selection are subtly affected. Full write-up: grammars/pico-8-lua/KNOWN_LIMITATIONS.md. Slated for v0.3 work alongside LSP integration ( which needs a correct AST ).
  • No language server. No completion, hover docs, or diagnostics for PICO-8 builtins yet — only a static function.builtin highlight on recognized names. See Roadmap.
  • No .p8.png support. Only the plain-text .p8 format is handled — the PNG-steganography variant is out of scope for a text-focused IDE extension.
  • Hex sections are unhighlighted blobs. __gfx__, __map__, __sfx__, __gff__, __music__, __label__ parse as opaque line bodies. Roadmap v0.4 covers per-section highlighters.

Repository layout

zed-p8/
  extension.toml                  ← Zed extension manifest
  package.json                    ← workspace root; hosts tree-sitter-cli
  grammars/
    p8-cart/                      ← cart-format tree-sitter grammar
      grammar.js
      tree-sitter.json
      src/                        ← generated parser ( committed )
    pico-8-lua/                   ← Pico-8 Lua dialect tree-sitter grammar
      grammar.js
      tree-sitter.json
      package.json                ← marks this dir as ESM for node
      src/                        ← generated parser + scanner.c ( committed )
      UPSTREAM-LICENSE.md         ← MIT, tree-sitter-lua by Munif Tanjim
  languages/
    pico-8-cart/                  ← Pico-8 Cartridge language files
      config.toml
      highlights.scm
      injections.scm              ← injects pico-8-lua into __lua__ body
      outline.scm
    pico-8-lua/                   ← Pico-8 Lua language files
      config.toml
      highlights.scm
      brackets.scm
      indents.scm
      injections.scm
      outline.scm
  examples/
    hello.p8                      ← minimal test cart
  references/                     ← upstream PICO-8 manual + Zed doc links

Both grammars live in subdirectories of this same repository. Zed's [grammars.*] block supports a path field, so the extension manifest points each grammar at this repo's git URL plus the relevant subdir.

Local development

Prerequisites: Node.js ( for tree-sitter-cli ) and Zed. Rust is not required unless / until we add a language-server harness ( v0.3 ).

npm install                        # one-time, installs tree-sitter-cli

Edit a grammar and reload

  1. Edit grammars/<name>/grammar.js.

  2. Regenerate from the grammar's directory:

    ( cd grammars/p8-cart   && npx tree-sitter generate )
    # or
    ( cd grammars/pico-8-lua && npx tree-sitter generate )
    
  3. Sanity-check by parsing a sample file:

    ( cd grammars/p8-cart   && npx tree-sitter parse ../../examples/hello.p8 )
    ( cd grammars/pico-8-lua && npx tree-sitter parse path/to/file.p8lua )
    
  4. Commit the regenerated src/parser.c along with the grammar change. Zed clones the grammar repo at the SHA in extension.toml, so changes only take effect after a commit.

  5. Update the rev field of the affected [grammars.*] block(s) in extension.toml to the new SHA, then in Zed run zed: install dev extension ( or Install Dev Extension on the Extensions page ) and select this directory. Reinstall after every commit that should be picked up.

    Logs: zed: open log. Run zed --foreground for live stdout.

Edit only language queries

Files under languages/*/ ( highlights.scm, injections.scm, etc. ) are loaded directly by Zed — no regeneration step needed. Reinstall the dev extension to pick up changes.

Tests

Sample carts live under examples/; parse them directly with tree-sitter parse <file> for ad-hoc checks.

The cart grammar has a corpus under grammars/p8-cart/test/corpus/ — run ( cd grammars/p8-cart && npx tree-sitter test ). The corpus covers the empty-section skeleton, normal Lua content, the case where a Lua identifier resembles a section marker ( e.g. local __foo__ = 1 must remain a line, not be re-tokenized as a marker ), and the fallback unknown_section rule.

Roadmap

v0.3 — Language server integration

Prerequisite: an external scanner for tree-sitter-pico8-lua so the shorthand-if and shorthand-while bodies are line-bounded the way PICO-8 defines them. See grammars/pico-8-lua/KNOWN_LIMITATIONS.md. LSP features that walk the AST ( unreachable-code lint, goto-definition through a conditional branch ) need correct structure.

Then wire up japhib/pico8-ls ( or whichever PICO-8 LSP is most maintained at the time ) for:

  • Completion of PICO-8 builtins ( spr, circfill, btn, flr, … ).
  • Signature help and hover docs sourced from the manual.
  • Cart-aware analysis — the LSP already understands .p8 section markers and only analyzes the __lua__ body.
  • Per-cart diagnostics.

This requires a Rust component ( the zed_extension_api crate ) that downloads the LSP binary and defines language_server_command. See Zed's developing-extensions docs.

v0.4 — Polish

  • LuaCATS / EmmyLua stub file enumerating PICO-8's ~110 globals, for users who'd rather wire up lua-language-server against their #include-d .p8lua files.
  • Per-section highlighters for the hex blocks: __gfx__ colored by palette index, __sfx__ / __music__ parsed as note streams, __map__ as tile indices, __gff__ as flag bytes.
  • Snippets for common idioms ( the _init / _update / _draw triad, for x=0,127 do … end, palette swap setup, etc. ).

References

License

The cart grammar and the Zed extension files are 0BSD ( see LICENSE ). The PICO-8 Lua grammar is a fork of MIT-licensed tree-sitter-lua; the upstream license is preserved at grammars/pico-8-lua/UPSTREAM-LICENSE.md and applies to the derived files in that directory.