carve-lsp
August 18, 2026 · View on GitHub
Language server (LSP) for Carve markup documents.
Provides editor intelligence for .crv files via the
Language Server Protocol.
Install
npm install -g @markup-carve/carve-lsp
Or run without installing:
npx @markup-carve/carve-lsp --stdio
The server communicates over stdio (--stdio flag).
Supported capabilities
| Capability | Details |
|---|---|
| Diagnostics | Push and LSP 3.17 pull diagnostics for syntax, migration hazards, silent failures, include failures, and invalid table metadata |
| Symbols | Nested document outline plus queryable workspace symbols, including included files |
| Hover | Markup, cross-reference, include, citation, and table-marker explanations |
| Completion | Admonitions, attributes, semantic spans, citations, references, workspace anchors, and contained include paths/fragments |
| Navigation | Document links, go-to-definition, highlights, and references for anchors, captions, footnotes, citations, and link labels |
| Rename | Workspace-wide, namespace-aware rename; generated heading ids become explicit when renamed |
| Code actions | Migration and lint quick-fixes, table marker repair, and creation of missing definitions |
| Code lens | Reference counts for anchors, captions, citations, footnotes, and link labels |
| Selection and folding | Syntax-aware selection expansion and folding, with tolerant fallbacks during incomplete edits |
| Formatting | Conservative document/range formatting, on-type continuation, or explicit migration formatting |
| Semantic tokens | Full, ranged, and delta token updates, including table structure and alignment metadata |
| Inlay hints | Generated heading ids (configurable) |
| Commands | carve.previewHtml and carve.showAst return an open document's rendered HTML or AST JSON |
| File inclusion | Resolves {{ path }} directives, watches dependencies, and reports failures as diagnostics - off by default, see below |
Workspace navigation is backed by a versioned .crv index. The initial scan
ignores .git and node_modules and is bounded at 10,000 files / 64 MiB;
open buffers replace their disk snapshot and closing a buffer restores it.
Language settings
Clients can send these under carve in initialization options or
workspace/didChangeConfiguration. If the client does not supply a carve
section, the server reads the first .carverc.json in the workspace roots and
watches it for changes.
| Setting | Default | Meaning |
|---|---|---|
platforms | [] | Extra platform lint profiles. Currently supports "github". |
extensions | [] | Enabled extension names, for example "semantic-span". |
inlayHints | true | Show generated heading identifiers. |
formatter | "conservative" | Use whitespace-only conservative formatting; "migration" opts into canonical whole-document conversion. |
severities | {} | Override a diagnostic code with "error", "warning", "information", "hint", or "off". |
Example .carverc.json:
{
"carve": {
"platforms": ["github"],
"extensions": ["semantic-span"],
"inlayHints": true,
"formatter": "conservative",
"severities": { "table-width-total": "error" }
}
}
File inclusion
Carve's {{ path }} directive is a processor-level feature (PART 9 §19 of the
grammar), not part of the parser. This server resolves it so that the thin
editor clients - helix-carve, emacs-carve, vim-carve, zed-carve,
sublime-carve - get inclusion support without each implementing it.
Resolution reads files from disk, so it is opt-in and off by default. The server enables it only when it is asked to, and only inside a containment root.
Include settings
Pass these under carve.includes, either in initializationOptions at
initialize time or through workspace/didChangeConfiguration. Changing them
re-publishes diagnostics for every open document.
| Setting | Default | Meaning |
|---|---|---|
enabled | "auto" | "auto" enables inclusion only for a workspace the client reports as trusted; "on" always; "off" never. A client that reports no trust gets inclusion off. |
includeRoot | workspace root | Containment root override. With no workspace, the document's own directory is used - never the server's working directory. |
allowAbsolute | false | Allow absolute include paths. They still have to canonicalize inside the root. |
allowedRemoteHosts | [] | Hosts a remote include may name. This server has no fetcher, so a remote target is refused either way; the list exists so the gate is explicit. |
maxDepth | 16 | Maximum transitive include depth. |
maxBytes | max(1 MiB, 8x document) | Total byte budget across the whole include graph, charged per occurrence. |
Client trust is read from initializationOptions.workspaceTrusted.
{
"carve": {
"includes": {
"enabled": "auto",
"maxDepth": 8
}
},
"workspaceTrusted": true
}
What is enforced
Every target has to canonicalize (symlinks resolved) to a file inside the
containment root. A symlink pointing out of the root, a .. path that leaves
it, and an absolute path outside it are all refused. A .. path whose real
target stays inside the root is fine - a chapter reaching a shared glossary is
ordinary layout. Remote URLs are never fetched. Recursion depth and total
expanded bytes are both bounded, so a file that includes another many times
cannot amplify without limit.
A refused target produces an include-unresolved diagnostic on the directive,
and the directive stays literal. The diagnostic deliberately does not say WHICH
check refused it: a distinguishable denial is a way to probe the layout of the
machine the server runs on.
The server watches every legal include target, including a missing target that may be created later. A child change invalidates its cached source and parsed tree, then revalidates every open document that includes it. Go to definition on a directive opens the resolved child, and child headings appear in the including document's symbol result with locations in the child file. All three features use the same contained resolver as diagnostics; refused paths are never registered as watchers or navigation targets.
Editor setup
VS Code
Install the vscode-carve extension, which bundles and auto-starts this server.
For a generic LSP client (e.g.
vscode-languageclient),
add to .vscode/settings.json:
{
"languageServerExample.serverCommand": "carve-lsp",
"languageServerExample.serverArgs": ["--stdio"]
}
Or wire it up in a custom extension:
const serverOptions: ServerOptions = {
command: 'carve-lsp',
args: ['--stdio'],
};
const clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: 'file', language: 'carve' }],
};
new LanguageClient('carve-lsp', 'Carve Language Server', serverOptions, clientOptions).start();
Neovim (nvim-lspconfig)
local lspconfig = require('lspconfig')
local configs = require('lspconfig.configs')
if not configs.carve_lsp then
configs.carve_lsp = {
default_config = {
cmd = { 'carve-lsp', '--stdio' },
filetypes = { 'carve', 'crv' },
root_dir = lspconfig.util.root_pattern('.git', '.'),
single_file_support = true,
},
}
end
lspconfig.carve_lsp.setup({})
Add a filetype detection entry if your Neovim does not already recognize .crv:
vim.filetype.add({
extension = {
crv = 'carve',
},
})
Other editors
Any editor with LSP support can start the server as an external process:
- Command:
carve-lsp --stdio - File extension:
.crv(language ID:carve) - Root pattern:
.gitor the project root
Development
npm install
npm run build
npm test
Run the server directly over stdio:
node dist/server.js --stdio