Architecture
This document describes the internal architecture of the Gherkin PowerTools Visual Studio Code extension.
High-Level Architecture
The extension is built on a Hybrid Parsing Engine.
While formatting is traditionally difficult for whitespace-sensitive languages, we use the official @cucumber/gherkin Abstract Syntax Tree (AST) to perform structural analysis.
If the AST fails due to catastrophic syntax errors (e.g., a user currently typing a malformed line), our Live Diagnostics Linter seamlessly falls back to a custom text-based scanner to ensure the extension continues to function.
Module Map
graph LR
A[extension.ts] --> B[formatter.ts]
A --> C[linter.ts]
A --> D[highlighter.ts]
A --> E[definition.ts]
A --> F[outline.ts]
A --> G[statistics.ts]
A --> H[codeAction.ts]
A --> I[completion.ts]
A --> J[cache.ts]
A --> K[logger.ts]
| Module | Responsibility |
|---|---|
extension.ts |
Entry point. Bundled via Esbuild for fast activation. Registers all commands, providers, and diagnostics. |
formatter.ts |
Core formatting engine: indentation, table alignment, auto-casing, tag wrapping |
highlighter.ts |
Custom semantic syntax highlighting via createTextEditorDecorationType |
linter.ts |
Real-time syntax checking via @cucumber/gherkin AST, with fallback text scanning. |
definition.ts |
Go-To-Definition provider: accesses cache.ts for instant lookups |
outline.ts |
Hierarchical tree of Feature > Rule > Scenario for the Outline panel |
statistics.ts |
Interactive HTML Webview dashboard displaying heuristic workspace metrics |
codeAction.ts |
Generates quick fixes (💡) for undefined steps or syntax typos |
completion.ts |
Smart IntelliSense autocompletion parsing regex into Snippets |
cache.ts |
In-memory caching engine that indexes the workspace on startup |
logger.ts |
Native VS Code Output Channel for tracing |
The Formatting Engine
While parsing relies on the AST for semantic validation, the formatter.ts leverages regex-based token extraction combined with AST localization to perform block-spacing and table alignment without altering invalid lines.
classDiagram
class GherkinFormattingEditProvider {
+provideDocumentFormattingEdits() TextEdit[]
+provideDocumentRangeFormattingEdits() TextEdit[]
+formatGherkin - lines and initialStepIndent to string array
-indentLine - line to string
-alignTable - tableLines and indentSpaces to string array
}
The core logic implements two key VS Code interfaces:
vscode.DocumentFormattingEditProvider(Full file formatting)vscode.DocumentRangeFormattingEditProvider(Selection formatting)
Line Parsing Workflow
flowchart TD
Start([Start Formatting]) --> ReadLines[Read Document Lines]
ReadLines --> Loop[For Each Line]
Loop --> CheckTable{Is Line a Table Row?}
CheckTable -- Yes --> Buffer[Buffer Table Line]
Buffer --> Loop
CheckTable -- No --> FlushCheck{Is Table Buffer Full?}
FlushCheck -- Yes --> Align[Align Buffered Table]
Align --> Indent[Process Current Line Indentation]
FlushCheck -- No --> Indent
Indent --> ExtractKeyword[Extract Gherkin Keyword]
ExtractKeyword --> CalculateDynamicIndent[Calculate Keyword Length + Base Indent]
CalculateDynamicIndent --> SaveState[Save lastStepIndent for future tables]
SaveState --> BlockSpacing[Check Block Spacing]
BlockSpacing --> IsNewBlock{Is Scenario, Rule, or tag?}
IsNewBlock -- Yes --> InsertBlank[Insert Blank Line if needed]
IsNewBlock -- No --> Push[Push Formatted Line]
InsertBlank --> Push
Push --> Loop
Loop -- No More Lines --> FinalFlush{Any Buffered Tables?}
FinalFlush -- Yes --> AlignFinal[Align Final Table]
AlignFinal --> End([End Formatting])
FinalFlush -- No --> End
Table Alignment Algorithm
The most complex part of the extension is the dynamic table alignment algorithm.
Example Trace
Given the following raw input:
- The parser hits
Given I have a database - It applies a base indent of
4 spaces - It runs a regex to capture the keyword
Given(length 5) - It calculates:
baseIndent (4) + keywordLength (5) + space (1) = 10 lastStepIndentis stored as10- The parser buffers the table rows
- Upon hitting the end, it flushes to
alignTable(buffer, 10) alignTablesplits columns by|, calculates max widths, and pads with.padEnd()
Result: