Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions GOTO_DEFINITION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Go to Definition Support for Gherkin Steps

This fork adds **Go to Definition** functionality for Gherkin steps in VS Code. When you Ctrl+Click (or F12) on a Gherkin step in a `.feature` file, the extension will now navigate to the corresponding keyword definition in your Robot Framework `.resource` files.

## Features Added

### 1. **Go to Definition**
- **Ctrl+Click** or press **F12** on any Gherkin step to jump to its implementation
- Searches for keyword definitions in `.resource` files within `steps/` directories
- Supports both exact matches and pattern matching for steps with variables

### 2. **Hover Information**
- **Hover** over Gherkin steps to see:
- Keyword signature
- File location with clickable link
- Arguments and their types
- Tags (if any)
- Documentation (if available)

### 3. **Variable Support**
- Handles Robot Framework variables like `${variable}` in step definitions
- Matches steps with quoted strings and variable placeholders
- Fuzzy matching for similar keywords

## How It Works

The extension:
1. **Caches keyword definitions** from `.resource` files in your workspace
2. **Parses Robot Framework syntax** to extract keyword names, arguments, and documentation
3. **Matches Gherkin steps** to keyword definitions using multiple strategies:
- Exact text matching (case-insensitive)
- Pattern matching for variables (e.g., `${name}` becomes a capture group)
- Fuzzy matching for partial word matches

## Example

Given this Gherkin step:
```gherkin
Given a blog post named "My First Post" with Markdown body
```

And this Robot Framework keyword definition in a `.resource` file:
```robotframework
*** Keywords ***
a blog post named "${name}" with Markdown body
[Tags] gherkin:step
[Arguments] ${body}

Log ${name}
Log ${body}
```

You can now:
- **Ctrl+Click** on the step to navigate to the keyword definition
- **Hover** to see the keyword signature and documentation

## File Structure Support

The extension looks for keyword definitions in:
- `**/steps/**/*.resource` (primary location for step definitions)
- `**/*.resource` (fallback for any `.resource` files in the workspace)

## Installation

This enhanced version works as a drop-in replacement for the original RobotCode Gherkin extension. Simply:

1. Install the modified extension
2. Open a workspace with `.feature` files and corresponding `.resource` files
3. Start using Go to Definition and Hover features!

## Technical Details

### Architecture
- **Definition Provider**: Implements VS Code's `vscode.DefinitionProvider` interface
- **Hover Provider**: Implements VS Code's `vscode.HoverProvider` interface
- **Caching**: Intelligent caching with 30-second timeout to balance performance and freshness
- **Pattern Matching**: Advanced regex patterns to handle Robot Framework variable substitution

### Performance
- **Lazy Loading**: Only parses files when needed
- **Cached Results**: Avoids re-parsing files on every request
- **Efficient Search**: Uses Maps for O(1) keyword lookups
- **Background Processing**: Non-blocking file parsing

This enhancement significantly improves the developer experience when working with Gherkin feature files in Robot Framework projects by providing the navigation features developers expect from modern IDEs.
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
# **Robot Framework Gherkin Parser**: Quick Overview
# **Robot Framework Gherkin Parser (Extended)**: Quick Overview

The **Robot Framework Gherkin Parser (Extended)** enables seamless integration of Gherkin feature files with the **Robot Framework**, facilitating behavior-driven development (BDD) with enhanced IDE support. This extended version adds **Go to Definition** and **Hover** functionality for Gherkin steps in VS Code, allowing you to navigate directly from Gherkin steps to their Robot Framework keyword implementations.

## 🆕 New Features in Extended Version

- **Go to Definition**: Ctrl+Click or F12 on any Gherkin step to jump to its keyword implementation in `.resource` files
- **Hover Information**: Hover over steps to see keyword signatures, documentation, arguments, and file locations
- **Variable Support**: Handles Robot Framework variables (`${variable}`) in step definitions
- **Smart Matching**: Supports exact matches, pattern matching, and fuzzy matching for keyword discovery

[**📖 Read detailed documentation about Go to Definition features**](./GOTO_DEFINITION.md)

---

The **Robot Framework Gherkin Parser** enables seamless integration of Gherkin feature files with the **Robot Framework**, facilitating behavior-driven development (BDD) with ease. This integration not only allows for the flexible execution of Gherkin feature files alongside **Robot Framework** test files but also highlights the complementary strengths of both approaches. Gherkin feature files, with their less technical and more scenario-focused syntax, emphasize the behavioral aspects of what is being tested, rather than the how. In contrast, **Robot Framework** test files tend to be more technical, focusing on the step-by-step implementation of test scenarios through keyword sequences.

Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "robotcode-gherkin",
"displayName": "RobotCode GherkinParser Support",
"description": "GherkinParser Support for RobotCode and Robot Framework",
"name": "robotcode-gherkin-extended",
"displayName": "RobotCode GherkinParser Support (Extended)",
"description": "GherkinParser Support for RobotCode and Robot Framework with Go to Definition",
"icon": "icons/icon.png",
"publisher": "d-biehl",
"version": "0.3.2",
"version": "0.4.0",
"author": {
"name": "Daniel Biehl",
"url": "https://github.com/d-biehl/"
Expand Down
221 changes: 221 additions & 0 deletions vscode-client/definitionProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import * as vscode from "vscode";

interface KeywordMatch {
keyword: string;
file: string;
line: number;
pattern?: RegExp;
priority?: number;
}

export class GherkinDefinitionProvider implements vscode.DefinitionProvider {
private keywordCache: Map<string, KeywordMatch[]> = new Map();
private cacheTimestamp: number = 0;
private readonly CACHE_TIMEOUT = 30000; // 30 seconds

async provideDefinition(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.Definition | undefined> {
// Check if we're in a Gherkin step
const line = document.lineAt(position.line);
const stepMatch = line.text.match(/^\s*(Given|When|Then|And|But)\s+(.+)$/i);

if (!stepMatch) {
return undefined;
}

const stepText = stepMatch[2].trim();
console.log(`[GherkinDefinitionProvider] Looking for definition of: "${stepText}"`);

// Refresh keyword cache if needed
await this.refreshKeywordCache(document.uri);

// Find matching keywords
const matches = this.findMatchingKeywords(stepText);
console.log(`[GherkinDefinitionProvider] Found ${matches.length} matches:`, matches.map(m => `${m.keyword} (priority: ${m.priority})`));

if (matches.length === 0) {
console.log(`[GherkinDefinitionProvider] No matches found for: "${stepText}"`);
return undefined;
}

// Convert matches to VS Code locations
const definitions: vscode.Location[] = [];
for (const match of matches) {
try {
const uri = vscode.Uri.file(match.file);
const location = new vscode.Location(uri, new vscode.Position(match.line, 0));
definitions.push(location);
} catch (error) {
console.error(`Error creating location for ${match.file}:${match.line}`, error);
}
}

return definitions.length > 0 ? definitions : undefined;
}

private async refreshKeywordCache(documentUri: vscode.Uri): Promise<void> {
const now = Date.now();
if (now - this.cacheTimestamp < this.CACHE_TIMEOUT && this.keywordCache.size > 0) {
return;
}

this.keywordCache.clear();
this.cacheTimestamp = now;

// Find workspace folder
const workspaceFolder = vscode.workspace.getWorkspaceFolder(documentUri);
if (!workspaceFolder) {
return;
}

// Look for .resource files in steps directories
const pattern = new vscode.RelativePattern(workspaceFolder, "**/steps/**/*.resource");
const resourceFiles = await vscode.workspace.findFiles(pattern);

// Also look for .resource files in the general workspace (as a fallback)
const generalPattern = new vscode.RelativePattern(workspaceFolder, "**/*.resource");
const allResourceFiles = await vscode.workspace.findFiles(generalPattern);

// Combine and deduplicate
const uniqueFiles = [...new Set([...resourceFiles, ...allResourceFiles])];

// Parse each resource file
for (const fileUri of uniqueFiles) {
await this.parseResourceFile(fileUri);
}
}

private async parseResourceFile(fileUri: vscode.Uri): Promise<void> {
try {
const document = await vscode.workspace.openTextDocument(fileUri);
const content = document.getText();
const lines = content.split(/\r?\n/);

let inKeywordsSection = false;
let currentKeyword: string | null = null;

for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trim();

// Check for *** Keywords *** section
if (trimmedLine.match(/^\*\*\*\s*Keywords\s*\*\*\*$/i)) {
inKeywordsSection = true;
continue;
}

// Check for other sections (*** Settings ***, *** Variables ***, etc.)
if (trimmedLine.match(/^\*\*\*.*\*\*\*$/)) {
inKeywordsSection = false;
continue;
}

if (inKeywordsSection && trimmedLine) {
// Check if this is a keyword definition (not indented or minimally indented)
if (!line.startsWith(' ') && !line.startsWith('\t') && trimmedLine !== '') {
// Skip lines that are clearly not keyword definitions
if (trimmedLine.startsWith('[') || trimmedLine.startsWith('#')) {
continue;
}

currentKeyword = trimmedLine;

// Store the keyword
const filePath = fileUri.fsPath;
const keywordMatch: KeywordMatch = {
keyword: currentKeyword,
file: filePath,
line: i,
pattern: this.createKeywordPattern(currentKeyword)
};

if (!this.keywordCache.has(currentKeyword.toLowerCase())) {
this.keywordCache.set(currentKeyword.toLowerCase(), []);
}
this.keywordCache.get(currentKeyword.toLowerCase())!.push(keywordMatch);
}
}
}
} catch (error) {
console.error(`Error parsing resource file ${fileUri.fsPath}:`, error);
}
}

private createKeywordPattern(keyword: string): RegExp {
// Handle Robot Framework variable patterns like ${variable}
// Convert them to regex patterns that can match actual values

// Escape special regex characters except ${} patterns
let pattern = keyword
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
.replace(/\\\$\\\{[^}]+\\\}/g, '(.+?)'); // Convert ${var} to capture groups

// Handle quoted strings with variables
pattern = pattern.replace(/["'](.+?)["']/g, (match, content) => {
// If the content has variables, make the quotes optional
if (content.includes('${')) {
return `(?:["']${content}["']|${content})`;
}
return match;
});

// Make the pattern case-insensitive and allow for flexible whitespace
pattern = pattern.replace(/\s+/g, '\\s+');

return new RegExp(`^${pattern}$`, 'i');
}

private findMatchingKeywords(stepText: string): KeywordMatch[] {
const matches: KeywordMatch[] = [];

// Clean the step text - remove quotes and normalize
const cleanStepText = stepText.replace(/^["']|["']$/g, '').trim();

for (const [, keywordMatches] of this.keywordCache) {
for (const keywordMatch of keywordMatches) {
// Normalize both step and keyword for comparison
const normalizedStep = this.normalizeForMatching(cleanStepText);
const normalizedKeyword = this.normalizeForMatching(keywordMatch.keyword);

console.log(`[GherkinDefinitionProvider] Comparing:`);
console.log(` Step: "${cleanStepText}" -> normalized: "${normalizedStep}"`);
console.log(` Keyword: "${keywordMatch.keyword}" -> normalized: "${normalizedKeyword}"`);

// Exact match after normalization - highest priority
if (normalizedStep === normalizedKeyword) {
console.log(` -> EXACT MATCH!`);
matches.push({ ...keywordMatch, priority: 1 });
continue;
}

// Pattern match with variables - high priority
if (keywordMatch.pattern && keywordMatch.pattern.test(cleanStepText)) {
console.log(` -> PATTERN MATCH!`);
matches.push({ ...keywordMatch, priority: 2 });
continue;
}

console.log(` -> No match`);
}
}

// Sort by priority (lower number = higher priority)
return matches.sort((a, b) => (a.priority || 999) - (b.priority || 999));
}

private normalizeForMatching(text: string): string {
return text
.toLowerCase()
// Remove Gherkin keywords at the start
.replace(/^(given|when|then|and|but)\s+/i, '')
// Replace variables with placeholder
.replace(/\$\{[^}]+\}/g, 'VAR')
// Normalize whitespace and punctuation
.replace(/[^\w\s]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
}
10 changes: 10 additions & 0 deletions vscode-client/extension.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as vscode from "vscode";
import { GherkinFormattingEditProvider } from "./formattingEditProvider";
import { GherkinDefinitionProvider } from "./definitionProvider";
import { GherkinHoverProvider } from "./hoverProvider";

export async function activateAsync(context: vscode.ExtensionContext): Promise<void> {
const robotcode = vscode.extensions.getExtension("d-biehl.robotcode");
if (!robotcode) {
Expand All @@ -11,8 +14,15 @@ export async function activateAsync(context: vscode.ExtensionContext): Promise<v
// return;
// }

const definitionProvider = new GherkinDefinitionProvider();
const hoverProvider = new GherkinHoverProvider();

context.subscriptions.push(
vscode.languages.registerDocumentFormattingEditProvider("gherkin", new GherkinFormattingEditProvider()),
vscode.languages.registerDefinitionProvider("gherkin", definitionProvider),
vscode.languages.registerDefinitionProvider("markdown", definitionProvider),
vscode.languages.registerHoverProvider("gherkin", hoverProvider),
vscode.languages.registerHoverProvider("markdown", hoverProvider),
);
}

Expand Down
Loading