The rstest-bdd-server crate provides a Language Server Protocol (LSP)
implementation that bridges Gherkin .feature files and Rust step definitions.
The binary is named rstest-bdd-lsp and communicates over stdin/stdout using
JSON-RPC, making it compatible with any editor supporting the LSP (VS Code,
Neovim, Zed, Helix, etc.).
Installation
Build and install the language server from the workspace:
cargo install --path crates/rstest-bdd-server
The binary rstest-bdd-lsp is placed in the Cargo bin directory.
Configuration
The server reads configuration from environment variables:
| Variable | Description | Default |
|---|---|---|
RSTEST_BDD_LSP_LOG_LEVEL |
Logging verbosity (trace, debug, info, warn, error) | info |
RSTEST_BDD_LSP_DEBOUNCE_MS |
Delay (ms) before processing file changes | 300 |
RSTEST_BDD_LSP_WORKSPACE_ROOT |
Override workspace root path for discovery | (auto) |
Example:
RSTEST_BDD_LSP_LOG_LEVEL=debug rstest-bdd-lsp
Command-line options
The server also accepts command-line flags that override environment variables:
rstest-bdd-lsp --log-level debug --debounce-ms 100 --workspace-root /path/to/project
| Flag | Description | Default |
|---|---|---|
--log-level |
Logging verbosity (trace, debug, info, warn, error) | info |
--debounce-ms |
Delay (ms) before processing file changes | 300 |
--workspace-root |
Override workspace root path; bypasses LSP client root | (auto) |
Configuration precedence: Default → Environment variables → CLI flags.
The --workspace-root flag overrides the LSP client's root URI and workspace
folders for workspace discovery. This is useful when the editor sends incorrect
paths or during headless or scripted testing scenarios.
Workspace discovery API
Applications embedding the rstest-bdd-server crate can discover Gherkin
feature files with discovery::find_feature_files:
use std::path::Path;
use rstest_bdd_server::discovery::find_feature_files;
use rstest_bdd_server::error::ServerError;
fn discover_features(root: &Path) -> Result<(), ServerError> {
let feature_files = find_feature_files(root)?;
println!("discovered {} feature files", feature_files.len());
Ok(())
}
The function returns Result<Vec<PathBuf>, ServerError>. An absent optional
feature directory is ignored, but workspace and feature-directory metadata,
read, and directory-entry iteration failures are returned as ServerError::Io.
Callers should propagate or report those errors rather than treating them as an
empty feature list.
Editor integration
VS Code
Add a configuration in the settings.json file or use an extension that allows
custom LSP servers. A minimal example using the
LSP-client
extension:
{
"easylsp.servers": [
{
"language": ["rust", "gherkin"],
"command": "rstest-bdd-lsp"
}
]
}
Neovim (nvim-lspconfig)
local lspconfig = require('lspconfig')
local configs = require('lspconfig.configs')
if not configs.rstest_bdd then
configs.rstest_bdd = {
default_config = {
cmd = { 'rstest-bdd-lsp' },
filetypes = { 'rust', 'cucumber' },
root_dir = lspconfig.util.root_pattern('Cargo.toml'),
},
}
end
lspconfig.rstest_bdd.setup({})
Zed
Zed uses its extension system to register language servers. Configure the
binary via Zed's settings.json:
{
"lsp": {
"rstest-bdd-lsp": {
"binary": {
"path": "rstest-bdd-lsp",
"arguments": ["--log-level", "info"]
}
}
},
"languages": {
"Rust": {
"language_servers": ["rust-analyzer", "rstest-bdd-lsp", "..."]
}
}
}
Diagnostics and navigation require saving files to trigger indexing.
Current capabilities
The language server provides the following capabilities:
- Lifecycle handlers: Responds to
initialize,initialized, andshutdownrequests per the LSP specification. - Workspace discovery: Uses
cargo metadatato locate the workspace root and enumerate packages. - Workspace preparation and save replay: Workspace capability installation
is asynchronous. Disk-backed
didSaveindexing waits until the workspace is ready; queued saves are coalesced to the latest notification per URI and replayed in order. The queue is limited to 128 notifications and 4 MiB of combined URI and source text. Saves that exceed either limit are dropped with a warning, and the server continues without failing. - Feature indexing (on save): Parses saved
.featurefiles using thegherkinparser and records steps, doc strings, data tables, and Examples header columns with byte offsets. Parse failures are logged. - Rust step indexing (on save): Parses saved
.rsfiles withsynand records#[given],#[when], and#[then]functions, including the step keyword, pattern string (including inferred patterns when the attribute has no arguments), the parameter list, and whether the step expects a data table or doc string. TheRustStepIndexResultAPI owns the file index and any recoverable per-function diagnostics. Invalid functions are reported without discarding valid neighbouring step definitions. Both Rust indexing entry points return this result after a successful read and whole-source parse; file-read and parse failures remainRustStepIndexErrorvalues. Disk-backed feature saves read through the validated workspace root using a root-relative path, while saves that supply source text useindex_feature_sourcewithout rereading the file from disk. - Step pattern registry (on save): Compiles the indexed step patterns with
rstest-bdd-patternsand caches compiled regex matchers in a keyword-keyed in-memory registry. The registry is updated incrementally per file save, so removed steps do not linger. - API note (embedding):
StepDefinitionRegistry::{steps_for_keyword, steps_for_file}returnsArc<CompiledStepDefinition>entries so the compiled matcher and metadata are shared between the per-file and per-keyword indices. - Structured logging: Configurable via environment variables; logs are
written to stderr using the
tracingframework.
Usage:
- Place the cursor on a Rust function annotated with
#[given],#[when], or#[then]. - Invoke "Go to Definition" (typically F12 or Ctrl+Click in most editors).
- The editor navigates to all matching steps in
.featurefiles.
When multiple feature files contain matching steps, the editor presents a list of locations to choose from.
How matching works:
- Matching is keyword-aware: a
#[given]step only matchesGivensteps in feature files. The parser correctly handlesAndandButkeywords by resolving them to their contextual step type. - Patterns with placeholders (e.g.,
"I have {count:u32} items") match feature steps using the same regex semantics as the runtime.
Go to Implementation (Feature → Rust)
The inverse navigation—from feature steps to Rust implementations—is provided
via the textDocument/implementation handler. This enables developers to jump
from a step line in a .feature file directly to the Rust function(s) that
implement it.
Usage:
- Place the cursor on a step line in a
.featurefile (e.g.,Given a user exists). - Invoke "Go to Implementation" (typically Ctrl+F12 or a similar keybinding in most editors).
- The editor navigates to all matching Rust step functions.
When multiple implementations match (duplicate step patterns), the editor presents a list of locations to choose from.
How matching works:
- Matching is keyword-aware: a
Givenstep in a feature file only matches#[given]implementations in Rust. - The step text is matched against the compiled regex patterns from the step registry, ensuring consistency with the runtime.
Diagnostics (on save)
The language server publishes diagnostics when files are saved, helping developers identify consistency issues between feature files and Rust step definitions:
-
Multiple step attributes (
multiple-step-attributes): When one Rust function has more than one of#[given],#[when], or#[then], saving its.rsfile publishes a recoverable warning naming that function. -
Invalid step attribute arguments (
invalid-step-attribute-arguments): When a step attribute has malformed arguments or an argument that is not a string literal, saving its.rsfile publishes a recoverable warning that includes the attribute and parser error.
These Rust-index warnings are recoverable per function. When the rest of the file parses successfully, valid neighbouring step definitions remain indexed and available for navigation and diagnostics; only the function with the offending attributes is omitted.
-
Unimplemented feature steps (
unimplemented-step): When a step in a.featurefile has no matching Rust implementation, a warning diagnostic is published at the step location. The message indicates the step keyword and text that needs an implementation. -
Unused step definitions (
unused-step-definition): When a Rust step definition (annotated with#[given],#[when], or#[then]) is not matched by any feature step, a warning diagnostic is published at the function definition. This helps identify dead code or typos in step patterns. -
Placeholder count mismatch (
placeholder-count-mismatch): When a step pattern contains a different number of placeholder occurrences than the function has step arguments, a warning diagnostic is published on the Rust step definition. Each placeholder occurrence is counted separately (e.g.,{x} and {x}counts as two placeholders), matching the macro's capture semantics. A step argument is a function parameter whose normalized name matches a placeholder name in the pattern;datatable,docstring, and fixture parameters are excluded from the count. -
Data table expected (
table-expected): When a Rust step expects a data table (has adatatableparameter) but the matching feature step does not provide one, a warning diagnostic is published on the feature step. -
Data table not expected (
table-not-expected): When a feature step provides a data table but the matching Rust implementation does not expect one, a warning diagnostic is published on the data table in the feature file. -
Doc string expected (
docstring-expected): When a Rust step expects a doc string (has adocstring: Stringparameter) but the matching feature step does not provide one, a warning diagnostic is published on the feature step. -
Doc string not expected (
docstring-not-expected): When a feature step provides a doc string but the matching Rust implementation does not expect one, a warning diagnostic is published on the doc string in the feature file. -
Missing Examples column (
example-column-missing): When a step in a Scenario Outline uses a placeholder (e.g.,<count>) that has no matching column header in the Examples table, a warning diagnostic is published on the step that references the undefined placeholder. The message lists the available columns to help identify typos or missing data. -
Surplus Examples column (
example-column-surplus): When an Examples table includes a column header that is not referenced by any<placeholder>in the Scenario Outline's steps, a warning diagnostic is published on the unused column header. This helps identify redundant data in the Examples table that may indicate a copy-paste error or an incomplete refactoring.
Diagnostics are updated incrementally:
- Saving a
.featurefile recomputes diagnostics for that file, including unimplemented steps, table/docstring expectation mismatches, and scenario outline column mismatches. - Saving a
.rsfile recomputes diagnostics for all feature files (since new or removed step definitions may affect which steps are implemented) and checks for unused definitions and placeholder count mismatches in the saved file.
When a recomputation finds nothing to report, the server publishes an empty diagnostic list for that file. Resolved warnings therefore clear from the Problems panel on the next save, rather than lingering until the editor or the server is restarted.
A file the server has not indexed is skipped rather than cleared. A .feature
file that has not been saved since the server started has no index yet, so
saving an unrelated file leaves whatever diagnostics the editor already shows
for it untouched instead of blanking them. Saving the file itself indexes it
and refreshes its diagnostics.
Diagnostics appear in the editor's Problems panel and as inline warnings,
similar to compiler diagnostics. They use the source rstest-bdd and the codes
listed above for filtering.