File: Completion\Delegation\TextEditResponseRewriter.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 Microsoft.AspNetCore.Razor.Language;
 
namespace Microsoft.CodeAnalysis.Razor.Completion.Delegation;
 
internal class TextEditResponseRewriter : IDelegatedCSharpCompletionResponseRewriter
{
    public RazorVSInternalCompletionList Rewrite(
        RazorVSInternalCompletionList completionList,
        RazorCodeDocument codeDocument,
        int hostDocumentIndex,
        Position projectedPosition,
        RazorCompletionOptions completionOptions)
    {
        var sourceText = codeDocument.Source.Text;
 
        var hostDocumentPosition = sourceText.GetPosition(hostDocumentIndex);
        completionList = TranslateTextEdits(hostDocumentPosition, projectedPosition, completionList);
 
        if (completionList.ItemDefaults?.EditRange is { } editRange)
        {
            if (editRange.TryGetFirst(out var range))
            {
                completionList.ItemDefaults.EditRange = TranslateRange(hostDocumentPosition, projectedPosition, range);
            }
            else if (editRange.TryGetSecond(out var insertReplaceRange))
            {
                insertReplaceRange.Insert = TranslateRange(hostDocumentPosition, projectedPosition, insertReplaceRange.Insert);
                insertReplaceRange.Replace = TranslateRange(hostDocumentPosition, projectedPosition, insertReplaceRange.Replace);
            }
        }
 
        return completionList;
    }
 
    private static RazorVSInternalCompletionList TranslateTextEdits(
        Position hostDocumentPosition,
        Position projectedPosition,
        RazorVSInternalCompletionList completionList)
    {
        // The TextEdit positions returned to us from the C#/HTML language servers are positions correlating to the virtual document.
        // We need to translate these positions to apply to the Razor document instead. Performance is a big concern here, so we want to
        // make the logic as simple as possible, i.e. no asynchronous calls.
        // The current logic takes the approach of assuming the original request's position (Razor doc) correlates directly to the positions
        // returned by the C#/HTML language servers. We use this assumption (+ math) to map from the virtual (projected) doc positions ->
        // Razor doc positions.
 
        foreach (var item in completionList.Items)
        {
            if (item.TextEdit is { } edit)
            {
                if (edit.TryGetFirst(out var textEdit))
                {
                    var translatedRange = TranslateRange(hostDocumentPosition, projectedPosition, textEdit.Range);
                    textEdit.Range = translatedRange;
                }
                else if (edit.TryGetSecond(out var insertReplaceEdit))
                {
                    insertReplaceEdit.Insert = TranslateRange(hostDocumentPosition, projectedPosition, insertReplaceEdit.Insert);
                    insertReplaceEdit.Replace = TranslateRange(hostDocumentPosition, projectedPosition, insertReplaceEdit.Replace);
                }
            }
            else if (item.AdditionalTextEdits is not null)
            {
                // Additional text edits should typically only be provided at resolve time. We don't support them in the normal completion flow.
                item.AdditionalTextEdits = null;
            }
        }
 
        return completionList;
    }
 
    private static LspRange TranslateRange(Position hostDocumentPosition, Position projectedPosition, LspRange textEditRange)
    {
        var offset = projectedPosition.Character - hostDocumentPosition.Character;
 
        var translatedStartPosition = TranslatePosition(offset, hostDocumentPosition, textEditRange.Start);
        var translatedEndPosition = TranslatePosition(offset, hostDocumentPosition, textEditRange.End);
 
        return LspFactory.CreateRange(translatedStartPosition, translatedEndPosition);
 
        static Position TranslatePosition(int offset, Position hostDocumentPosition, Position editPosition)
        {
            var translatedCharacter = editPosition.Character - offset;
 
            // Note: If this completion handler ever expands to deal with multi-line TextEdits, this logic will likely need to change since
            // it assumes we're only dealing with single-line TextEdits.
            return LspFactory.CreatePosition(hostDocumentPosition.Line, translatedCharacter);
        }
    }
}