BDD Gherkin Step Lens

Reqnroll Step Lens

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.

● v0.0.7 Publisher: VarunKumarG VS Code ^1.74.0 Workspace extension 722 LOC · single file
Overview

What the extension actually does

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.

Activation & indexing timeline

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.

1. activate() open .feature 2. spinner findFiles(Steps/*.cs) 3. extract attrs [Given/When/Then/And/But] 4. compile regex {int}/{word}/(.*) → RegExp 5. build index 3-word prefix → [defs] 6. decorate paint colours + minimap 7. ✓ ready N steps ⟳ BDD: Indexing steps… ✓ BDD: 312 steps
✓ BDD: 312 steps — shown in the bottom-left status bar after indexing completes
Features · 1 of 8

01Gherkin keyword colouring

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.

login.feature
1Feature: Letter Builder Sanity Scenarios
2
3 Background:
4 Given I navigate to LetterBuilder application
5
6 Scenario Outline: Verify profile and login history
7 Given I login with valid <username>
8 When I click on the login button
9 Then I should be logged in to the application
10 And I verify the profile icon is displayed
11
12 Examples:
13 | username |
14 | Company1.Inn@yopmail.com |

Each keyword has its own role and its own colour

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.

  • #A0C4FF — Feature:, Background:, Examples:, Scenario:, Rule:
  • #4FC1FF — Scenario Outline: keyword (bold)
  • #98C379 — the title text that follows Scenario Outline:
  • #569CD6 — every step keyword: Given, When, Then, And, But
Implementation note — matching is done with two simple regexes, /^(\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.
Features · 2 of 8

02Parameter value highlighting

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 matched values light up in #CE9178

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.

  • Multiple captures per line are fully supported — each match gets its own highlight
  • Bold weight on parameters so you can spot them even when reading fast
  • Used both for step parameters and for data row cells in Examples: tables
Cucumber Expression support — {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.
📄 NavigationSteps.cs
42[When("I navigate to (.*) page under settings option")]
43public async Task NavigateToPage(string option) { ... }
settings.feature
8 When I navigate to merge fields page under settings option
(.*) in the pattern → "merge fields" captured
Features · 3 of 8

03Scenario Outline placeholders

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.

Pink always wins over orange Scenario Outline: Login verification Given I login with valid <username> without DBConnection When I click on the login button Then I should be logged in to the application Examples: | username | | Company1.Inn@yopmail.com | ↑ same name binds them

Pink takes priority over orange

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.

  • Pink placeholder in the step → pink header cell in Examples: → orange data cell in each row
  • This rule is hard-coded — Scenario Outline placeholders always win over (.*) captures, even when the regex would also match them
  • Detected by the regex /<([^>]+)>/g running per line
Features · 4 of 8

04Examples table colouring & auto-formatting

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.

Three colour roles inside a table

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.

  • #569CD6 — every | pipe (bold)
  • #FF69B4 — cells in the first row (header)
  • #CE9178 — cells in every subsequent row (data)

Auto-format on edit

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.

A formattingTable guard prevents the formatter from re-firing on its own edit, so there's no infinite loop.

// you typed this →

outline.feature — before
15 Examples:
16 | name | role | email |
17 | Alex | Admin | a@x.io |
18 | Priyanka | QA | priyanka@reqnroll.dev |

// ≈ 400ms later — auto-aligned ↓

outline.feature — after
15 Examples:
16 | name | role | email |
17 | Alex | Admin | a@x.io |
18 | Priyanka | QA | priyanka@reqnroll.dev |
Features · 5 of 8

05Four step states · matched, ambiguous, undefined, wrong-keyword

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.

step line read Given/When/Then/And/But match count? (filtered by keyword) ✓ MATCHED orange params blue keyword = 1 ⚠ AMBIGUOUS yellow wavy underline gold band in minimap ≥ 2 text matches under any keyword? = 0 ✗ WRONG KEYWORD red keyword only text exists under different kw yes ✗ UNDEFINED FULL LINE red no matching def anywhere no

Side-by-side: all four states in one feature file

mixed-states.feature
1Feature: Demonstrate every step state
2
3 Scenario: All four colour states
4 When I navigate to merge fields page under settings option
5 # ↑ MATCHED — single def, params painted orange
6 Then I see the dashboard with all the widgets
7 # ↑ AMBIGUOUS — 2 patterns match: "I see (.*)" and "I see (.*) with (.*)"
8 Given I click the green submit button at the bottom of the screen
9 # ↑ UNDEFINED — no def matches this text at all
10 When I navigate to merge fields page under settings option
11 # ↑ WRONG-KEYWORD — text exists under [Given], not [When]

← 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

Rich hover tooltips — one per state

✅ Step Matched
→ Steps/NavigationSteps.cs line 43
`I navigate to (.*) page under settings option`
⚠️ Ambiguous Step — 2 definitions match this step:
Fix: make the step text more specific so only one definition matches.

→ Steps/DashboardSteps.cs line 17
`I see (.*)`
→ Steps/DashboardSteps.cs line 31
`I see (.*) with (.*)`
❌ Undefined Step
No step definition matches this step text.
`I click the green submit button at the bottom of the screen`

Create a matching C# method in a Steps class:
[Given("I click the green submit button…")] public async Task MethodName() { }
❌ Wrong Keyword
A step definition for this text exists, but not under When.
Check whether the step should be Given, When, or Then.
Keyword-aware matching (Reqnroll semantics, v0.0.6+) — a [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.
Features · 6 of 8

06Go to Definition · F12

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.

⬢ login.feature Feature: Login Scenario: Click profile When I click on the profile icon ← cursor on this line press F12 jump 📄 ProfileSteps.cs [When("I click on the profile icon")] public async Task WhenIClickOnTheProfileIcon() { await _page.ClickOnProfileButtonOnHeader(); }
"Most specific match" rule — the definition provider tries every candidate regex, sums the lengths of all captured groups for each match, and picks the definition with the smallest sum. So if your step is "I click on the login button", the literal pattern "I click on the login button" (captures 0 chars) beats "I click on the (.*) button" (captures 5 chars) and beats "I (.*)" (captures 23 chars).
Features · 7 of 8

07Automatic, incremental step reload

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.

FileSystemWatcher **/Steps/**/*.cs onDidCreate onDidChange onDidDelete loadStepDefinitions(uri) filter old, reparse ONE file buildIndex() + redecorate visible editors
Features · 8 of 8

08Built for large projects · four optimisations

The 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.

01 · INDEX

Prefix index (3-word literal)

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[]>
02 · CACHE

Version-keyed decoration cache

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.

decorCache: Map<docUri, ranges>
03 · DEBOUNCE

250 ms debounce on keystrokes

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 = 250
04 · SINGLE-PASS

One loop, all decorations

Every 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)

What this looks like in practice

Steps checked per feature-file line (lower is better) without index: with index: ~500 regex tests / line ~5 regex tests / line ≈ 100× fewer regex evaluations per line
Reference

Complete colour system

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 —

Decoration types vs. semantic tokens — why both?

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.

Internals

How the engine actually works

The whole extension is a single 722-line extension.js file with no external dependencies. Here's the pipeline, end to end.

A · INDEXING

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.

attrRe
B · COMPILE

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>.

case-insensitive · anchored ^…$
C · BUCKET

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.

stepIndex
D · DECORATE

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.

single linear pass
E · CACHE

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.

decorCache: Map<uri, …>
F · HOVER

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.

isTrusted = true (links allowed)
G · DEFINITION

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).

registered for: feature · gherkin · file:**/*.feature
H · WATCH

FileSystemWatcher

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.

incremental — no full re-scan
I · FORMAT

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.

400 ms debounce · undoStopBefore: false
J · SEMANTIC

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).

renders the minimap colours
K · GUARD

stepDefsLoaded flag

Suppresses 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 steps
L · COMMAND

reqnrollStepLens.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.

preserveFocus: false
Setup

Configuration · Activation · Supported attributes

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.

Activation events

The extension activates on any of these triggers, declared in package.json:

package.json — activationEvents
1"activationEvents": [
2 "workspaceContains:**/*.feature",
3 "onLanguage:feature",
4 "onLanguage:gherkin"
5]

Supported C# step attribute formats

📄 ExampleSteps.cs
1[Given("...")]
2[When("...")]
3[Then("...")]
4[And("...")]
5[But("...")]
6
7// Multiple attributes on one method — both are indexed
8[When("I click on the (.*) button")]
9[When("I click on the (.*) option")]
10public async Task ClickButton(string name) { ... }
11
12// Verbatim strings with literal parentheses — fully supported
13[Then(@"the column \(s\) (.*) exist")]

Cucumber Expression tokens

TokenCompiles toCaptures
{int}(-?\d+)signed integer
{float} · {double}(-?\d*\.?\d+)signed decimal
{word}(\w+)one word
{string}("[^"]*"|'[^']*')quoted string
{}(.*)anything

Recommended workspace settings

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.

.vscode/settings.json
1{
2 "cucumberautocomplete.steps": ["CP.Automation.Tests/Steps/**/*.cs"],
3 "cucumberautocomplete.gherkinDefinitionPart": "\\[?(Given|When|Then|And|But)\\s*\\(\"",
4 "cucumberautocomplete.stepRegExSymbol": "\"",
5 "files.associations": { "*.feature": "feature" }
6}