A zero-config VS Code extension that turns your Gherkin
.feature
files into a live lens over your Reqnroll / SpecFlow C# step definitions —
colorising matches, detecting problems, and navigating to source.
When you open a .feature file, the extension scans every
**/Steps/**/*.cs file in your workspace, extracts every
[Given] / [When] / [Then] /
[And] / [But] attribute, compiles each pattern
into a regular expression, indexes the regexes by their first three literal words, and then
walks every line of your feature file matching steps against the index. The result is a
live, four-state visual classification of every step — matched, ambiguous,
undefined, or wrong-keyword — with parameter values, outline placeholders,
table cells, scenario titles and Gherkin keywords each painted in their own dedicated colour.
Hovering on any step gives you a rich tooltip; pressing F12 jumps
straight to the C# method that backs it.
The diagram below traces the lifecycle from "open a .feature file" → "every step coloured". The status bar updates twice — once with a spinner while indexing, once with the final indexed step count.
Every Gherkin structural keyword is painted in a dedicated, semantic colour so the
shape of your scenario reads at a glance — without any syntax theme installed.
Feature/Background/Examples get the soft blue
#A0C4FF,
Scenario Outline keywords get the bold cyan
#4FC1FF with a green
#98C379 title, and the
step keywords Given/When/Then/And/But get a bold blue
#569CD6.
The colours are not arbitrary — they encode structure. Step keywords pop in bold blue because they're the things you scan for; the feature/background/examples keywords are softer because they're frames, not content.
Feature:, Background:, Examples:, Scenario:, Rule:Scenario Outline: keyword (bold)Scenario Outline:Given, When, Then, And, But/^(\s*)(Given|When|Then|And|But)(\s+)/i for steps and
/^(\s*)(Feature:|Scenario Outline:|Scenario:|Background:|Examples:|Rule:)/i
for structural keywords, applied line by line.
For every step that matches a C# definition, the values captured by
(.*), {int}, {word},
{string}, {float}, {double}
or {} are highlighted in orange — so you can see exactly
which parts of your sentence are data and which parts are structure.
The extension reads each [When("…")] attribute, converts
Cucumber Expressions to regex, runs the regex against your step text, and then
uses indexOf to find where each captured group landed on
the line. Those character ranges become orange decorations.
Examples: tables{int} matches
-?\d+, {word} matches \w+,
{string} matches quoted strings, and {} matches
anything. The conversion happens once at indexing time, so matching at runtime is fast.
Tokens written as <placeholder> inside a Scenario Outline
are highlighted in pink #FF69B4 — a
colour deliberately distinct from the orange parameter colour, so you always know which
values come from the Examples: data table versus which were
captured by a (.*) group.
Inside a Scenario Outline, the regex match against your step text would normally
capture <username> as a parameter and paint it orange.
The extension explicitly detects the <…> form and
paints it pink instead — overriding the orange — so the visual link between the
placeholder and the data table header is unmistakable.
Examples: → orange data cell in each row(.*) captures, even when the regex would also match them/<([^>]+)>/g running per line
Every line under an Examples: block that starts with |
is treated as a table row. The first such row is the header (pink), every row after it is data (orange),
and the pipes themselves are bold blue. As you type into a cell, the entire table is debounced
and auto-aligned to the widest cell in each column.
The renderer tracks an afterExamples flag — set to true on the
Examples: line, cleared on the first non-table line — and uses
examplesHeaderSeen to mark the very first |-row
as the header.
| pipe (bold)Edit any cell in a table and the extension waits 400 ms, finds the table boundary, computes the maximum width per column, and rewrites every row with each cell padded to that width — producing perfectly aligned Markdown-style tables without keystrokes.
formattingTable guard prevents the formatter from re-firing
on its own edit, so there's no infinite loop.
// you typed this →
// ≈ 400ms later — auto-aligned ↓
For every step in your feature file, the extension classifies it into exactly one of four states, and paints it accordingly. This is the heart of the extension — the "lens" — and it gives you instant feedback on which steps will run cleanly, which will fail, and which need your attention.
← the strip on the right is the VS Code minimap; the extension paints whole-line bands so problem lines are visible across the whole file
Steps class:When.Given, When, or Then.[When("…")] definition only matches When steps,
and any And / But steps that follow a
When in the same scenario. The current keyword context is tracked per-line
and reset at every Scenario / Background boundary —
so a broad [Then("I (.*) …")] pattern will never accidentally turn your
When step yellow. [And] and [But]
attributes remain keyword-agnostic.
Press F12 on any step line, or right-click → Go to Definition, and the editor jumps straight to the C# method that backs it. When two patterns could match the same step, the extension picks the most specific one — the pattern that captures the fewest characters via its capture groups — so you land on the right definition every time.
"I click on the login button" (captures 0 chars) beats
"I click on the (.*) button" (captures 5 chars) and beats
"I (.*)" (captures 23 chars).
Add a new [Given], edit an existing one, or delete a whole steps file —
every .feature file in your workspace re-colours automatically,
without a window reload. The reload is incremental: only the changed
.cs file is re-parsed, then the index is rebuilt.
decorCache, ambiguousLineMap, matchedLineMap all resetThe extension is engineered so that even projects with hundreds of step definitions and hundreds of feature files feel instant. Four optimisations stack on top of each other.
Every step definition is keyed by the first three literal words of its pattern. When matching a step, the extension first tries the full 3-word key, then 2 words, then 1 word — checking only ~5 candidate regexes per line instead of all 500+.
stepIndex: Map<string, def[]>
Each computed set of decoration ranges is stored against the document's
version number. Switching back to a previously-decorated
file just re-applies cached ranges — zero recomputation.
As you type, recomputation is deferred until you pause for 250 ms — so the extension never runs on every keystroke, only when you stop. Per-document timers mean editing one file doesn't delay another.
DEBOUNCE_MS = 250Every decoration type — keywords, params, outline placeholders, table cells, unmatched/ambiguous ranges — is computed in one linear pass over the document, not one pass per decoration type.
computeDecorations(doc)
Every colour the extension paints comes from one of these eleven semantic tokens.
They're registered as VS Code semanticTokenTypes with the prefix
bdd, so they show up in the minimap and overview ruler in addition
to the editor itself.
| Token | What it paints | Hex | Weight |
|---|---|---|---|
bddStepKw |
Step keywords — Given When Then And But |
#569CD6 |
Bold |
bddFeatureKw |
Frame keywords — Feature: Background: Examples: Scenario: Rule: |
#A0C4FF |
Normal |
bddScenarioLine |
Whole-line band for non-outline scenario headers in the semantic-token pass | #C586C0 |
Bold |
bddScenarioKw |
The Scenario Outline: keyword specifically |
#4FC1FF |
Bold |
bddScenarioTitle |
The title text after Scenario Outline: |
#98C379 |
Bold |
bddParam |
Captured parameter values (.*), {int}, {word}, etc. |
#CE9178 |
Bold |
bddTableHeader / outline |
First row of an Examples: table · outline placeholders <name> |
#FF69B4 |
Bold |
bddTableData |
All non-header rows of an Examples: table |
#CE9178 |
Bold |
bddUnmatched |
Undefined steps (whole line) & wrong-keyword steps (keyword only) — wavy underline + minimap band | #F44747 |
Bold |
bddAmbiguous · text |
Ambiguous steps (≥ 2 patterns match) — wavy underline | #DCDCAA |
Bold |
bddAmbiguous · minimap |
Overview ruler & minimap band for ambiguous lines | #FFD700 |
— |
The extension registers both regular TextEditorDecorationTypes
(for the main editor surface, with wavy underlines and overview-ruler colours) and
a DocumentSemanticTokensProvider (so the minimap renders the same
colours). Decorations alone don't paint the minimap — only semantic tokens do — so this
dual approach is what gives you the coloured strip on the right edge of the editor.
The whole extension is a single 722-line extension.js file with
no external dependencies. Here's the pipeline, end to end.
loadStepDefinitions()
On activation, calls findFiles('**/Steps/**/*.cs', '**/bin/**')
and runs the attribute regex
/\[\s*(Given|When|Then|And|But)\s*\(\s*(@?)"((?:[^"\\]|\\.)*)"\s*\)/g
across each file. For verbatim strings (@"…") the raw pattern
is kept; for non-verbatim strings, \\ escapes are unescaped.
patternToRegex()
Converts each pattern string into a compiled RegExp.
Cucumber Expressions become regex ({int} → -?\d+),
escaped literal parens (\(s\)) are temporarily swapped for
private-use placeholders so they survive the split on capture groups, and capture
groups that contain regex operators are wrapped so they also accept outline placeholders
<name>.
buildIndex()
Each compiled definition has a prefix field — the first three
literal words of the pattern, lower-cased. These are used as keys in a
Map<string, def[]>, so candidate lookups for a step line
are O(1) on the prefix and then check only a tiny bucket of candidates.
computeDecorations(doc)
One pass over every line. Detects table rows, feature keywords, step keywords;
tracks the active contextKeyword so And/But
inherit from Given/When/Then;
filters candidates by keyword; counts matches; produces a result object with eleven
range-arrays and four line-number sets.
decorateEditor(editor)
Checks the decorCache against the document's
version — a hit means just re-apply the cached ranges
via setDecorations. A miss runs the compute pass and
stores the result. The semantic-token emitter is fired so the minimap repaints.
hoverProvider
Returns a MarkdownString based on the cached state of the
line under the cursor: a green "Step Matched" card with a link, a yellow "Ambiguous"
list of every matching definition, a red "Undefined" with a copy-pasteable boilerplate,
or a red "Wrong Keyword" hint. Links use the custom command
reqnrollStepLens.goToStep.
definitionProvider
For F12 / right-click → Go to Definition. Iterates the candidate bucket, tries every
regex against the step text, and returns a vscode.Location
pointing at the definition whose total captured-character length is smallest
(the "most specific" pattern wins).
A workspace.createFileSystemWatcher('**/Steps/**/*.cs') fires on
create/change/delete. Each event calls loadStepDefinitions(uri)
which incrementally re-parses just that one file, rebuilds the index, clears all caches,
and redecorates visible editors.
scheduleTableFormat()
On every text-document change, if the changed line starts with |,
a 400 ms timer schedules a table reformat. The formatter expands the table to its full
row range, computes the max width per column, and replaces each row with cells padded
to that width. A formattingTable guard prevents loops.
semanticProvider
A DocumentSemanticTokensProvider with eleven custom token
types. Emits whole-line tokens for unmatched / ambiguous / table / feature-keyword
lines, and per-range tokens for matched-step keyword + params. Tokens are sorted
by line then character (required by SemanticTokensBuilder).
stepDefsLoaded flagSuppresses all red highlighting until the very first full scan completes — so you don't see a flash of red on a freshly-opened workspace while the extension is still finding step definitions.
false → spinner · true → ✓ N stepsreqnrollStepLens.goToStep
A custom command invoked from hover-tooltip links. Receives
{ uri, line }, opens the document, and positions the
cursor at the start of the target line. Uses VS Code's 0-based
Position — the v0.0.7 fix corrected a previous off-by-one.
The extension is zero-config in the sense that it works the moment you open a
.feature file in a workspace that contains C# step definitions
under a Steps/ folder. Here's the full surface area.
The extension activates on any of these triggers, declared in package.json:
| Token | Compiles to | Captures |
|---|---|---|
{int} | (-?\d+) | signed integer |
{float} · {double} | (-?\d*\.?\d+) | signed decimal |
{word} | (\w+) | one word |
{string} | ("[^"]*"|'[^']*') | quoted string |
{} | (.*) | anything |
These aren't required — the extension works without them — but they pair well with the popular Cucumber (Gherkin) Full Support extension and ensure feature files render correctly.