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);