Language server

Version 0.5.0 Updated Feb 09, 2026

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, and shutdown requests per the LSP specification.
  • Workspace discovery: Uses cargo metadata to locate the workspace root and enumerate packages.
  • Feature indexing (on save): Parses saved .feature files using the gherkin parser 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 .rs files with syn and 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-patterns and 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} returns Arc<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 tracing framework.

Navigation (Go to Definition)

The language server supports navigation from Rust step definitions to matching feature steps. This enables developers to quickly find all usages of a step definition across feature files.

Usage:

  1. Place the cursor on a Rust function annotated with #[given], #[when], or #[then].
  2. Invoke "Go to Definition" (typically F12 or Ctrl+Click in most editors).
  3. The editor navigates to all matching steps in .feature files.

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 matches Given steps in feature files. The parser correctly handles And and But keywords 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:

  1. Place the cursor on a step line in a .feature file (e.g., Given a user exists).
  2. Invoke "Go to Implementation" (typically Ctrl+F12 or a similar keybinding in most editors).
  3. 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 Given step 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 .feature file 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 a datatable parameter) 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 a docstring: String parameter) 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 .feature file recomputes diagnostics for that file, including unimplemented steps, table/docstring expectation mismatches, and scenario outline column mismatches.
  • Saving a .rs file 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.