File: CodeActions\Razor\PromoteUsingCodeActionResolver.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.CodeAnalysis.Razor.Workspaces\Microsoft.CodeAnalysis.Razor.Workspaces.csproj (Microsoft.CodeAnalysis.Razor.Workspaces)
// 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.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.Razor.CodeActions.Models;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Utilities;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.Razor.CodeActions;
 
internal class PromoteUsingCodeActionResolver(IFileSystem fileSystem) : IRazorCodeActionResolver
{
    private readonly IFileSystem _fileSystem = fileSystem;
 
    public string Action => LanguageServerConstants.CodeActions.PromoteUsingDirective;
 
    public async Task<WorkspaceEdit?> ResolveAsync(DocumentContext documentContext, JsonElement data, RazorFormattingOptions options, CancellationToken cancellationToken)
    {
        var actionParams = data.Deserialize<PromoteToUsingCodeActionParams>();
        if (actionParams is null)
        {
            return null;
        }
 
        var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
 
        var importsFileName = PromoteUsingCodeActionProvider.GetImportsFileName(documentContext.Snapshot.FileKind);
 
        var file = FilePathNormalizer.Normalize(documentContext.Uri.GetAbsoluteOrUNCPath());
        var folder = Path.GetDirectoryName(file).AssumeNotNull();
        var importsFile = Path.GetFullPath(Path.Combine(folder, "..", importsFileName));
        var importFileUri = new DocumentUri(LspFactory.CreateFilePathUri(importsFile));
 
        using var edits = new PooledArrayBuilder<SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>>();
 
        var textToInsert = sourceText.ToString(TextSpan.FromBounds(actionParams.UsingStart, actionParams.UsingEnd));
        var insertLocation = new LinePosition(0, 0);
        if (!_fileSystem.FileExists(importsFile))
        {
            edits.Add(new CreateFile() { DocumentUri = importFileUri });
        }
        else
        {
            // TODO: Update IFileSystem.ReadFile(...) to return a SourceText without reading a huge string.
            var st = SourceText.From(_fileSystem.ReadFile(importsFile));
            var lastLine = st.Lines[^1];
            insertLocation = new LinePosition(lastLine.LineNumber, 0);
            if (lastLine.GetFirstNonWhitespaceOffset() is { } nonWhiteSpaceOffset)
            {
                // Last line isn't blank, so add a newline, and insert at the end
                textToInsert = Environment.NewLine + textToInsert;
                insertLocation = new LinePosition(insertLocation.Line, lastLine.SpanIncludingLineBreak.Length);
            }
        }
 
        edits.Add(new TextDocumentEdit
        {
            TextDocument = new OptionalVersionedTextDocumentIdentifier() { DocumentUri = importFileUri },
            Edits = [LspFactory.CreateTextEdit(insertLocation, textToInsert)]
        });
 
        var removeRange = sourceText.GetRange(actionParams.RemoveStart, actionParams.RemoveEnd);
 
        edits.Add(new TextDocumentEdit
        {
            TextDocument = new OptionalVersionedTextDocumentIdentifier() { DocumentUri = new(documentContext.Uri) },
            Edits = [LspFactory.CreateTextEdit(removeRange, string.Empty)]
        });
 
        return new WorkspaceEdit
        {
            DocumentChanges = edits.ToArray()
        };
    }
}