noyalib-lsp
Language Server Protocol implementation for noyalib — YAML formatting, validation, and hover information delivered to any LSP-aware editor over stdio JSON-RPC.
Contents
- Install — Cargo, distro packages
- Requirements — toolchain floor, platforms, the core pin
- Quick Start — smoke test
- Why this approach? — design rationale
- Surface — LSP methods supported
- Editor configuration — VS Code, Zed, Neovim, Helix, Sublime
- Examples — JSON-RPC scripts
- Performance — formatter cost
- When not to use noyalib-lsp
- Documentation
- License
Install
cargo install noyalib-lsp
Releases ship the crate archive with a CycloneDX SBOM and
sigstore bundles. Pre-built binary tarballs and distro packages
are not published yet; cargo install is the supported path.
MSRV: Rust 1.89.0, the floor shared across the noyalib family and the
maintainer’s other Rust crates (noyalib ADR-0014). The
toolchain-driven floor is lower: criterion 0.8 (the benchmark dev-dependency) declares
rust-version = 1.86, so cargo check --all-targets and the
bench suite fail on 1.85 (criterion@0.8.2 requires rustc 1.86),
though cargo check --lib still builds. We publish the number we
verify.
Requirements
- Rust 1.89.0 or newer to build from source:
rust-versionin the manifest, enforced by themsrv-coreCI job on every push. - Any tier-1 platform. CI runs the tests on Linux, macOS, and Windows with the stable, beta, and nightly toolchains; stable is the gate, beta and nightly are early warning.
- The matching core. This crate pins
noyalibat the identical=0.0.Xand releases in lockstep with it; Cargo resolves that pin for you. - An LSP client (any editor in
docs/editor-setup.md); the server speaks stdio withContent-Lengthframing.
Quick Start
# Smoke test — drives a one-shot LSP handshake over stdio.
noyalib-lsp --version
# As a child process spawned by your editor (the typical path).
# See "Editor configuration" below for VS Code / Zed / Neovim /
# Helix / Sublime examples.
Why this approach?
The market has two YAML language servers (yaml-language-server,
taplo-style hybrids) and both make tradeoffs noyalib-lsp avoids:
- Byte-faithful formatting.
textDocument/formattingruns through noyalib’s lossless CST. An already-canonical document produces an emptyTextEdit[]— your editor doesn’t churn whitespace on save. Comments stay where they were; indent width follows the file’s dominant style; only quoting and inter-key whitespace normalise. - Real diagnostics. Parse errors flow through
textDocument/publishDiagnosticswith line / column locations the editor’s gutter can highlight directly. No best-effort parsers, no recovery hand-waving. - Schema-aware hover. When a JSON Schema is attached, hover surfaces the resolved field type. Schema descriptions land in the hover card in a follow-up.
- Stdio transport. Standard
Content-Length-framed JSON-RPC 2.0. Works with every LSP-compliant client; no client-specific protocol extensions. - Pure-Rust, zero
unsafe. Same#![forbid(unsafe_code)]guarantee as the noyalib core library.
The whole thing is ~5 KLOC of Rust; the heavy lifting is in the
noyalib library, the LSP wrapper just bridges JSON-RPC to the
library’s CST + parser surface.
Surface
| LSP method | What it does |
|---|---|
initialize / initialized / shutdown / exit | Full LSP lifecycle handshake. |
textDocument/didOpen / didChange / didClose | Full-text document sync (TextDocumentSyncKind = 1). |
textDocument/publishDiagnostics | Parse-error diagnostics emitted on every open + change. Line / column locations. |
textDocument/formatting | Full-document TextEdit[] from the CST formatter. Empty array when the document is already canonical. |
textDocument/hover | Markdown card with cursor position + document type. Schema-driven descriptions tracked for follow-up. |
Server capabilities response includes textDocumentSync = 1,
documentFormattingProvider = true, hoverProvider = true.
Future capabilities (rangeFormatting, documentSymbols,
codeActions) are gated behind the same lossless-CST surface
in the library.
Editor configuration
Visual Studio Code
The extension lives in editors/vscode. Once the
listing is live it installs from the Marketplace:
ext install sebastienrousseau.noyalib
CI also packages a .vsix on every push, so you can download it from a
workflow run (or build it with npm install && npm run package in that
directory) and use “Extensions: Install from VSIX…”. Either way it
starts the noyalib-lsp binary on your PATH; point at another one
with:
{
"noyalib.path": "/usr/local/bin/noyalib-lsp",
"[yaml]": {
"editor.defaultFormatter": "sebastienrousseau.noyalib",
"editor.formatOnSave": true
}
}
Zed
~/.config/zed/settings.json:
{
"languages": {
"YAML": {
"language_servers": ["noyalib-lsp"],
"format_on_save": "on"
}
},
"lsp": {
"noyalib-lsp": {
"binary": { "path": "noyalib-lsp" }
}
}
}
Neovim (via nvim-lspconfig)
require("lspconfig.configs").noyalib = {
default_config = {
cmd = { "noyalib-lsp" },
filetypes = { "yaml" },
root_dir = require("lspconfig.util").find_git_ancestor,
},
}
require("lspconfig").noyalib.setup {
on_attach = function(_, bufnr)
vim.api.nvim_create_autocmd("BufWritePre", {
buffer = bufnr,
callback = function() vim.lsp.buf.format() end,
})
end,
}
Helix
~/.config/helix/languages.toml:
[[language]]
name = "yaml"
language-servers = ["noyalib-lsp"]
auto-format = true
[language-server.noyalib-lsp]
command = "noyalib-lsp"
Sublime Text (via LSP package)
~/.config/sublime-text/Packages/User/LSP.sublime-settings:
{
"clients": {
"noyalib-lsp": {
"enabled": true,
"command": ["noyalib-lsp"],
"selector": "source.yaml"
}
}
}
Examples
Editor-driving demos under
examples/:
| Script | What it shows |
|---|---|
handshake.sh | One-shot initialize / initialized / shutdown / exit round-trip. Smoke test for protocol compliance. |
format-on-save.sh | didOpen → textDocument/formatting. Returns the TextEdit[] an editor would apply on save. |
hover-cursor.sh | didOpen → textDocument/hover at a specific (line, column). |
Each script pipes a sequence of Content-Length-framed JSON-RPC
messages into noyalib-lsp over stdio and prints the response
stream. POSIX-shell only — no jq, no node dependencies.
chmod +x crates/noyalib-lsp/examples/*.sh
crates/noyalib-lsp/examples/handshake.sh
Performance
textDocument/formatting for a 1 MiB YAML document on Apple
M-series ≈ 12 ms (the same wall-clock as noyafmt --write
since both share the CST). For per-keystroke formatting under
human-perceivable latency, the server returns an empty
TextEdit[] when the document is already canonical, so the
editor avoids a round-trip on every didChange after a save.
textDocument/publishDiagnostics runs on every didChange;
parse cost on a freshly-edited buffer is dominated by the byte
range that changed, not the buffer size.
When not to use noyalib-lsp
- You want OpenAPI / AsyncAPI / Kubernetes-CRD hover docs out
of the box. That comes from the JSON Schema attached to
the buffer. noyalib-lsp doesn’t ship a schema registry; you
point it at a schema explicitly via the
yaml.schemas(style) settings exposed by your editor. - You need WebDAV-style multi-document operations
(
workspace/applyEditfor cross-file refactor). Not implemented yet; tracked as a v0.1.x capability extension.
Documentation
The four entry points, identical across every repo in the family:
-
User Manual — this crate’s rendered book: its guides, architecture, and release notes; the family manual for the core library is at https://sebastienrousseau.github.io/noyalib/manual/
-
API reference — rustdoc on docs.rs
-
Developer docs — this repo’s dev entry point, pointing at the family guide
-
Ecosystem map — the six crates, the lockstep model, the scorecard
-
Engineering policies (MSRV, SemVer, security, performance, concurrency, platform support, feature flags):
docs/POLICIES.md -
Security policy:
SECURITY.md -
API reference: https://docs.rs/noyalib-lsp
-
Editor setup (VS Code, Neovim, Emacs, Helix, Zed, Sublime):
docs/editor-setup.md -
Protocol coverage (which LSP methods are implemented):
docs/protocol-coverage.md -
Workspace README: https://github.com/sebastienrousseau/noyalib#readme
-
LSP specification: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/
Conformance
Every push runs the official yaml-test-suite
through this server’s diagnostics, from the same vendored suite and the same
core commit as the noyalib core: 406 of 406 (valid cases produce no
diagnostic, invalid cases produce one at the failing position). A two-document
configuration that uses most of YAML at once (tests/fixtures/ultra-complex/)
opens with no diagnostics. Details and the family table:
noyalib.com/conformance.
License
Dual-licensed under Apache 2.0 or MIT, at your option.