// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.DocumentHighlighting;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.NavigateTo;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.SpellCheck;
using Microsoft.CodeAnalysis.Tags;
using Microsoft.CodeAnalysis.Text;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Text.Adornments;
using Roslyn.Utilities;
using Logger = Microsoft.CodeAnalysis.Internal.Log.Logger;
using LSP = Roslyn.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.LanguageServer;

internal static partial class ProtocolConversions
{
    private const string CSharpMarkdownLanguageName = "csharp";
    private const string VisualBasicMarkdownLanguageName = "vb";
    private const string BlockCodeFence = "```";
    private const string InlineCodeFence = "`";

    private static readonly char[] s_dirSeparators = [PathUtilities.DirectorySeparatorChar, PathUtilities.AltDirectorySeparatorChar];

    private static readonly Regex s_markdownEscapeRegex = new(@"([\\`\*_\{\}\[\]\(\)#+\-\.!<>])", RegexOptions.Compiled);

    // NOTE: While the spec allows it, don't use Function and Method, as both VS and VS Code display them the same
    // way which can confuse users

    /// <summary>
    /// Mapping from tags to lsp completion item kinds.  The value lists the potential lsp kinds from
    /// least-preferred to most preferred.  More preferred kinds will be chosen if the client states they support
    /// it.  This mapping allows values including extensions to the kinds defined by VS (but not in the core LSP
    /// spec).
    /// </summary>
    public static readonly ImmutableDictionary<string, ImmutableArray<LSP.CompletionItemKind>> RoslynTagToCompletionItemKinds = new Dictionary<string, ImmutableArray<LSP.CompletionItemKind>>()
    {
        { WellKnownTags.Public, ImmutableArray.Create(LSP.CompletionItemKind.Keyword) },
        { WellKnownTags.Protected, ImmutableArray.Create(LSP.CompletionItemKind.Keyword) },
        { WellKnownTags.Private, ImmutableArray.Create(LSP.CompletionItemKind.Keyword) },
        { WellKnownTags.Internal, ImmutableArray.Create(LSP.CompletionItemKind.Keyword) },
        { WellKnownTags.File, ImmutableArray.Create(LSP.CompletionItemKind.File) },
        { WellKnownTags.Project, ImmutableArray.Create(LSP.CompletionItemKind.File) },
        { WellKnownTags.Folder, ImmutableArray.Create(LSP.CompletionItemKind.Folder) },
        { WellKnownTags.Assembly, ImmutableArray.Create(LSP.CompletionItemKind.File) },
        { WellKnownTags.Class, ImmutableArray.Create(LSP.CompletionItemKind.Class) },
        { WellKnownTags.Constant, ImmutableArray.Create(LSP.CompletionItemKind.Constant) },
        { WellKnownTags.Delegate, ImmutableArray.Create(LSP.CompletionItemKind.Class, LSP.CompletionItemKind.Delegate) },
        { WellKnownTags.Enum, ImmutableArray.Create(LSP.CompletionItemKind.Enum) },
        { WellKnownTags.EnumMember, ImmutableArray.Create(LSP.CompletionItemKind.EnumMember) },
        { WellKnownTags.Event, ImmutableArray.Create(LSP.CompletionItemKind.Event) },
        { WellKnownTags.ExtensionMethod, ImmutableArray.Create(LSP.CompletionItemKind.Method, LSP.CompletionItemKind.ExtensionMethod) },
        { WellKnownTags.Field, ImmutableArray.Create(LSP.CompletionItemKind.Field) },
        { WellKnownTags.Interface, ImmutableArray.Create(LSP.CompletionItemKind.Interface) },
        { WellKnownTags.Intrinsic, ImmutableArray.Create(LSP.CompletionItemKind.Text) },
        { WellKnownTags.Keyword, ImmutableArray.Create(LSP.CompletionItemKind.Keyword) },
        { WellKnownTags.Label, ImmutableArray.Create(LSP.CompletionItemKind.Text) },
        { WellKnownTags.Local, ImmutableArray.Create(LSP.CompletionItemKind.Variable) },
        { WellKnownTags.Namespace, ImmutableArray.Create(LSP.CompletionItemKind.Module, LSP.CompletionItemKind.Namespace) },
        { WellKnownTags.Method, ImmutableArray.Create(LSP.CompletionItemKind.Method) },
        { WellKnownTags.Module, ImmutableArray.Create(LSP.CompletionItemKind.Module) },
        { WellKnownTags.Operator, ImmutableArray.Create(LSP.CompletionItemKind.Operator) },
        { WellKnownTags.Parameter, ImmutableArray.Create(LSP.CompletionItemKind.Variable) },
        { WellKnownTags.Property, ImmutableArray.Create(LSP.CompletionItemKind.Property) },
        { WellKnownTags.RangeVariable, ImmutableArray.Create(LSP.CompletionItemKind.Variable) },
        { WellKnownTags.Reference, ImmutableArray.Create(LSP.CompletionItemKind.Reference) },
        { WellKnownTags.Structure, ImmutableArray.Create(LSP.CompletionItemKind.Struct) },
        { WellKnownTags.TypeParameter, ImmutableArray.Create(LSP.CompletionItemKind.TypeParameter) },
        { WellKnownTags.Snippet, ImmutableArray.Create(LSP.CompletionItemKind.Snippet) },
        { WellKnownTags.Error, ImmutableArray.Create(LSP.CompletionItemKind.Text) },
        { WellKnownTags.Warning, ImmutableArray.Create(LSP.CompletionItemKind.Text) },
        { WellKnownTags.StatusInformation, ImmutableArray.Create(LSP.CompletionItemKind.Text) },
        { WellKnownTags.AddReference, ImmutableArray.Create(LSP.CompletionItemKind.Text) },
        { WellKnownTags.NuGet, ImmutableArray.Create(LSP.CompletionItemKind.Text) }
    }.ToImmutableDictionary();

    /// <summary>
    /// Mapping from tags to LSP completion item tags.  The value lists the potential LSP tags from
    /// least-preferred to most preferred.  More preferred kinds will be chosen if the client states they support
    /// it.  This mapping allows values including extensions to the kinds defined by VS (but not in the core LSP
    /// spec).
    /// </summary>
    public static readonly ImmutableDictionary<string, ImmutableArray<LSP.CompletionItemTag>> RoslynTagToCompletionItemTags = new Dictionary<string, ImmutableArray<LSP.CompletionItemTag>>()
    {
        { WellKnownTags.Deprecated, ImmutableArray.Create(LSP.CompletionItemTag.Deprecated) },
    }.ToImmutableDictionary();

    public static JsonSerializerOptions AddLspSerializerOptions(this JsonSerializerOptions options)
    {
        LSP.VSInternalExtensionUtilities.AddVSInternalExtensionConverters(options);
        options.Converters.Add(new LSP.NaturalObjectConverter());
        options.Converters.Add(new DocumentUriConverter());
        return options;
    }

    /// <summary>
    /// Options that know how to serialize / deserialize basic LSP types.
    /// Useful when there are particular fields that are not serialized or deserialized by normal request handling (for example
    /// deserializing a field that is typed as object instead of a concrete type).
    /// </summary>
    public static JsonSerializerOptions LspJsonSerializerOptions = new JsonSerializerOptions().AddLspSerializerOptions();

    // TO-DO: More LSP.CompletionTriggerKind mappings are required to properly map to Roslyn CompletionTriggerKinds.
    // https://dev.azure.com/devdiv/DevDiv/_workitems/edit/1178726
    public static async Task<Completion.CompletionTrigger> LSPToRoslynCompletionTriggerAsync(
        LSP.CompletionContext? context,
        Document document,
        int position,
        CancellationToken cancellationToken)
    {
        if (context is null)
        {
            // Some LSP clients don't support sending extra context, so all we can do is invoke
            return Completion.CompletionTrigger.Invoke;
        }
        else if (context.TriggerKind is LSP.CompletionTriggerKind.Invoked or LSP.CompletionTriggerKind.TriggerForIncompleteCompletions)
        {
            if (context is not LSP.VSInternalCompletionContext vsCompletionContext)
            {
                return Completion.CompletionTrigger.Invoke;
            }

            switch (vsCompletionContext.InvokeKind)
            {
                case LSP.VSInternalCompletionInvokeKind.Explicit:
                    return Completion.CompletionTrigger.Invoke;

                case LSP.VSInternalCompletionInvokeKind.Typing:
                    var insertionChar = await GetInsertionCharacterAsync(document, position, cancellationToken).ConfigureAwait(false);
                    return Completion.CompletionTrigger.CreateInsertionTrigger(insertionChar);

                case LSP.VSInternalCompletionInvokeKind.Deletion:
                    Contract.ThrowIfNull(context.TriggerCharacter);
                    Contract.ThrowIfFalse(char.TryParse(context.TriggerCharacter, out var triggerChar));
                    return Completion.CompletionTrigger.CreateDeletionTrigger(triggerChar);

                default:
                    // LSP added an InvokeKind that we need to support.
                    Logger.Log(FunctionId.LSPCompletion_MissingLSPCompletionInvokeKind);
                    return Completion.CompletionTrigger.Invoke;
            }
        }
        else if (context.TriggerKind is LSP.CompletionTriggerKind.TriggerCharacter)
        {
            Contract.ThrowIfNull(context.TriggerCharacter);
            Contract.ThrowIfFalse(char.TryParse(context.TriggerCharacter, out var triggerChar));
            return Completion.CompletionTrigger.CreateInsertionTrigger(triggerChar);
        }
        else
        {
            // LSP added a TriggerKind that we need to support.
            Logger.Log(FunctionId.LSPCompletion_MissingLSPCompletionTriggerKind);
            return Completion.CompletionTrigger.Invoke;
        }

        // Local functions
        static async Task<char> GetInsertionCharacterAsync(Document document, int position, CancellationToken cancellationToken)
        {
            var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);

            // We use 'position - 1' here since we want to find the character that was just inserted.
            Contract.ThrowIfTrue(position < 1);
            var triggerCharacter = text[position - 1];
            return triggerCharacter;
        }
    }

    public static bool IsSourceGeneratedScheme(string scheme)
    {
        return scheme == SourceGeneratedDocumentUri.Scheme;
    }

    /// <summary>
    /// Converts an absolute local file path or an absolute URL string to <see cref="Uri"/>.
    /// </summary>
    /// <exception cref="UriFormatException">
    /// The <paramref name="absolutePath"/> can't be represented as <see cref="Uri"/>.
    /// For example, UNC paths with invalid characters in server name.
    /// </exception>
    public static Uri CreateAbsoluteUri(string absolutePath)
    {
        var uriString = IsAscii(absolutePath) ? absolutePath : GetAbsoluteUriString(absolutePath);
        try
        {
#pragma warning disable RS0030 // Do not use banned APIs
            return new(uriString, UriKind.Absolute);
#pragma warning restore

        }
        catch (UriFormatException e)
        {
            // The standard URI format exception does not include the failing path, however
            // in pretty much all cases we need to know the URI string (and original string) in order to fix the issue.
            throw new UriFormatException($"Failed create URI from '{uriString}'; original string: '{absolutePath}'", e);
        }
    }

    /// <summary>
    /// Converts an absolute local file path or an absolute URL string to <see cref="DocumentUri"/>.
    /// For use with callers (generally LSP) that require <see cref="DocumentUri"/>
    /// </summary>
    /// <remarks>
    /// Unlike <see cref="CreateAbsoluteUri"/>, this method gracefully handles paths that
    /// <see cref="Uri"/> cannot parse (e.g., UNC paths with <c>$</c> in the server name).
    /// In such cases, it falls back to manually constructing the URI string, which
    /// <see cref="DocumentUri"/> stores without requiring <see cref="Uri"/> parsing.
    /// </remarks>
    public static DocumentUri CreateAbsoluteDocumentUri(string absolutePath)
    {
        try
        {
            return new(CreateAbsoluteUri(absolutePath));
        }
        catch (UriFormatException)
        {
            // System.Uri can't handle certain valid paths (e.g. UNC paths with $ in server name).
            // Fall back to constructing the URI string manually.
            return new(GetAbsoluteUriString(absolutePath));
        }
    }

    internal static DocumentUri CreateRelativePatternBaseUri(string path)
    {
        // According to VSCode LSP RelativePattern spec,
        // found at https://github.com/microsoft/vscode/blob/9e1974682eb84eebb073d4ae775bad1738c281f6/src/vscode-dts/vscode.d.ts#L2226
        // the baseUri should not end in a trailing separator, nor should it
        // have any relative segmeents (., ..)
        if (path[^1] == System.IO.Path.DirectorySeparatorChar)
        {
            path = path[..^1];
        }

        Debug.Assert(!path.Split(System.IO.Path.DirectorySeparatorChar).Any(p => p == "." || p == ".."));

        return CreateAbsoluteDocumentUri(path);
    }

    // Implements workaround for https://github.com/dotnet/runtime/issues/89538:
    internal static string GetAbsoluteUriString(string absolutePath)
    {
        if (!PathUtilities.IsAbsolute(absolutePath))
        {
            return absolutePath;
        }

        var parts = absolutePath.Split(s_dirSeparators);

        if (PathUtilities.IsUnixLikePlatform)
        {
            // Unix path: first part is empty, all parts should be escaped
            return "file://" + string.Join("/", parts.Select(EscapeUriPart));
        }

        if (parts is ["", "", var serverName, ..])
        {
            // UNC path: first non-empty part is server name and shouldn't be escaped
            return "file://" + serverName + "/" + string.Join("/", parts.Skip(3).Select(EscapeUriPart));
        }

        // Drive-rooted path: first part is "C:" and shouldn't be escaped
        return "file:///" + parts[0] + "/" + string.Join("/", parts.Skip(1).Select(EscapeUriPart));

#pragma warning disable SYSLIB0013 // Type or member is obsolete
        static string EscapeUriPart(string stringToEscape)
            => Uri.EscapeUriString(stringToEscape).Replace("#", "%23");
#pragma warning restore
    }

    private static bool IsAscii(char c)
        => (uint)c <= '\x007f';

    private static bool IsAscii(string filePath)
    {
        for (var i = 0; i < filePath.Length; i++)
        {
            if (!IsAscii(filePath[i]))
            {
                return false;
            }
        }

        return true;
    }

    public static LSP.TextDocumentPositionParams PositionToTextDocumentPositionParams(int position, SourceText text, Document document)
    {
        return new LSP.TextDocumentPositionParams()
        {
            TextDocument = DocumentToTextDocumentIdentifier(document),
            Position = LinePositionToPosition(text.Lines.GetLinePosition(position))
        };
    }

    public static LSP.TextDocumentIdentifier DocumentToTextDocumentIdentifier(TextDocument document)
        => new() { DocumentUri = document.GetURI() };

    public static LSP.VersionedTextDocumentIdentifier DocumentToVersionedTextDocumentIdentifier(Document document)
        => new() { DocumentUri = document.GetURI() };

    public static LinePosition PositionToLinePosition(LSP.Position position)
        => new(position.Line, position.Character);

    public static LinePositionSpan RangeToLinePositionSpan(LSP.Range range)
        => new(PositionToLinePosition(range.Start), PositionToLinePosition(range.End));

    public static TextSpan RangeToTextSpan(LSP.Range range, SourceText text)
    {
        var linePositionSpan = RangeToLinePositionSpan(range);

        linePositionSpan = new LinePositionSpan(
            ClampPositionToLineEnd(linePositionSpan.Start, text),
            ClampPositionToLineEnd(linePositionSpan.End, text));

        // Handle the specific case where the end position is exactly one line beyond the document bounds
        // and the end character is 0 (start of the non-existent next line).
        // This can happen when deleting the last line, where LSP clients are allowed (by the spec) to
        // send an end position referencing the start of the next line (which doesn't exist).
        if (text.Lines.Count > 0 &&
            linePositionSpan.End.Line == text.Lines.Count &&
            linePositionSpan.End.Character == 0)
        {
            // Clamp the end position to the end of the last line
            var lastLine = text.Lines[text.Lines.Count - 1];
            var clampedEnd = new LinePosition(text.Lines.Count - 1, lastLine.End - lastLine.Start);
            linePositionSpan = new LinePositionSpan(linePositionSpan.Start, clampedEnd);
        }

        try
        {
            try
            {
                return text.Lines.GetTextSpan(linePositionSpan);
            }
            catch (ArgumentException ex)
            {
                // Create a custom error for this so we can examine the data we're getting.
                throw new ArgumentException($"Range={RangeToString(range)}. text.Length={text.Length}. text.Lines.Count={text.Lines.Count}", ex);
            }
        }
        // Temporary exception reporting to investigate https://github.com/dotnet/roslyn/issues/66258.
        catch (Exception e) when (FatalError.ReportAndPropagate(e))
        {
            throw;
        }

        static string RangeToString(LSP.Range range)
            => $"{{ Start={PositionToString(range.Start)}, End={PositionToString(range.End)} }}";

        static string PositionToString(LSP.Position position)
            => $"{{ Line={position.Line}, Character={position.Character} }}";

        static LinePosition ClampPositionToLineEnd(LinePosition position, SourceText text)
        {
            if (position.Line < 0 || position.Line >= text.Lines.Count)
                return position;

            var line = text.Lines[position.Line];
            var lineLength = line.End - line.Start;
            return position.Character > lineLength
                ? new LinePosition(position.Line, lineLength)
                : position;
        }
    }

    public static LSP.TextEdit TextChangeToTextEdit(TextChange textChange, SourceText oldText)
    {
        Contract.ThrowIfNull(textChange.NewText);
        return new LSP.TextEdit
        {
            NewText = textChange.NewText,
            Range = TextSpanToRange(textChange.Span, oldText)
        };
    }

    public static TextChange TextEditToTextChange(LSP.TextEdit edit, SourceText oldText)
        => new(RangeToTextSpan(edit.Range, oldText), edit.NewText);

    public static TextChange ContentChangeEventToTextChange(LSP.TextDocumentContentChangePartial changeEvent, SourceText text)
        => new(RangeToTextSpan(changeEvent.Range, text), changeEvent.Text);

    public static LSP.Position LinePositionToPosition(LinePosition linePosition)
        => new() { Line = linePosition.Line, Character = linePosition.Character };

    public static LSP.Range LinePositionToRange(LinePositionSpan linePositionSpan)
        => new() { Start = LinePositionToPosition(linePositionSpan.Start), End = LinePositionToPosition(linePositionSpan.End) };

    public static LSP.Range TextSpanToRange(TextSpan textSpan, SourceText text)
    {
        var linePosSpan = text.Lines.GetLinePositionSpan(textSpan);
        return LinePositionToRange(linePosSpan);
    }

    public static Task<LSP.Location?> DocumentSpanToLocationAsync(DocumentSpan documentSpan, CancellationToken cancellationToken)
        => TextSpanToLocationAsync(documentSpan.Document, documentSpan.SourceSpan, isStale: false, cancellationToken);

    public static async Task<LSP.VSInternalLocation?> DocumentSpanToLocationWithTextAsync(
        DocumentSpan documentSpan, ClassifiedTextElement text, CancellationToken cancellationToken)
    {
        var location = await TextSpanToLocationAsync(
            documentSpan.Document, documentSpan.SourceSpan, isStale: false, cancellationToken).ConfigureAwait(false);

        return location == null ? null : new LSP.VSInternalLocation
        {
            DocumentUri = location.DocumentUri,
            Range = location.Range,
            Text = text
        };
    }

    /// <summary>
    /// Compute all the <see cref="LSP.TextDocumentEdit"/> for the input list of changed documents.
    /// Additionally maps the locations of the changed documents if necessary.
    /// </summary>
    public static async Task<LSP.TextDocumentEdit[]> ChangedDocumentsToTextDocumentEditsAsync(Solution newSolution, Solution oldSolution, CancellationToken cancellationToken)
    {
        var solutionChanges = newSolution.GetChanges(oldSolution);

        var changedDocuments = solutionChanges
            .GetProjectChanges()
            .SelectMany(p => p.GetChangedDocuments(onlyGetDocumentsWithTextChanges: true))
            .GroupBy(docId => newSolution.GetRequiredDocument(docId).FilePath, StringComparer.OrdinalIgnoreCase).Select(group => group.First());

        var textDiffService = newSolution.Services.GetRequiredService<IDocumentTextDifferencingService>();

        using var _ = ArrayBuilder<(DocumentUri Uri, LSP.TextEdit TextEdit)>.GetInstance(out var uriToTextEdits);

        foreach (var docId in changedDocuments)
        {
            var newDocument = newSolution.GetRequiredDocument(docId);
            var oldDocument = oldSolution.GetRequiredDocument(docId);

            var oldText = await oldDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false);

            ImmutableArray<TextChange> textChanges;

            // Normal documents have a unique service for calculating minimal text edits. If we used the standard 'GetTextChanges'
            // method instead, we would get a change that spans the entire document, which we ideally want to avoid.
            if (newDocument is Document newDoc && oldDocument is Document oldDoc)
            {
                Contract.ThrowIfNull(textDiffService);
                textChanges = await textDiffService.GetTextChangesAsync(oldDoc, newDoc, cancellationToken).ConfigureAwait(false);
            }
            else
            {
                var newText = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
                textChanges = [.. newText.GetTextChanges(oldText)];
            }

            // Map all the text changes' spans for this document.
            var mappedResults = await SpanMappingHelper.TryGetMappedSpanResultAsync(oldDocument, [.. textChanges.Select(tc => tc.Span)], cancellationToken).ConfigureAwait(false);
            if (mappedResults == null)
            {
                // There's no span mapping available, just create text edits from the original text changes.
                foreach (var textChange in textChanges)
                {
                    uriToTextEdits.Add((oldDocument.GetURI(), TextChangeToTextEdit(textChange, oldText)));
                }
            }
            else
            {
                // We have mapping results, so create text edits from the mapped text change spans.
                for (var i = 0; i < textChanges.Length; i++)
                {
                    var mappedSpan = mappedResults.Value[i];
                    var textChange = textChanges[i];
                    if (!mappedSpan.IsDefault)
                    {
                        uriToTextEdits.Add((CreateAbsoluteDocumentUri(mappedSpan.FilePath), new LSP.TextEdit
                        {
                            Range = MappedSpanResultToRange(mappedSpan),
                            NewText = textChange.NewText ?? string.Empty
                        }));
                    }
                }
            }
        }

        // Now process source generated documents that might have changed, via the source generated document mapping service
        // We have to ensure the old solution has run the generators so the mapper has something to compare to, but we only do
        // it for FrozenSourceGeneratedDocumentStates, so only documents that the rename engine thought were worthy of touching,
        // which in practical terms at time of writing this comment, means Razor.
        foreach (var (docId, state) in solutionChanges.NewSolution.CompilationState.FrozenSourceGeneratedDocumentStates.States)
        {
            var document = await solutionChanges.OldSolution.GetRequiredDocumentAsync(docId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false);
            Contract.ThrowIfFalse(document.IsRazorSourceGeneratedDocument());
        }

        var sourceGeneratedDocumentMappingService = newSolution.Services.GetService<ISourceGeneratedDocumentSpanMappingService>();
        foreach (var docId in solutionChanges.GetExplicitlyChangedSourceGeneratedDocuments())
        {
            var oldDocument = solutionChanges.OldSolution.GetRequiredSourceGeneratedDocumentForAlreadyGeneratedId(docId);
            var newDocument = solutionChanges.NewSolution.GetRequiredSourceGeneratedDocumentForAlreadyGeneratedId(docId);

            if (sourceGeneratedDocumentMappingService?.CanMapSpans(oldDocument) == true)
            {
                var mappedTextChanges = await sourceGeneratedDocumentMappingService.GetMappedTextChangesAsync(oldDocument, newDocument, cancellationToken).ConfigureAwait(false);
                foreach (var (filePath, textChange) in mappedTextChanges)
                {
                    var mappedDocId = oldSolution.GetDocumentIdsWithFilePath(filePath).FirstOrDefault(d => d.ProjectId == oldDocument.Id.ProjectId);
                    // Can't map to an edit in an unknown document
                    if (mappedDocId is null)
                        continue;

                    var mappedDoc = oldSolution.GetRequiredTextDocument(mappedDocId);
                    var mappedText = await mappedDoc.GetTextAsync(cancellationToken).ConfigureAwait(false);
                    uriToTextEdits.Add((CreateAbsoluteDocumentUri(filePath), new LSP.TextEdit
                    {
                        Range = TextSpanToRange(textChange.Span, mappedText),
                        NewText = textChange.NewText ?? string.Empty
                    }));
                }
            }
            else
            {
                // There's no span mapping available, just create text edits from the original text changes.
                var oldText = await oldDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
                var textChanges = await textDiffService.GetTextChangesAsync(oldDocument, newDocument, cancellationToken).ConfigureAwait(false);
                foreach (var textChange in textChanges)
                {
                    uriToTextEdits.Add((oldDocument.GetURI(), TextChangeToTextEdit(textChange, oldText)));
                }
            }
        }

        var documentEdits = uriToTextEdits.GroupBy(uriAndEdit => uriAndEdit.Uri, uriAndEdit => new LSP.SumType<LSP.TextEdit, LSP.AnnotatedTextEdit>(uriAndEdit.TextEdit), (uri, edits) => new LSP.TextDocumentEdit
        {
            TextDocument = new LSP.OptionalVersionedTextDocumentIdentifier { DocumentUri = uri },
            Edits = [.. edits],
        }).ToArray();

        return documentEdits;
    }

    public static Task<LSP.Location?> TextSpanToLocationAsync(
        TextDocument document,
        TextSpan textSpan,
        bool isStale,
        CancellationToken cancellationToken)
    {
        return TextSpanToLocationAsync(document, textSpan, isStale, context: null, cancellationToken);
    }

    public static async Task<LSP.Location?> TextSpanToLocationAsync(
        TextDocument document,
        TextSpan textSpan,
        bool isStale,
        RequestContext? context,
        CancellationToken cancellationToken)
    {
        Debug.Assert(document.FilePath != null);

        if (document is Document d &&
            SpanMappingHelper.CanMapSpans(d))
        {
            var result = await SpanMappingHelper.TryGetMappedSpanResultAsync(d, [textSpan], cancellationToken).ConfigureAwait(false);
            if (result is not [{ IsDefault: false } mappedSpan])
            {
                // Couldn't map the span, but mapping is supported, so the mapper must not want to show include this result
                return null;
            }

            DocumentUri? uri = null;
            try
            {
                if (PathUtilities.IsAbsolute(mappedSpan.FilePath))
                    uri = CreateAbsoluteDocumentUri(mappedSpan.FilePath);
            }
            catch (UriFormatException)
            {
            }

            if (uri == null)
            {
                context?.TraceWarning($"Could not convert '{mappedSpan.FilePath}' to uri");
                return null;
            }

            return new LSP.Location
            {
                DocumentUri = uri,
                Range = MappedSpanResultToRange(mappedSpan)
            };
        }

        return await ConvertTextSpanToLocationAsync(document, textSpan, isStale, cancellationToken).ConfigureAwait(false);

        static async Task<LSP.Location> ConvertTextSpanToLocationAsync(
            TextDocument document,
            TextSpan span,
            bool isStale,
            CancellationToken cancellationToken)
        {
            var uri = document.GetURI();

            var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
            if (isStale)
            {
                // in the case of a stale item, the span may be out of bounds of the document. Cap
                // us to the end of the document as that's where we're going to navigate the user
                // to.
                span = TextSpan.FromBounds(
                    Math.Min(text.Length, span.Start),
                    Math.Min(text.Length, span.End));
            }

            return ConvertTextSpanWithTextToLocation(span, text, uri);
        }

        static LSP.Location ConvertTextSpanWithTextToLocation(TextSpan span, SourceText text, DocumentUri documentUri)
        {
            var location = new LSP.Location
            {
                DocumentUri = documentUri,
                Range = TextSpanToRange(span, text),
            };

            return location;
        }
    }

    public static LSP.CodeDescription? HelpLinkToCodeDescription(Uri? uri)
        => (uri != null) ? new LSP.CodeDescription { Href = new(uri) } : null;

    public static LSP.SymbolKind NavigateToKindToSymbolKind(string kind)
    {
        if (Enum.TryParse<LSP.SymbolKind>(kind, out var symbolKind))
        {
            return symbolKind;
        }

        // TODO - Define conversion from NavigateToItemKind to LSP Symbol kind
        switch (kind)
        {
            case NavigateToItemKind.EnumItem:
                return LSP.SymbolKind.EnumMember;
            case NavigateToItemKind.Structure:
                return LSP.SymbolKind.Struct;
            case NavigateToItemKind.Delegate:
                return LSP.SymbolKind.Function;
            default:
                return LSP.SymbolKind.Object;
        }
    }

    public static LSP.DocumentHighlightKind HighlightSpanKindToDocumentHighlightKind(HighlightSpanKind kind)
    {
        switch (kind)
        {
            case HighlightSpanKind.Reference:
                return LSP.DocumentHighlightKind.Read;
            case HighlightSpanKind.WrittenReference:
                return LSP.DocumentHighlightKind.Write;
            default:
                return LSP.DocumentHighlightKind.Text;
        }
    }

    public static LSP.VSInternalSpellCheckableRangeKind SpellCheckSpanKindToSpellCheckableRangeKind(SpellCheckKind kind)
        => kind switch
        {
            SpellCheckKind.Identifier => LSP.VSInternalSpellCheckableRangeKind.Identifier,
            SpellCheckKind.Comment => LSP.VSInternalSpellCheckableRangeKind.Comment,
            SpellCheckKind.String => LSP.VSInternalSpellCheckableRangeKind.String,
            _ => throw ExceptionUtilities.UnexpectedValue(kind),
        };

    public static Glyph SymbolKindToGlyph(LSP.SymbolKind kind)
    {
        switch (kind)
        {
            case LSP.SymbolKind.File:
                return Glyph.CSharpFile;
            case LSP.SymbolKind.Module:
                return Glyph.ModulePublic;
            case LSP.SymbolKind.Namespace:
                return Glyph.Namespace;
            case LSP.SymbolKind.Package:
                return Glyph.Assembly;
            case LSP.SymbolKind.Class:
                return Glyph.ClassPublic;
            case LSP.SymbolKind.Method:
                return Glyph.MethodPublic;
            case LSP.SymbolKind.Property:
                return Glyph.PropertyPublic;
            case LSP.SymbolKind.Field:
                return Glyph.FieldPublic;
            case LSP.SymbolKind.Constructor:
                return Glyph.MethodPublic;
            case LSP.SymbolKind.Enum:
                return Glyph.EnumPublic;
            case LSP.SymbolKind.Interface:
                return Glyph.InterfacePublic;
            case LSP.SymbolKind.Function:
                return Glyph.DelegatePublic;
            case LSP.SymbolKind.Variable:
                return Glyph.Local;
            case LSP.SymbolKind.Constant:
            case LSP.SymbolKind.Number:
                return Glyph.ConstantPublic;
            case LSP.SymbolKind.String:
            case LSP.SymbolKind.Boolean:
            case LSP.SymbolKind.Array:
            case LSP.SymbolKind.Object:
            case LSP.SymbolKind.Key:
            case LSP.SymbolKind.Null:
                return Glyph.Local;
            case LSP.SymbolKind.EnumMember:
                return Glyph.EnumMemberPublic;
            case LSP.SymbolKind.Struct:
                return Glyph.StructurePublic;
            case LSP.SymbolKind.Event:
                return Glyph.EventPublic;
            case LSP.SymbolKind.Operator:
                return Glyph.OperatorPublic;
            case LSP.SymbolKind.TypeParameter:
                return Glyph.TypeParameter;
            default:
                return Glyph.None;
        }
    }

    public static LSP.SymbolKind GlyphToSymbolKind(Glyph glyph)
    {
        // Glyph kinds have accessibility modifiers in their name, e.g. ClassPrivate.
        // Remove the accessibility modifier and try to convert to LSP symbol kind.
        var glyphString = glyph.ToString().Replace(nameof(Accessibility.Public), string.Empty)
                                          .Replace(nameof(Accessibility.Protected), string.Empty)
                                          .Replace(nameof(Accessibility.Private), string.Empty)
                                          .Replace(nameof(Accessibility.Internal), string.Empty);

        if (Enum.TryParse<LSP.SymbolKind>(glyphString, out var symbolKind))
        {
            return symbolKind;
        }

        switch (glyph)
        {
            case Glyph.Assembly:
            case Glyph.BasicProject:
            case Glyph.CSharpProject:
            case Glyph.NuGet:
                return LSP.SymbolKind.Package;
            case Glyph.BasicFile:
            case Glyph.CSharpFile:
                return LSP.SymbolKind.File;
            case Glyph.DelegatePublic:
            case Glyph.DelegateProtected:
            case Glyph.DelegatePrivate:
            case Glyph.DelegateInternal:
            case Glyph.ExtensionMethodPublic:
            case Glyph.ExtensionMethodProtected:
            case Glyph.ExtensionMethodPrivate:
            case Glyph.ExtensionMethodInternal:
                return LSP.SymbolKind.Method;
            case Glyph.Local:
            case Glyph.Parameter:
            case Glyph.RangeVariable:
            case Glyph.Reference:
                return LSP.SymbolKind.Variable;
            case Glyph.StructurePublic:
            case Glyph.StructureProtected:
            case Glyph.StructurePrivate:
            case Glyph.StructureInternal:
                return LSP.SymbolKind.Struct;
            default:
                return LSP.SymbolKind.Object;
        }
    }

    public static Glyph CompletionItemKindToGlyph(LSP.CompletionItemKind kind)
    {
        switch (kind)
        {
            case LSP.CompletionItemKind.Text:
                return Glyph.None;
            case LSP.CompletionItemKind.Method:
            case LSP.CompletionItemKind.Constructor:
            case LSP.CompletionItemKind.Function:    // We don't use Function, but map it just in case. It has the same icon as Method in VS and VS Code
                return Glyph.MethodPublic;
            case LSP.CompletionItemKind.Field:
                return Glyph.FieldPublic;
            case LSP.CompletionItemKind.Variable:
            case LSP.CompletionItemKind.Unit:
            case LSP.CompletionItemKind.Value:
                return Glyph.Local;
            case LSP.CompletionItemKind.Class:
                return Glyph.ClassPublic;
            case LSP.CompletionItemKind.Interface:
                return Glyph.InterfacePublic;
            case LSP.CompletionItemKind.Module:
                return Glyph.ModulePublic;
            case LSP.CompletionItemKind.Property:
                return Glyph.PropertyPublic;
            case LSP.CompletionItemKind.Enum:
                return Glyph.EnumPublic;
            case LSP.CompletionItemKind.Keyword:
                return Glyph.Keyword;
            case LSP.CompletionItemKind.Snippet:
                return Glyph.Snippet;
            case LSP.CompletionItemKind.Color:
                return Glyph.None;
            case LSP.CompletionItemKind.File:
                return Glyph.CSharpFile;
            case LSP.CompletionItemKind.Reference:
                return Glyph.Reference;
            case LSP.CompletionItemKind.Folder:
                return Glyph.OpenFolder;
            case LSP.CompletionItemKind.EnumMember:
                return Glyph.EnumMemberPublic;
            case LSP.CompletionItemKind.Constant:
                return Glyph.ConstantPublic;
            case LSP.CompletionItemKind.Struct:
                return Glyph.StructurePublic;
            case LSP.CompletionItemKind.Event:
                return Glyph.EventPublic;
            case LSP.CompletionItemKind.Operator:
                return Glyph.OperatorPublic;
            case LSP.CompletionItemKind.TypeParameter:
                return Glyph.TypeParameter;
            default:
                return Glyph.None;
        }
    }

    // The mappings here are roughly based off of SymbolUsageInfoExtensions.ToSymbolReferenceKinds.
    public static LSP.VSInternalReferenceKind[] SymbolUsageInfoToReferenceKinds(SymbolUsageInfo symbolUsageInfo)
    {
        using var _ = ArrayBuilder<LSP.VSInternalReferenceKind>.GetInstance(out var referenceKinds);
        if (symbolUsageInfo.ValueUsageInfoOpt.HasValue)
        {
            var usageInfo = symbolUsageInfo.ValueUsageInfoOpt.Value;
            if (usageInfo.IsReadFrom())
            {
                referenceKinds.Add(LSP.VSInternalReferenceKind.Read);
            }

            if (usageInfo.IsWrittenTo())
            {
                referenceKinds.Add(LSP.VSInternalReferenceKind.Write);
            }

            if (usageInfo.IsReference())
            {
                referenceKinds.Add(LSP.VSInternalReferenceKind.Reference);
            }

            if (usageInfo.IsNameOnly())
            {
                referenceKinds.Add(LSP.VSInternalReferenceKind.Name);
            }
        }

        if (symbolUsageInfo.TypeOrNamespaceUsageInfoOpt.HasValue)
        {
            var usageInfo = symbolUsageInfo.TypeOrNamespaceUsageInfoOpt.Value;
            if ((usageInfo & TypeOrNamespaceUsageInfo.Qualified) != 0)
            {
                referenceKinds.Add(LSP.VSInternalReferenceKind.Qualified);
            }

            if ((usageInfo & TypeOrNamespaceUsageInfo.TypeArgument) != 0)
            {
                referenceKinds.Add(LSP.VSInternalReferenceKind.TypeArgument);
            }

            if ((usageInfo & TypeOrNamespaceUsageInfo.TypeConstraint) != 0)
            {
                referenceKinds.Add(LSP.VSInternalReferenceKind.TypeConstraint);
            }

            if ((usageInfo & TypeOrNamespaceUsageInfo.Base) != 0)
            {
                referenceKinds.Add(LSP.VSInternalReferenceKind.BaseType);
            }

            // Preserving the same mapping logic that SymbolUsageInfoExtensions.ToSymbolReferenceKinds uses
            if ((usageInfo & TypeOrNamespaceUsageInfo.ObjectCreation) != 0)
            {
                referenceKinds.Add(LSP.VSInternalReferenceKind.Constructor);
            }

            if ((usageInfo & TypeOrNamespaceUsageInfo.Import) != 0)
            {
                referenceKinds.Add(LSP.VSInternalReferenceKind.Import);
            }

            // Preserving the same mapping logic that SymbolUsageInfoExtensions.ToSymbolReferenceKinds uses
            if ((usageInfo & TypeOrNamespaceUsageInfo.NamespaceDeclaration) != 0)
            {
                referenceKinds.Add(LSP.VSInternalReferenceKind.Declaration);
            }
        }

        return referenceKinds.ToArray();
    }

    public static string ProjectIdToProjectContextId(ProjectId id)
    {
        return id.Id + "|" + id.DebugName;
    }

    public static ProjectId ProjectContextToProjectId(LSP.VSProjectContext projectContext)
    {
        var delimiter = projectContext.Id.IndexOf('|');

        return ProjectId.CreateFromSerialized(
            Guid.Parse(projectContext.Id[..delimiter]),
            debugName: projectContext.Id[(delimiter + 1)..]);
    }

    public static LSP.VSProjectContext ProjectToProjectContext(Project project)
    {
        var projectContext = new LSP.VSProjectContext
        {
            Id = ProjectIdToProjectContextId(project.Id),
            Label = project.Name,
            // IsMiscellaneous controls whether a toast appears which warns that editor features are not available.
            // In case HasAllInformation is true, though, we do actually have all information needed to light up any features user is trying to use related to the project.
            IsMiscellaneous = project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles && !project.State.HasAllInformation,
        };

        if (project.Language == LanguageNames.CSharp)
        {
            projectContext.Kind = LSP.VSProjectKind.CSharp;
        }
        else if (project.Language == LanguageNames.VisualBasic)
        {
            projectContext.Kind = LSP.VSProjectKind.VisualBasic;
        }

        return projectContext;
    }

    public static async Task<SyntaxFormattingOptions> GetFormattingOptionsAsync(
        LSP.FormattingOptions? options,
        Document document,
        CancellationToken cancellationToken)
    {
        var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);

        if (options != null)
        {
            // LSP doesn't currently support indent size as an option. However, except in special
            // circumstances, indent size is usually equivalent to tab size, so we'll just set it.
            formattingOptions = formattingOptions with
            {
                LineFormatting = new()
                {
                    UseTabs = !options.InsertSpaces,
                    TabSize = options.TabSize,
                    IndentationSize = options.TabSize,
                    NewLine = formattingOptions.NewLine
                }
            };
        }

        return formattingOptions;
    }

    public static LSP.MarkupContent GetDocumentationMarkupContent(ImmutableArray<TaggedText> tags, TextDocument document, bool featureSupportsMarkdown)
        => GetDocumentationMarkupContent(tags, document.Project.Language, featureSupportsMarkdown);

    public static LSP.MarkupContent GetDocumentationMarkupContent(ImmutableArray<TaggedText> tags, string language, bool featureSupportsMarkdown)
    {
        if (!featureSupportsMarkdown)
        {
            return new LSP.MarkupContent
            {
                Kind = LSP.MarkupKind.PlainText,
                Value = tags.GetFullText(),
            };
        }

        using var markdownBuilder = new MarkdownContentBuilder();
        string? codeFence = null;
        foreach (var taggedText in tags)
        {
            switch (taggedText.Tag)
            {
                case TextTags.CodeBlockStart:
                    if (markdownBuilder.IsLineEmpty())
                    {
                        // If the current line is empty, we can append a code block.
                        codeFence = BlockCodeFence;
                        var codeBlockLanguageName = GetCodeBlockLanguageName(language);
                        markdownBuilder.AppendLine($"{codeFence}{codeBlockLanguageName}");
                        markdownBuilder.AppendLine(taggedText.Text);
                    }
                    else
                    {
                        // There is text on the line already - we should append an in-line code block.
                        codeFence = InlineCodeFence;
                        markdownBuilder.Append(codeFence + taggedText.Text);
                    }

                    break;
                case TextTags.CodeBlockEnd:
                    if (codeFence == BlockCodeFence)
                    {
                        markdownBuilder.AppendLine(codeFence);
                        markdownBuilder.AppendLine(taggedText.Text);
                    }
                    else if (codeFence == InlineCodeFence)
                    {
                        markdownBuilder.Append(codeFence + taggedText.Text);
                    }
                    else
                    {
                        throw ExceptionUtilities.UnexpectedValue(codeFence);
                    }

                    codeFence = null;

                    break;
                case TextTags.Text when taggedText.Style == (TaggedTextStyle.Code | TaggedTextStyle.PreserveWhitespace):
                    // This represents a block of code (`<code></code>`) in doc comments.
                    // Since code elements optionally support a `lang` attribute and we do not have access to the
                    // language which was specified at this point, we tell the client to render it as plain text.

                    if (!markdownBuilder.IsLineEmpty())
                        AppendLineBreak(markdownBuilder);

                    // The current line is empty, we can append a code block.
                    markdownBuilder.AppendLine($"{BlockCodeFence}text");
                    markdownBuilder.AppendLine(taggedText.Text);
                    markdownBuilder.AppendLine(BlockCodeFence);

                    break;
                case TextTags.LineBreak:
                    AppendLineBreak(markdownBuilder);
                    break;
                default:
                    var styledText = GetStyledText(taggedText, codeFence != null);
                    markdownBuilder.Append(styledText);
                    break;
            }
        }

        var content = markdownBuilder.Build(Environment.NewLine);

        return new LSP.MarkupContent
        {
            Kind = LSP.MarkupKind.Markdown,
            Value = content,
        };

        static void AppendLineBreak(MarkdownContentBuilder markdownBuilder)
        {
            // A line ending with double space and a new line indicates to markdown
            // to render a single-spaced line break.
            markdownBuilder.Append("  ");
            markdownBuilder.AppendLine();
        }

        static string GetCodeBlockLanguageName(string language)
        {
            return language switch
            {
                (LanguageNames.CSharp) => CSharpMarkdownLanguageName,
                (LanguageNames.VisualBasic) => VisualBasicMarkdownLanguageName,
                _ => throw new InvalidOperationException($"{language} is not supported"),
            };
        }

        static string GetStyledText(TaggedText taggedText, bool isInCodeBlock)
        {
            var isCode = isInCodeBlock || taggedText.Style is TaggedTextStyle.Code;
            var text = isCode ? taggedText.Text : s_markdownEscapeRegex.Replace(taggedText.Text, @"\$1");

            // For non-cref links, the URI is present in both the hint and target.
            if (!string.IsNullOrEmpty(taggedText.NavigationHint) && taggedText.NavigationHint == taggedText.NavigationTarget)
                return $"[{text}]({taggedText.NavigationHint})";

            // Markdown ignores spaces at the start of lines outside of code blocks,
            // so we replace regular spaces with non-breaking spaces to ensure structural space is retained.
            // We want to use regular spaces everywhere else to allow the client to wrap long text.
            if (!isCode && taggedText.Tag is TextTags.Space or TextTags.ContainerStart)
                text = text.Replace(" ", "&nbsp;");

            return taggedText.Style switch
            {
                TaggedTextStyle.None => text,
                TaggedTextStyle.Strong => $"**{text}**",
                TaggedTextStyle.Emphasis => $"_{text}_",
                TaggedTextStyle.Underline => $"<u>{text}</u>",
                // Use double backticks to escape code which contains a backtick.
                TaggedTextStyle.Code => text.Contains('`') ? $"``{text}``" : $"`{text}`",
                _ => text,
            };
        }
    }

    private static LSP.Range MappedSpanResultToRange(MappedSpanResult mappedSpanResult)
    {
        return new LSP.Range
        {
            Start = LinePositionToPosition(mappedSpanResult.LinePositionSpan.Start),
            End = LinePositionToPosition(mappedSpanResult.LinePositionSpan.End)
        };
    }
}
