diff --git a/Reqnroll.VisualStudio/Configuration/CSharpCodeGenerationConfiguration.cs b/Reqnroll.VisualStudio/Configuration/CSharpCodeGenerationConfiguration.cs
new file mode 100644
index 0000000..8398b49
--- /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 507d804..4906475 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 bac6206..7d1fe9a 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 2045bde..0f0f1e4 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 22ba4b0..3d8e996 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 17bc2a8..219e3d0 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 0000000..b29aeb2
--- /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 fba8031..95d2ec8 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 dd45a7b..d24b482 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();
}