feat(lang): Nix language support (outline, semantic indexing, ast-grep)
Approved issue
Approved issue: cortexkit/aft#492 (design-approved)
What changed
Adds Nix as a supported language for:
- Outline — the entry is the file's top-level attrset: a lambda body, a
let … inbody (itsletbindings are listed at file level too), or an attrset reached throughwith/assert/parentheses or by applying a function to an attrset. Only an application's final argument is traversed; earlier arguments and list/if///expressions contribute nothing. The same rule applies to nested values, soconfig = lib.mkIf cond { … }nests its children. The outline lists the entry bindings, then exactly one level of nested attrsets or lambda bodies returning attrsets. Bindings are named by their dotted attrpath (config.services.demo.enable); function bindings carry a one-line header as the signature;inheritclauses are skipped; two top-level bindings with identical full names are both kept without expanding their children. - Zoom — resolves the same dotted paths, including paths through an enclosing outlined binding.
- AST search/replace —
AstGrepLang::Nixontree-sitter-nix0.3.0. Patterns are written with$NAME, which the_expando rewrites to_NAMEbecause the grammar rejectsµ. Complete Nix expressions match through the normal path; a standalone binding orinheritclause is not an expression, socompile_patternfalls back to a contextual pattern that selects the sole binding-set member's own kind (binding,inherit,inherit_from). This keeps sibling bindings intact on replace (a whole-attrset pattern would delete them), and a wrapper mixing members — two bindings, or a binding plus aninherit— is rejected rather than matched piecemeal. Because_is also a legal Nix identifier start, a key written_plus an all-uppercase name (_FOO) is read as a metavariable;_Foo,__functor, and_module.argsstay literal. - Semantic indexing —
.nixis in the extension policy and the chunk census label map. - Formatter —
nixfmtis the only formatter Nix resolves, and only through an explicitformatter.nixkey (no auto-detection;formatter.nix = "none"still disables formatting).nixfmtis accepted by the Rust config resolver, both plugin config schemas, and the generated JSON schema. Checker validation stays unsupported. - Agent surface —
nixadded to both harness plugin AST enums and the regeneratedsubc_tool_schemas.json+ tool-provider catalog goldens. - Registry sweep — indent (two spaces), inspect labels, callgraph-store labels (including
join::language_id, which the views plane requires even though Nix has no call edges), views lazy-read label map. - Docs — README language row and Nix caveat, the
formatter.nixkey indocs/config.md, theConfig::formatterdoc comment, and accepted-language lists plus error messages in the AST tools now naming every canonical language.
Unsupported for now (approved scope): imports, refactor, call graph. The call graph reports no Nix edges.
Dependency
tree-sitter-nix = "0.3.0" (nix-community, MIT, ~45 KB, ABI 13, external scanner.c). numtide has no crates.io release yet, so this is the published pin the issue approved; switch when a numtide crate publishes. 0.3.0 predates the |> pipe operator and bare inherit;, so a file using them parses with ERROR nodes and may outline partially. The workspace's tree-sitter 0.26.7 has a minimum compatible ABI of 13, so the grammar loads at the floor.
Checks
Re-run on 1cd536049:
cargo test -p agent-file-tools --lib nix_— 29 passed, of which 14 are Nix tests (the filter also matches 15 unrelatedunix*tests): detection, wrapper table, dotted outline, signatures, depth stop, inherit, applied-entry shapes, nested application values, full-name collisions, ast-grep contextual patterns (bindings, inherit clauses, multi-binding rejection), formatter candidates, indent, duplicates exclusion, complexity skip, semantic chunks, config resolver.cargo test -p agent-file-tools --test integration nix_test— 7 passed (outline/zoom/language label on a schema-valid flake fixture, ast search/replace sibling preservation, unsupported import/refactor, empty call-graph annotations, validation skip, views callgraph payload, dotted provider symbols).cargo fmt --all -- --check— clean.
Passed on the first commit 02104292b and unaffected by the follow-up diff, which touches only Nix paths and documentation; CI re-runs them:
cargo test -p agent-file-tools --test integration config_parityandis_semantic_indexed_extension— passed.cargo test -p agent-file-tools --lib full_catalog_goldens_and_digest_only_have_exact_identity— passed after regenerating the catalog fixtures.cargo test -p agent-file-tools --features semantic-chunk-census --test semantic language_label— passed.cargo test -p agent-file-tools --test integration format_integration_nixfmt_config_runs_nixfmt— passed (a configurednixfmtstub runs on write, so the formatter scenario covers invocation, not only candidate resolution).- Manual smoke check:
printf '%s\n' '{"id":"o","command":"outline","file":"flake.nix"}' | aft→ symbol list; swap"command":"ast_search","pattern":"$NAME = true;","lang":"nix","paths":["."]for structural search. bun run typecheck(root, all four packages) andbun scripts/build-tool-schemas.ts --check(opencode-plugin) — passed.
Corpus validation
Against a local NixOS configuration corpus (121 .nix files, 494,563 bytes, 13,929 lines), re-measured after the nested-application change:
| Measure | Result |
|---|---|
Files parsed with no ERROR/MISSING nodes | 121 of 121 |
Files whose files-mode outline entry reports language nix | 120 of 121 (files mode honours .gitignore) |
| Outline symbols | 2,344 (2,119 before nested application expansion) |
| Files with at least one nested symbol | 114 of 120 (113 before) |
| Files whose entry is an applied function | 66 of 121 parsed files (entry discovery untouched) |
| Files with zero symbols | 0 of 120 outlined files |
| Structural-search matches; dry-run rewrites succeeded | 335; 335 of 335 |
| Zoom on a nested dotted symbol | resolved the expected line |
Write with validate: "full" | Succeeds; validate_skipped_reason = unsupported_language |
Only aggregate counts and size are reported. Corpus paths, file names, and content stay on the machine it was found on.
Release binary size
target/release/aft, rustc 1.95.0, cargo build --release -p agent-file-tools:
| Build | target/release/aft bytes |
|---|---|
main at 7ca30b526 | 130,206,840 |
branch at 1cd536049 | 130,333,816 |
| delta | +126,976 (+0.10%) |
Notes
- The cfg-gated
aft_views_lazy_benchmarkbuild has a pre-existing type error atviews/lazy_read_benchmark.rs:318(&strvsString); the Nix arm there compiles once that is patched. Not introduced here. - An existing integration-test helper deploys the binary via
/bin/cat, which is absent on the NixOS host used for local verification; tests were run in a mount namespace with/bin/catprovided. CI is unaffected.