File: NestedFiles\RemoteAddNestedFileService.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.CodeAnalysis.Remote.Razor\Microsoft.CodeAnalysis.Remote.Razor.csproj (Microsoft.CodeAnalysis.Remote.Razor)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.CodeActions;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.NestedFiles;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
 
namespace Microsoft.CodeAnalysis.Remote.Razor;
 
internal sealed class RemoteAddNestedFileService(in ServiceArgs args)
    : RazorDocumentServiceBase(in args), IRemoteAddNestedFileService
{
    internal sealed class Factory : FactoryBase<IRemoteAddNestedFileService>
    {
        protected override IRemoteAddNestedFileService CreateService(in ServiceArgs args)
            => new RemoteAddNestedFileService(in args);
    }
 
    private readonly IRoslynCodeActionHelpers _roslynCodeActionHelpers =
        args.ExportProvider.GetExportedValue<IRoslynCodeActionHelpers>();
 
    private readonly LanguageServerFeatureOptions _languageServerFeatureOptions =
        args.ExportProvider.GetExportedValue<LanguageServerFeatureOptions>();
 
    public ValueTask<WorkspaceEdit?> GetNewNestedFileWorkspaceEditAsync(
        JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
        JsonSerializableDocumentId documentId,
        NestedFileKind fileKind,
        CancellationToken cancellationToken)
        => RunServiceAsync(
            solutionInfo,
            documentId,
            context => GetNewNestedFileWorkspaceEditAsync(context, fileKind, cancellationToken),
            cancellationToken);
 
    private async ValueTask<WorkspaceEdit?> GetNewNestedFileWorkspaceEditAsync(
        RemoteDocumentContext context,
        NestedFileKind fileKind,
        CancellationToken cancellationToken)
    {
        var razorFilePath = context.Snapshot.FilePath;
        if (GetNestedFilePath(razorFilePath, fileKind) is not string nestedFilePath)
        {
            return null;
        }
 
        var nestedFileUri = LspFactory.CreateFilePathUri(nestedFilePath, _languageServerFeatureOptions);
 
        var content = await GenerateContentAsync(
            fileKind, context, razorFilePath, nestedFileUri, cancellationToken).ConfigureAwait(false);
 
        var nestedFileDocumentIdentifier = new OptionalVersionedTextDocumentIdentifier
        {
            DocumentUri = new DocumentUri(nestedFileUri)
        };
 
        var documentChanges = new SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>[]
        {
            new CreateFile { DocumentUri = nestedFileDocumentIdentifier.DocumentUri },
            new TextDocumentEdit
            {
                TextDocument = nestedFileDocumentIdentifier,
                Edits = [LspFactory.CreateTextEdit(position: (0, 0), content)]
            }
        };
 
        return new WorkspaceEdit
        {
            DocumentChanges = documentChanges,
        };
    }
 
    private static string? GetNestedFilePath(string razorFilePath, NestedFileKind fileKind)
    {
        return fileKind switch
        {
            NestedFileKind.Css => razorFilePath + ".css",
            NestedFileKind.CSharp => razorFilePath + ".cs",
            NestedFileKind.JavaScript => razorFilePath + ".js",
            _ => null
        };
    }
 
    private async Task<string> GenerateContentAsync(
        NestedFileKind fileKind,
        RemoteDocumentContext documentContext,
        string razorFilePath,
        Uri nestedFileUri,
        CancellationToken cancellationToken)
    {
        return fileKind switch
        {
            NestedFileKind.CSharp => await GenerateCSharpContentAsync(
                documentContext, razorFilePath, nestedFileUri, cancellationToken).ConfigureAwait(false),
            NestedFileKind.Css => GenerateCssContent(razorFilePath),
            NestedFileKind.JavaScript => GenerateJavaScriptContent(razorFilePath),
            _ => string.Empty
        };
    }
 
    private static string GenerateCssContent(string razorFilePath)
    {
        var componentName = Path.GetFileNameWithoutExtension(razorFilePath);
        var fileType = FileKinds.GetFileKindFromPath(razorFilePath).IsLegacy() ? "view" : "component";
        return $"/* CSS for {componentName} {fileType} */\r\n";
    }
 
    private static string GenerateJavaScriptContent(string razorFilePath)
    {
        var componentName = Path.GetFileNameWithoutExtension(razorFilePath);
        var fileType = FileKinds.GetFileKindFromPath(razorFilePath).IsLegacy() ? "view" : "component";
        return $"// JavaScript for {componentName} {fileType}\r\n";
    }
 
    private async Task<string> GenerateCSharpContentAsync(
        RemoteDocumentContext documentContext,
        string razorFilePath,
        Uri nestedFileUri,
        CancellationToken cancellationToken)
    {
        var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
        var className = Path.GetFileNameWithoutExtension(razorFilePath);
 
        // Use the Razor compiler's namespace resolution which handles @namespace directives,
        // _Imports.razor, and SDK-provided root namespace
        if (!codeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var ns) || ns.IsNullOrEmpty())
        {
            Logger.LogWarning($"Could not determine namespace for: {razorFilePath}");
            ns = "Unknown";
        }
 
        var content = GenerateCodeBehindClass(className, ns);
 
        // Format via Roslyn (handles file-scoped namespaces, indentation, etc.)
        content = await _roslynCodeActionHelpers.GetFormattedNewFileContentsAsync(
            documentContext.Snapshot.Project,
            nestedFileUri,
            content,
            cancellationToken).ConfigureAwait(false);
 
        return content;
    }
 
    private static string GenerateCodeBehindClass(string className, string namespaceName)
    {
        using var _ = StringBuilderPool.GetPooledObject(out var builder);
 
        builder.AppendLine($"namespace {namespaceName}");
        builder.AppendLine("{");
        builder.AppendLine($"public partial class {className}");
        builder.AppendLine("{");
        builder.AppendLine("}");
        builder.AppendLine("}");
 
        return builder.ToString();
    }
}