Skip to content

Commit 46dee91

Browse files
304NotModifiedCopilotgasparnagy
authored
Support file scoped namespace declarations when generating code (#140)
* Support csharp_style_namespace_declarations = file_scoped * Update CHANGELOG.md * extract GenerateStepDefinitionClass and add snapshot tests * 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> * Remove nuget.exe from .gitignore Remove nuget.exe from .gitignore * Fix typo in CHANGELOG for namespace support * refactor * fix changelog * small cleanup * fix: load the editor config through the right project --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Gáspár Nagy <gaspar.nagy@gmail.com>
1 parent 8f6c990 commit 46dee91

14 files changed

+468
-29
lines changed

CHANGELOG.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
## Improvements:
44

5-
* Improved handling of scope tag expressions, hook and scope errors (#150)
6-
* Improved logging for binding discovery (#154)
5+
* Support file scoped namespace declarations when generating code (#140)
76

87
## Bug fixes:
98

10-
*Contributors of this release (in alphabetical order):* @clrudolphi, @gasparnagy
9+
* Improved handling of scope tag expressions, hook and scope errors (#150)
10+
* Improved logging for binding discovery (#154)
11+
12+
*Contributors of this release (in alphabetical order):* @304NotModified, @clrudolphi, @gasparnagy
1113

1214
# v2025.3.395 - 2025-12-17
1315

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace Reqnroll.VisualStudio.Configuration;
2+
3+
public class CSharpCodeGenerationConfiguration
4+
{
5+
/// <summary>
6+
/// Specifies the namespace declaration style for generated C# code.
7+
/// Uses file-scoped namespaces when set to "file_scoped", otherwise uses block-scoped namespaces.
8+
/// </summary>
9+
[EditorConfigSetting("csharp_style_namespace_declarations")]
10+
public string? NamespaceDeclarationStyle { get; set; } = "block_scoped";
11+
12+
/// <summary>
13+
/// Determines if file-scoped namespaces should be used based on the EditorConfig setting.
14+
/// </summary>
15+
public bool UseFileScopedNamespaces =>
16+
NamespaceDeclarationStyle != null &&
17+
NamespaceDeclarationStyle.StartsWith("file_scoped", StringComparison.OrdinalIgnoreCase);
18+
}

Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs

Lines changed: 108 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ namespace Reqnroll.VisualStudio.Editor.Commands;
44
[Export(typeof(IDeveroomFeatureEditorCommand))]
55
public class DefineStepsCommand : DeveroomEditorCommandBase, IDeveroomFeatureEditorCommand
66
{
7+
private readonly IEditorConfigOptionsProvider _editorConfigOptionsProvider;
8+
79
[ImportingConstructor]
810
public DefineStepsCommand(
911
IIdeScope ideScope,
1012
IBufferTagAggregatorFactoryService aggregatorFactory,
11-
IDeveroomTaggerProvider taggerProvider)
13+
IDeveroomTaggerProvider taggerProvider,
14+
IEditorConfigOptionsProvider editorConfigOptionsProvider)
1215
: base(ideScope, aggregatorFactory, taggerProvider)
1316
{
17+
_editorConfigOptionsProvider = editorConfigOptionsProvider;
1418
}
1519

1620
public override DeveroomEditorCommandTargetKey[] Targets => new[]
@@ -94,7 +98,7 @@ public override bool PreExec(IWpfTextView textView, DeveroomEditorCommandTargetK
9498
}
9599

96100
var combinedSnippet = string.Join(newLine,
97-
viewModel.Items.Where(i => i.IsSelected).Select(i => i.Snippet.Indent(indent + indent)));
101+
viewModel.Items.Where(i => i.IsSelected).Select(i => i.Snippet.Indent(indent)));
98102

99103
MonitoringService.MonitorCommandDefineSteps(viewModel.Result, viewModel.Items.Count(i => i.IsSelected));
100104

@@ -124,42 +128,104 @@ private void SaveAsStepDefinitionClass(IProjectScope projectScope, string combin
124128
if (IdeScope.FileSystem.Directory.Exists(stepDefinitionsFolder))
125129
{
126130
targetFolder = stepDefinitionsFolder;
127-
fileNamespace = fileNamespace + ".StepDefinitions";
131+
fileNamespace += ".StepDefinitions";
128132
}
129-
var projectTraits = projectScope.GetProjectSettings().ReqnrollProjectTraits;
130-
var isSpecFlow = projectTraits.HasFlag(ReqnrollProjectTraits.LegacySpecFlow) || projectTraits.HasFlag(ReqnrollProjectTraits.SpecFlowCompatibility);
131-
var libraryNameSpace = isSpecFlow ? "SpecFlow" : "Reqnroll";
132133

133-
var template = "using System;" + newLine +
134-
$"using {libraryNameSpace};" + newLine +
135-
newLine +
136-
$"namespace {fileNamespace}" + newLine +
137-
"{" + newLine +
138-
$"{indent}[Binding]" + newLine +
139-
$"{indent}public class {className}" + newLine +
140-
$"{indent}{{" + newLine +
141-
combinedSnippet +
142-
$"{indent}}}" + newLine +
143-
"}" + newLine;
134+
// Get C# code generation configuration from EditorConfig using target .cs file path
135+
var targetFilePath = Path.Combine(targetFolder, className + ".cs");
136+
var csharpConfig = new CSharpCodeGenerationConfiguration();
137+
var editorConfigOptions = _editorConfigOptionsProvider.GetEditorConfigOptionsByPath(targetFilePath);
138+
editorConfigOptions.UpdateFromEditorConfig(csharpConfig);
139+
140+
var projectTraits = projectScope.GetProjectSettings().ReqnrollProjectTraits;
141+
var generatedContent = GenerateStepDefinitionClass(
142+
combinedSnippet,
143+
className,
144+
fileNamespace,
145+
projectTraits,
146+
csharpConfig,
147+
indent,
148+
newLine);
144149

145150
var targetFile = FileDetails
146151
.FromPath(targetFolder, className + ".cs")
147-
.WithCSharpContent(template);
152+
.WithCSharpContent(generatedContent);
148153

149154
if (IdeScope.FileSystem.File.Exists(targetFile.FullName))
150155
if (IdeScope.Actions.ShowSyncQuestion("Overwrite file?",
151156
$"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?",
152157
defaultButton: MessageBoxResult.No) != MessageBoxResult.Yes)
153158
return;
154159

155-
projectScope.AddFile(targetFile, template);
160+
projectScope.AddFile(targetFile, generatedContent);
156161
projectScope.IdeScope.Actions.NavigateTo(new SourceLocation(targetFile, 9, 1));
157162
IDiscoveryService discoveryService = projectScope.GetDiscoveryService();
158163

159164
projectScope.IdeScope.FireAndForget(
160165
() => RebuildBindingRegistry(discoveryService, targetFile), _ => { Finished.Set(); });
161166
}
162167

168+
internal static string GenerateStepDefinitionClass(
169+
string combinedSnippet,
170+
string className,
171+
string fileNamespace,
172+
ReqnrollProjectTraits projectTraits,
173+
CSharpCodeGenerationConfiguration csharpConfig,
174+
string indent,
175+
string newLine)
176+
{
177+
var isSpecFlow = projectTraits.HasFlag(ReqnrollProjectTraits.LegacySpecFlow) ||
178+
projectTraits.HasFlag(ReqnrollProjectTraits.SpecFlowCompatibility);
179+
var libraryNameSpace = isSpecFlow ? "SpecFlow" : "Reqnroll";
180+
181+
// Estimate template size for StringBuilder capacity
182+
var estimatedSize = 200 + fileNamespace.Length + className.Length + combinedSnippet.Length;
183+
var template = new StringBuilder(estimatedSize);
184+
template.AppendLine("using System;");
185+
template.AppendLine($"using {libraryNameSpace};");
186+
template.AppendLine();
187+
188+
// Determine indentation level based on namespace style
189+
var classIndent = csharpConfig.UseFileScopedNamespaces ? "" : indent;
190+
191+
// Add namespace declaration
192+
if (csharpConfig.UseFileScopedNamespaces)
193+
{
194+
template.AppendLine($"namespace {fileNamespace};");
195+
template.AppendLine();
196+
}
197+
else
198+
{
199+
template.AppendLine($"namespace {fileNamespace}");
200+
template.AppendLine("{");
201+
}
202+
203+
// Add class declaration (common structure with appropriate indentation)
204+
template.AppendLine($"{classIndent}[Binding]");
205+
template.AppendLine($"{classIndent}public class {className}");
206+
template.AppendLine($"{classIndent}{{");
207+
208+
// Add snippet with appropriate indentation based on namespace style
209+
if (csharpConfig.UseFileScopedNamespaces)
210+
{
211+
template.AppendLine(combinedSnippet);
212+
}
213+
else
214+
{
215+
AppendLinesWithIndent(template, combinedSnippet, indent, newLine);
216+
}
217+
218+
template.AppendLine($"{classIndent}}}");
219+
220+
// Close namespace if block-scoped
221+
if (!csharpConfig.UseFileScopedNamespaces)
222+
{
223+
template.AppendLine("}");
224+
}
225+
226+
return template.ToString();
227+
}
228+
163229
private async Task RebuildBindingRegistry(IDiscoveryService discoveryService,
164230
CSharpStepDefinitionFile stepDefinitionFile)
165231
{
@@ -168,4 +234,27 @@ await discoveryService.BindingRegistryCache
168234

169235
Finished.Set();
170236
}
237+
238+
private static void AppendLinesWithIndent(StringBuilder builder, string content, string indent, string newLine)
239+
{
240+
if (string.IsNullOrEmpty(content))
241+
return;
242+
243+
var lines = content.Split(new[] { newLine }, StringSplitOptions.None);
244+
245+
for (int i = 0; i < lines.Length; i++)
246+
{
247+
var line = lines[i];
248+
249+
// Add indentation to non-empty lines
250+
if (!string.IsNullOrWhiteSpace(line))
251+
{
252+
builder.Append(indent).AppendLine(line);
253+
}
254+
else
255+
{
256+
builder.AppendLine(line);
257+
}
258+
}
259+
}
171260
}

Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsExtensions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,16 @@ public static void UpdateFromEditorConfig<TConfig>(this IEditorConfigOptions edi
2626
.Select(p => new
2727
{
2828
PropertyInfo = p,
29-
((EditorConfigSettingAttribute) Attribute.GetCustomAttribute(p, typeof(EditorConfigSettingAttribute)))
29+
EditorConfigKey = ((EditorConfigSettingAttribute) Attribute.GetCustomAttribute(p, typeof(EditorConfigSettingAttribute)))
3030
?.EditorConfigSettingName
3131
})
32-
.Where(p => p.EditorConfigSettingName != null);
32+
.Where(p => p.EditorConfigKey != null);
3333

3434
foreach (var property in propertiesWithEditorConfig)
3535
{
3636
var currentValue = property.PropertyInfo.GetValue(config);
3737
var updatedValue = editorConfigOptions.GetOption(property.PropertyInfo.PropertyType,
38-
property.EditorConfigSettingName, currentValue);
38+
property.EditorConfigKey, currentValue);
3939
if (!Equals(currentValue, updatedValue))
4040
property.PropertyInfo.SetValue(config, updatedValue);
4141
}

Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsProvider.cs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@ public IEditorConfigOptions GetEditorConfigOptions(IWpfTextView textView)
2626
return new EditorConfigOptions(options);
2727
}
2828

29+
public IEditorConfigOptions GetEditorConfigOptionsByPath(string filePath)
30+
{
31+
if (string.IsNullOrEmpty(filePath))
32+
return NullEditorConfigOptions.Instance;
33+
34+
var document = CreateAdHocDocumentByPath(filePath);
35+
if (document == null)
36+
return NullEditorConfigOptions.Instance;
37+
38+
var options =
39+
ThreadHelper.JoinableTaskFactory.Run(() => document.GetOptionsAsync());
40+
41+
return new EditorConfigOptions(options);
42+
}
43+
2944
private Document GetDocument(IWpfTextView textView) =>
3045
textView.TextBuffer.GetRelatedDocuments().FirstOrDefault() ??
3146
CreateAdHocDocument(textView);
@@ -35,10 +50,32 @@ private Document CreateAdHocDocument(IWpfTextView textView)
3550
var editorFilePath = GetPath(textView);
3651
if (editorFilePath == null)
3752
return null;
38-
var project = _visualStudioWorkspace.CurrentSolution.Projects.FirstOrDefault();
53+
return CreateAdHocDocumentByPath(editorFilePath);
54+
}
55+
56+
private Document CreateAdHocDocumentByPath(string filePath)
57+
{
58+
bool IsInProject(Project project)
59+
{
60+
if (project.FilePath == null)
61+
return false;
62+
var projectDir = Path.GetDirectoryName(project.FilePath);
63+
if (projectDir == null) return false;
64+
return Path.GetFullPath(filePath)
65+
.StartsWith(projectDir + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
66+
}
67+
68+
if (string.IsNullOrEmpty(filePath))
69+
return null;
70+
71+
// We try to create the ad-hoc document in the project that contains (or would contain) the file,
72+
// because otherwise the editorconfig options may not be correctly resolved.
73+
var project =
74+
_visualStudioWorkspace.CurrentSolution.Projects.FirstOrDefault(IsInProject) ??
75+
_visualStudioWorkspace.CurrentSolution.Projects.FirstOrDefault();
3976
if (project == null)
4077
return null;
41-
return project.AddDocument(editorFilePath, string.Empty, filePath: editorFilePath);
78+
return project.AddDocument(filePath, string.Empty, filePath: filePath);
4279
}
4380

4481
public static string GetPath(IWpfTextView textView)

Reqnroll.VisualStudio/Editor/Services/EditorConfig/IEditorConfigOptionsProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ namespace Reqnroll.VisualStudio.Editor.Services.EditorConfig;
33
public interface IEditorConfigOptionsProvider
44
{
55
IEditorConfigOptions GetEditorConfigOptions(IWpfTextView textView);
6+
IEditorConfigOptions GetEditorConfigOptionsByPath(string filePath);
67
}

Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/ProjectSystemSteps.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,8 @@ private void PerformCommand(string commandName, string parameter = null,
478478
}
479479
case "Define Steps":
480480
{
481-
_invokedCommand = new DefineStepsCommand(_ideScope, aggregatorFactoryService, taggerProvider);
481+
_invokedCommand = new DefineStepsCommand(_ideScope, aggregatorFactoryService, taggerProvider,
482+
new StubEditorConfigOptionsProvider());
482483
_invokedCommand.PreExec(_wpfTextView, _invokedCommand.Targets.First());
483484
return;
484485
}

0 commit comments

Comments
 (0)