ajhahn.de
← Flash
Markdown 143 lines
# Flash language — Neovim / Vim support

Two independent pieces: regex **syntax highlighting** (works in any
Vim/Neovim, zero build) and the **language server** `flashd` (Neovim's
built-in LSP client, live diagnostics).

## Language server (flashd)

`flashd` speaks LSP over stdio and publishes compiler diagnostics: parse
errors as you type, semantic findings on save — straight from the
self-hosted compiler frontend running in-process, so the messages are
exactly what `flashc` would say.

Build it once (`zig build lsp` puts it at `zig-out/bin/flashd`), then add
to your `init.lua`, with the `cmd` path pointing at your clone:

```lua
vim.api.nvim_create_autocmd("FileType", {
  pattern = "flash",
  callback = function(args)
    vim.lsp.start({
      name = "flashd",
      cmd = { "/path/to/Flash/zig-out/bin/flashd" },
      root_dir = vim.fs.dirname(args.file),
    })
  end,
})
```

Open a `.flash` file with an error and the squiggle appears; fix it and
the squiggle clears (`vim.diagnostic` defaults apply — `:h vim.diagnostic`
to style it). The server checks each file on its own — no project-wide
analysis yet — and navigation features (hover, go-to-definition) are
planned for a later phase.

## Format on save

`flashc fmt` rewrites a file to canonical Flash — there is exactly one
accepted rendering, and the earlier a file is canonical the less diff
churn it causes later. Formatting on save keeps every buffer canonical
from the start.

`flashc fmt` formats in place on disk, so the recipe routes the unsaved
buffer through a temp file on `BufWritePre`: the buffer is formatted
*before* the write, and what lands on disk is already canonical. Add to
your `init.lua`, with the path pointing at your clone:

```lua
vim.api.nvim_create_autocmd("BufWritePre", {
  pattern = "*.flash",
  callback = function(args)
    local lines = vim.api.nvim_buf_get_lines(args.buf, 0, -1, false)
    local tmp = vim.fn.tempname() .. ".flash"
    vim.fn.writefile(lines, tmp)
    vim.fn.system({ "/path/to/Flash/zig-out/bin/flashc", "fmt", tmp })
    if vim.v.shell_error == 0 then
      local formatted = vim.fn.readfile(tmp)
      if not vim.deep_equal(lines, formatted) then
        vim.api.nvim_buf_set_lines(args.buf, 0, -1, false, formatted)
      end
    end
    vim.fn.delete(tmp)
  end,
})
```

A file with a parse error is left untouched — the formatter refuses
rather than destroys, and `vim.v.shell_error` guards the buffer from
being replaced on a refusal. The diagnostics squiggle from `flashd`
tells you what to fix.

## Syntax highlighting

Regex syntax highlighting for `.flash` files via a classic Vim syntax file.
Zero build, zero plugin manager: it links Flash tokens to the standard highlight
groups (`Keyword`, `Type`, `String`, `Comment`, `Function`, `Number`, …) so your
colorscheme paints them — `.flash` files look like every other language.

Three files:

- `ftdetect/flash.vim` — associates `*.flash` with the `flash` filetype.
- `syntax/flash.vim` — the highlighting rules.
- `ftplugin/flash.vim` — forces the regex syntax over treesitter for `.flash`
  buffers (see "Why regex, not treesitter" below). Optional for a plain Neovim;
  **required** if your config (e.g. NvChad) auto-starts a treesitter highlighter
  for `flash`.

## Install (Neovim)

Symlink all three into your Neovim config so they survive updates. Run from the
repository root so `$PWD` resolves correctly:

```sh
mkdir -p ~/.config/nvim/syntax ~/.config/nvim/ftdetect ~/.config/nvim/ftplugin
ln -s "$PWD/editors/nvim/syntax/flash.vim"   ~/.config/nvim/syntax/flash.vim
ln -s "$PWD/editors/nvim/ftdetect/flash.vim" ~/.config/nvim/ftdetect/flash.vim
ln -s "$PWD/editors/nvim/ftplugin/flash.vim" ~/.config/nvim/ftplugin/flash.vim
```

Open any `examples/*.flash` file — it highlights immediately. Check detection
with `:set filetype?` (expect `filetype=flash`).

For classic Vim, use `~/.vim/` instead of `~/.config/nvim/`.

## Uninstall

```sh
rm ~/.config/nvim/syntax/flash.vim ~/.config/nvim/ftdetect/flash.vim ~/.config/nvim/ftplugin/flash.vim
```

Removing just `ftplugin/flash.vim` reverts standalone `.flash` files to whatever
your config did before (e.g. the treesitter-zig reuse).

## Why regex, not treesitter

Flash has no treesitter parser of its own. A common workaround is to reuse the
**zig** parser, since Flash is Zig-shaped. It mostly works — but Flash-specific
syntax (`use X link Y` imports, `:=`) is a parse error to the zig grammar: on a
real Flash file like `examples/fsh.flash` that is ~0.5 ERROR nodes per line, and
treesitter leaves those regions uncolored.

So standalone `.flash` files use this **regex** syntax instead: coarser than a
real parser (no semantic function/parameter distinction), but Flash-exact with
zero parse errors — every keyword, type, string, and `#builtin` colored. The
`ftplugin` stops treesitter and switches on `syntax=flash` for `flash` buffers.

This is scoped to `filetype=flash` buffers only. If you also reuse the zig
parser for ```flash code fences inside markdown (e.g. a tutorial), that is a
separate injection path on a `markdown` buffer and is **not** affected.

## Tree-sitter (optional upgrade)

A real `tree-sitter-flash` grammar (structural *and* Flash-exact: better nesting,
injections, incremental reparse) is the nicer long-term path, but needs a
`grammar.js`, `tree-sitter generate`, a C toolchain, and an `nvim-treesitter`
parser registration. Not built yet; ask if you want it — it would supersede this
regex syntax.

## Source of truth

The token set mirrors `src/token.zig` and `src/lexer.zig`. On a new keyword or
literal form, update `syntax/flash.vim` to match.