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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Added
- Add `cucumber.unescapeBackslashes` configuration option to unescape backslashes in Cucumber Expression patterns from JavaScript step definitions

## [1.11.0] - 2025-05-18
### Changed
- Update dependency @cucumber/language-server to 1.7.0
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,29 @@ For example, if you're using the `actor` parameter type from [@cucumber/screenpl

[//]: # (</cucumber.parameterTypes>)

### `cucumber.unescapeBackslashes`

[//]: # (<cucumber.unescapeBackslashes>)
Enable the `cucumber.unescapeBackslashes` setting if you need to unescape backslashes
in Cucumber Expression patterns from JavaScript or TypeScript step definitions.

In Cucumber Expressions, you need to escape a forward slash with a backslash (`\/`) for
[alternative text behavior](https://github.com/cucumber/cucumber-expressions?tab=readme-ov-file#alternative-text).
In JavaScript source code, backslashes themselves must be escaped, so you write `"\\/"`
to achieve `\/` at runtime. However, sometimes the library interprets `\\/` as `\\/`
(two backslashes) instead of `\/` (one backslash). This setting converts escaped
backslashes (`\\`) back to regular backslashes (`\`) for correct interpretation.

Default value:

```json
{
"cucumber.unescapeBackslashes": false
}
```

[//]: # (</cucumber.unescapeBackslashes>)

## Feedback

If you discover a bug, or have a suggestion for a feature request, please
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@
"type": "array",
"required": false,
"default": []
},
"cucumber.unescapeBackslashes": {
"markdownDescription": "Enable the `cucumber.unescapeBackslashes` setting if you need to unescape backslashes\nin Cucumber Expression patterns from JavaScript or TypeScript step definitions.\n\nIn Cucumber Expressions, you need to escape a forward slash with a backslash (`\/`) for\n[alternative text behavior](https://github.com/cucumber/cucumber-expressions?tab=readme-ov-file#alternative-text).\nIn JavaScript source code, backslashes themselves must be escaped, so you write `\"\\/\"`\nto achieve `\/` at runtime. However, sometimes the library interprets `\\/` as `\\/`\n(two backslashes) instead of `\/` (one backslash). This setting converts escaped\nbackslashes (`\\\\`) back to regular backslashes (`\\`) for correct interpretation.\n\nDefault value:\n\n```json\n{\n \"cucumber.unescapeBackslashes\": false\n}\n```",
"type": "boolean",
"required": false,
"default": false
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion scripts/update-settings-docs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,10 @@ updateMarkdownDescription "cucumber.glue"
updateResult2=$?
updateMarkdownDescription "cucumber.parameterTypes"
updateResult3=$?
updateMarkdownDescription "cucumber.unescapeBackslashes"
updateResult4=$?

if [ "$updateResult1" -eq 1 ] || [ "$updateResult2" -eq 1 ] || [ "$updateResult3" -eq 1 ]; then
if [ "$updateResult1" -eq 1 ] || [ "$updateResult2" -eq 1 ] || [ "$updateResult3" -eq 1 ] || [ "$updateResult4" -eq 1 ]; then
echo "The settings descriptions and default values in 'package.json' do not match those specified in 'README.md'. Updating 'package.json' to match."
exit 1
else
Expand Down
14 changes: 13 additions & 1 deletion src/VscodeFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ import { FileSystem, Uri, workspace } from 'vscode'
export class VscodeFiles implements Files {
constructor(private readonly fs: FileSystem) {}

private shouldUnescapeBackslashes(): boolean {
return workspace.getConfiguration('cucumber').get<boolean>('unescapeBackslashes', false)
}

private unescapeBackslashesInContent(content: string): string {
if (this.shouldUnescapeBackslashes()) {
return content.replace(/\\\\/g, '\\')
}
return content
}

async exists(uri: string): Promise<boolean> {
try {
await this.fs.stat(Uri.parse(uri))
Expand All @@ -15,7 +26,8 @@ export class VscodeFiles implements Files {

async readFile(uri: string): Promise<string> {
const data = await this.fs.readFile(Uri.parse(uri))
return new TextDecoder().decode(data)
const content = new TextDecoder().decode(data)
return this.unescapeBackslashesInContent(content)
}

async findUris(glob: string): Promise<readonly string[]> {
Expand Down
216 changes: 216 additions & 0 deletions src/test/suite/VscodeFiles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import assert from 'assert'
import * as vscode from 'vscode'
import { VscodeFiles } from '../../VscodeFiles'

suite('VscodeFiles Test Suite', () => {
let mockFileSystem: vscode.FileSystem
let testContent: Uint8Array

setup(() => {
// Create a mock file system
mockFileSystem = vscode.workspace.fs
// Create test content with escaped backslashes
testContent = new TextEncoder().encode('const pattern = "\\\\/"')
})

test('readFile should not unescape backslashes when config is disabled (default)', async () => {
// Mock workspace configuration to return false (default)
const originalGetConfiguration = vscode.workspace.getConfiguration
const mockGetConfiguration = () => ({
get: <T>(key: string, defaultValue: T): T => {
if (key === 'unescapeBackslashes') {
return false as T
}
return defaultValue
},
}) as vscode.WorkspaceConfiguration

// Temporarily replace getConfiguration
;(vscode.workspace as any).getConfiguration = mockGetConfiguration

try {
const vscodeFiles = new VscodeFiles(mockFileSystem)

// Create a temporary file with escaped backslashes
const testUri = vscode.Uri.file(
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath + '/test-file.js' || '/test-file.js'
)

// Write test content
await vscode.workspace.fs.writeFile(testUri, testContent)

// Read the file
const content = await vscodeFiles.readFile(testUri.toString())

// Content should remain unchanged (backslashes still escaped)
assert.strictEqual(
content.includes('\\\\/'),
true,
'Content should contain escaped backslashes when config is disabled'
)

// Clean up
await vscode.workspace.fs.delete(testUri)
} finally {
// Restore original getConfiguration
;(vscode.workspace as any).getConfiguration = originalGetConfiguration
}
})

test('readFile should unescape backslashes when config is enabled', async () => {
// Mock workspace configuration to return true
const originalGetConfiguration = vscode.workspace.getConfiguration
const mockGetConfiguration = () => ({
get: <T>(key: string, defaultValue: T): T => {
if (key === 'unescapeBackslashes') {
return true as T
}
return defaultValue
},
}) as vscode.WorkspaceConfiguration

// Temporarily replace getConfiguration
;(vscode.workspace as any).getConfiguration = mockGetConfiguration

try {
const vscodeFiles = new VscodeFiles(mockFileSystem)

// Create a temporary file with escaped backslashes
const testUri = vscode.Uri.file(
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath + '/test-file.js' || '/test-file.js'
)

// Write test content with escaped backslashes
const contentWithEscaped = new TextEncoder().encode('const pattern = "\\\\/"')
await vscode.workspace.fs.writeFile(testUri, contentWithEscaped)

// Read the file
const content = await vscodeFiles.readFile(testUri.toString())

// Content should have backslashes unescaped
assert.strictEqual(
content.includes('\\/'),
true,
'Content should contain unescaped backslash when config is enabled'
)
assert.strictEqual(
content.includes('\\\\/'),
false,
'Content should not contain escaped backslashes when config is enabled'
)

// Clean up
await vscode.workspace.fs.delete(testUri)
} finally {
// Restore original getConfiguration
;(vscode.workspace as any).getConfiguration = originalGetConfiguration
}
})

test('readFile should handle multiple escaped backslashes', async () => {
// Mock workspace configuration to return true
const originalGetConfiguration = vscode.workspace.getConfiguration
const mockGetConfiguration = () => ({
get: <T>(key: string, defaultValue: T): T => {
if (key === 'unescapeBackslashes') {
return true as T
}
return defaultValue
},
}) as vscode.WorkspaceConfiguration

;(vscode.workspace as any).getConfiguration = mockGetConfiguration

try {
const vscodeFiles = new VscodeFiles(mockFileSystem)

const testUri = vscode.Uri.file(
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath + '/test-file.js' || '/test-file.js'
)

// Write test content with multiple escaped backslashes
const contentWithMultipleEscaped = new TextEncoder().encode(
'const pattern1 = "\\\\/"; const pattern2 = "\\\\test\\\\path"'
)
await vscode.workspace.fs.writeFile(testUri, contentWithMultipleEscaped)

const content = await vscodeFiles.readFile(testUri.toString())

// All escaped backslashes should be unescaped
assert.strictEqual(
content.includes('\\/'),
true,
'Should unescape backslashes in pattern1'
)
assert.strictEqual(
content.includes('\\test\\path'),
true,
'Should unescape backslashes in pattern2'
)
assert.strictEqual(
content.includes('\\\\'),
false,
'Should not contain any escaped backslashes'
)

await vscode.workspace.fs.delete(testUri)
} finally {
;(vscode.workspace as any).getConfiguration = originalGetConfiguration
}
})

test('readFile should read config dynamically on each call', async () => {
const originalGetConfiguration = vscode.workspace.getConfiguration
let configValue = false

const mockGetConfiguration = () => ({
get: <T>(key: string, defaultValue: T): T => {
if (key === 'unescapeBackslashes') {
return configValue as T
}
return defaultValue
},
}) as vscode.WorkspaceConfiguration

;(vscode.workspace as any).getConfiguration = mockGetConfiguration

try {
const vscodeFiles = new VscodeFiles(mockFileSystem)

const testUri = vscode.Uri.file(
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath + '/test-file.js' || '/test-file.js'
)

const contentWithEscaped = new TextEncoder().encode('const pattern = "\\\\/"')
await vscode.workspace.fs.writeFile(testUri, contentWithEscaped)

// First read with config disabled
configValue = false
let content = await vscodeFiles.readFile(testUri.toString())
assert.strictEqual(
content.includes('\\\\/'),
true,
'Should not unescape when config is false'
)

// Second read with config enabled (simulating config change)
configValue = true
content = await vscodeFiles.readFile(testUri.toString())
assert.strictEqual(
content.includes('\\/'),
true,
'Should unescape when config is true'
)
assert.strictEqual(
content.includes('\\\\/'),
false,
'Should not contain escaped backslashes when config is true'
)

await vscode.workspace.fs.delete(testUri)
} finally {
;(vscode.workspace as any).getConfiguration = originalGetConfiguration
}
})
})

Loading