File: Rename\RemoteRenameService.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.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.Rename;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text;
using Microsoft.NET.Sdk.Razor.SourceGenerators;
using static Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse<Roslyn.LanguageServer.Protocol.WorkspaceEdit?>;
using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers;
 
namespace Microsoft.CodeAnalysis.Remote.Razor;
 
internal sealed class RemoteRenameService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteRenameService
{
    internal sealed class Factory : FactoryBase<IRemoteRenameService>
    {
        protected override IRemoteRenameService CreateService(in ServiceArgs args)
            => new RemoteRenameService(in args);
    }
 
    private readonly IRenameService _renameService = args.ExportProvider.GetExportedValue<IRenameService>();
    private readonly IRazorEditService _razorEditService = args.ExportProvider.GetExportedValue<IRazorEditService>();
 
    public ValueTask<RemoteResponse<WorkspaceEdit?>> GetRenameEditAsync(
        JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
        JsonSerializableDocumentId documentId,
        Position position,
        string newName,
        CancellationToken cancellationToken)
        => RunServiceAsync(
            solutionInfo,
            documentId,
            context => GetRenameEditAsync(context, position, newName, cancellationToken),
            cancellationToken);
 
    private async ValueTask<RemoteResponse<WorkspaceEdit?>> GetRenameEditAsync(
        RemoteDocumentContext context,
        Position position,
        string newName,
        CancellationToken cancellationToken)
    {
        var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
 
        var hostDocumentIndex = codeDocument.Source.Text.GetRequiredAbsoluteIndex(position);
        hostDocumentIndex = codeDocument.AdjustPositionForComponentEndTag(hostDocumentIndex);
 
        var positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex, preferCSharpOverHtml: true);
 
        var generatedDocument = await context.Snapshot
            .GetGeneratedDocumentAsync(cancellationToken)
            .ConfigureAwait(false);
 
        var razorEdit = await _renameService
            .TryGetRazorRenameEditsAsync(context, positionInfo, newName, context.GetSolutionQueryOperations(), cancellationToken)
            .ConfigureAwait(false);
 
        if (razorEdit.Edit is null && positionInfo.LanguageKind != CodeAnalysis.Razor.Protocol.RazorLanguageKind.CSharp)
        {
            return CallHtml;
        }
 
        if (razorEdit.Edit is null && !razorEdit.FallbackToCSharp)
        {
            return NoFurtherHandling;
        }
 
        var csharpEdit = await ExternalHandlers.Rename
            .GetRenameEditAsync(generatedDocument, positionInfo.Position.ToLinePosition(), newName, cancellationToken)
            .ConfigureAwait(false);
 
        if (csharpEdit is null)
        {
            return NoFurtherHandling;
        }
 
        await _razorEditService.MapWorkspaceEditAsync(context.Snapshot, csharpEdit, cancellationToken).ConfigureAwait(false);
 
        return Results(csharpEdit.Concat(razorEdit.Edit));
    }
 
    public ValueTask<WorkspaceEdit?> GetFileRenameEditAsync(
        JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
        RenameFilesParams fileRenameRequest,
        CancellationToken cancellationToken)
        => RunServiceAsync(
            solutionInfo,
            context => GetFileRenameEditAsync(context, fileRenameRequest, cancellationToken),
            cancellationToken);
 
    private async ValueTask<WorkspaceEdit?> GetFileRenameEditAsync(Solution solution, RenameFilesParams fileRenameRequest, CancellationToken cancellationToken)
    {
        var response = new WorkspaceEdit();
 
        foreach (var file in fileRenameRequest.Files)
        {
            var newFilePath = file.NewUri.GetAbsoluteOrUNCPath();
 
            // We know we won't get called unless the OldUri is a .razor document, but we are responsible to confirm that
            // the new Uri is still a .razor document.
            if (!FileUtilities.IsRazorComponentFilePath(newFilePath, PathUtilities.OSSpecificPathComparison))
            {
                continue;
            }
 
            var newFileName = Path.GetFileNameWithoutExtension(newFilePath);
 
            // We also need to make sure OldUri is a document we know about
            if (!solution.TryGetRazorDocument(file.OldUri.GetRequiredParsedUri(), out var oldDoc))
            {
                continue;
            }
 
            // Make sure that the filename itself is actually changing (ie, we don't care about file moves)
            if (newFileName == Path.GetFileNameWithoutExtension(oldDoc.FilePath))
            {
                continue;
            }
 
            Logger.LogDebug($"Rename for Razor document from {oldDoc.FilePath} to {newFileName}.");
 
            var documentContext = CreateRazorDocumentContext(solution, oldDoc.Id);
            if (documentContext is null)
            {
                continue;
            }
 
            var documentEdit = await GetEditsAsync(documentContext, newFileName, cancellationToken).ConfigureAwait(false);
            response = response.Concat(documentEdit);
        }
 
        if (response.DocumentChanges is null)
        {
            return null;
        }
 
        return response;
    }
 
    private async Task<WorkspaceEdit?> GetEditsAsync(RemoteDocumentContext context, string newFileName, CancellationToken cancellationToken)
    {
        if (!context.Snapshot.FileKind.IsComponent())
        {
            return null;
        }
 
        var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync(cancellationToken).ConfigureAwait(false);
        var text = await generatedDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
        var tree = await generatedDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var declaration = tree.AssumeNotNull().DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault();
        if (declaration is null)
        {
            return null;
        }
 
        var position = text.GetLinePosition(declaration.Identifier.SpanStart);
 
        string newComponentName;
        using (var _ = StringBuilderPool.GetPooledObject(out var builder))
        {
            RazorSourceGenerator.BuildIdentifierFromPath(builder, newFileName);
            newComponentName = builder.ToString();
        }
 
        var csharpEdit = await ExternalHandlers.Rename
            .GetRenameEditAsync(generatedDocument, position, newComponentName, cancellationToken)
            .ConfigureAwait(false);
 
        if (csharpEdit is null)
        {
            return null;
        }
 
        await _razorEditService.MapWorkspaceEditAsync(context.Snapshot, csharpEdit, cancellationToken).ConfigureAwait(false);
 
        _renameService.TryGetRazorFileRenameEdit(context, newFileName, out var razorEdit);
 
        return csharpEdit.Concat(razorEdit);
    }
}