A Language Server Protocol implementation for the OpenSIPS routing
script language (opensips.cfg).
| Feature | How |
|---|---|
| Diagnostics | Runs opensips -C -f <file> on open/save and maps its parse errors (file:line:col) to LSP diagnostics — full-fidelity, version-exact semantic validation by the real parser. A fast analyzer layer warns between saves (debounced on change): undefined route() targets and duplicate route definitions. |
| Completion | Context-sensitive: module names after loadmodule " / modparam(", the module's parameters inside the second modparam argument, exported functions of loaded modules plus core functions/parameters, route names inside route( and in route bodies, keywords, and pseudo-variables after $ (replacing the typed token). Duplicate labels collapse; include_file/import_file closures count. |
| Hover | Documentation for module functions, parameters, and modules, harvested from the OpenSIPS docs. |
| Signature help | The innermost unclosed call's signature with the active parameter, on ( and ,. |
| Go to definition | route(name) references resolve to their route[name] block — in this file or any included file. |
| References / rename / highlights | Every call site + definition of a route name; rename rewrites them all (charset-gated, quoted call sites handled). prepareRename blocks F2 off-symbol and pre-selects the name. |
| Document symbols | All route blocks (route, failure_route, onreply_route, …) with full block extents, nested outline. |
| Folding | Route-family blocks fold; brace matching is string/comment-safe. |
| Workspace symbols | Ctrl+T route search across open files and includes. |
| Code lenses | Reference counts above route definitions (include-closure-wide). |
| Call hierarchy | The route call graph: who calls route[X], and what route[X] calls, across the include closure. Main-table only — a failure_route is armed by a module-function string the server cannot see, so it makes calls but reports no callers. |
| Quick fixes | Load the module exporting an unknown function; create a missing route stub. |
| Refactorings | Extract a selection into a route[...] of its own, leaving a call behind; remove duplicate loadmodule lines (a second load is a parse error, not untidiness). |
| Catalog-pinned validation | Undocumented modparam parameters flagged against YOUR source tree's docs. |
| Include links | include_file/import_file paths are Ctrl+Click document links. |
| Included files | An include_file/import_file fragment opened on its own is answered in its ROOT's context: the analyzer runs over the root's closure and opensips -C is run on the root, with each error routed to the file it names. Otherwise a fragment reports every route its parent defines as undefined. The VS Code client also gives an unassociated .cfg the language when the workspace's configuration includes it. |
| Formatting | Whole-document and range formatting re-indents by brace depth and strips trailing whitespace, honouring the editor's tab settings. Line-preserving: it never joins, splits or reorders lines, and never touches a string or comment body. |
| Semantic tokens | Route names and pseudo-variables colored by analysis; editors that ask for a semanticTokens/range get exactly the tokens in view. |
| Inlay hints | Parameter names from the documentation drawn at module-function call sites, for the visible range only. |
| Pull diagnostics | textDocument/diagnostic and workspace/diagnostic. The workspace sweep reports only ROOT configs — a file another config includes is a fragment, not a program. Pushing stops when the client pulls, so nothing is reported twice. |
| Watched files | An include or a documentation tree changing on disk — a git checkout, a rebuild — re-checks and re-harvests without the buffer being touched. |
| Live settings | Runtime toggles (analyzer, snippets, code lens, max diagnostics, check timeout) apply over workspace/didChangeConfiguration without a server restart. |
| CLI | opensips-lsp check [--strict] [--bin <opensips>] <file>... for CI/git hooks. |
Positions are exchanged in UTF-16 units (the LSP default) and are correct on multibyte lines; doc harvests are cached per source tree (see the admin guide's Caching section).
The documentation catalog is harvested at startup from an OpenSIPS
source tree. The 4.x markdown docs (modules/*/README.md) are the
most current and win; docbook (modules/*/doc/*_admin.xml) is the
fallback for older trees or placeholder READMEs. Core-language docs
(functions, parameters, pseudo-variables) come from docs/manual/.
Supported and version-proven: OpenSIPS 4.0.x (4.0.1) and
3.6.x (3.6.8) — the proof suite runs against a real tree and
binary of each (OPENSIPS_LSP_TEST_TREE/OPENSIPS_LSP_TEST_BIN),
built from the tag rather than from a moving branch. Nothing pins a
version: module and core docs come from the tree you point at, and
diagnostics from your own binary.
Via LSP initializationOptions (or environment fallback):
| Option | Env | Default | Meaning |
|---|---|---|---|
opensipsPath |
OPENSIPS_LSP_BIN |
opensips |
Binary used for -C diagnostics. |
opensipsSrc |
OPENSIPS_LSP_SRC |
(none) | Source tree to harvest documentation from. Not needed to get started: the core language and all 186 documented modules are built in, harvested from OpenSIPS 4.0.1 and used whenever no tree is configured, so log_level, loadmodule " and is_method all complete on a fresh install. A configured tree wins and replaces them wholesale, which is what you want when your build differs from 4.0.1. |
Diagnostics fidelity note: -C loads the modules the cfg references,
so it needs a tree/installation where those .so files exist (an
unresolvable module is itself reported as a diagnostic, which is
usually what you want).
New to all of this? Follow the Getting Started guide — one-command install plus click-by-click usage instructions. Short version:
curl -fsSL https://raw.githubusercontent.com/NormB/opensips-lsp/main/install.sh | shPrebuilt server binaries (Linux, macOS, and Windows (x86_64 and arm64)) and the VS Code
.vsix ship with every GitHub release:
tar xzf opensips-lsp-<version>-x86_64-linux-gnu.tar.gz
install -m755 opensips-lsp ~/.local/bin/cargo build --release # server binary: target/release/opensips-lsp
eval "$(scripts/proof-env.sh)" # real OpenSIPS tree + binary, once
cargo test # full suite, includes a stdio LSP e2e testA skipped test is a failed test here. Parts of the suite prove
behaviour against a real OpenSIPS tree and a real opensips binary, and
they refuse to run without one rather than reporting green while
proving nothing. scripts/proof-env.sh provisions both into
.proof/ (gitignored) and prints the environment to export; CI runs
that same script, so a green CI means the proofs actually ran. A gate
in the suite fails if any test ever announces a skip again.
tree-sitter-opensips/ carries an error-tolerant grammar for editors
that highlight and fold via tree-sitter (Neovim, Helix, Zed): corpus
tests run in CI; tree-sitter generate builds the parser locally.
docs/FEATURES.md— every feature, every setting (VS Code / init option / environment), and the snippet set.docs/ADMIN.md— admin guide in the OpenSIPS module-doc structure (overview, dependencies, exported parameters, security, FAQ). Its structure is itself validated by the test suite through this project's own OpenSIPS-README harvester.docs/EDITORS.md— worked setup for VS Code, Neovim, coc.nvim, Helix, Emacs, Vim, Sublime Text, Kate and JetBrains (LSP4IJ), plus any other LSP client, plus using thecheckCLI in CI and git hooks with no editor at all.docs/ZED.md— Zed step by step: Zed needs a small WebAssembly extension, and this builds one for you.- API docs:
cargo doc --open(missing_docsisdeny).
-
VS Code: the
client/directory contains the extension (npm install && npm run compile, then run/package with vsce). Settings:opensipsLsp.serverPath,opensipsLsp.opensipsPath,opensipsLsp.opensipsSrc. -
Neovim (0.10+):
vim.api.nvim_create_autocmd("FileType", { pattern = "opensips-cfg", callback = function() vim.lsp.start({ name = "opensips-lsp", cmd = { "opensips-lsp" }, init_options = { opensipsPath = "/usr/local/sbin/opensips", opensipsSrc = "/path/to/opensips", }, }) end, })
src/catalog.rs— docbook + markdown documentation harvestersrc/analyze.rs— comment/string-aware lexical scan of cfg text (loadmodules, routes, cursor context); deliberately not a grammarsrc/diag.rs—opensips -Coutput parsersrc/logic.rs— pure completion/hover/definition assemblysrc/server.rs— tower-lsp-server wiring
Semantic truth stays in OpenSIPS itself (-C); the server never
guesses about grammar validity, so it is automatically correct for
whatever OpenSIPS version it is pointed at.
Nothing this server sees leaves your machine. There is no network
client in it: the crate links no HTTP library — check Cargo.lock —
opens no sockets, and speaks JSON-RPC to your editor over
stdin/stdout. No telemetry, no analytics, no crash reporting, no
update check, and no model: hover and completion text is parsed out
of OpenSIPS's own documentation on disk, never generated.
What it touches, all of it local:
- Reads — the cfg you opened and every file its
include_file/import_fileclosure names, plus theopensipsSrctree you configure. - Writes — one documentation-catalog cache under
cacheDir(default: the platform cache directory). It holds documentation harvested from that tree; your configuration is never written to it. - Runs — your own
opensipsbinary, asopensips -Con the file you opened, to produce diagnostics. That is code execution; see Security note for what gates it.
Two caveats worth knowing:
- Setting
opensipsLsp.trace.servertomessagesorverboseechoes the LSP traffic — which carries your cfg text — into the editor's output channel. It stays on the machine, but it is the one place config content lands in a log that is easy to paste into a bug report. The default isoff. - Your editor is a separate question. Its own telemetry, an AI assistant extension, or anything else with access to the buffer sees what you type regardless of what this server does. That boundary is outside this project.
The only outbound connection anywhere in this repository is
install.sh/install.ps1 fetching a release from GitHub, and your
editor fetching the extension. Both are downloads.
opensips -C dlopens the modules the cfg loads — their
constructors run. Opening a config from an untrusted source therefore
executes code paths you did not write. Rely on your editor's
workspace-trust prompt, and/or disable diagnostics entirely by
setting opensipsPath (or OPENSIPS_LSP_BIN) to an empty string
— completion, hover, and navigation keep working without it.
-C runs are serialized and bounded (10s default,
OPENSIPS_LSP_CHECK_TIMEOUT_MS to tune).
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.