From 13cfb3a844ece29a2be751d0717b7f11e95fd5bc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 5 Aug 2025 22:50:55 +0200
Subject: [PATCH 01/10] Support csharp_style_namespace_declarations =
file_scoped
---
.../CSharpCodeGenerationConfiguration.cs | 18 ++
.../Editor/Commands/DefineStepsCommand.cs | 99 +++++++++--
.../EditorConfigOptionsExtensions.cs | 6 +-
.../EditorConfigOptionsProvider.cs | 24 ++-
.../IEditorConfigOptionsProvider.cs | 1 +
.../StepDefinitions/ProjectSystemSteps.cs | 3 +-
.../CSharpCodeGenerationConfigurationTests.cs | 154 ++++++++++++++++++
.../Commands/DefineStepsCommandTests.cs | 3 +-
.../StubEditorConfigOptionsProvider.cs | 1 +
9 files changed, 287 insertions(+), 22 deletions(-)
create mode 100644 Reqnroll.VisualStudio/Configuration/CSharpCodeGenerationConfiguration.cs
create mode 100644 Tests/Reqnroll.VisualStudio.Tests/Configuration/CSharpCodeGenerationConfigurationTests.cs
diff --git a/Reqnroll.VisualStudio/Configuration/CSharpCodeGenerationConfiguration.cs b/Reqnroll.VisualStudio/Configuration/CSharpCodeGenerationConfiguration.cs
new file mode 100644
index 00000000..8398b493
--- /dev/null
+++ b/Reqnroll.VisualStudio/Configuration/CSharpCodeGenerationConfiguration.cs
@@ -0,0 +1,18 @@
+namespace Reqnroll.VisualStudio.Configuration;
+
+public class CSharpCodeGenerationConfiguration
+{
+ ///
+ /// Specifies the namespace declaration style for generated C# code.
+ /// Uses file-scoped namespaces when set to "file_scoped", otherwise uses block-scoped namespaces.
+ ///
+ [EditorConfigSetting("csharp_style_namespace_declarations")]
+ public string NamespaceDeclarationStyle { get; set; } = "block_scoped";
+
+ ///
+ /// Determines if file-scoped namespaces should be used based on the EditorConfig setting.
+ ///
+ public bool UseFileScopedNamespaces =>
+ NamespaceDeclarationStyle != null &&
+ NamespaceDeclarationStyle.StartsWith("file_scoped", StringComparison.OrdinalIgnoreCase);
+}
\ No newline at end of file
diff --git a/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs b/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs
index 507d804f..4906475c 100644
--- a/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs
+++ b/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs
@@ -1,16 +1,23 @@
#nullable disable
+
+using System.Text;
+
namespace Reqnroll.VisualStudio.Editor.Commands;
[Export(typeof(IDeveroomFeatureEditorCommand))]
public class DefineStepsCommand : DeveroomEditorCommandBase, IDeveroomFeatureEditorCommand
{
+ private readonly IEditorConfigOptionsProvider _editorConfigOptionsProvider;
+
[ImportingConstructor]
public DefineStepsCommand(
IIdeScope ideScope,
IBufferTagAggregatorFactoryService aggregatorFactory,
- IDeveroomTaggerProvider taggerProvider)
+ IDeveroomTaggerProvider taggerProvider,
+ IEditorConfigOptionsProvider editorConfigOptionsProvider)
: base(ideScope, aggregatorFactory, taggerProvider)
{
+ _editorConfigOptionsProvider = editorConfigOptionsProvider;
}
public override DeveroomEditorCommandTargetKey[] Targets => new[]
@@ -101,7 +108,7 @@ public override bool PreExec(IWpfTextView textView, DeveroomEditorCommandTargetK
switch (viewModel.Result)
{
case CreateStepDefinitionsDialogResult.Create:
- SaveAsStepDefinitionClass(projectScope, combinedSnippet, viewModel.ClassName, indent, newLine);
+ SaveAsStepDefinitionClass(projectScope, combinedSnippet, viewModel.ClassName, indent, newLine, textView);
break;
case CreateStepDefinitionsDialogResult.CopyToClipboard:
Logger.LogVerbose($"Copy to clipboard: {combinedSnippet}");
@@ -114,7 +121,7 @@ public override bool PreExec(IWpfTextView textView, DeveroomEditorCommandTargetK
}
private void SaveAsStepDefinitionClass(IProjectScope projectScope, string combinedSnippet, string className,
- string indent, string newLine)
+ string indent, string newLine, IWpfTextView textView)
{
string targetFolder = projectScope.ProjectFolder;
var projectSettings = projectScope.GetProjectSettings();
@@ -130,21 +137,54 @@ private void SaveAsStepDefinitionClass(IProjectScope projectScope, string combin
var isSpecFlow = projectTraits.HasFlag(ReqnrollProjectTraits.LegacySpecFlow) || projectTraits.HasFlag(ReqnrollProjectTraits.SpecFlowCompatibility);
var libraryNameSpace = isSpecFlow ? "SpecFlow" : "Reqnroll";
- var template = "using System;" + newLine +
- $"using {libraryNameSpace};" + newLine +
- newLine +
- $"namespace {fileNamespace}" + newLine +
- "{" + newLine +
- $"{indent}[Binding]" + newLine +
- $"{indent}public class {className}" + newLine +
- $"{indent}{{" + newLine +
- combinedSnippet +
- $"{indent}}}" + newLine +
- "}" + newLine;
+ // Get C# code generation configuration from EditorConfig using target .cs file path
+ var targetFilePath = Path.Combine(targetFolder, className + ".cs");
+ var csharpConfig = new CSharpCodeGenerationConfiguration();
+ var editorConfigOptions = _editorConfigOptionsProvider.GetEditorConfigOptionsByPath(targetFilePath);
+ editorConfigOptions.UpdateFromEditorConfig(csharpConfig);
+
+ // Estimate template size for StringBuilder capacity
+ var estimatedSize = 200 + fileNamespace.Length + className.Length + combinedSnippet.Length;
+ var template = new StringBuilder(estimatedSize);
+ template.AppendLine("using System;");
+ template.AppendLine($"using {libraryNameSpace};");
+ template.AppendLine();
+
+ // Determine indentation level based on namespace style
+ var classIndent = csharpConfig.UseFileScopedNamespaces ? "" : indent;
+
+ // Adjust combinedSnippet indentation based on namespace style
+ var adjustedSnippet = csharpConfig.UseFileScopedNamespaces
+ ? AdjustIndentationForFileScopedNamespace(combinedSnippet, indent, newLine)
+ : combinedSnippet;
+ // Add namespace declaration
+ if (csharpConfig.UseFileScopedNamespaces)
+ {
+ template.AppendLine($"namespace {fileNamespace};");
+ template.AppendLine();
+ }
+ else
+ {
+ template.AppendLine($"namespace {fileNamespace}");
+ template.AppendLine("{");
+ }
+
+ // Add class declaration (common structure with appropriate indentation)
+ template.AppendLine($"{classIndent}[Binding]");
+ template.AppendLine($"{classIndent}public class {className}");
+ template.AppendLine($"{classIndent}{{");
+ template.Append(adjustedSnippet);
+ template.AppendLine($"{classIndent}}}");
+
+ // Close namespace if block-scoped
+ if (!csharpConfig.UseFileScopedNamespaces)
+ {
+ template.AppendLine("}");
+ }
var targetFile = FileDetails
.FromPath(targetFolder, className + ".cs")
- .WithCSharpContent(template);
+ .WithCSharpContent(template.ToString());
if (IdeScope.FileSystem.File.Exists(targetFile.FullName))
if (IdeScope.Actions.ShowSyncQuestion("Overwrite file?",
@@ -152,7 +192,7 @@ private void SaveAsStepDefinitionClass(IProjectScope projectScope, string combin
defaultButton: MessageBoxResult.No) != MessageBoxResult.Yes)
return;
- projectScope.AddFile(targetFile, template);
+ projectScope.AddFile(targetFile, template.ToString());
projectScope.IdeScope.Actions.NavigateTo(new SourceLocation(targetFile, 9, 1));
IDiscoveryService discoveryService = projectScope.GetDiscoveryService();
@@ -168,4 +208,31 @@ await discoveryService.BindingRegistryCache
Finished.Set();
}
+
+ private static string AdjustIndentationForFileScopedNamespace(string snippet, string indent, string newLine)
+ {
+ if (string.IsNullOrEmpty(snippet))
+ return snippet;
+
+ // Split into lines and process each line
+ var lines = snippet.Split(new[] { newLine }, StringSplitOptions.None);
+ var adjustedLines = new string[lines.Length];
+
+ for (int i = 0; i < lines.Length; i++)
+ {
+ var line = lines[i];
+
+ // If line starts with double indentation, reduce it to single indentation
+ if (line.StartsWith(indent + indent))
+ {
+ adjustedLines[i] = indent + line.Substring((indent + indent).Length);
+ }
+ else
+ {
+ adjustedLines[i] = line;
+ }
+ }
+
+ return string.Join(newLine, adjustedLines);
+ }
}
diff --git a/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsExtensions.cs b/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsExtensions.cs
index bac62066..7d1fe9a2 100644
--- a/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsExtensions.cs
+++ b/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsExtensions.cs
@@ -26,16 +26,16 @@ public static void UpdateFromEditorConfig(this IEditorConfigOptions edi
.Select(p => new
{
PropertyInfo = p,
- ((EditorConfigSettingAttribute) Attribute.GetCustomAttribute(p, typeof(EditorConfigSettingAttribute)))
+ EditorConfigKey = ((EditorConfigSettingAttribute) Attribute.GetCustomAttribute(p, typeof(EditorConfigSettingAttribute)))
?.EditorConfigSettingName
})
- .Where(p => p.EditorConfigSettingName != null);
+ .Where(p => p.EditorConfigKey != null);
foreach (var property in propertiesWithEditorConfig)
{
var currentValue = property.PropertyInfo.GetValue(config);
var updatedValue = editorConfigOptions.GetOption(property.PropertyInfo.PropertyType,
- property.EditorConfigSettingName, currentValue);
+ property.EditorConfigKey, currentValue);
if (!Equals(currentValue, updatedValue))
property.PropertyInfo.SetValue(config, updatedValue);
}
diff --git a/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsProvider.cs b/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsProvider.cs
index 2045bdea..0f0f1e4b 100644
--- a/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsProvider.cs
+++ b/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsProvider.cs
@@ -26,6 +26,21 @@ public IEditorConfigOptions GetEditorConfigOptions(IWpfTextView textView)
return new EditorConfigOptions(options);
}
+ public IEditorConfigOptions GetEditorConfigOptionsByPath(string filePath)
+ {
+ if (string.IsNullOrEmpty(filePath))
+ return NullEditorConfigOptions.Instance;
+
+ var document = CreateAdHocDocumentByPath(filePath);
+ if (document == null)
+ return NullEditorConfigOptions.Instance;
+
+ var options =
+ ThreadHelper.JoinableTaskFactory.Run(() => document.GetOptionsAsync());
+
+ return new EditorConfigOptions(options);
+ }
+
private Document GetDocument(IWpfTextView textView) =>
textView.TextBuffer.GetRelatedDocuments().FirstOrDefault() ??
CreateAdHocDocument(textView);
@@ -35,10 +50,17 @@ private Document CreateAdHocDocument(IWpfTextView textView)
var editorFilePath = GetPath(textView);
if (editorFilePath == null)
return null;
+ return CreateAdHocDocumentByPath(editorFilePath);
+ }
+
+ private Document CreateAdHocDocumentByPath(string filePath)
+ {
+ if (string.IsNullOrEmpty(filePath))
+ return null;
var project = _visualStudioWorkspace.CurrentSolution.Projects.FirstOrDefault();
if (project == null)
return null;
- return project.AddDocument(editorFilePath, string.Empty, filePath: editorFilePath);
+ return project.AddDocument(filePath, string.Empty, filePath: filePath);
}
public static string GetPath(IWpfTextView textView)
diff --git a/Reqnroll.VisualStudio/Editor/Services/EditorConfig/IEditorConfigOptionsProvider.cs b/Reqnroll.VisualStudio/Editor/Services/EditorConfig/IEditorConfigOptionsProvider.cs
index 22ba4b0e..3d8e9964 100644
--- a/Reqnroll.VisualStudio/Editor/Services/EditorConfig/IEditorConfigOptionsProvider.cs
+++ b/Reqnroll.VisualStudio/Editor/Services/EditorConfig/IEditorConfigOptionsProvider.cs
@@ -3,4 +3,5 @@ namespace Reqnroll.VisualStudio.Editor.Services.EditorConfig;
public interface IEditorConfigOptionsProvider
{
IEditorConfigOptions GetEditorConfigOptions(IWpfTextView textView);
+ IEditorConfigOptions GetEditorConfigOptionsByPath(string filePath);
}
diff --git a/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/ProjectSystemSteps.cs b/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/ProjectSystemSteps.cs
index 17bc2a84..219e3d08 100644
--- a/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/ProjectSystemSteps.cs
+++ b/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/ProjectSystemSteps.cs
@@ -478,7 +478,8 @@ private void PerformCommand(string commandName, string parameter = null,
}
case "Define Steps":
{
- _invokedCommand = new DefineStepsCommand(_ideScope, aggregatorFactoryService, taggerProvider);
+ _invokedCommand = new DefineStepsCommand(_ideScope, aggregatorFactoryService, taggerProvider,
+ new StubEditorConfigOptionsProvider());
_invokedCommand.PreExec(_wpfTextView, _invokedCommand.Targets.First());
return;
}
diff --git a/Tests/Reqnroll.VisualStudio.Tests/Configuration/CSharpCodeGenerationConfigurationTests.cs b/Tests/Reqnroll.VisualStudio.Tests/Configuration/CSharpCodeGenerationConfigurationTests.cs
new file mode 100644
index 00000000..b29aeb2d
--- /dev/null
+++ b/Tests/Reqnroll.VisualStudio.Tests/Configuration/CSharpCodeGenerationConfigurationTests.cs
@@ -0,0 +1,154 @@
+using Reqnroll.VisualStudio.Configuration;
+using Reqnroll.VisualStudio.Editor.Services.EditorConfig;
+using Xunit;
+
+namespace Reqnroll.VisualStudio.Tests.Configuration;
+
+public class CSharpCodeGenerationConfigurationTests
+{
+ [Fact]
+ public void UseFileScopedNamespaces_WhenFileScopedSet_ReturnsTrue()
+ {
+ // Arrange
+ var config = new CSharpCodeGenerationConfiguration
+ {
+ NamespaceDeclarationStyle = "file_scoped"
+ };
+
+ // Act & Assert
+ Assert.True(config.UseFileScopedNamespaces);
+ }
+
+ [Fact]
+ public void UseFileScopedNamespaces_WhenFileScopedWithSeveritySet_ReturnsTrue()
+ {
+ // Arrange
+ var config = new CSharpCodeGenerationConfiguration
+ {
+ NamespaceDeclarationStyle = "file_scoped:warning"
+ };
+
+ // Act & Assert
+ Assert.True(config.UseFileScopedNamespaces);
+ }
+
+ [Fact]
+ public void UseFileScopedNamespaces_WhenBlockScopedSet_ReturnsFalse()
+ {
+ // Arrange
+ var config = new CSharpCodeGenerationConfiguration
+ {
+ NamespaceDeclarationStyle = "block_scoped"
+ };
+
+ // Act & Assert
+ Assert.False(config.UseFileScopedNamespaces);
+ }
+
+ [Fact]
+ public void UseFileScopedNamespaces_WhenDefaultValue_ReturnsFalse()
+ {
+ // Arrange
+ var config = new CSharpCodeGenerationConfiguration();
+
+ // Act & Assert
+ Assert.False(config.UseFileScopedNamespaces);
+ }
+
+ [Fact]
+ public void UseFileScopedNamespaces_WhenNullValue_ReturnsFalse()
+ {
+ // Arrange
+ var config = new CSharpCodeGenerationConfiguration
+ {
+ NamespaceDeclarationStyle = null
+ };
+
+ // Act & Assert
+ Assert.False(config.UseFileScopedNamespaces);
+ }
+
+ [Fact]
+ public void UseFileScopedNamespaces_WhenUnknownValue_ReturnsFalse()
+ {
+ // Arrange
+ var config = new CSharpCodeGenerationConfiguration
+ {
+ NamespaceDeclarationStyle = "unknown_style"
+ };
+
+ // Act & Assert
+ Assert.False(config.UseFileScopedNamespaces);
+ }
+
+ [Fact]
+ public void UpdateFromEditorConfig_WhenFileScopedValue_SetsCorrectValue()
+ {
+ // Arrange
+ var config = new CSharpCodeGenerationConfiguration();
+ var editorConfigOptions = new TestEditorConfigOptions("file_scoped:silent");
+
+ // Act
+ editorConfigOptions.UpdateFromEditorConfig(config);
+
+ // Assert
+ Assert.Equal("file_scoped:silent", config.NamespaceDeclarationStyle);
+ Assert.True(config.UseFileScopedNamespaces);
+ }
+
+ [Fact]
+ public void UpdateFromEditorConfig_WhenBlockScopedValue_SetsCorrectValue()
+ {
+ // Arrange
+ var config = new CSharpCodeGenerationConfiguration();
+ var editorConfigOptions = new TestEditorConfigOptions("block_scoped");
+
+ // Act
+ editorConfigOptions.UpdateFromEditorConfig(config);
+
+ // Assert
+ Assert.Equal("block_scoped", config.NamespaceDeclarationStyle);
+ Assert.False(config.UseFileScopedNamespaces);
+ }
+
+ [Fact]
+ public void UpdateFromEditorConfig_WhenNoValue_KeepsDefault()
+ {
+ // Arrange
+ var config = new CSharpCodeGenerationConfiguration();
+ var editorConfigOptions = new TestEditorConfigOptions(null);
+
+ // Act
+ editorConfigOptions.UpdateFromEditorConfig(config);
+
+ // Assert
+ Assert.Equal("block_scoped", config.NamespaceDeclarationStyle); // Should keep default
+ Assert.False(config.UseFileScopedNamespaces);
+ }
+}
+
+// Test EditorConfig options provider that simulates reading specific values
+public class TestEditorConfigOptions : IEditorConfigOptions
+{
+ private readonly string _namespaceStyle;
+
+ public TestEditorConfigOptions(string namespaceStyle)
+ {
+ _namespaceStyle = namespaceStyle;
+ }
+
+ public TResult GetOption(string editorConfigKey, TResult defaultValue)
+ {
+ if (editorConfigKey == "csharp_style_namespace_declarations" && _namespaceStyle != null)
+ {
+ return (TResult)(object)_namespaceStyle;
+ }
+
+ return defaultValue;
+ }
+
+ public bool GetBoolOption(string editorConfigKey, bool defaultValue)
+ {
+ return defaultValue;
+ }
+}
\ No newline at end of file
diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/Commands/DefineStepsCommandTests.cs b/Tests/Reqnroll.VisualStudio.Tests/Editor/Commands/DefineStepsCommandTests.cs
index fba8031c..95d2ec84 100644
--- a/Tests/Reqnroll.VisualStudio.Tests/Editor/Commands/DefineStepsCommandTests.cs
+++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/Commands/DefineStepsCommandTests.cs
@@ -6,7 +6,8 @@ public class DefineStepsCommandTests : CommandTestBase
{
public DefineStepsCommandTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper, (ps, tp) =>
- new DefineStepsCommand(ps.IdeScope, new StubBufferTagAggregatorFactoryService(tp), tp),
+ new DefineStepsCommand(ps.IdeScope, new StubBufferTagAggregatorFactoryService(tp), tp,
+ new StubEditorConfigOptionsProvider()),
"ShowProblem: User Notification: ")
{
}
diff --git a/Tests/Reqnroll.VisualStudio.VsxStubs/StubEditorConfigOptionsProvider.cs b/Tests/Reqnroll.VisualStudio.VsxStubs/StubEditorConfigOptionsProvider.cs
index dd45a7b1..d24b4823 100644
--- a/Tests/Reqnroll.VisualStudio.VsxStubs/StubEditorConfigOptionsProvider.cs
+++ b/Tests/Reqnroll.VisualStudio.VsxStubs/StubEditorConfigOptionsProvider.cs
@@ -3,4 +3,5 @@ namespace Reqnroll.VisualStudio.VsxStubs;
public class StubEditorConfigOptionsProvider : IEditorConfigOptionsProvider
{
public IEditorConfigOptions GetEditorConfigOptions(IWpfTextView textView) => new NullEditorConfigOptions();
+ public IEditorConfigOptions GetEditorConfigOptionsByPath(string filePath) => new NullEditorConfigOptions();
}
From 2301fcbbaf742d98c59fb79bcbbb27b1b662e788 Mon Sep 17 00:00:00 2001
From: Julian Verdurmen <5808377+304NotModified@users.noreply.github.com>
Date: Sun, 14 Dec 2025 01:56:25 +0100
Subject: [PATCH 02/10] Update CHANGELOG.md
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5698d459..bb8ef00d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
## Improvements:
* Update docs - .NET 10, TUnit, VS2026 (#138)
+* Support file_scoped namespace declarations when generating code (#140)
## Bug fixes:
From c143f3e8bcd5211550397e44a3caf80cfd1426a2 Mon Sep 17 00:00:00 2001
From: Julian Verdurmen <5808377+304NotModified@users.noreply.github.com>
Date: Sun, 14 Dec 2025 02:18:33 +0100
Subject: [PATCH 03/10] extract GenerateStepDefinitionClass and add snapshot
tests
---
.../Editor/Commands/DefineStepsCommand.cs | 62 ++++++++++-----
...Scenario.Reqnroll_BlockScoped.approved.txt | 14 ++++
...rScenario.Reqnroll_FileScoped.approved.txt | 13 ++++
...Scenario.SpecFlow_BlockScoped.approved.txt | 14 ++++
...rScenario.SpecFlow_FileScoped.approved.txt | 13 ++++
.../Commands/DefineStepsCommandTests.cs | 76 +++++++++++++++++++
6 files changed, 173 insertions(+), 19 deletions(-)
create mode 100644 Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_BlockScoped.approved.txt
create mode 100644 Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_FileScoped.approved.txt
create mode 100644 Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_BlockScoped.approved.txt
create mode 100644 Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_FileScoped.approved.txt
diff --git a/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs b/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs
index 4906475c..07c2197f 100644
--- a/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs
+++ b/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs
@@ -133,9 +133,6 @@ private void SaveAsStepDefinitionClass(IProjectScope projectScope, string combin
targetFolder = stepDefinitionsFolder;
fileNamespace = fileNamespace + ".StepDefinitions";
}
- var projectTraits = projectScope.GetProjectSettings().ReqnrollProjectTraits;
- var isSpecFlow = projectTraits.HasFlag(ReqnrollProjectTraits.LegacySpecFlow) || projectTraits.HasFlag(ReqnrollProjectTraits.SpecFlowCompatibility);
- var libraryNameSpace = isSpecFlow ? "SpecFlow" : "Reqnroll";
// Get C# code generation configuration from EditorConfig using target .cs file path
var targetFilePath = Path.Combine(targetFolder, className + ".cs");
@@ -143,6 +140,47 @@ private void SaveAsStepDefinitionClass(IProjectScope projectScope, string combin
var editorConfigOptions = _editorConfigOptionsProvider.GetEditorConfigOptionsByPath(targetFilePath);
editorConfigOptions.UpdateFromEditorConfig(csharpConfig);
+ var projectTraits = projectScope.GetProjectSettings().ReqnrollProjectTraits;
+ var generatedContent = GenerateStepDefinitionClass(
+ combinedSnippet,
+ className,
+ fileNamespace,
+ projectTraits,
+ csharpConfig,
+ indent,
+ newLine);
+
+ var targetFile = FileDetails
+ .FromPath(targetFolder, className + ".cs")
+ .WithCSharpContent(generatedContent);
+
+ if (IdeScope.FileSystem.File.Exists(targetFile.FullName))
+ if (IdeScope.Actions.ShowSyncQuestion("Overwrite file?",
+ $"The selected step definition file '{targetFile}' already exists. By overwriting the existing file you might lose work. {Environment.NewLine}Do you want to overwrite the file?",
+ defaultButton: MessageBoxResult.No) != MessageBoxResult.Yes)
+ return;
+
+ projectScope.AddFile(targetFile, generatedContent);
+ projectScope.IdeScope.Actions.NavigateTo(new SourceLocation(targetFile, 9, 1));
+ IDiscoveryService discoveryService = projectScope.GetDiscoveryService();
+
+ projectScope.IdeScope.FireAndForget(
+ () => RebuildBindingRegistry(discoveryService, targetFile), _ => { Finished.Set(); });
+ }
+
+ internal static string GenerateStepDefinitionClass(
+ string combinedSnippet,
+ string className,
+ string fileNamespace,
+ ReqnrollProjectTraits projectTraits,
+ CSharpCodeGenerationConfiguration csharpConfig,
+ string indent,
+ string newLine)
+ {
+ var isSpecFlow = projectTraits.HasFlag(ReqnrollProjectTraits.LegacySpecFlow) ||
+ projectTraits.HasFlag(ReqnrollProjectTraits.SpecFlowCompatibility);
+ var libraryNameSpace = isSpecFlow ? "SpecFlow" : "Reqnroll";
+
// Estimate template size for StringBuilder capacity
var estimatedSize = 200 + fileNamespace.Length + className.Length + combinedSnippet.Length;
var template = new StringBuilder(estimatedSize);
@@ -157,6 +195,7 @@ private void SaveAsStepDefinitionClass(IProjectScope projectScope, string combin
var adjustedSnippet = csharpConfig.UseFileScopedNamespaces
? AdjustIndentationForFileScopedNamespace(combinedSnippet, indent, newLine)
: combinedSnippet;
+
// Add namespace declaration
if (csharpConfig.UseFileScopedNamespaces)
{
@@ -182,22 +221,7 @@ private void SaveAsStepDefinitionClass(IProjectScope projectScope, string combin
template.AppendLine("}");
}
- var targetFile = FileDetails
- .FromPath(targetFolder, className + ".cs")
- .WithCSharpContent(template.ToString());
-
- if (IdeScope.FileSystem.File.Exists(targetFile.FullName))
- if (IdeScope.Actions.ShowSyncQuestion("Overwrite file?",
- $"The selected step definition file '{targetFile}' already exists. By overwriting the existing file you might lose work. {Environment.NewLine}Do you want to overwrite the file?",
- defaultButton: MessageBoxResult.No) != MessageBoxResult.Yes)
- return;
-
- projectScope.AddFile(targetFile, template.ToString());
- projectScope.IdeScope.Actions.NavigateTo(new SourceLocation(targetFile, 9, 1));
- IDiscoveryService discoveryService = projectScope.GetDiscoveryService();
-
- projectScope.IdeScope.FireAndForget(
- () => RebuildBindingRegistry(discoveryService, targetFile), _ => { Finished.Set(); });
+ return template.ToString();
}
private async Task RebuildBindingRegistry(IDiscoveryService discoveryService,
diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_BlockScoped.approved.txt b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_BlockScoped.approved.txt
new file mode 100644
index 00000000..49045d6b
--- /dev/null
+++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_BlockScoped.approved.txt
@@ -0,0 +1,14 @@
+using System;
+using Reqnroll;
+
+namespace MyNamespace.MyProject
+{
+ [Binding]
+ public class Feature1StepDefinitions
+ {
+ [When(@"I press add")]
+ public void WhenIPressAdd()
+ {
+ throw new PendingStepException();
+ } }
+}
diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_FileScoped.approved.txt b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_FileScoped.approved.txt
new file mode 100644
index 00000000..00b17034
--- /dev/null
+++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_FileScoped.approved.txt
@@ -0,0 +1,13 @@
+using System;
+using Reqnroll;
+
+namespace MyNamespace.MyProject;
+
+[Binding]
+public class Feature1StepDefinitions
+{
+ [When(@"I press add")]
+ public void WhenIPressAdd()
+ {
+ throw new PendingStepException();
+ }}
diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_BlockScoped.approved.txt b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_BlockScoped.approved.txt
new file mode 100644
index 00000000..85763f04
--- /dev/null
+++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_BlockScoped.approved.txt
@@ -0,0 +1,14 @@
+using System;
+using SpecFlow;
+
+namespace MyNamespace.MyProject
+{
+ [Binding]
+ public class Feature1StepDefinitions
+ {
+ [When(@"I press add")]
+ public void WhenIPressAdd()
+ {
+ throw new PendingStepException();
+ } }
+}
diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_FileScoped.approved.txt b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_FileScoped.approved.txt
new file mode 100644
index 00000000..010f9d6c
--- /dev/null
+++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_FileScoped.approved.txt
@@ -0,0 +1,13 @@
+using System;
+using SpecFlow;
+
+namespace MyNamespace.MyProject;
+
+[Binding]
+public class Feature1StepDefinitions
+{
+ [When(@"I press add")]
+ public void WhenIPressAdd()
+ {
+ throw new PendingStepException();
+ }}
diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/Commands/DefineStepsCommandTests.cs b/Tests/Reqnroll.VisualStudio.Tests/Editor/Commands/DefineStepsCommandTests.cs
index 95d2ec84..1d016283 100644
--- a/Tests/Reqnroll.VisualStudio.Tests/Editor/Commands/DefineStepsCommandTests.cs
+++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/Commands/DefineStepsCommandTests.cs
@@ -2,6 +2,8 @@
namespace Reqnroll.VisualStudio.Tests.Editor.Commands;
+[UseReporter /*(typeof(VisualStudioReporter))*/]
+[UseApprovalSubdirectory("../ApprovalTestData")]
public class DefineStepsCommandTests : CommandTestBase
{
public DefineStepsCommandTests(ITestOutputHelper testOutputHelper)
@@ -68,4 +70,78 @@ public async Task Step_definition_class_saved(string _, string expression)
await BindingRegistryIsModified(expression);
}
+
+ [Theory]
+ [InlineData(ProjectType.Reqnroll, NamespaceStyle.BlockScoped)]
+ [InlineData(ProjectType.Reqnroll, NamespaceStyle.FileScoped)]
+ [InlineData(ProjectType.SpecFlow, NamespaceStyle.BlockScoped)]
+ [InlineData(ProjectType.SpecFlow, NamespaceStyle.FileScoped)]
+ public void GenerateStepDefinitionClass(ProjectType projectType, NamespaceStyle namespaceStyle)
+ {
+ // Arrange
+ // Snippet should have double indentation (8 spaces) as it would come from the command
+ var snippet = """
+ [When(@"I press add")]
+ public void WhenIPressAdd()
+ {
+ throw new PendingStepException();
+ }
+ """;
+ const string className = "Feature1StepDefinitions";
+ const string @namespace = "MyNamespace.MyProject";
+
+ var projectTraits = GetProjectTraits(projectType);
+ var csharpConfig = new CSharpCodeGenerationConfiguration
+ {
+ NamespaceDeclarationStyle = GetNamespaceStyleValue(namespaceStyle)
+ };
+
+ // Act
+ var result = DefineStepsCommand.GenerateStepDefinitionClass(
+ snippet, className, @namespace, projectTraits, csharpConfig, " ", Environment.NewLine);
+
+ // Assert
+ VerifyWithScenario(result, projectType, namespaceStyle);
+ }
+
+ private static void VerifyWithScenario(string result, ProjectType projectType, NamespaceStyle namespaceStyle)
+ {
+ var scenarioName = $"{projectType}_{namespaceStyle}";
+ using (ApprovalResults.ForScenario(scenarioName))
+ {
+ Approvals.Verify(result);
+ }
+ }
+
+ private static ReqnrollProjectTraits GetProjectTraits(ProjectType projectType)
+ {
+ return projectType switch
+ {
+ ProjectType.Reqnroll => ReqnrollProjectTraits.CucumberExpression | ReqnrollProjectTraits.MsBuildGeneration,
+ ProjectType.SpecFlow => ReqnrollProjectTraits.LegacySpecFlow | ReqnrollProjectTraits.CucumberExpression | ReqnrollProjectTraits.MsBuildGeneration,
+ _ => throw new ArgumentOutOfRangeException(nameof(projectType))
+ };
+ }
+
+ private static string GetNamespaceStyleValue(NamespaceStyle namespaceStyle)
+ {
+ return namespaceStyle switch
+ {
+ NamespaceStyle.BlockScoped => "block_scoped",
+ NamespaceStyle.FileScoped => "file_scoped",
+ _ => throw new ArgumentOutOfRangeException(nameof(namespaceStyle))
+ };
+ }
+
+ public enum ProjectType
+ {
+ Reqnroll,
+ SpecFlow
+ }
+
+ public enum NamespaceStyle
+ {
+ BlockScoped,
+ FileScoped
+ }
}
From 7107c990e1c4c1e95b9a5fe46ffb323d5f67efa6 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Tue, 16 Dec 2025 21:13:06 +0100
Subject: [PATCH 04/10] Fix indentation in generated step definition classes
(#144)
* Initial plan
* Fix indentation in generated step definition classes
Co-authored-by: 304NotModified <5808377+304NotModified@users.noreply.github.com>
* Complete indentation fix - all checks passed
Co-authored-by: 304NotModified <5808377+304NotModified@users.noreply.github.com>
* Remove accidentally committed nuget.exe binary
Co-authored-by: 304NotModified <5808377+304NotModified@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: 304NotModified <5808377+304NotModified@users.noreply.github.com>
---
.gitignore | 1 +
Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs | 2 +-
...finitionClass.ForScenario.Reqnroll_BlockScoped.approved.txt | 3 ++-
...efinitionClass.ForScenario.Reqnroll_FileScoped.approved.txt | 3 ++-
...finitionClass.ForScenario.SpecFlow_BlockScoped.approved.txt | 3 ++-
...efinitionClass.ForScenario.SpecFlow_FileScoped.approved.txt | 3 ++-
6 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/.gitignore b/.gitignore
index edf6abae..81400c10 100644
--- a/.gitignore
+++ b/.gitignore
@@ -271,3 +271,4 @@ launchSettings.json
/Tests/ExternalPackages/Reqnroll*.nupkg
/.ncrunch/cache/
+.nuget/nuget.exe
diff --git a/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs b/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs
index 07c2197f..9e7b8655 100644
--- a/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs
+++ b/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs
@@ -212,7 +212,7 @@ internal static string GenerateStepDefinitionClass(
template.AppendLine($"{classIndent}[Binding]");
template.AppendLine($"{classIndent}public class {className}");
template.AppendLine($"{classIndent}{{");
- template.Append(adjustedSnippet);
+ template.AppendLine(adjustedSnippet);
template.AppendLine($"{classIndent}}}");
// Close namespace if block-scoped
diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_BlockScoped.approved.txt b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_BlockScoped.approved.txt
index 49045d6b..e49d0e45 100644
--- a/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_BlockScoped.approved.txt
+++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_BlockScoped.approved.txt
@@ -10,5 +10,6 @@ namespace MyNamespace.MyProject
public void WhenIPressAdd()
{
throw new PendingStepException();
- } }
+ }
+ }
}
diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_FileScoped.approved.txt b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_FileScoped.approved.txt
index 00b17034..5317920a 100644
--- a/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_FileScoped.approved.txt
+++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_FileScoped.approved.txt
@@ -10,4 +10,5 @@ public class Feature1StepDefinitions
public void WhenIPressAdd()
{
throw new PendingStepException();
- }}
+ }
+}
diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_BlockScoped.approved.txt b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_BlockScoped.approved.txt
index 85763f04..426c1745 100644
--- a/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_BlockScoped.approved.txt
+++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_BlockScoped.approved.txt
@@ -10,5 +10,6 @@ namespace MyNamespace.MyProject
public void WhenIPressAdd()
{
throw new PendingStepException();
- } }
+ }
+ }
}
diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_FileScoped.approved.txt b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_FileScoped.approved.txt
index 010f9d6c..6420a851 100644
--- a/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_FileScoped.approved.txt
+++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_FileScoped.approved.txt
@@ -10,4 +10,5 @@ public class Feature1StepDefinitions
public void WhenIPressAdd()
{
throw new PendingStepException();
- }}
+ }
+}
From 71b2b2116c2684c56d070e987ad2b706423e3bd7 Mon Sep 17 00:00:00 2001
From: Julian Verdurmen <5808377+304NotModified@users.noreply.github.com>
Date: Tue, 16 Dec 2025 21:14:24 +0100
Subject: [PATCH 05/10] Remove nuget.exe from .gitignore
Remove nuget.exe from .gitignore
---
.gitignore | 1 -
1 file changed, 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index 81400c10..edf6abae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -271,4 +271,3 @@ launchSettings.json
/Tests/ExternalPackages/Reqnroll*.nupkg
/.ncrunch/cache/
-.nuget/nuget.exe
From 7f2c8f44a262f8859a92fd94af020a0739fd5484 Mon Sep 17 00:00:00 2001
From: Julian Verdurmen <5808377+304NotModified@users.noreply.github.com>
Date: Wed, 17 Dec 2025 02:31:25 +0100
Subject: [PATCH 06/10] Fix typo in CHANGELOG for namespace support
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bb8ef00d..2bcf1681 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@
## Improvements:
* Update docs - .NET 10, TUnit, VS2026 (#138)
-* Support file_scoped namespace declarations when generating code (#140)
+* Support file scoped namespace declarations when generating code (#140)
## Bug fixes:
From 4f36a8dabe0207708e3950658ec323ea02f805e5 Mon Sep 17 00:00:00 2001
From: Julian Verdurmen <5808377+304NotModified@users.noreply.github.com>
Date: Wed, 17 Dec 2025 22:26:57 +0100
Subject: [PATCH 07/10] refactor
---
.../Editor/Commands/DefineStepsCommand.cs | 41 ++++++++++---------
.../Commands/DefineStepsCommandTests.cs | 13 +++---
2 files changed, 28 insertions(+), 26 deletions(-)
diff --git a/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs b/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs
index 9e7b8655..39178fcf 100644
--- a/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs
+++ b/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs
@@ -101,7 +101,7 @@ public override bool PreExec(IWpfTextView textView, DeveroomEditorCommandTargetK
}
var combinedSnippet = string.Join(newLine,
- viewModel.Items.Where(i => i.IsSelected).Select(i => i.Snippet.Indent(indent + indent)));
+ viewModel.Items.Where(i => i.IsSelected).Select(i => i.Snippet.Indent(indent)));
MonitoringService.MonitorCommandDefineSteps(viewModel.Result, viewModel.Items.Count(i => i.IsSelected));
@@ -191,11 +191,6 @@ internal static string GenerateStepDefinitionClass(
// Determine indentation level based on namespace style
var classIndent = csharpConfig.UseFileScopedNamespaces ? "" : indent;
- // Adjust combinedSnippet indentation based on namespace style
- var adjustedSnippet = csharpConfig.UseFileScopedNamespaces
- ? AdjustIndentationForFileScopedNamespace(combinedSnippet, indent, newLine)
- : combinedSnippet;
-
// Add namespace declaration
if (csharpConfig.UseFileScopedNamespaces)
{
@@ -212,7 +207,17 @@ internal static string GenerateStepDefinitionClass(
template.AppendLine($"{classIndent}[Binding]");
template.AppendLine($"{classIndent}public class {className}");
template.AppendLine($"{classIndent}{{");
- template.AppendLine(adjustedSnippet);
+
+ // Add snippet with appropriate indentation based on namespace style
+ if (csharpConfig.UseFileScopedNamespaces)
+ {
+ template.AppendLine(combinedSnippet);
+ }
+ else
+ {
+ AppendLinesWithIndent(template, combinedSnippet, indent, newLine);
+ }
+
template.AppendLine($"{classIndent}}}");
// Close namespace if block-scoped
@@ -233,30 +238,26 @@ await discoveryService.BindingRegistryCache
Finished.Set();
}
- private static string AdjustIndentationForFileScopedNamespace(string snippet, string indent, string newLine)
+ private static void AppendLinesWithIndent(StringBuilder builder, string content, string indent, string newLine)
{
- if (string.IsNullOrEmpty(snippet))
- return snippet;
-
- // Split into lines and process each line
- var lines = snippet.Split(new[] { newLine }, StringSplitOptions.None);
- var adjustedLines = new string[lines.Length];
+ if (string.IsNullOrEmpty(content))
+ return;
+ var lines = content.Split(new[] { newLine }, StringSplitOptions.None);
+
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i];
- // If line starts with double indentation, reduce it to single indentation
- if (line.StartsWith(indent + indent))
+ // Add indentation to non-empty lines
+ if (!string.IsNullOrWhiteSpace(line))
{
- adjustedLines[i] = indent + line.Substring((indent + indent).Length);
+ builder.Append(indent).AppendLine(line);
}
else
{
- adjustedLines[i] = line;
+ builder.AppendLine(line);
}
}
-
- return string.Join(newLine, adjustedLines);
}
}
diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/Commands/DefineStepsCommandTests.cs b/Tests/Reqnroll.VisualStudio.Tests/Editor/Commands/DefineStepsCommandTests.cs
index 1d016283..f8ce1e46 100644
--- a/Tests/Reqnroll.VisualStudio.Tests/Editor/Commands/DefineStepsCommandTests.cs
+++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/Commands/DefineStepsCommandTests.cs
@@ -79,13 +79,14 @@ public async Task Step_definition_class_saved(string _, string expression)
public void GenerateStepDefinitionClass(ProjectType projectType, NamespaceStyle namespaceStyle)
{
// Arrange
- // Snippet should have double indentation (8 spaces) as it would come from the command
+ // Snippet should have single indentation (4 spaces) which will be used for file-scoped namespaces
+ // and will have extra indentation added for block-scoped namespaces
var snippet = """
- [When(@"I press add")]
- public void WhenIPressAdd()
- {
- throw new PendingStepException();
- }
+ [When(@"I press add")]
+ public void WhenIPressAdd()
+ {
+ throw new PendingStepException();
+ }
""";
const string className = "Feature1StepDefinitions";
const string @namespace = "MyNamespace.MyProject";
From 57b8ce0dab5fd307a6cdd9674ab166de514dfaac Mon Sep 17 00:00:00 2001
From: Julian Verdurmen <5808377+304NotModified@users.noreply.github.com>
Date: Wed, 17 Dec 2025 22:44:39 +0100
Subject: [PATCH 08/10] fix changelog
---
CHANGELOG.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7360d2c7..420ab0dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,9 +2,11 @@
## Improvements:
+* Support file scoped namespace declarations when generating code (#140)
+
## Bug fixes:
-*Contributors of this release (in alphabetical order):*
+*Contributors of this release (in alphabetical order):* @304NotModified
# v2025.3.395 - 2025-12-17
@@ -13,7 +15,6 @@
* Renamed to Reqnroll for Visual Studio 2022 & 2026 (#136)
* The 'Define Steps' command honors the StepDefinitionSkeletonStyle setting in the project reqnroll.json configuration file and will generate step skeletons using 'Async' appropriately. (#129)
* Update docs - .NET 10, TUnit, VS2026 (#138)
-* Support file scoped namespace declarations when generating code (#140)
*Contributors of this release (in alphabetical order):* @304NotModified, @clrudolphi
From 1bfbc8ce0d2e36c238bb36de212556aabff08cc6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?G=C3=A1sp=C3=A1r=20Nagy?=
Date: Tue, 27 Jan 2026 16:02:22 +0100
Subject: [PATCH 09/10] small cleanup
---
.../Configuration/CSharpCodeGenerationConfiguration.cs | 2 +-
.../Editor/Commands/DefineStepsCommand.cs | 9 +++------
2 files changed, 4 insertions(+), 7 deletions(-)
diff --git a/Reqnroll.VisualStudio/Configuration/CSharpCodeGenerationConfiguration.cs b/Reqnroll.VisualStudio/Configuration/CSharpCodeGenerationConfiguration.cs
index 8398b493..48ccf0bb 100644
--- a/Reqnroll.VisualStudio/Configuration/CSharpCodeGenerationConfiguration.cs
+++ b/Reqnroll.VisualStudio/Configuration/CSharpCodeGenerationConfiguration.cs
@@ -7,7 +7,7 @@ public class CSharpCodeGenerationConfiguration
/// Uses file-scoped namespaces when set to "file_scoped", otherwise uses block-scoped namespaces.
///
[EditorConfigSetting("csharp_style_namespace_declarations")]
- public string NamespaceDeclarationStyle { get; set; } = "block_scoped";
+ public string? NamespaceDeclarationStyle { get; set; } = "block_scoped";
///
/// Determines if file-scoped namespaces should be used based on the EditorConfig setting.
diff --git a/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs b/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs
index 39178fcf..32e17710 100644
--- a/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs
+++ b/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs
@@ -1,7 +1,4 @@
#nullable disable
-
-using System.Text;
-
namespace Reqnroll.VisualStudio.Editor.Commands;
[Export(typeof(IDeveroomFeatureEditorCommand))]
@@ -108,7 +105,7 @@ public override bool PreExec(IWpfTextView textView, DeveroomEditorCommandTargetK
switch (viewModel.Result)
{
case CreateStepDefinitionsDialogResult.Create:
- SaveAsStepDefinitionClass(projectScope, combinedSnippet, viewModel.ClassName, indent, newLine, textView);
+ SaveAsStepDefinitionClass(projectScope, combinedSnippet, viewModel.ClassName, indent, newLine);
break;
case CreateStepDefinitionsDialogResult.CopyToClipboard:
Logger.LogVerbose($"Copy to clipboard: {combinedSnippet}");
@@ -121,7 +118,7 @@ public override bool PreExec(IWpfTextView textView, DeveroomEditorCommandTargetK
}
private void SaveAsStepDefinitionClass(IProjectScope projectScope, string combinedSnippet, string className,
- string indent, string newLine, IWpfTextView textView)
+ string indent, string newLine)
{
string targetFolder = projectScope.ProjectFolder;
var projectSettings = projectScope.GetProjectSettings();
@@ -131,7 +128,7 @@ private void SaveAsStepDefinitionClass(IProjectScope projectScope, string combin
if (IdeScope.FileSystem.Directory.Exists(stepDefinitionsFolder))
{
targetFolder = stepDefinitionsFolder;
- fileNamespace = fileNamespace + ".StepDefinitions";
+ fileNamespace += ".StepDefinitions";
}
// Get C# code generation configuration from EditorConfig using target .cs file path
From 5b281d012530b2aa59581c54e0ddcf46d8b28d76 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?G=C3=A1sp=C3=A1r=20Nagy?=
Date: Tue, 27 Jan 2026 16:02:48 +0100
Subject: [PATCH 10/10] fix: load the editor config through the right project
---
.../EditorConfig/EditorConfigOptionsProvider.cs | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsProvider.cs b/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsProvider.cs
index 0f0f1e4b..da66f035 100644
--- a/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsProvider.cs
+++ b/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsProvider.cs
@@ -55,9 +55,24 @@ private Document CreateAdHocDocument(IWpfTextView textView)
private Document CreateAdHocDocumentByPath(string filePath)
{
+ bool IsInProject(Project project)
+ {
+ if (project.FilePath == null)
+ return false;
+ var projectDir = Path.GetDirectoryName(project.FilePath);
+ if (projectDir == null) return false;
+ return Path.GetFullPath(filePath)
+ .StartsWith(projectDir + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
+ }
+
if (string.IsNullOrEmpty(filePath))
return null;
- var project = _visualStudioWorkspace.CurrentSolution.Projects.FirstOrDefault();
+
+ // We try to create the ad-hoc document in the project that contains (or would contain) the file,
+ // because otherwise the editorconfig options may not be correctly resolved.
+ var project =
+ _visualStudioWorkspace.CurrentSolution.Projects.FirstOrDefault(IsInProject) ??
+ _visualStudioWorkspace.CurrentSolution.Projects.FirstOrDefault();
if (project == null)
return null;
return project.AddDocument(filePath, string.Empty, filePath: filePath);