Language server

Version 0.6.0 Updated Sep 14, 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
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, and shutdown requests per the LSP specification.
  • Workspace discovery: Uses cargo metadata to locate the workspace root and enumerate packages.
  • Workspace preparation and save replay: Workspace capability installation is asynchronous. Disk-backed didSave indexing 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 .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. The RustStepIndexResult API 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 remain RustStepIndexError values. Disk-backed feature saves read through the validated workspace root using a root-relative path, while saves that supply source text use index_feature_source without rereading the file from disk.
  • 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:

  • Multiple step attributes (multiple-step-attributes): When one Rust function has more than one of #[given], #[when], or #[then], saving its .rs file 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 .rs file 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 .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.

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.