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 |
Example:
RSTEST_BDD_LSP_LOG_LEVEL=debug rstest-bdd-lsp
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({})
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. - 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. - 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:
-
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.
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.