A language server for Cap'n Proto .capnp schema files.
Speaks LSP over stdio. Wraps the official capnp compiler for authoritative
diagnostics and symbol resolution, and uses tree-sitter-capnp
for editor-resilient highlighting.
WARNING: This repository is 100% AI generated code and was intentionally done hackily and quickly to deliver value. It therefore should not be relied on.
- Diagnostics — parse and schema errors from
capnp compile, mapped to LSPDiagnostics with file/line/column ranges. - Go-to-definition for types, enums, annotations, including:
- cross-file via the compiler's
CodeGeneratorRequestsource info, usingalias redirects (local andReceiver.Membercross-file dotted refs),- the path string inside
import "...", - name-based fallback for cases the compiler doesn't track (e.g. type parameters
inside
List(T)).
- cross-file via the compiler's
- Hover — kind + name + the node's doc comment from
cgr.sourceInfo. - Semantic-token highlighting via tree-sitter-capnp's bundled queries, mapped
to standard LSP token types (built-ins get the
defaultLibrarymodifier). - Completion with cursor-context awareness:
- after
:/(/,→ built-in primitives + user types, - after
$→ annotations, - after
Namespace.→ members of the imported file (uses index, falls back to a surface scan when no nodes from that import survived to the CGR), - in unknown contexts → top-level keywords (
struct,enum,interface, …), - after
@→ the next valid field ordinal in the enclosing struct's ID space (scoped correctly across groups, unions, and nested structs).
- after
- Signature help for annotation applications (
$Foo.bar(field = :Type, …)) and generic instantiations (List(T),MyStruct(A, B)). - Formatting (
textDocument/formatting) — conservative whitespace normalisation derived from the KJ style guide and Kenton's canonical schemas:- 2-space indentation, brace-on-same-line,
name @N :Typecolon spacing, - blank line between top-level decls, trailing whitespace stripped, single final newline,
- doc-comment blocks re-indent with their declaration but contents are preserved verbatim (no paragraph reflow),
- hard configurable column limit (default 100): trailing inline comments
get pushed onto a new line when they push past it; long
$Annotation(...)chains break before each$; long generic argument lists break inside(...)one arg per line, - long lines that don't match any wrapper produce a
WARNINGdiagnostic, # capnpfmt: off/# capnpfmt: onmarkers preserve a region verbatim,- bails (returns no edits) on any parse error so broken buffers aren't destructively rewritten.
- 2-space indentation, brace-on-same-line,
- Live-buffer overlay — analysis runs on unsaved edits. The cached symbol index is retained across compile failures so completion and goto stay useful while you have a syntax error mid-edit.
- Rust toolchain (build only):
cargo,rustc. - A Cap'n Proto installation: the
capnpbinary on$PATHand itscapnp/schema.capnpavailable under one of the install's include directories (Homebrew, MacPorts, apt and most manual installs put it there automatically). Tested with 1.3.0.
build.rs regenerates the Rust bindings from the installed schema.capnp so
the server gets the latest startByte/endByte and FileSourceInfo accessors.
Override the search with CAPNP_SCHEMA=/path/to/schema.capnp if needed.
From crates.io (recommended):
# latest published release
cargo install capnprotols --locked
# pin to a specific version
cargo install capnprotols --locked --version 0.2.1Both binaries (capnprotols and capnpfmt) are installed under ~/.cargo/bin/.
cargo build --release
# binaries at target/release/{capnprotols,capnpfmt}Or install the working tree onto $PATH:
cargo install --path .A standalone formatter for .capnp schema files. Installed alongside the
language server.
# Format a file in place
capnpfmt schema.capnp
# Format multiple files
capnpfmt *.capnp
# Read stdin, write formatted output to stdout
capnpfmt < schema.capnp > formatted.capnp
# Check mode — exit 1 if any file isn't formatted (useful in CI)
capnpfmt --check *.capnp
# Custom column limit (default: 100)
capnpfmt --width 80 schema.capnpBails on parse errors (leaves the file unchanged) so it's safe to run on save or in pre-commit hooks.
capnpfmt honours .capnpfmtignore files (gitignore syntax) so you can
run it against a recursive glob without rewriting vendored or generated
schemas:
capnpfmt $(find . -name '*.capnp')For each input file, capnpfmt walks up its ancestor directories
collecting any .capnpfmtignore files until it hits a .git directory
(or the filesystem root). The collected rules are applied in the usual
gitignore order — deeper files can override shallower ones with !
whitelist patterns. Matched files are skipped silently.
Example .capnpfmtignore:
# Don't touch vendored schemas.
vendor/
# But do format our local fork.
!vendor/local-fork/Pass --no-ignore to bypass all .capnpfmtignore files for a single
invocation.
Settings are passed via initializationOptions on the initialize request.
JSON shape:
{
"compilerPath": "capnp", // path to the capnp binary; default "capnp" on $PATH
"importPaths": ["/abs/dir/one"], // extra -I paths for `import "/..."` resolution
"format": {
"enabled": true, // master switch for textDocument/formatting
"maxWidth": 100, // hard column limit (KJ style guide default)
"warnLongLines": true // diagnose lines we can't auto-wrap
}
}Standard import roots are auto-discovered on startup. The server probes:
- user-supplied
importPaths, <install_prefix>/includederived from the resolvedcapnpbinary,- capnp's hardcoded paths (
/usr/local/include,/usr/include), - common platform defaults (
/opt/homebrew/include,/opt/local/include).
Each non-user candidate is kept only if it actually contains
capnp/c++.capnp. This covers Homebrew (Apple Silicon and Intel), MacPorts,
apt, and most manual installs without configuration.
CAPNPROTOLS_LOG=info (or debug, trace) enables tracing-style logs on
stderr. The default is info. Logs go to stderr only — stdout is reserved for
the LSP framing.
The server speaks vanilla LSP over stdio with no custom extensions. Any LSP client works.
Add to your .vimrc:
let g:ycm_langauge_server += [
\ {
\ 'name': 'capnprotols',
\ 'cmdline': [ '/path/to/capnprotols' ],
\ 'filetypes': [ 'capnp' ],
\ },
]A minimal extension is included under extension/ — a thin
LSP client that launches the capnprotols binary. See its
README for build/install steps.
compiler.rsshells out tocapnp compile -o-against an overlay file written alongside the original (so relative imports still resolve), then path-remaps the compiler-reported overlay path back to the real file.index.rsdecodes the CGR into a per-file FSI table (sorted byte-ranges → resolved typeIds) plus a per-node table (kind, displayName, fields, generic parameters, doc comment, source byte range).aliases.rshandlesusing NAME = …and surface-scans top-level declarations for cases where a referenced file isn't represented in the current CGR.ordinals.rsbrace-tracks the buffer to find the enclosing struct and compute the next contiguous@<n>ordinal.semantic_tokens.rsruns tree-sitter-capnp'sHIGHLIGHTS_QUERYand emits LSP semantic tokens.server.rswires everything intotower-lspand detects cursor contexts (type / annotation / member / field-ordinal slots) for completion and signature help.
capnprotols follows SemVer. While the crate is on
0.x, breaking changes bump the minor version (0.1.0 → 0.2.0) and
backward-compatible changes bump the patch version (0.1.0 → 0.1.1).
Releases are cut by the Release workflow.
To publish a new version:
- Make sure
mainis green on CI and contains everything you want in the release. In particular thegrammar in syncjob must be passing — the published crate ships the vendoredgrammar/copy, not thevendor/tree-sitter-capnpsubmodule, which is excluded from the package. - From the GitHub Actions tab, run Release via Run workflow and
enter either an explicit version (e.g.
0.3.0, novprefix) or a bump level (patch,minor,major).
The workflow then:
- runs
cargo test; - bumps
versioninCargo.toml, refreshesCargo.lock, and rewrites the pinnedcargo installsnippet in this README; - runs a verifying
cargo publishas a sanity check; - commits the bump as
Release vX.Y.Z, tagsvX.Y.Z, and pushes both; - publishes to crates.io;
- creates a GitHub release with auto-generated notes.
Branch and version validation live in [package.metadata.release] in
Cargo.toml — allow-branch = ["main"] is what keeps releases to main,
and cargo release rejects a version or bump level it can't parse.
docs.rs picks up the new version from crates.io automatically — give it a few minutes and check https://docs.rs/capnprotols.
CARGO_REGISTRY_TOKENrepository secret — a crates.io API token from https://crates.io/me, scoped topublish-updateforcapnprotols.- The default
GITHUB_TOKENis enough for the commit/tag push and release creation, providedmaindoesn't have branch protection that blocks pushes fromgithub-actions[bot]. If it does, either relax the rule for the bot or swap in a fine-grained PAT.
MIT.
0 comments
log in to comment.