| 1 | using System.Text; |
| 2 | using Microsoft.CodeAnalysis; |
| 3 | using Microsoft.CodeAnalysis.CSharp; |
| 4 | using Microsoft.CodeAnalysis.CSharp.Syntax; |
| 5 | using Microsoft.CodeAnalysis.MSBuild; |
| 6 | |
| 7 | namespace RoslynMcp.ServiceLayer; |
| 8 | |
| 9 | /// <summary>Перенос выбранных членов типа в новый файл с объявлением <c>partial</c> того же типа; в исходном файле добавляется <c>partial</c> при необходимости.</summary> |
| 10 | public static class MoveMembersToPartialFile |
| 11 | { |
| 12 | private static string NormalizePath(string path) |
| 13 | { |
| 14 | var p = Path.GetFullPath(path.Trim()); |
| 15 | if (p.EndsWith(Path.DirectorySeparatorChar)) |
| 16 | p = p.TrimEnd(Path.DirectorySeparatorChar); |
| 17 | return p; |
| 18 | } |
| 19 | |
| 20 | /// <summary> |
| 21 | /// Перенос членов по именам в новый .cs файл (тот же тип, partial). Позиция — на имени типа или внутри тела типа. |
| 22 | /// Конструктор: укажи имя типа или <c>.ctor</c>. Индексатор: <c>this</c>. Перегрузки методов с одним именем переносятся все. |
| 23 | /// </summary> |
| 24 | public static async Task<string> MoveAsync( |
| 25 | string solutionOrProjectPath, |
| 26 | string filePath, |
| 27 | int line, |
| 28 | int column, |
| 29 | IReadOnlyList<string> memberNames, |
| 30 | string outputFilePath, |
| 31 | bool apply, |
| 32 | bool addDependentUpon = true, |
| 33 | CancellationToken cancellationToken = default) |
| 34 | { |
| 35 | if (!File.Exists(solutionOrProjectPath)) |
| 36 | return $"Error: solution/project not found: {solutionOrProjectPath}"; |
| 37 | if (!File.Exists(filePath)) |
| 38 | return $"Error: file not found: {filePath}"; |
| 39 | if (memberNames is null || memberNames.Count == 0) |
| 40 | return "Error: member_names must be a non-empty array."; |
| 41 | var trimmedNames = memberNames.Select(static n => n.Trim()).Where(static n => n.Length > 0).ToArray(); |
| 42 | if (trimmedNames.Length == 0) |
| 43 | return "Error: member_names must contain at least one non-empty name."; |
| 44 | |
| 45 | var outPath = Path.GetFullPath(outputFilePath.Trim()); |
| 46 | if (apply && File.Exists(outPath)) |
| 47 | return $"Error: output file already exists (delete or choose another path): {outPath}"; |
| 48 | |
| 49 | var targetPath = NormalizePath(filePath); |
| 50 | MSBuildWorkspace? workspace = null; |
| 51 | try |
| 52 | { |
| 53 | workspace = MSBuildWorkspace.Create(RoslynMcpWorkspaceProperties.MsBuild); |
| 54 | var solution = await WorkspaceOpen.OpenSolutionOrProjectAsync(workspace, solutionOrProjectPath, cancellationToken).ConfigureAwait(false); |
| 55 | if (solution is null) |
| 56 | return "Error: failed to open solution."; |
| 57 | |
| 58 | var document = solution.Projects |
| 59 | .SelectMany(p => p.Documents) |
| 60 | .FirstOrDefault(d => string.Equals(NormalizePath(d.FilePath ?? ""), targetPath, StringComparison.OrdinalIgnoreCase)); |
| 61 | if (document is null) |
| 62 | return $"Error: file not found in solution: {filePath}"; |
| 63 | |
| 64 | var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); |
| 65 | var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); |
| 66 | if (root is null || semanticModel is null) |
| 67 | return "Error: could not get syntax/semantic model."; |
| 68 | |
| 69 | var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); |
| 70 | var lines = sourceText.Lines; |
| 71 | if (line < 1 || line > lines.Count) |
| 72 | return $"Error: line {line} out of range (1..{lines.Count})."; |
| 73 | var lineInfo = lines[line - 1]; |
| 74 | var columnIndex = column - 1; |
| 75 | if (columnIndex < 0) |
| 76 | return "Error: column must be >= 1."; |
| 77 | var position = lineInfo.Span.Length == 0 |
| 78 | ? lineInfo.Start |
| 79 | : lineInfo.Start + Math.Min(columnIndex, lineInfo.Span.Length); |
| 80 | |
| 81 | var node = root.FindToken(position, findInsideTrivia: true).Parent; |
| 82 | TypeDeclarationSyntax? typeDecl = null; |
| 83 | INamedTypeSymbol? typeSymbol = null; |
| 84 | while (node is not null) |
| 85 | { |
| 86 | cancellationToken.ThrowIfCancellationRequested(); |
| 87 | if (node is TypeDeclarationSyntax tds) |
| 88 | { |
| 89 | var sym = semanticModel.GetDeclaredSymbol(tds, cancellationToken); |
| 90 | if (sym is INamedTypeSymbol named && named.TypeKind is TypeKind.Class or TypeKind.Struct or TypeKind.Interface) |
| 91 | { |
| 92 | typeDecl = tds; |
| 93 | typeSymbol = named; |
| 94 | break; |
| 95 | } |
| 96 | } |
| 97 | node = node.Parent; |
| 98 | } |
| 99 | |
| 100 | if (typeDecl is null || typeSymbol is null) |
| 101 | return $"Error: no class, struct, or interface at {filePath}:{line}:{column}. Put the cursor on the type name or inside its body."; |
| 102 | |
| 103 | if (typeSymbol.TypeKind == TypeKind.Interface) |
| 104 | return "Error: moving members out of an interface is not supported."; |
| 105 | |
| 106 | var nameSet = new HashSet<string>(trimmedNames, StringComparer.OrdinalIgnoreCase); |
| 107 | var collectError = TryCollectMembersToMove( |
| 108 | typeDecl, semanticModel, typeSymbol, nameSet, cancellationToken, out var toMove); |
| 109 | if (collectError is not null) |
| 110 | return collectError; |
| 111 | |
| 112 | if (toMove.Count == 0) |
| 113 | { |
| 114 | var available = string.Join(", ", ListMemberNamesForHint(typeDecl, semanticModel, cancellationToken).Take(40)); |
| 115 | var hint = string.IsNullOrEmpty(available) ? "" : $"\n# Hint (first members in type): {available}"; |
| 116 | return $"Error: no members matched member_names for type {typeSymbol.Name}. Check spelling (constructors: type name or .ctor; indexer: this).{hint}"; |
| 117 | } |
| 118 | |
| 119 | var compilationUnit = root is CompilationUnitSyntax cu ? cu : null; |
| 120 | if (compilationUnit is null) |
| 121 | return "Error: unexpected root (expected compilation unit)."; |
| 122 | |
| 123 | var fileScopedNs = IsDeclaredInFileScopedNamespace(typeDecl); |
| 124 | var newTypePart = EnsurePartialKeyword(CloneTypeShellWithMembers(typeDecl, toMove)); |
| 125 | var remainingAfterRemove = typeDecl.RemoveNodes(toMove, SyntaxRemoveOptions.KeepEndOfLine) ?? typeDecl; |
| 126 | var remainingType = EnsurePartialKeyword(remainingAfterRemove); |
| 127 | |
| 128 | var newFileText = BuildPartialPartFileText(compilationUnit, newTypePart, typeSymbol, fileScopedNs); |
| 129 | var modifiedRoot = root.ReplaceNode(typeDecl, remainingType); |
| 130 | var modifiedText = modifiedRoot.GetText(); |
| 131 | |
| 132 | var sb = new StringBuilder(); |
| 133 | sb.AppendLineInvariant($"# Type: {typeSymbol.ToDisplayString()}"); |
| 134 | sb.AppendLineInvariant($"# Members moved: {toMove.Count} ({string.Join(", ", toMove.Select(GetMemberLabel))})"); |
| 135 | sb.AppendLineInvariant($"# Output: {outPath}"); |
| 136 | sb.AppendLineInvariant($"# Source updated: {filePath}"); |
| 137 | sb.AppendLine(apply ? "# apply: true — writing files." : "# apply: false — preview only. Pass apply: true to write."); |
| 138 | sb.AppendLine(); |
| 139 | |
| 140 | if (!apply) |
| 141 | { |
| 142 | sb.AppendLine("## New file content"); |
| 143 | sb.AppendLine(); |
| 144 | sb.AppendLine(newFileText); |
| 145 | sb.AppendLine(); |
| 146 | sb.AppendLine("## Modified source (excerpt, first 80 lines)"); |
| 147 | sb.AppendLine(); |
| 148 | var fullModified = modifiedText.ToString(); |
| 149 | var lineArr = fullModified.Split(['\n', '\r'], StringSplitOptions.None); |
| 150 | var take = Math.Min(80, lineArr.Length); |
| 151 | for (var i = 0; i < take; i++) |
| 152 | sb.AppendLine(lineArr[i]); |
| 153 | if (lineArr.Length > 80) |
| 154 | sb.AppendLine("..."); |
| 155 | return sb.ToString(); |
| 156 | } |
| 157 | |
| 158 | var project = document.Project; |
| 159 | if (project.FilePath is not null) |
| 160 | { |
| 161 | var projDir = Path.GetDirectoryName(Path.GetFullPath(project.FilePath)); |
| 162 | var outDir = Path.GetDirectoryName(outPath); |
| 163 | if (projDir is not null && outDir is not null) |
| 164 | { |
| 165 | var fullProj = Path.GetFullPath(projDir); |
| 166 | var fullOut = Path.GetFullPath(outDir); |
| 167 | if (!fullOut.StartsWith(fullProj, Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)) |
| 168 | sb.AppendLineInvariant($"# Warning: output path is outside project directory ({fullProj}). SDK glob may not include the file; prefer a path under the project."); |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | var solutionWithOriginal = solution.WithDocumentText(document.Id, modifiedText); |
| 173 | var projectAfter = solutionWithOriginal.GetDocument(document.Id)?.Project |
| 174 | ?? throw new InvalidOperationException("Document missing after update."); |
| 175 | var folders = GetRelativeFolderSegments(projectAfter.FilePath, outPath); |
| 176 | var fileName = Path.GetFileName(outPath); |
| 177 | var withNewDoc = projectAfter.AddDocument(fileName, newFileText, folders, outPath); |
| 178 | var finalSolution = withNewDoc.Project.Solution; |
| 179 | |
| 180 | if (!workspace.TryApplyChanges(finalSolution)) |
| 181 | return "Error: TryApplyChanges returned false (files may be read-only or path invalid)."; |
| 182 | |
| 183 | sb.AppendLine("Applied: source file updated and new partial file created."); |
| 184 | |
| 185 | var csprojPathApply = withNewDoc.Project.FilePath; |
| 186 | if (!string.IsNullOrEmpty(csprojPathApply)) |
| 187 | { |
| 188 | var projDirApply = Path.GetDirectoryName(Path.GetFullPath(csprojPathApply)); |
| 189 | if (projDirApply is not null) |
| 190 | { |
| 191 | var childRelApply = Path.GetRelativePath(projDirApply, outPath).Replace('/', Path.DirectorySeparatorChar); |
| 192 | var stripMsg = DependentUponCsproj.TryRemoveRedundantSdkCompileInclude(csprojPathApply, childRelApply); |
| 193 | sb.AppendLineInvariant($"# Sdk Compile: {stripMsg}"); |
| 194 | |
| 195 | if (addDependentUpon) |
| 196 | { |
| 197 | var depVal = DependentUponCsproj.ComputeDependentUponValue(projDirApply, outPath, targetPath); |
| 198 | var depMsg = DependentUponCsproj.AddOrUpdateDependentUpon(csprojPathApply, childRelApply, depVal, dryRun: false); |
| 199 | sb.AppendLineInvariant($"# DependentUpon: {depMsg}"); |
| 200 | } |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | return sb.ToString(); |
| 205 | } |
| 206 | catch (InvalidOperationException ex) when (ex.Message.Contains("slnx") || ex.Message.Contains("Slnx")) |
| 207 | { |
| 208 | return "Error: .slnx format is not supported. Use .sln or .csproj."; |
| 209 | } |
| 210 | finally |
| 211 | { |
| 212 | workspace?.Dispose(); |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | private static string GetMemberLabel(MemberDeclarationSyntax m) => m switch |
| 217 | { |
| 218 | MethodDeclarationSyntax md => md.Identifier.ValueText, |
| 219 | ConstructorDeclarationSyntax cd => cd.Identifier.ValueText.Length > 0 ? cd.Identifier.ValueText : ".ctor", |
| 220 | DestructorDeclarationSyntax => "Finalize", |
| 221 | PropertyDeclarationSyntax pd => pd.Identifier.ValueText, |
| 222 | IndexerDeclarationSyntax => "this", |
| 223 | FieldDeclarationSyntax fd => string.Join(", ", fd.Declaration.Variables.Select(v => v.Identifier.ValueText)), |
| 224 | EventDeclarationSyntax ed => ed.Identifier.ValueText, |
| 225 | EventFieldDeclarationSyntax efd => string.Join(", ", efd.Declaration.Variables.Select(v => v.Identifier.ValueText)), |
| 226 | TypeDeclarationSyntax td => td.Identifier.ValueText, |
| 227 | EnumDeclarationSyntax en => en.Identifier.ValueText, |
| 228 | _ => m.Kind().ToString() |
| 229 | }; |
| 230 | |
| 231 | private static string[] GetRelativeFolderSegments(string? projectFilePath, string documentFullPath) |
| 232 | { |
| 233 | if (string.IsNullOrEmpty(projectFilePath)) |
| 234 | return []; |
| 235 | var projDir = Path.GetDirectoryName(Path.GetFullPath(projectFilePath)); |
| 236 | var docDir = Path.GetDirectoryName(Path.GetFullPath(documentFullPath)); |
| 237 | if (projDir is null || docDir is null) |
| 238 | return []; |
| 239 | var rel = Path.GetRelativePath(projDir, docDir); |
| 240 | if (rel is "." or { Length: 0 }) |
| 241 | return []; |
| 242 | return rel.Split([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar], StringSplitOptions.RemoveEmptyEntries); |
| 243 | } |
| 244 | |
| 245 | private static IEnumerable<string> ListMemberNamesForHint(TypeDeclarationSyntax typeDecl, SemanticModel model, CancellationToken ct) |
| 246 | { |
| 247 | foreach (var m in typeDecl.Members) |
| 248 | { |
| 249 | ct.ThrowIfCancellationRequested(); |
| 250 | switch (m) |
| 251 | { |
| 252 | case FieldDeclarationSyntax fd: |
| 253 | foreach (var v in fd.Declaration.Variables) |
| 254 | { |
| 255 | ct.ThrowIfCancellationRequested(); |
| 256 | if (model.GetDeclaredSymbol(v, ct) is IFieldSymbol fs) |
| 257 | yield return fs.Name; |
| 258 | } |
| 259 | break; |
| 260 | case EventFieldDeclarationSyntax efd: |
| 261 | foreach (var v in efd.Declaration.Variables) |
| 262 | { |
| 263 | ct.ThrowIfCancellationRequested(); |
| 264 | if (model.GetDeclaredSymbol(v, ct) is IEventSymbol es) |
| 265 | yield return es.Name; |
| 266 | } |
| 267 | break; |
| 268 | default: |
| 269 | var sym = model.GetDeclaredSymbol(m, ct); |
| 270 | if (sym is null) |
| 271 | continue; |
| 272 | yield return sym.Name switch |
| 273 | { |
| 274 | ".ctor" => typeDecl.Identifier.ValueText, |
| 275 | _ when sym is IPropertySymbol { IsIndexer: true } => "this", |
| 276 | _ => sym.Name |
| 277 | }; |
| 278 | break; |
| 279 | } |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | /// <summary> |
| 284 | /// Поля и event-поля: символ у каждого <see cref="VariableDeclaratorSyntax"/>; для узла <see cref="FieldDeclarationSyntax"/> целиком |
| 285 | /// <see cref="SemanticModel.GetDeclaredSymbol(Microsoft.CodeAnalysis.CSharp.Syntax.MemberDeclarationSyntax, CancellationToken)"/> часто даёт null (const и др.). |
| 286 | /// </summary> |
| 287 | private static string? TryCollectMembersToMove( |
| 288 | TypeDeclarationSyntax typeDecl, |
| 289 | SemanticModel semanticModel, |
| 290 | INamedTypeSymbol typeSymbol, |
| 291 | HashSet<string> nameSet, |
| 292 | CancellationToken ct, |
| 293 | out List<MemberDeclarationSyntax> toMove) |
| 294 | { |
| 295 | toMove = new List<MemberDeclarationSyntax>(); |
| 296 | foreach (var member in typeDecl.Members) |
| 297 | { |
| 298 | ct.ThrowIfCancellationRequested(); |
| 299 | switch (member) |
| 300 | { |
| 301 | case FieldDeclarationSyntax fd: |
| 302 | { |
| 303 | var err = TryAddFieldOrEventFieldDeclaration( |
| 304 | fd, fd.Declaration.Variables, semanticModel, nameSet, typeSymbol.Name, toMove, ct); |
| 305 | if (err is not null) |
| 306 | return err; |
| 307 | break; |
| 308 | } |
| 309 | case EventFieldDeclarationSyntax efd: |
| 310 | { |
| 311 | var err = TryAddFieldOrEventFieldDeclaration( |
| 312 | efd, efd.Declaration.Variables, semanticModel, nameSet, typeSymbol.Name, toMove, ct); |
| 313 | if (err is not null) |
| 314 | return err; |
| 315 | break; |
| 316 | } |
| 317 | default: |
| 318 | { |
| 319 | var sym = semanticModel.GetDeclaredSymbol(member, ct); |
| 320 | if (sym is null) |
| 321 | continue; |
| 322 | if (ShouldMoveMember(sym, nameSet, typeSymbol.Name) && !toMove.Contains(member)) |
| 323 | toMove.Add(member); |
| 324 | break; |
| 325 | } |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | return null; |
| 330 | } |
| 331 | |
| 332 | /// <summary> |
| 333 | /// Если в одном объявлении несколько переменных и в <paramref name="nameSet"/> попадает только часть имён — перенос невозможен без разбиения объявления. |
| 334 | /// </summary> |
| 335 | private static string? TryAddFieldOrEventFieldDeclaration( |
| 336 | MemberDeclarationSyntax declarationNode, |
| 337 | SeparatedSyntaxList<VariableDeclaratorSyntax> variables, |
| 338 | SemanticModel semanticModel, |
| 339 | HashSet<string> nameSet, |
| 340 | string typeName, |
| 341 | List<MemberDeclarationSyntax> toMove, |
| 342 | CancellationToken ct) |
| 343 | { |
| 344 | if (variables.Count == 0) |
| 345 | return null; |
| 346 | |
| 347 | var anyMatch = false; |
| 348 | var anyResolvedNonMatch = false; |
| 349 | foreach (var variable in variables) |
| 350 | { |
| 351 | ct.ThrowIfCancellationRequested(); |
| 352 | var sym = semanticModel.GetDeclaredSymbol(variable, ct); |
| 353 | if (sym is null) |
| 354 | continue; |
| 355 | if (ShouldMoveMember(sym, nameSet, typeName)) |
| 356 | anyMatch = true; |
| 357 | else |
| 358 | anyResolvedNonMatch = true; |
| 359 | } |
| 360 | |
| 361 | if (!anyMatch) |
| 362 | return null; |
| 363 | |
| 364 | if (anyResolvedNonMatch) |
| 365 | { |
| 366 | var line = declarationNode.GetLocation().GetLineSpan().StartLinePosition.Line + 1; |
| 367 | return $"Error: field/event declaration at line {line} declares multiple variables; cannot move a subset in one step. Split into separate declarations or list all names in member_names."; |
| 368 | } |
| 369 | |
| 370 | if (!toMove.Contains(declarationNode)) |
| 371 | toMove.Add(declarationNode); |
| 372 | return null; |
| 373 | } |
| 374 | |
| 375 | private static bool ShouldMoveMember(ISymbol sym, HashSet<string> names, string typeName) |
| 376 | { |
| 377 | return sym switch |
| 378 | { |
| 379 | IMethodSymbol m when m.MethodKind == MethodKind.Constructor => |
| 380 | names.Contains(typeName) || names.Contains(".ctor"), |
| 381 | IMethodSymbol m when m.MethodKind == MethodKind.Destructor => |
| 382 | names.Contains("Finalize") || names.Contains("~" + typeName), |
| 383 | IPropertySymbol { IsIndexer: true } => |
| 384 | names.Contains("this"), |
| 385 | IMethodSymbol m => |
| 386 | names.Contains(m.Name), |
| 387 | IPropertySymbol p => |
| 388 | names.Contains(p.Name), |
| 389 | IFieldSymbol f => |
| 390 | names.Contains(f.Name), |
| 391 | IEventSymbol e => |
| 392 | names.Contains(e.Name), |
| 393 | INamedTypeSymbol nt when nt.TypeKind is TypeKind.Class or TypeKind.Struct or TypeKind.Enum or TypeKind.Interface => |
| 394 | names.Contains(nt.Name), |
| 395 | _ => false |
| 396 | }; |
| 397 | } |
| 398 | |
| 399 | /// <summary> |
| 400 | /// Добавляет <c>partial</c> если его ещё нет. Порядок: <c>partial</c> — последний модификатор перед keyword типа |
| 401 | /// (<c>class</c>/<c>struct</c>/<c>record</c>/<c>interface</c>), иначе CS0267 (<c>public partial sealed class</c> неверно). |
| 402 | /// </summary> |
| 403 | private static TypeDeclarationSyntax EnsurePartialKeyword(TypeDeclarationSyntax typeDecl) |
| 404 | { |
| 405 | if (typeDecl.Modifiers.Any(SyntaxKind.PartialKeyword)) |
| 406 | return typeDecl; |
| 407 | var partialToken = SyntaxFactory.Token(SyntaxKind.PartialKeyword).WithTrailingTrivia(SyntaxFactory.Space); |
| 408 | return typeDecl.WithModifiers(typeDecl.Modifiers.Add(partialToken)); |
| 409 | } |
| 410 | |
| 411 | private static TypeDeclarationSyntax CloneTypeShellWithMembers(TypeDeclarationSyntax original, IReadOnlyList<MemberDeclarationSyntax> members) => |
| 412 | original |
| 413 | .WithMembers(SyntaxFactory.List(members)) |
| 414 | .WithAttributeLists(SyntaxFactory.List<AttributeListSyntax>()) |
| 415 | .WithLeadingTrivia(SyntaxFactory.EndOfLine(Environment.NewLine)); |
| 416 | |
| 417 | private static bool IsDeclaredInFileScopedNamespace(TypeDeclarationSyntax typeDecl) |
| 418 | { |
| 419 | for (var p = typeDecl.Parent; p != null; p = p.Parent) |
| 420 | { |
| 421 | if (p is FileScopedNamespaceDeclarationSyntax) |
| 422 | return true; |
| 423 | if (p is NamespaceDeclarationSyntax) |
| 424 | return false; |
| 425 | } |
| 426 | return false; |
| 427 | } |
| 428 | |
| 429 | private static string BuildPartialPartFileText( |
| 430 | CompilationUnitSyntax originalCu, |
| 431 | TypeDeclarationSyntax typePart, |
| 432 | INamedTypeSymbol typeSymbol, |
| 433 | bool fileScopedNamespace) |
| 434 | { |
| 435 | var globalNs = typeSymbol.ContainingNamespace is null or { IsGlobalNamespace: true }; |
| 436 | var nsDisplay = globalNs ? null : typeSymbol.ContainingNamespace!.ToDisplayString(); |
| 437 | var fileScoped = fileScopedNamespace && !globalNs; |
| 438 | |
| 439 | MemberDeclarationSyntax typeAsMember = typePart; |
| 440 | MemberDeclarationSyntax wrapped; |
| 441 | |
| 442 | if (globalNs) |
| 443 | wrapped = typeAsMember; |
| 444 | else if (string.IsNullOrEmpty(nsDisplay)) |
| 445 | wrapped = typeAsMember; |
| 446 | else if (fileScoped) |
| 447 | { |
| 448 | wrapped = SyntaxFactory.FileScopedNamespaceDeclaration( |
| 449 | SyntaxFactory.ParseName(nsDisplay)) |
| 450 | .WithMembers(SyntaxFactory.SingletonList(typeAsMember)); |
| 451 | } |
| 452 | else |
| 453 | { |
| 454 | wrapped = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(nsDisplay!)) |
| 455 | .WithMembers(SyntaxFactory.SingletonList(typeAsMember)) |
| 456 | .WithOpenBraceToken(SyntaxFactory.Token(SyntaxKind.OpenBraceToken).WithLeadingTrivia(SyntaxFactory.CarriageReturnLineFeed)) |
| 457 | .WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken).WithLeadingTrivia(SyntaxFactory.CarriageReturnLineFeed)); |
| 458 | } |
| 459 | |
| 460 | var cu = SyntaxFactory.CompilationUnit() |
| 461 | .WithExterns(originalCu.Externs) |
| 462 | .WithUsings(originalCu.Usings); |
| 463 | |
| 464 | cu = globalNs || string.IsNullOrEmpty(nsDisplay) |
| 465 | ? cu.AddMembers(typeAsMember) |
| 466 | : cu.AddMembers(wrapped); |
| 467 | |
| 468 | return cu.NormalizeWhitespace(elasticTrivia: true).ToFullString(); |
| 469 | } |
| 470 | } |
| 471 | |