Skip to content

Commit a47a9d2

Browse files
timfennisclaude
andcommitted
feat(ext): add tree-sitter grammar and refresh TextMate grammar 🌳
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
1 parent 55a76e0 commit a47a9d2

19 files changed

Lines changed: 56861 additions & 2 deletions

‎ext/andy-cpp/CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Change Log
22

3+
## [Unreleased]
4+
5+
- Syntax highlighting for type annotations: built-in type names (`Int`, `String`, `List`, `Map`, …), the return-type arrow `->`, and `:` annotation separators
6+
- Highlight parameter names and type annotations inside function signatures
7+
- Detect the `continue` keyword and the `NaN` constant
8+
- Fixed augmented assignment: `^=` was mis-detected and `\=` (floor-division assign) was missing
9+
310
## [v0.3.0] (2026-02-28)
411

512
- Added configurable `andy-cpp.ndcPath` setting for specifying the path to the `ndc` binary (useful when `ndc` is not on the PATH used by VSCode, e.g. when installed via cargo in a fish shell)

‎ext/andy-cpp/syntaxes/andy-cpp.tmLanguage.json‎

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
{
1919
"include": "#functions"
2020
},
21+
{
22+
"include": "#types"
23+
},
2124
{
2225
"include": "#call"
2326
},
@@ -47,12 +50,26 @@
4750
}
4851
]
4952
},
53+
"types": {
54+
"patterns": [
55+
{
56+
"comment": "built-in type names used in annotations like `let x: Int` or `-> List<Int>`",
57+
"name": "support.type.andy-cpp",
58+
"match": "\\b(Any|Never|Bool|Number|Float|Int|Rational|Complex|String|Option|Sequence|List|Iterator|MinHeap|MaxHeap|Deque|Tuple|Map)\\b"
59+
}
60+
]
61+
},
5062
"functions": {
5163
"patterns": [
5264
{
5365
"comment": "function definition",
5466
"name": "meta.function.definition.andy-cpp",
5567
"begin": "\\b((pure)\\s)?(fn)\\s+([A-Za-z_][A-Za-z_0-9]*)",
68+
"patterns": [
69+
{
70+
"include": "#expression"
71+
}
72+
],
5673
"beginCaptures": {
5774
"2": {
5875
"name": "keyword.other.pure.andy-cpp"
@@ -78,6 +95,11 @@
7895
"comment": "anonymous function definition",
7996
"name": "meta.function.definition.anonymous.andy-cpp",
8097
"begin": "\\b((pure)\\s)?(fn)",
98+
"patterns": [
99+
{
100+
"include": "#expression"
101+
}
102+
],
81103
"beginCaptures": {
82104
"2": {
83105
"name": "keyword.other.pure.andy-cpp"
@@ -144,6 +166,11 @@
144166
"match": "\\bInf\\b",
145167
"name": "constant.numeric.infinity.andy-cpp"
146168
},
169+
{
170+
"comment": "Not a Number",
171+
"match": "\\bNaN\\b",
172+
"name": "constant.numeric.nan.andy-cpp"
173+
},
147174
{
148175
"comment": "booleans",
149176
"name": "constant.language.bool.andy-cpp",
@@ -193,7 +220,7 @@
193220
{
194221
"comment": "control flow keywords",
195222
"name": "keyword.control.andy-cpp",
196-
"match": "\\b(break|if|else|while|for|return)\\b"
223+
"match": "\\b(break|continue|if|else|while|for|return)\\b"
197224
},
198225
{
199226
"comment": "storage keywords",
@@ -214,6 +241,11 @@
214241
},
215242
"operators": {
216243
"patterns": [
244+
{
245+
"comment": "arrow operators (return type and short function body)",
246+
"name": "keyword.operator.arrow.andy-cpp",
247+
"match": "(->|=>)"
248+
},
217249
{
218250
"comment": "logical operators",
219251
"name": "keyword.operator.logical.andy-cpp",
@@ -227,7 +259,7 @@
227259
{
228260
"comment": "augmented assignment operators",
229261
"name": "keyword.operator.assignment.andy-cpp",
230-
"match": "((\\+\\+?|-|\\*|/|\\|\\^|&|\\||%%?|<<|>>|~|<>)=)"
262+
"match": "((\\+\\+?|-|\\*|/|\\\\|\\^|&|\\||%%?|<<|>>|~|<>)=)"
231263
},
232264
{
233265
"comment": "function augmented assignment opreators",
@@ -377,6 +409,11 @@
377409
"comment": "comma",
378410
"name": "punctuation.comma.andy-cpp",
379411
"match": ","
412+
},
413+
{
414+
"comment": "colon (type annotation / key separator)",
415+
"name": "punctuation.separator.colon.andy-cpp",
416+
"match": ":"
380417
}
381418
]
382419
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Node
2+
node_modules/
3+
4+
# Build artifacts (the generated parser in src/ IS committed)
5+
build/
6+
*.o
7+
*.so
8+
*.dylib
9+
*.wasm
10+
*.node
11+
12+
# Editor / OS
13+
.DS_Store
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# tree-sitter-andy-cpp
2+
3+
A [tree-sitter](https://tree-sitter.github.io/tree-sitter/) grammar for the
4+
**Andy C++** (`.ndc`) language.
5+
6+
Tree-sitter powers incremental, error-tolerant syntax trees used by editors such
7+
as **Neovim**, **Helix**, **Zed**, and **Emacs** for highlighting, structural
8+
selection, folding, and code navigation. (VS Code does not use tree-sitter for
9+
highlighting — it uses the TextMate grammar in `../andy-cpp/syntaxes/`.)
10+
11+
The grammar mirrors the precedence ladder and constructs implemented in
12+
`ndc_lexer` / `ndc_parser`. It is validated against the interpreter's full
13+
functional-test corpus: every valid `.ndc` program under
14+
`tests/functional/programs/` parses without errors.
15+
16+
## Layout
17+
18+
```
19+
grammar.js # the grammar definition
20+
tree-sitter.json # package metadata (generated/maintained by the CLI)
21+
queries/
22+
highlights.scm # syntax highlighting
23+
locals.scm # scopes & definitions (variables, params, functions)
24+
injections.scm # `#!` shebang line highlighted as bash
25+
test/corpus/ # tree-sitter test cases
26+
src/ # generated parser (run `tree-sitter generate`)
27+
```
28+
29+
## Developing
30+
31+
Requires Node.js. The tree-sitter CLI is a dev dependency.
32+
33+
```bash
34+
cd ext/tree-sitter-andy-cpp
35+
npm install # installs tree-sitter-cli
36+
npx tree-sitter generate # regenerate src/parser.c from grammar.js
37+
npx tree-sitter test # run test/corpus
38+
npx tree-sitter parse path.ndc # dump the parse tree for a file
39+
```
40+
41+
Re-run `generate` after every edit to `grammar.js`. Commit the regenerated
42+
`src/` so consumers can build without the CLI.
43+
44+
### Re-validating against the interpreter corpus
45+
46+
```bash
47+
cd ext/tree-sitter-andy-cpp
48+
fail=0
49+
for f in $(find ../../tests/functional/programs -name '*.ndc'); do
50+
npx tree-sitter parse -q "$f" >/dev/null 2>&1 || { echo "ERR $f"; fail=1; }
51+
done
52+
[ $fail -eq 0 ] && echo "all valid programs parse"
53+
```
54+
55+
The only files that report errors are the interpreter's deliberate
56+
`// expect-error:` cases (malformed input) — that is the expected outcome.
57+
58+
## Editor integration
59+
60+
The parser's language name is **`andy_cpp`** (the symbol exported by the
61+
generated parser is `tree_sitter_andy_cpp`).
62+
63+
The instructions below drive each editor's **built-in** tree-sitter runtime, so
64+
they don't depend on a plugin manager or a specific nvim-treesitter version.
65+
Building the parser needs Node.js and a C compiler.
66+
67+
### Optional: a language server
68+
69+
The interpreter ships an LSP server, started with `ndc lsp` over stdio. It
70+
provides hover (inferred types), completion, go-to-definition, document symbols
71+
and inlay hints. Install the `ndc` binary so it's on your `PATH`:
72+
73+
```bash
74+
cargo install --git https://github.com/timfennis/andy-cpp
75+
```
76+
77+
The editor sections below wire this up alongside highlighting.
78+
79+
### Neovim
80+
81+
Neovim has a built-in tree-sitter runtime, so nvim-treesitter is not required to
82+
load this grammar.
83+
84+
1. Build the parser and install it with the queries where Neovim's runtimepath
85+
can find them (the output file must be named `andy_cpp.so`):
86+
87+
```bash
88+
cd ext/tree-sitter-andy-cpp
89+
npm install
90+
mkdir -p ~/.config/nvim/parser ~/.config/nvim/queries/andy_cpp
91+
npx tree-sitter build -o ~/.config/nvim/parser/andy_cpp.so
92+
cp queries/*.scm ~/.config/nvim/queries/andy_cpp/
93+
```
94+
95+
2. Add to your config (`init.lua`):
96+
97+
```lua
98+
-- Treat .ndc files as the `andy_cpp` filetype.
99+
vim.filetype.add({ extension = { ndc = "andy_cpp" } })
100+
101+
-- Start tree-sitter highlighting for those buffers.
102+
vim.api.nvim_create_autocmd("FileType", {
103+
pattern = "andy_cpp",
104+
callback = function(args)
105+
pcall(vim.treesitter.start, args.buf, "andy_cpp")
106+
end,
107+
})
108+
109+
-- Language server (Neovim 0.11+). Skip if you didn't install `ndc`.
110+
vim.lsp.config("ndc_lsp", {
111+
cmd = { "ndc", "lsp" },
112+
filetypes = { "andy_cpp" },
113+
root_markers = { ".git" }, -- falls back to the file's directory
114+
})
115+
vim.lsp.enable("ndc_lsp")
116+
117+
-- Optional: show inlay hints once the server attaches.
118+
vim.api.nvim_create_autocmd("LspAttach", {
119+
callback = function(args)
120+
local client = vim.lsp.get_client_by_id(args.data.client_id)
121+
if client and client.name == "ndc_lsp" then
122+
pcall(vim.lsp.inlay_hint.enable, true, { bufnr = args.buf })
123+
end
124+
end,
125+
})
126+
```
127+
128+
Rebuild (step 1) after each `tree-sitter generate`, re-copy the queries after
129+
editing them, then restart Neovim. After rebuilding the `ndc` binary, reload the
130+
server with `:LspRestart`.
131+
132+
> **Already map `.ndc` to a different filetype?** (for example, via an existing
133+
> `ftdetect` rule.) Keep that filetype, drop the `vim.filetype.add` call, and
134+
> point the parser at it with
135+
> `vim.treesitter.language.register("andy_cpp", "<your_filetype>")`. Use
136+
> `<your_filetype>` as the autocmd `pattern` and in the LSP `filetypes` list.
137+
138+
### Helix
139+
140+
Helix has built-in tree-sitter and LSP support. Add to
141+
`~/.config/helix/languages.toml`:
142+
143+
```toml
144+
[[language]]
145+
name = "andy-cpp"
146+
scope = "source.andy-cpp"
147+
file-types = ["ndc"]
148+
comment-tokens = ["//"]
149+
indent = { tab-width = 4, unit = " " }
150+
language-servers = ["ndc-lsp"] # omit if you didn't install `ndc`
151+
152+
[language-server.ndc-lsp]
153+
command = "ndc"
154+
args = ["lsp"]
155+
156+
[[grammar]]
157+
name = "andy-cpp"
158+
source = { git = "https://github.com/timfennis/andy-cpp", subpath = "ext/tree-sitter-andy-cpp" }
159+
```
160+
161+
Fetch and build the grammar, then install the queries:
162+
163+
```bash
164+
hx --grammar fetch
165+
hx --grammar build
166+
mkdir -p ~/.config/helix/runtime/queries/andy-cpp
167+
cp ext/tree-sitter-andy-cpp/queries/*.scm ~/.config/helix/runtime/queries/andy-cpp/
168+
```
169+
170+
## Known limitations
171+
172+
- **Raw strings with embedded quotes** (`r#"he said "hi""#`) are not modelled;
173+
the token stops at the first `"`. Plain raw strings (`r"..."`, `r#"..."#`
174+
without inner quotes) work. None appear in the current corpus.
175+
- **Doubly-nested generics** in type annotations (`List<List<Int>>`) can
176+
mis-tokenize the closing `>>`. Single-level generics (`Map<String, Int>`,
177+
`Option<Any>`) are fine.
178+
- **Named augmented assignment** (`acc max= x`) is not highlighted specially, to
179+
avoid mis-tokenizing space-free comparisons like `a==b`. Symbolic compound
180+
operators (`+=`, `%%=`, `<>=`, …) are fully supported.

0 commit comments

Comments
 (0)