A Language Server Protocol (LSP) implementation for SurrealQL, the query language of SurrealDB.
- Syntax diagnostics via tree-sitter
- Semantic analysis with schema inference from DDL and query flow
- Hover with type info, permission posture, function signatures, and language badges (SurrealQL vs JavaScript)
- Contextual completions for
record<table>types, field names, builtin functions, and statement keywords - Go-to definition and references for tables, fields, functions, and params
- Safe rename of local function definitions
- Code actions for missing
PERMISSIONSclauses - Signature help for builtin and user-defined functions
- Call hierarchy with inbound/outbound function call tracking
- Document symbols outlining tables, fields, events, indexes, and functions
function() { ... }bodies parse cleanly with no false diagnostics;DEFINE FUNCTIONbodies containing scripting functions are detected and labelled as JavaScript
The language server compiles against a tree-sitter SurrealQL grammar that must be checked out as a sibling directory:
parent/
├── surrealql-language-server/ ← this repo
└── surrealql-tree-sitter/ ← grammar (sibling checkout)
Run the setup script to clone or update the grammar:
bash scripts/setup-grammar.shOr set TREE_SITTER_SURREALQL_DIR to point to an existing checkout:
TREE_SITTER_SURREALQL_DIR=/path/to/surrealql-tree-sitter cargo buildThe grammar is pinned to a specific commit (GRAMMAR_REF in
scripts/setup-grammar.sh and the checkout steps
in CI) because the analysis layer is coupled to the grammar's node kinds.
Bump it deliberately alongside any src/semantic/node_kind.rs
change. Known grammar parse gaps (and the tests that track them) are listed in
docs/grammar-gaps.md.
cargo build --release
# binary at: target/release/surrealql-language-serverBuild the wasm-bindgen npm package (outputs to pkg/):
bash scripts/build-wasm.shRequirements:
wasm-bindgenCLI (cargo install wasm-bindgen-cli --version 0.2.108)wasm-opt(cargo install wasm-opt)- On macOS, a wasm-capable clang (e.g.
brew install llvm; the script auto-detects Homebrew LLVM)
Initialize the module with fetch + arrayBuffer, then pass the bytes to the default export — the same pattern used by @surrealdb/wasm. Avoid passing a URL string directly to init() when the build pipeline pre-gzips .wasm assets in place: browsers only gunzip automatically when the response carries Content-Encoding: gzip (S3 production uploads do; many static preview servers do not).
import init, { WasmLanguageServer } from "@surrealdb/surrealql-language-server";
import wasmUrl from "@surrealdb/surrealql-language-server/surrealql_language_server_bg.wasm?url";
const wasmCode = await fetch(wasmUrl).then((response) => response.arrayBuffer());
await init({ module_or_path: wasmCode });
const server = new WasmLanguageServer({ /* callbacks */ });The ./surrealql_language_server_bg.wasm export is declared in pkg/package.json for bundlers that resolve deep imports.
Tests need the grammar sibling checkout (see Requirements above) —
run bash scripts/setup-grammar.sh or set TREE_SITTER_SURREALQL_DIR
first, then:
cargo test.
├── src/
│ ├── main.rs # LSP stdio entry point (+ panic hook)
│ ├── config.rs # workspace settings (+ validation warnings)
│ ├── grammar.rs # tree-sitter language binding, curated builtin prose
│ ├── grammar_generated.rs # @generated builtin catalogue — do not edit by hand
│ ├── core/
│ │ ├── server.rs # transport-agnostic request handlers
│ │ ├── dispatch.rs # JSON-RPC dispatch table (shared with WASM)
│ │ ├── client.rs # LspNotifier / WorkspaceLoader / MetadataProvider traits
│ │ ├── state.rs # shared server state
│ │ └── completion_context.rs
│ ├── native/ # tower-lsp adapter, walkdir loader, SurrealDB metadata,
│ │ # and the headless `check` subcommand (check.rs)
│ ├── wasm/ # wasm-bindgen adapter (Surrealist)
│ └── semantic/
│ ├── analyzer.rs # document analysis (parse + extract + syntax diagnostics)
│ ├── model.rs # merged workspace model, semantic diagnostics, code actions
│ ├── codes.rs # stable Diagnostic.code registry
│ ├── types.rs # DocumentAnalysis, TableDef, FunctionDef, ...
│ ├── type_expr.rs # SurrealQL type expression parser
│ └── text.rs # LSP range utilities
├── xtask/ # code generator (see Builtin Function Catalogue)
│ └── src/
│ ├── engine_tables.rs # names, dispatch and rename tables
│ ├── signatures.rs # argument types, from the `fnc/` implementations
│ ├── returns.rs # return types, from the function registry
│ ├── methods.rs # method receiver tables
│ ├── probe.rs # `verify-returns`: checks the engine by running it
│ ├── emit.rs # joins them and renders the catalogue as Rust
│ └── emit_json.rs # renders the same catalogue as builtins.json
├── tests/
│ ├── lsp.rs # analyzer/model integration tests
│ ├── core_server.rs # end-to-end server tests (mock notifier)
│ ├── dispatch.rs # JSON-RPC wire tests
│ ├── check.rs # end-to-end tests for the check subcommand
│ ├── conformance.rs # silence sweep over SurrealDB's own corpus
│ ├── generated_catalogue.rs # catalogue freshness + shape invariants
│ ├── compat.rs # backwards-compatibility tripwires
│ └── common/ # shared mocks for the three boundary traits
├── docs/
│ ├── ai-plan.md # opportunity map for the AI-agent surfaces
│ ├── grammar-gaps.md # known gaps at the pinned grammar revision
│ ├── pain-points.md # audited pain-point catalog + status
│ ├── perf-baseline.md # latency baseline measurements
│ └── perf-plan.md # latency targets `cargo bench` gates on
├── AGENTS.md # agent-facing contract: check loop, codes, gaps
├── llms.txt # machine-readable resource index
├── builtins.json # @generated catalogue-as-data — do not edit by hand
├── build.rs # compiles tree-sitter grammar (C)
└── scripts/
└── setup-grammar.sh # clones/updates the grammar sibling repo
The server communicates over stdio and works with any LSP-compatible editor.
Settings arrive via initializationOptions or workspace/didChangeConfiguration,
either under a surrealql key or at the root. Every key accepts both camelCase
and snake_case. An unknown key is reported through window/logMessage with a
did-you-mean suggestion rather than ignored.
Decides which diagnostics apply to a table declared SCHEMALESS, where ad-hoc
fields are legal SurrealQL.
| Value | Behavior |
|---|---|
quiet (default) |
Report none of unknown-field, field-type, unknown-type, permission-denied, permission-unknown on such a table. |
errors |
Report only field-type and unknown-type — the two faults SurrealDB itself raises. The advisory three stay quiet. |
strict |
No exemption: check a SCHEMALESS table exactly as a SCHEMAFULL one. |
errors is worth preferring over the default if you want a quiet editor without
hiding real failures. SurrealDB coerces DEFAULT, VALUE and COMPUTED to the
declared type on a SCHEMALESS table too, and it refuses to parse an unknown
type name at all — so under quiet a file that always fails can look clean.
The setting keys on the keyword. A bare DEFINE TABLE t is schemaless to the
engine, but it declares nothing, so it keeps the diagnostics it would have had
anyway.
Upper bound on syntax diagnostics (parse, unknown-type) per document, so
a pathological buffer cannot flood the problems panel. Default 2000, raised
from 100. Set it to 0 to report every one.
{ "surrealql": { "analysis": { "maxSyntaxDiagnostics": 0 } } }This counts diagnostics, not lines — no setting limits how long a document may be. Semantic and type diagnostics are uncapped; they are derived from the definitions and query facts in the file, so the code itself bounds them.
Two unrelated limits do apply to the workspace scan, and neither is
configurable: files over 2 MB are skipped, and at most 5,000 .surql files are
indexed. Both are reported through window/logMessage when they bite. They
affect which files contribute schema, not the diagnostics on the file you have
open.
| Key | Default | Effect when false |
|---|---|---|
analysis.enableTypeChecking |
true |
Turns off the whole type pass: argument-type, argument-count, let-type, return-type, operator-type, unknown-method, undefined-variable, field-type, renamed-function, not-callable. unknown-type survives — it is a syntax fault. |
analysis.enablePermissionAnalysis |
true |
Turns off permission-denied and permission-unknown on every table. |
analysis.externalParams |
[] |
Not a toggle: names the variables your caller binds at runtime (db.query(sql).bind(("id", id)), or Surrealist's variables panel) so undefined-variable does not flag them. |
The same analysis the editor shows is available headless, so coding agents, CI jobs and pre-commit hooks can run the generate → check → repair loop:
surrealql-language-server check queries/
surrealql-language-server check --stdin --stdin-filename src/feed.surql --workspace schema/
surrealql-language-server check queries/ --format json --fail-on warning| Exit | Meaning |
|---|---|
| 0 | Ran to completion; nothing at or above --fail-on (default error). |
| 1 | Ran to completion; diagnostics at or above the threshold. |
| 2 | Usage error, unreadable input, or a skipped target file. |
--format json prints one object whose diagnostics are LSP wire objects
verbatim (stable codes, 0-based UTF-16 ranges, structured data hints),
plus a summary, the scan losses, and the exitCode:
{
"files": [{ "path": "queries/feed.surql", "diagnostics": [ /* LSP Diagnostic */ ] }],
"summary": { "filesChecked": 1, "errors": 0, "warnings": 1, "information": 0, "hints": 0 },
"scan": { "walkErrors": 0, "skippedOversize": 0, "skippedUnreadable": 0, "fileCapHit": false },
"exitCode": 0
}AGENTS.md is the agent-facing contract: the check loop, the
diagnostic-code table, and the known grammar gaps an agent must not "fix".
llms.txt indexes the machine-consumable resources, including
builtins.json.
Recipes:
- CI: run
checkover your.surqldirectories with--fail-on warning; exit codes 1 and 2 fail the job. - pre-commit:
surrealql-language-server check $(git diff --cached --name-only -- '*.surql')(skip when the list is empty). - Claude Code: add "Run
surrealql-language-server check <file>after every.surqledit" toCLAUDE.md, or point it atAGENTS.md. - Cursor: the same instruction in
.cursor/rules. - Zed: the extension already ships this server for editing; use
checkfor the headless loop.
The tree-sitter grammar lives in the sibling surrealql-tree-sitter repo. After editing grammar.js:
cd ../surrealql-tree-sitter
npx tree-sitter generate
npx tree-sitter testThe src/parser.c is auto-generated and should not be edited directly. JavaScript scripting function bodies (function() { ... }) are handled by an external C scanner at src/scanner.c which tracks brace depth, strings, template literals, and comments.
src/grammar_generated.rs holds every builtin SurrealDB accepts — 434 functions with their argument types, return types, arity and method receivers. It is committed, and generated from a SurrealDB checkout rather than written by hand:
make builtins # or: cargo xtask generate-builtins --surrealdb ../surrealdb
make builtins-check # compare without writingPass --surrealdb <path> or set SURREALDB_DIR (make builtins SURREALDB=/path/to/surrealdb). The checkout must be at the revision the catalogue header records. --check is what tests/generated_catalogue.rs runs. Never edit the generated files by hand.
The same generator run also writes builtins.json — the
catalogue as data, for anyone building SurrealQL tooling outside this crate.
It is committed, freshness-checked in CI alongside the Rust rendering,
attached to every GitHub release, and shipped in the npm package
(@surrealdb/surrealql-language-server/builtins.json). One encoding rule
matters to consumers: params: null means the signature is unknown —
never read it as zero-arity.
These targets do not need the grammar checkout: cargo run --package xtask never builds the root package, so build.rs does not run.
The generator reads four places in the engine: syn/parser/builtin.rs for the names, fnc/mod.rs for the dispatch and method tables, the pub fn signatures under fnc/ for the argument types, and exec/function/builtin/ for the return types.
SurrealDB never reads its own return-type registry, so a wrong declaration there would compile and ship. To check them by running them:
make verify-returns # or: cargo run -p xtask --features probe -- verify-returns --surrealdb ../surrealdbThis boots an in-memory engine, calls every function with synthesised arguments, and compares the answer with what the catalogue records. It compiles the whole engine, hence the feature flag and the few minutes on a cold build. Run it after a SurrealDB version bump.
GitHub Actions runs cargo fmt --check, cargo test and cargo test -p xtask on every push and pull request. The grammar and SurrealDB sibling repos are cloned automatically, both pinned, so the catalogue freshness check runs in CI.
Push a v* tag (e.g. v0.1.5). CI builds platform binaries, uploads them to the GitHub Release, and publishes the Rust crate to crates.io.
The scoped package @surrealdb/surrealql-language-server is built with scripts/build-wasm.sh (cargo → wasm-bindgen → wasm-opt) and published to npm on the same v* tag. A .tgz is also attached to the GitHub Release.
Release checklist:
- Bump
versioninCargo.tomlandpkg/package.json. - Push the tag:
git tag vX.Y.Z && git push origin vX.Y.Z. - Confirm the
wasmCI job succeeds and the package appears on npm. - Confirm the release assets include the platform binaries, the npm
.tgz, andbuiltins.json.
npm publishing uses Trusted Publishing (OIDC from GitHub Actions). Before the first publish, an @surrealdb org admin must configure a trusted publisher on the package's npm Access page (@surrealdb/surrealql-language-server) with:
- Repository owner:
surrealdb - Repository name:
surrealql-language-server - Workflow filename:
ci.yml(exact match, case-sensitive)
The CI workflow intentionally does not set registry-url on actions/setup-node — that option writes an .npmrc which forces token auth and breaks OIDC (npm/cli#8730). Do not add a NODE_AUTH_TOKEN secret for this job.
If the first CI publish still fails with a misleading 404, an org admin can bootstrap the package once locally (bash scripts/build-wasm.sh && npm publish --access public from pkg/), then configure the trusted publisher for subsequent tag releases.
{ "surrealql": { "analysis": { "schemalessDiagnostics": "errors" } } }