Metadata-Version: 2.4
Name: pytest-bdd-language-server
Version: 1.0.0
Summary: Language server for Gherkin .feature files backed by pytest-bdd step definitions
Keywords: pytest,pytest-bdd,gherkin
Author: Romain TAPREST
Author-email: Romain TAPREST <romain@taprest.fr>
License-Expression: MPL-2.0
Requires-Dist: gherkin-official>=24
Requires-Dist: parse>=1.22.1
Requires-Dist: parse-type>=0.6.6
Requires-Dist: pygls>=2.1.1
Requires-Python: >=3.10
Project-URL: homepage, https://framagit.org/RomainTT/pytest-bdd-language-server
Project-URL: repository, https://framagit.org/RomainTT/pytest-bdd-language-server
Project-URL: Bug Tracker, https://framagit.org/RomainTT/pytest-bdd-language-server/-/issues
Description-Content-Type: text/markdown

# pytest-bdd-language-server 🥒

[![Python Version](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://pypi.org/project/pytest-bdd-language-server/)

A Language Server Protocol (LSP) implementation for Gherkin `.feature` files backed by [pytest-bdd](https://pytest-bdd.readthedocs.io/) step definitions, with support for go to definition, find references, code completion, hover documentation, workspace symbols, diagnostics, and more!

> **Built with AI, reviewed with care** 🤖
>
> This project was built with the help of an AI coding agent, with each feature reviewed and tested (unit + end-to-end, via a real server subprocess) before moving to the next. If you find any issues, please open an issue or submit a pull request!

## Table of Contents

- [Features](#features)
  - [🎯 Go to Definition](#-go-to-definition)
  - [🔍 Find References](#-find-references)
  - [📚 Hover Documentation](#-hover-documentation)
  - [✨ Code Completion](#-code-completion)
  - [🔎 Workspace Symbols](#-workspace-symbols)
  - [⚠️ Diagnostics](#️-diagnostics)
  - [🧹 Find Unused Steps](#-find-unused-steps)
  - [⏳ Progress & Scan Feedback](#-progress--scan-feedback)
- [Installation](#installation)
- [Setup](#setup)
  - [Neovim](#neovim)
  - [VS Code](#vs-code)
  - [Other Editors](#other-editors)
- [Configuration](#configuration)
- [Supported Step Patterns](#supported-step-patterns)
- [CLI Commands](#cli-commands)
- [Architecture](#architecture)
- [Design Decisions](#design-decisions)
- [Known Limitations](#known-limitations)
- [Development](#development)
- [Contributing](#contributing)
- [License](#license)
- [Acknowledgments](#acknowledgments)

## Features

### 🎯 Go to Definition

Jump directly from a Gherkin step to the Python function that implements it:

- Literal steps (`@given("I am an author")`)
- `parsers.parse`/`parsers.cfparse` steps, matched with the same libraries pytest-bdd itself uses
- `parsers.re` steps, matched with Python's own `re` module directly
- `And`/`But`/`*` steps, resolved to their effective Given/When/Then keyword first

### 🔍 Find References

Works from either end:

- From a step definition's `def` line, find every `.feature` step that resolves to it
- From a Gherkin step, find every other place that same step is used
- `include_declaration` is respected on both ends

### 📚 Hover Documentation

Hover over a step to see which definition(s) match it, without jumping:

- Function name and source location
- The matched pattern and its kind (literal / parse / cfparse / re)
- A clear note when no definition matches, or when the only candidate can't be verified statically

### ✨ Code Completion

Autocomplete existing step patterns while writing a new step:

- Filtered by the effective keyword (Given/When/Then, `And`/`But`/`*` resolved)
- `parsers.parse`/`cfparse` patterns are offered as real LSP snippets — `{start:d}` becomes a tabstop you fill in, not literal placeholder text
- `parsers.re` patterns are intentionally excluded — a regex's source text isn't text it matches, so there's nothing sensible to insert

### 🔎 Workspace Symbols

Search across the whole project, not just step definitions:

- **Features and Scenarios**: search by name; matching a Feature's name surfaces every Scenario under it too
- **Step definitions**: search by keyword and pattern
- Powers editor-side pickers — see [`pytest-bdd.nvim`](https://framagit.org/RomainTT/pytest-bdd.nvim)'s Telescope integration for an example

The per-file counterpart, `textDocument/documentSymbol`, gives the same Feature/Scenario/step structure for the currently open document, nested rather than flat — this is what powers your editor's outline view, breadcrumbs, or sticky scroll.

### ⚠️ Diagnostics

Catches problems before you ever run `pytest`:

- **Undefined step**: a step with no matching definition anywhere — the same error pytest-bdd would raise at runtime, surfaced while you type instead
- **Unverifiable step**: the only candidate definition uses `parsers.cfparse(..., extra_types=...)`, whose custom converters live in your own code and can't be checked statically — reported as informational, not a warning, since it isn't necessarily wrong

### 🧹 Find Unused Steps

A custom `pytest-bdd.unusedSteps` command (via `workspace/executeCommand`, since no standard LSP method fits "list definitions with no usages") lists every step definition with no matching usage in any `.feature` file — likely dead code left behind after a scenario was reworded or removed. `unresolved` definitions are excluded, since there's no way to confirm they're truly unused rather than just unverifiable.

### ⏳ Progress & Scan Feedback

- **`$/progress`**: workspace indexing reports progress if the client supports `window.workDoneProgress`, so a large project doesn't feel like it's hanging
- **`window/showMessage`**: files that couldn't be read or parsed are reported in a single, capped summary — not silently skipped, and not one popup per broken file
- **`workspace/didChangeWatchedFiles`**: the index refreshes automatically on external changes (`git checkout`, another process, a second editor window) if the client supports dynamic registration, in addition to on-save

## Installation

```bash
# Using uv (recommended)
uv tool install pytest-bdd-language-server

# Or with pip, inside your project's virtualenv
pip install pytest-bdd-language-server

# Or with pipx (isolated environment)
pipx install pytest-bdd-language-server
```

Not yet published to PyPI — install from source in the meantime:

```bash
git clone https://framagit.org/RomainTT/pytest-bdd-language-server
cd pytest-bdd-language-server
uv tool install --editable .
```

## Setup

### Neovim

The [`pytest-bdd.nvim`](https://framagit.org/RomainTT/pytest-bdd.nvim) plugin handles this for you (filetype detection, virtualenv resolution, a Telescope picker). Without it, Neovim 0.11+'s native config works directly:

```lua
vim.filetype.add({ extension = { feature = "cucumber" } })

vim.lsp.config("pytest_bdd_language_server", {
  cmd = { "pytest-bdd-language-server" },
  filetypes = { "cucumber" },
  root_markers = { ".git", "pyproject.toml", "setup.py", "setup.cfg" },
})

vim.lsp.enable("pytest_bdd_language_server")
```

### VS Code

The [`pytest-bdd-vscode`](https://framagit.org/RomainTT/pytest-bdd.vscode) extension handles server resolution and adds two commands (`pytest-bdd: Browse Steps`, `pytest-bdd: Find Unused Steps`) built on `workspace/symbol` and the custom command above. Not yet published to the Marketplace — see its README for building a local `.vsix`.

### Other Editors

Any editor with LSP support can use `pytest-bdd-language-server`. Point it at the `pytest-bdd-language-server` command over stdio, with a document selector matching `.feature` files.

## Configuration

There are currently **no user-configurable settings** — no `initializationOptions`, no `workspace/didChangeConfiguration` handling. Behavior is entirely determined by the workspace folder(s) reported at `initialize` and the client's declared capabilities (for `$/progress` and the file watcher). See [Known Limitations](#known-limitations) for what that means in practice.

## Supported Step Patterns

All four of pytest-bdd's step decorator syntaxes are recognized and matched using the same libraries pytest-bdd itself relies on at runtime — not a reimplementation of their semantics:

```python
from pytest_bdd import given, parsers

# Literal text
@given("I am an author")
def author():
    ...

# parsers.parse (matched via the `parse` library)
@given(parsers.parse("there are {count:d} cucumbers"))
def cucumbers(count):
    ...

# parsers.cfparse (matched via parse_type.cfparse)
@given(parsers.cfparse("there are {count:d} cucumber(s)"))
def cucumbers_cf(count):
    ...

# parsers.re (matched via Python's own `re`)
@given(parsers.re(r"there are (?P<count>\d+) cucumbers"))
def cucumbers_re(count):
    ...
```

`parsers.cfparse(..., extra_types=...)` is the one case that can't be statically verified — its custom converters live in your own code, so it's flagged as unresolved rather than guessed at (see [Diagnostics](#️-diagnostics)).

## CLI Commands

Beyond the LSP server itself (`pytest-bdd-language-server serve`, the default when no command is given — see [Setup](#setup)), the same package exposes standalone commands that don't speak LSP at all, reusing the exact same scanning and matching logic:

```bash
# List step definitions with no matching .feature usage anywhere.
# Exits 1 if any are found -- built for CI pipelines and pre-commit hooks.
pytest-bdd-language-server unused-steps [PATHS...] [--format text|json]

# List .feature step usages with no matching step definition -- the mirror
# of unused-steps. Exits 1 if any are found.
pytest-bdd-language-server undefined-steps [PATHS...] [--format text|json]

# List every step definition found, optionally filtered by keyword/pattern.
pytest-bdd-language-server list-steps [PATHS...] [--format text|json] [--query TEXT]
```

All three default to scanning the current directory if no `PATHS` are given. `undefined-steps` catches a gap pytest-bdd's own step resolution doesn't cover: `StepDefinitionNotFoundError` is only raised when a step actually runs, so an undefined step in a scenario that's skipped, filtered out (`-k`, markers, `--lf`, CI sharding), or simply never wired into a test file via `scenarios(...)` can go undetected indefinitely. This command checks every `.feature` file directly, independent of what a given pytest invocation happens to execute -- and without running any test code, fixtures, or setup to find out. Usages only matched by an unresolved `cfparse`+`extra_types` candidate are reported separately as unverifiable and don't affect the exit code, consistent with how [Diagnostics](#️-diagnostics) treats the same situation.

Example, wired into CI:

```bash
pytest-bdd-language-server undefined-steps . || {
  echo "Found .feature steps with no matching definition -- see above."
  exit 1
}
pytest-bdd-language-server unused-steps . || {
  echo "Found step definitions with no .feature usage -- see above."
  exit 1
}
```

## Architecture

- **Language**: Python 3.10+
- **LSP framework**: [pygls](https://github.com/openlawlibrary/pygls)
- **Gherkin parsing**: [gherkin-official](https://pypi.org/project/gherkin-official/) — the same parser pytest-bdd uses internally
- **Step pattern matching**: [`parse`](https://pypi.org/project/parse/) and [`parse_type`](https://pypi.org/project/parse-type/) — again, pytest-bdd's own dependencies, not a reimplementation
- **Step discovery**: Python's `ast` module — statically parses decorators rather than importing your code

## Design Decisions

A few choices worth knowing if you're reading the source:

- **Step definitions are discovered via `ast`, not by importing your code.** Importing would require resolving your project's virtualenv, running arbitrary module-level side effects, and handling failures in third-party code — none of which belong in a scan that runs on every save. The tradeoff is that `extra_types` custom converters can't be resolved (see [Known Limitations](#known-limitations)).
- **Matching reuses pytest-bdd's own libraries** rather than reimplementing their semantics, so a step either statically matches or doesn't, exactly the way pytest-bdd itself would decide at runtime.
- **Workspace scans run in a thread executor**, not directly on the event loop, so a large project doesn't block other LSP requests (hover, completion...) while indexing.

## Known Limitations

- **`cfparse` with `extra_types`** can't be matched statically — flagged in diagnostics/hover as unverifiable rather than silently mismatched.
- **`references`/`definition` from the Python side** only recognize the cursor on the `def` line itself, not the decorator line above it.
- **No rename, formatting, call hierarchy, code actions, or semantic tokens.** Gherkin syntax highlighting and folding are expected to come from a Treesitter grammar on the client side, not this server.
- **No incremental re-indexing.** A `.py` or `.feature` change triggers a full workspace rescan rather than a targeted update — fast enough for typical project sizes, but a known scaling limit.

## Development

This project uses [uv](https://docs.astral.sh/uv/) for dependency management, [ruff](https://docs.astral.sh/ruff/) for linting, and [pytest](https://docs.pytest.org/) (with [pytest-lsp](https://pytest-lsp.readthedocs.io/) for end-to-end tests that drive a real server subprocess over LSP) for testing.

```bash
# Install dependencies (including dev dependencies)
uv sync

# Run the full test suite
uv run pytest

# Run only the fast, non-LSP unit tests
uv run pytest tests/unit

# Lint
uv run ruff check .
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

Licensed under [Mozilla Public License v2](./LICENSE)

## Acknowledgments

Built with:

- [pygls](https://github.com/openlawlibrary/pygls) — LSP framework
- [gherkin-official](https://pypi.org/project/gherkin-official/) — Gherkin parsing
- [parse](https://pypi.org/project/parse/) / [parse_type](https://pypi.org/project/parse-type/) — step pattern matching

Special thanks to the [pytest-bdd](https://pytest-bdd.readthedocs.io/) team for the framework this server exists to support.

---

*Built with AI assistance, reviewed with care.*
