File: LanguageServer\AbstractLanguageServerProtocolTests.cs
Web Access
Project: src\src\EditorFeatures\TestUtilities\Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities.csproj (Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities)
// 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.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Test;
using Microsoft.CodeAnalysis.Editor.UnitTests;
using Microsoft.CodeAnalysis.Extensions;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageServer;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.LanguageServer.Handler.CodeActions;
using Microsoft.CodeAnalysis.LanguageServer.Handler.Completion;
using Microsoft.CodeAnalysis.LanguageServer.UnitTests;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CommonLanguageServerProtocol.Framework;
using Nerdbank.Streams;
using Roslyn.Utilities;
using StreamJsonRpc;
using Xunit;
using Xunit.Abstractions;
using LSP = Roslyn.LanguageServer.Protocol;
 
namespace Roslyn.Test.Utilities
{
    [UseExportProvider]
    public abstract partial class AbstractLanguageServerProtocolTests
    {
        protected static readonly JsonSerializerOptions JsonSerializerOptions = RoslynLanguageServer.CreateJsonMessageFormatter().JsonSerializerOptions;
 
        private protected readonly AbstractLspLogger TestOutputLspLogger;
        protected AbstractLanguageServerProtocolTests(ITestOutputHelper? testOutputHelper)
        {
            TestOutputLspLogger = testOutputHelper != null ? new TestOutputLspLogger(testOutputHelper) : NoOpLspLogger.Instance;
        }
 
        protected static readonly TestComposition EditorFeaturesLspComposition = EditorTestCompositions.LanguageServerProtocolEditorFeatures
            .AddParts(typeof(TestDocumentTrackingService))
            .AddParts(typeof(TestWorkspaceRegistrationService));
 
        protected static readonly TestComposition FeaturesLspComposition = LspTestCompositions.LanguageServerProtocol
            .AddParts(typeof(TestDocumentTrackingService))
            .AddParts(typeof(TestWorkspaceRegistrationService));
 
        private class TestSpanMapperProvider : IDocumentServiceProvider
        {
            TService? IDocumentServiceProvider.GetService<TService>() where TService : class
                => typeof(TService) == typeof(ISpanMappingService) ? (TService)(object)new TestSpanMapper() : null;
        }
 
        internal class TestSpanMapper : ISpanMappingService
        {
            private static readonly LinePositionSpan s_mappedLinePosition = new LinePositionSpan(new LinePosition(0, 0), new LinePosition(0, 5));
            private static readonly string s_mappedFilePath = "c:\\MappedFile_\ue25b\ud86d\udeac.cs";
 
            internal static readonly string GeneratedFileName = "GeneratedFile_\ue25b\ud86d\udeac.cs";
 
            internal static readonly LSP.Location MappedFileLocation = new LSP.Location
            {
                Range = ProtocolConversions.LinePositionToRange(s_mappedLinePosition),
                Uri = ProtocolConversions.CreateAbsoluteUri(s_mappedFilePath)
            };
 
            /// <summary>
            /// LSP tests are simulating the new razor system which does support mapping import directives.
            /// </summary>
            public bool SupportsMappingImportDirectives => true;
 
            public Task<ImmutableArray<MappedSpanResult>> MapSpansAsync(Document document, IEnumerable<TextSpan> spans, CancellationToken cancellationToken)
            {
                ImmutableArray<MappedSpanResult> mappedResult = default;
                if (document.Name == GeneratedFileName)
                {
                    mappedResult = spans.Select(span => new MappedSpanResult(s_mappedFilePath, s_mappedLinePosition, new TextSpan(0, 5))).ToImmutableArray();
                }
 
                return Task.FromResult(mappedResult);
            }
 
            public Task<ImmutableArray<(string mappedFilePath, TextChange mappedTextChange)>> GetMappedTextChangesAsync(
                Document oldDocument,
                Document newDocument,
                CancellationToken cancellationToken)
            {
                throw new NotImplementedException();
            }
        }
 
        private protected class OrderLocations : Comparer<LSP.Location?>
        {
            public override int Compare(LSP.Location? x, LSP.Location? y) => CompareLocations(x, y);
        }
 
        protected virtual TestComposition Composition => EditorFeaturesLspComposition;
 
        private protected virtual TestAnalyzerReferenceByLanguage CreateTestAnalyzersReference()
            => new(DiagnosticExtensions.GetCompilerDiagnosticAnalyzersMap());
 
        private protected static LSP.ClientCapabilities CapabilitiesWithVSExtensions => new LSP.VSInternalClientCapabilities { SupportsVisualStudioExtensions = true };
 
        private protected static LSP.ClientCapabilities GetCapabilities(bool isVS)
            => isVS ? CapabilitiesWithVSExtensions : new LSP.ClientCapabilities();
 
        /// <summary>
        /// Asserts two objects are equivalent by converting to JSON and ignoring whitespace.
        /// </summary>
        /// <param name="expected">the expected object to be converted to JSON.</param>
        /// <param name="actual">the actual object to be converted to JSON.</param>
        public static void AssertJsonEquals<T1, T2>(T1 expected, T2 actual)
        {
            var expectedStr = JsonSerializer.Serialize(expected, JsonSerializerOptions);
            var actualStr = JsonSerializer.Serialize(actual, JsonSerializerOptions);
            AssertEqualIgnoringWhitespace(expectedStr, actualStr);
        }
 
        protected static void AssertEqualIgnoringWhitespace(string expected, string actual)
        {
            var expectedWithoutWhitespace = Regex.Replace(expected, @"\s+", string.Empty);
            var actualWithoutWhitespace = Regex.Replace(actual, @"\s+", string.Empty);
            AssertEx.Equal(expectedWithoutWhitespace, actualWithoutWhitespace);
        }
 
        /// <summary>
        /// Assert that two location lists are equivalent.
        /// Locations are not always returned in a consistent order so they must be sorted.
        /// </summary>
        private protected static void AssertLocationsEqual(IEnumerable<LSP.Location> expectedLocations, IEnumerable<LSP.Location> actualLocations)
        {
            var orderedActualLocations = actualLocations.OrderBy(CompareLocations);
            var orderedExpectedLocations = expectedLocations.OrderBy(CompareLocations);
 
            AssertJsonEquals(orderedExpectedLocations, orderedActualLocations);
        }
 
        private protected static int CompareLocations(LSP.Location? l1, LSP.Location? l2)
        {
            if (ReferenceEquals(l1, l2))
                return 0;
 
            if (l1 is null)
                return -1;
 
            if (l2 is null)
                return 1;
 
            var compareDocument = l1.Uri.AbsoluteUri.CompareTo(l2.Uri.AbsoluteUri);
            var compareRange = CompareRange(l1.Range, l2.Range);
            return compareDocument != 0 ? compareDocument : compareRange;
        }
 
        private protected static int CompareRange(LSP.Range r1, LSP.Range r2)
        {
            var compareLine = r1.Start.Line.CompareTo(r2.Start.Line);
            var compareChar = r1.Start.Character.CompareTo(r2.Start.Character);
            return compareLine != 0 ? compareLine : compareChar;
        }
 
        private protected static string ApplyTextEdits(LSP.TextEdit[] edits, SourceText originalMarkup)
        {
            var text = originalMarkup;
            foreach (var edit in edits)
            {
                var lines = text.Lines;
                var startPosition = ProtocolConversions.PositionToLinePosition(edit.Range.Start);
                var endPosition = ProtocolConversions.PositionToLinePosition(edit.Range.End);
                var textSpan = lines.GetTextSpan(new LinePositionSpan(startPosition, endPosition));
                text = text.Replace(textSpan, edit.NewText);
            }
 
            return text.ToString();
        }
 
        internal static LSP.SymbolInformation CreateSymbolInformation(LSP.SymbolKind kind, string name, LSP.Location location, Glyph glyph, string? containerName = null)
        {
            var imageId = glyph.GetImageId();
 
#pragma warning disable CS0618 // SymbolInformation is obsolete, need to switch to DocumentSymbol/WorkspaceSymbol
            var info = new LSP.VSSymbolInformation()
            {
                Kind = kind,
                Name = name,
                Location = location,
                Icon = new LSP.VSImageId { Guid = imageId.Guid, Id = imageId.Id },
            };
 
            if (containerName != null)
                info.ContainerName = containerName;
#pragma warning restore CS0618
 
            return info;
        }
 
        private protected static LSP.TextDocumentIdentifier CreateTextDocumentIdentifier(Uri uri, ProjectId? projectContext = null)
        {
            var documentIdentifier = new LSP.VSTextDocumentIdentifier { Uri = uri };
 
            if (projectContext != null)
            {
                documentIdentifier.ProjectContext =
                    new LSP.VSProjectContext { Id = ProtocolConversions.ProjectIdToProjectContextId(projectContext), Label = projectContext.DebugName!, Kind = LSP.VSProjectKind.CSharp };
            }
 
            return documentIdentifier;
        }
 
        private protected static LSP.TextDocumentPositionParams CreateTextDocumentPositionParams(LSP.Location caret, ProjectId? projectContext = null)
            => new LSP.TextDocumentPositionParams()
            {
                TextDocument = CreateTextDocumentIdentifier(caret.Uri, projectContext),
                Position = caret.Range.Start
            };
 
        private protected static LSP.MarkupContent CreateMarkupContent(LSP.MarkupKind kind, string value)
            => new LSP.MarkupContent()
            {
                Kind = kind,
                Value = value
            };
 
        private protected static LSP.CompletionParams CreateCompletionParams(
            LSP.Location caret,
            LSP.VSInternalCompletionInvokeKind invokeKind,
            string triggerCharacter,
            LSP.CompletionTriggerKind triggerKind)
            => new LSP.CompletionParams()
            {
                TextDocument = CreateTextDocumentIdentifier(caret.Uri),
                Position = caret.Range.Start,
                Context = new LSP.VSInternalCompletionContext()
                {
                    InvokeKind = invokeKind,
                    TriggerCharacter = triggerCharacter,
                    TriggerKind = triggerKind,
                }
            };
 
        private protected static async Task<LSP.VSInternalCompletionItem> CreateCompletionItemAsync(
            string label,
            LSP.CompletionItemKind kind,
            string[] tags,
            LSP.CompletionParams request,
            Document document,
            bool preselect = false,
            ImmutableArray<char>? commitCharacters = null,
            LSP.TextEdit? textEdit = null,
            string? textEditText = null,
            string? sortText = null,
            string? filterText = null,
            long resultId = 0,
            bool vsResolveTextEditOnCommit = false,
            LSP.CompletionItemLabelDetails? labelDetails = null)
        {
            var position = await document.GetPositionFromLinePositionAsync(
                ProtocolConversions.PositionToLinePosition(request.Position), CancellationToken.None).ConfigureAwait(false);
            var completionTrigger = await ProtocolConversions.LSPToRoslynCompletionTriggerAsync(
                request.Context, document, position, CancellationToken.None).ConfigureAwait(false);
 
            var item = new LSP.VSInternalCompletionItem()
            {
                TextEdit = textEdit,
                TextEditText = textEditText,
                FilterText = filterText,
                Label = label,
                SortText = sortText,
                InsertTextFormat = LSP.InsertTextFormat.Plaintext,
                Kind = kind,
                Data = JsonSerializer.SerializeToElement(new CompletionResolveData(resultId, ProtocolConversions.DocumentToTextDocumentIdentifier(document)), JsonSerializerOptions),
                Preselect = preselect,
                VsResolveTextEditOnCommit = vsResolveTextEditOnCommit,
                LabelDetails = labelDetails
            };
 
            if (tags != null)
                item.Icon = tags.ToImmutableArray().GetFirstGlyph().GetImageElement().ToLSPImageElement();
 
            if (commitCharacters != null)
                item.CommitCharacters = commitCharacters.Value.Select(c => c.ToString()).ToArray();
 
            return item;
        }
 
        private protected static LSP.TextEdit GenerateTextEdit(string newText, int startLine, int startChar, int endLine, int endChar)
            => new LSP.TextEdit
            {
                NewText = newText,
                Range = new LSP.Range
                {
                    Start = new LSP.Position { Line = startLine, Character = startChar },
                    End = new LSP.Position { Line = endLine, Character = endChar }
                }
            };
 
        private protected static CodeActionResolveData CreateCodeActionResolveData(string uniqueIdentifier, LSP.Location location, string[] codeActionPath, IEnumerable<string>? customTags = null)
            => new(uniqueIdentifier, customTags.ToImmutableArrayOrEmpty(), location.Range, CreateTextDocumentIdentifier(location.Uri), fixAllFlavors: null, nestedCodeActions: null, codeActionPath: codeActionPath);
 
        private protected Task<TestLspServer> CreateTestLspServerAsync(string markup, bool mutatingLspWorkspace, LSP.ClientCapabilities clientCapabilities, bool callInitialized = true)
            => CreateTestLspServerAsync([markup], LanguageNames.CSharp, mutatingLspWorkspace, new InitializationOptions { ClientCapabilities = clientCapabilities, CallInitialized = callInitialized });
 
        private protected Task<TestLspServer> CreateTestLspServerAsync(string markup, bool mutatingLspWorkspace, InitializationOptions? initializationOptions = null, TestComposition? composition = null)
            => CreateTestLspServerAsync([markup], LanguageNames.CSharp, mutatingLspWorkspace, initializationOptions, composition);
 
        private protected Task<TestLspServer> CreateTestLspServerAsync(string[] markups, bool mutatingLspWorkspace, InitializationOptions? initializationOptions = null, TestComposition? composition = null)
            => CreateTestLspServerAsync(markups, LanguageNames.CSharp, mutatingLspWorkspace, initializationOptions, composition);
 
        private protected Task<TestLspServer> CreateVisualBasicTestLspServerAsync(string markup, bool mutatingLspWorkspace, InitializationOptions? initializationOptions = null, TestComposition? composition = null)
            => CreateTestLspServerAsync([markup], LanguageNames.VisualBasic, mutatingLspWorkspace, initializationOptions, composition);
 
        private protected Task<TestLspServer> CreateTestLspServerAsync(
            string[] markups, string languageName, bool mutatingLspWorkspace, InitializationOptions? initializationOptions, TestComposition? composition = null, bool commonReferences = true)
        {
            var lspOptions = initializationOptions ?? new InitializationOptions();
 
            var workspace = CreateWorkspace(lspOptions, workspaceKind: null, mutatingLspWorkspace, composition);
 
            workspace.InitializeDocuments(
                TestWorkspace.CreateWorkspaceElement(languageName, files: markups, fileContainingFolders: lspOptions.DocumentFileContainingFolders, sourceGeneratedFiles: lspOptions.SourceGeneratedMarkups, commonReferences: commonReferences),
                openDocuments: false);
 
            return CreateTestLspServerAsync(workspace, lspOptions, languageName);
        }
 
        private async Task<TestLspServer> CreateTestLspServerAsync(EditorTestWorkspace workspace, InitializationOptions initializationOptions, string languageName)
        {
            var solution = workspace.CurrentSolution;
 
            foreach (var document in workspace.Documents)
            {
                if (document.IsSourceGenerated)
                    continue;
 
                solution = solution.WithDocumentFilePath(document.Id, GetDocumentFilePathFromName(document.Name));
 
                var documentText = await solution.GetRequiredDocument(document.Id).GetTextAsync(CancellationToken.None);
                solution = solution.WithDocumentText(document.Id, SourceText.From(documentText.ToString(), System.Text.Encoding.UTF8, SourceHashAlgorithms.Default));
            }
 
            foreach (var project in workspace.Projects)
            {
                // Ensure all the projects have a valid file path.
                solution = solution.WithProjectFilePath(project.Id, GetDocumentFilePathFromName(project.FilePath));
            }
 
            var analyzerReferencesByLanguage = CreateTestAnalyzersReference();
            if (initializationOptions.AdditionalAnalyzers != null)
                analyzerReferencesByLanguage = analyzerReferencesByLanguage.WithAdditionalAnalyzers(languageName, initializationOptions.AdditionalAnalyzers);
 
            solution = solution.WithAnalyzerReferences([analyzerReferencesByLanguage]);
            await workspace.ChangeSolutionAsync(solution);
 
            return await TestLspServer.CreateAsync(workspace, initializationOptions, TestOutputLspLogger);
        }
 
        private protected async Task<TestLspServer> CreateXmlTestLspServerAsync(
            string xmlContent,
            bool mutatingLspWorkspace,
            string? workspaceKind = null,
            InitializationOptions? initializationOptions = null)
        {
            var lspOptions = initializationOptions ?? new InitializationOptions();
 
            var workspace = CreateWorkspace(lspOptions, workspaceKind, mutatingLspWorkspace);
 
            workspace.InitializeDocuments(XElement.Parse(xmlContent), openDocuments: false);
            workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences([CreateTestAnalyzersReference()]));
 
            return await TestLspServer.CreateAsync(workspace, lspOptions, TestOutputLspLogger);
        }
 
        internal EditorTestWorkspace CreateWorkspace(
            InitializationOptions? options, string? workspaceKind, bool mutatingLspWorkspace, TestComposition? composition = null)
        {
            var workspace = new EditorTestWorkspace(
                composition ?? Composition, workspaceKind, configurationOptions: new WorkspaceConfigurationOptions(ValidateCompilationTrackerStates: true), supportsLspMutation: mutatingLspWorkspace);
            options?.OptionUpdater?.Invoke(workspace.GetService<IGlobalOptionService>());
 
            workspace.GetService<LspWorkspaceRegistrationService>().Register(workspace);
 
            return workspace;
        }
 
        /// <summary>
        /// Waits for the async operations on the workspace to complete.
        /// This ensures that events like workspace registration / workspace changes are processed by the time we exit this method.
        /// </summary>
        protected static async Task WaitForWorkspaceOperationsAsync(EditorTestWorkspace workspace)
        {
            var workspaceWaiter = GetWorkspaceWaiter(workspace);
            await workspaceWaiter.ExpeditedWaitAsync();
        }
 
        private static IAsynchronousOperationWaiter GetWorkspaceWaiter(EditorTestWorkspace workspace)
        {
            var operations = workspace.ExportProvider.GetExportedValue<AsynchronousOperationListenerProvider>();
            return operations.GetWaiter(FeatureAttribute.Workspace);
        }
 
        protected static void AddMappedDocument(Workspace workspace, string markup)
        {
            var generatedDocumentId = DocumentId.CreateNewId(workspace.CurrentSolution.ProjectIds.First());
            var version = VersionStamp.Create();
            var loader = TextLoader.From(TextAndVersion.Create(SourceText.From(markup), version, TestSpanMapper.GeneratedFileName));
            var generatedDocumentInfo = DocumentInfo.Create(
                generatedDocumentId,
                TestSpanMapper.GeneratedFileName,
                loader: loader,
                filePath: $"C:\\{TestSpanMapper.GeneratedFileName}",
                isGenerated: true)
                .WithDocumentServiceProvider(new TestSpanMapperProvider());
 
            var newSolution = workspace.CurrentSolution.AddDocument(generatedDocumentInfo);
            workspace.TryApplyChanges(newSolution);
        }
 
        protected static async Task<AnalyzerReference> AddGeneratorAsync(ISourceGenerator generator, EditorTestWorkspace workspace)
        {
            var analyzerReference = new TestGeneratorReference(generator);
 
            var solution = workspace.CurrentSolution
                .Projects.Single()
                .AddAnalyzerReference(analyzerReference)
                .Solution;
 
            await workspace.ChangeSolutionAsync(solution);
            await WaitForWorkspaceOperationsAsync(workspace);
            return analyzerReference;
        }
 
        protected static async Task RemoveGeneratorAsync(AnalyzerReference reference, EditorTestWorkspace workspace)
        {
            var solution = workspace.CurrentSolution
                .Projects.Single()
                .RemoveAnalyzerReference(reference)
                .Solution;
 
            await workspace.ChangeSolutionAsync(solution);
            await WaitForWorkspaceOperationsAsync(workspace);
        }
 
        internal static async Task<Dictionary<string, IList<LSP.Location>>> GetAnnotatedLocationsAsync(EditorTestWorkspace workspace, Solution solution)
        {
            var locations = new Dictionary<string, IList<LSP.Location>>();
            foreach (var testDocument in workspace.Documents)
            {
                var document = await solution.GetRequiredDocumentAsync(testDocument.Id, includeSourceGenerated: true, CancellationToken.None);
                var text = await document.GetTextAsync(CancellationToken.None);
                foreach (var (name, spans) in testDocument.AnnotatedSpans)
                {
                    Contract.ThrowIfNull(document.FilePath);
 
                    var locationsForName = locations.GetValueOrDefault(name, new List<LSP.Location>());
                    locationsForName.AddRange(spans.Select(span => ConvertTextSpanWithTextToLocation(span, text, document.GetURI())));
 
                    // Linked files will return duplicate annotated Locations for each document that links to the same file.
                    // Since the test output only cares about the actual file, make sure we de-dupe before returning.
                    locations[name] = locationsForName.Distinct().ToList();
                }
            }
 
            return locations;
 
            static LSP.Location ConvertTextSpanWithTextToLocation(TextSpan span, SourceText text, Uri documentUri)
            {
                var location = new LSP.Location
                {
                    Uri = documentUri,
                    Range = ProtocolConversions.TextSpanToRange(span, text),
                };
 
                return location;
            }
        }
 
        private protected static LSP.Location GetLocationPlusOne(LSP.Location originalLocation)
        {
            var newPosition = new LSP.Position { Character = originalLocation.Range.Start.Character + 1, Line = originalLocation.Range.Start.Line };
            return new LSP.Location
            {
                Uri = originalLocation.Uri,
                Range = new LSP.Range { Start = newPosition, End = newPosition }
            };
        }
 
        private static string GetDocumentFilePathFromName(string documentName)
            => "C:\\" + documentName;
 
        private static LSP.DidChangeTextDocumentParams CreateDidChangeTextDocumentParams(
            Uri documentUri,
            ImmutableArray<(LSP.Range Range, string Text)> changes)
        {
            var changeEvents = changes.Select(change => new LSP.TextDocumentContentChangeEvent
            {
                Text = change.Text,
                Range = change.Range,
            }).ToArray();
 
            return new LSP.DidChangeTextDocumentParams()
            {
                TextDocument = new LSP.VersionedTextDocumentIdentifier
                {
                    Uri = documentUri
                },
                ContentChanges = changeEvents
            };
        }
 
        private static LSP.DidOpenTextDocumentParams CreateDidOpenTextDocumentParams(Uri uri, string source, string languageId = "")
            => new LSP.DidOpenTextDocumentParams
            {
                TextDocument = new LSP.TextDocumentItem
                {
                    Text = source,
                    Uri = uri,
                    LanguageId = languageId
                }
            };
 
        private static LSP.DidCloseTextDocumentParams CreateDidCloseTextDocumentParams(Uri uri)
           => new LSP.DidCloseTextDocumentParams()
           {
               TextDocument = new LSP.TextDocumentIdentifier
               {
                   Uri = uri
               }
           };
 
        internal sealed class TestLspServer : IAsyncDisposable
        {
            public readonly EditorTestWorkspace TestWorkspace;
            private readonly Dictionary<string, IList<LSP.Location>> _locations;
            private readonly JsonRpc _clientRpc;
            private readonly ICodeAnalysisDiagnosticAnalyzerService _codeAnalysisService;
 
            private readonly RoslynLanguageServer LanguageServer;
 
            public LSP.ClientCapabilities ClientCapabilities { get; }
 
            private TestLspServer(
                EditorTestWorkspace testWorkspace,
                Dictionary<string, IList<LSP.Location>> locations,
                LSP.ClientCapabilities clientCapabilities,
                RoslynLanguageServer target,
                Stream clientStream,
                object? clientTarget = null,
                IJsonRpcMessageFormatter? clientMessageFormatter = null)
            {
                TestWorkspace = testWorkspace;
                ClientCapabilities = clientCapabilities;
                _locations = locations;
                _codeAnalysisService = testWorkspace.Services.GetRequiredService<ICodeAnalysisDiagnosticAnalyzerService>();
 
                LanguageServer = target;
 
                clientMessageFormatter ??= RoslynLanguageServer.CreateJsonMessageFormatter();
 
                _clientRpc = new JsonRpc(new HeaderDelimitedMessageHandler(clientStream, clientStream, clientMessageFormatter), clientTarget)
                {
                    ExceptionStrategy = ExceptionProcessing.ISerializable,
                };
 
                // Workspace listener events do not run in tests, so we manually register the lsp misc workspace.
                TestWorkspace.GetService<LspWorkspaceRegistrationService>().Register(GetManagerAccessor().GetLspMiscellaneousFilesWorkspace());
 
                InitializeClientRpc();
            }
 
            private void InitializeClientRpc()
            {
                _clientRpc.StartListening();
 
                var workspaceWaiter = GetWorkspaceWaiter(TestWorkspace);
                Assert.False(workspaceWaiter.HasPendingWork);
            }
 
            internal static async Task<TestLspServer> CreateAsync(EditorTestWorkspace testWorkspace, InitializationOptions initializationOptions, AbstractLspLogger logger)
            {
                // Important: We must wait for workspace creation operations to finish.
                // Otherwise we could have a race where workspace change events triggered by creation are changing the state
                // created by the initial test steps. This can interfere with the expected test state.
                await WaitForWorkspaceOperationsAsync(testWorkspace);
 
                var locations = await GetAnnotatedLocationsAsync(testWorkspace, testWorkspace.CurrentSolution);
 
                var (clientStream, serverStream) = FullDuplexStream.CreatePair();
                var languageServer = CreateLanguageServer(serverStream, serverStream, testWorkspace, initializationOptions.ServerKind, logger);
 
                var server = new TestLspServer(testWorkspace, locations, initializationOptions.ClientCapabilities, languageServer, clientStream, initializationOptions.ClientTarget, initializationOptions.ClientMessageFormatter);
 
                if (initializationOptions.CallInitialize)
                {
                    await server.ExecuteRequestAsync<LSP.InitializeParams, LSP.InitializeResult>(LSP.Methods.InitializeName, new LSP.InitializeParams
                    {
                        Capabilities = initializationOptions.ClientCapabilities,
                        Locale = initializationOptions.Locale,
                    }, CancellationToken.None);
                }
 
                if (initializationOptions.CallInitialized)
                {
                    await server.ExecuteRequestAsync<LSP.InitializedParams, object?>(LSP.Methods.InitializedName, new LSP.InitializedParams { }, CancellationToken.None);
                }
 
                return server;
            }
 
            internal static async Task<TestLspServer> CreateAsync(EditorTestWorkspace testWorkspace, LSP.ClientCapabilities clientCapabilities, RoslynLanguageServer target, Stream clientStream)
            {
                var locations = await GetAnnotatedLocationsAsync(testWorkspace, testWorkspace.CurrentSolution);
                var server = new TestLspServer(testWorkspace, locations, clientCapabilities, target, clientStream);
 
                await server.ExecuteRequestAsync<LSP.InitializeParams, LSP.InitializeResult>(LSP.Methods.InitializeName, new LSP.InitializeParams
                {
                    Capabilities = clientCapabilities,
                }, CancellationToken.None);
 
                await server.ExecuteRequestAsync<LSP.InitializedParams, object?>(LSP.Methods.InitializedName, new LSP.InitializedParams { }, CancellationToken.None);
 
                return server;
            }
 
            private static RoslynLanguageServer CreateLanguageServer(Stream inputStream, Stream outputStream, EditorTestWorkspace workspace, WellKnownLspServerKinds serverKind, AbstractLspLogger logger)
            {
                var capabilitiesProvider = workspace.ExportProvider.GetExportedValue<ExperimentalCapabilitiesProvider>();
                var factory = workspace.ExportProvider.GetExportedValue<ILanguageServerFactory>();
 
                var jsonMessageFormatter = RoslynLanguageServer.CreateJsonMessageFormatter();
                var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(outputStream, inputStream, jsonMessageFormatter))
                {
                    ExceptionStrategy = ExceptionProcessing.ISerializable,
                };
 
                var languageServer = (RoslynLanguageServer)factory.Create(jsonRpc, jsonMessageFormatter.JsonSerializerOptions, capabilitiesProvider, serverKind, logger, workspace.Services.HostServices);
 
                jsonRpc.StartListening();
                return languageServer;
            }
 
            public async Task<Document> GetDocumentAsync(Uri uri)
            {
                var document = await GetCurrentSolution().GetDocumentAsync(new LSP.TextDocumentIdentifier { Uri = uri }, CancellationToken.None).ConfigureAwait(false);
                Contract.ThrowIfNull(document, $"Unable to find document with {uri} in solution");
                return document;
            }
 
            public async Task<SourceText> GetDocumentTextAsync(Uri uri)
            {
                var document = await GetDocumentAsync(uri).ConfigureAwait(false);
                return await document.GetTextAsync(CancellationToken.None).ConfigureAwait(false);
            }
 
            public async Task<ResponseType?> ExecuteRequestAsync<RequestType, ResponseType>(string methodName, RequestType request, CancellationToken cancellationToken) where RequestType : class
            {
                // If creating the LanguageServer threw we might timeout without this.
                var result = await _clientRpc.InvokeWithParameterObjectAsync<ResponseType>(methodName, request, cancellationToken: cancellationToken).ConfigureAwait(false);
                return result;
            }
 
            public async Task<ResponseType?> ExecuteRequest0Async<ResponseType>(string methodName, CancellationToken cancellationToken)
            {
                // If creating the LanguageServer threw we might timeout without this.
                var result = await _clientRpc.InvokeWithParameterObjectAsync<ResponseType>(methodName, cancellationToken: cancellationToken).ConfigureAwait(false);
                return result;
            }
 
            public Task ExecuteNotificationAsync<RequestType>(string methodName, RequestType request) where RequestType : class
            {
                return _clientRpc.NotifyWithParameterObjectAsync(methodName, request);
            }
 
            public Task ExecuteNotification0Async(string methodName)
            {
                return _clientRpc.NotifyWithParameterObjectAsync(methodName);
            }
 
            public Task ExecutePreSerializedRequestAsync(string methodName, JsonDocument serializedRequest)
            {
                return _clientRpc.InvokeWithParameterObjectAsync(methodName, serializedRequest);
            }
 
            public async Task OpenDocumentAsync(Uri documentUri, string? text = null, string languageId = "")
            {
                if (text == null)
                {
                    // LSP open files don't care about the project context, just the file contents with the URI.
                    // So pick any of the linked documents to get the text from.
                    var sourceText = await GetDocumentTextAsync(documentUri).ConfigureAwait(false);
                    text = sourceText.ToString();
                }
 
                var didOpenParams = CreateDidOpenTextDocumentParams(documentUri, text.ToString(), languageId);
                await ExecuteRequestAsync<LSP.DidOpenTextDocumentParams, object>(LSP.Methods.TextDocumentDidOpenName, didOpenParams, CancellationToken.None);
            }
 
            public Task ReplaceTextAsync(Uri documentUri, params (LSP.Range Range, string Text)[] changes)
            {
                var didChangeParams = CreateDidChangeTextDocumentParams(
                    documentUri,
                    changes.ToImmutableArray());
                return ExecuteRequestAsync<LSP.DidChangeTextDocumentParams, object>(LSP.Methods.TextDocumentDidChangeName, didChangeParams, CancellationToken.None);
            }
 
            public Task InsertTextAsync(Uri documentUri, params (int Line, int Column, string Text)[] changes)
            {
                return ReplaceTextAsync(documentUri, changes.Select(change => (new LSP.Range
                {
                    Start = new LSP.Position { Line = change.Line, Character = change.Column },
                    End = new LSP.Position { Line = change.Line, Character = change.Column }
                }, change.Text)).ToArray());
            }
 
            public Task DeleteTextAsync(Uri documentUri, params (int StartLine, int StartColumn, int EndLine, int EndColumn)[] changes)
            {
                return ReplaceTextAsync(documentUri, changes.Select(change => (new LSP.Range
                {
                    Start = new LSP.Position { Line = change.StartLine, Character = change.StartColumn },
                    End = new LSP.Position { Line = change.EndLine, Character = change.EndColumn }
                }, string.Empty)).ToArray());
            }
 
            public Task CloseDocumentAsync(Uri documentUri)
            {
                var didCloseParams = CreateDidCloseTextDocumentParams(documentUri);
                return ExecuteRequestAsync<LSP.DidCloseTextDocumentParams, object>(LSP.Methods.TextDocumentDidCloseName, didCloseParams, CancellationToken.None);
            }
 
            public async Task ShutdownTestServerAsync()
            {
                await _clientRpc.InvokeAsync(LSP.Methods.ShutdownName).ConfigureAwait(false);
            }
 
            public async Task ExitTestServerAsync()
            {
                // Since exit is a notification that disposes of the json rpc stream we cannot wait on the result
                // of the request itself since it will throw a ConnectionLostException.
                // Instead we wait for the server's exit task to be completed.
                await _clientRpc.NotifyAsync(LSP.Methods.ExitName).ConfigureAwait(false);
                await LanguageServer.WaitForExitAsync().ConfigureAwait(false);
            }
 
            public IList<LSP.Location> GetLocations(string locationName) => _locations[locationName];
 
            public Dictionary<string, IList<LSP.Location>> GetLocations() => _locations;
 
            public Solution GetCurrentSolution() => TestWorkspace.CurrentSolution;
 
            public async Task AssertServerShuttingDownAsync()
            {
                var queueAccessor = GetQueueAccessor()!.Value;
                await queueAccessor.WaitForProcessingToStopAsync().ConfigureAwait(false);
 
                var shutdownTask = GetServerAccessor().GetShutdownTaskAsync();
                AssertEx.NotNull(shutdownTask, "Unexpected shutdown not started");
 
                // Shutdown task will close the queue, so we need to wait for it to complete.
                await shutdownTask.ConfigureAwait(false);
                Assert.True(queueAccessor.IsComplete(), "Unexpected queue not complete");
            }
 
            internal async Task WaitForDiagnosticsAsync()
            {
                var listenerProvider = TestWorkspace.GetService<IAsynchronousOperationListenerProvider>();
 
                await listenerProvider.GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync();
                await listenerProvider.GetWaiter(FeatureAttribute.SolutionCrawlerLegacy).ExpeditedWaitAsync();
                await listenerProvider.GetWaiter(FeatureAttribute.DiagnosticService).ExpeditedWaitAsync();
            }
 
            internal RequestExecutionQueue<RequestContext>.TestAccessor? GetQueueAccessor() => LanguageServer.GetTestAccessor().GetQueueAccessor();
 
            internal LspWorkspaceManager.TestAccessor GetManagerAccessor() => GetRequiredLspService<LspWorkspaceManager>().GetTestAccessor();
 
            internal LspWorkspaceManager GetManager() => GetRequiredLspService<LspWorkspaceManager>();
 
            internal AbstractLanguageServer<RequestContext>.TestAccessor GetServerAccessor() => LanguageServer.GetTestAccessor();
 
            internal T GetRequiredLspService<T>() where T : class, ILspService => LanguageServer.GetTestAccessor().GetRequiredLspService<T>();
 
            internal ImmutableArray<SourceText> GetTrackedTexts() => GetManager().GetTrackedLspText().Values.Select(v => v.Text).ToImmutableArray();
 
            internal Task RunCodeAnalysisAsync(ProjectId? projectId)
                => _codeAnalysisService.RunAnalysisAsync(GetCurrentSolution(), projectId, onAfterProjectAnalyzed: _ => { }, CancellationToken.None);
 
            public async ValueTask DisposeAsync()
            {
                TestWorkspace.GetService<LspWorkspaceRegistrationService>().Deregister(TestWorkspace);
                TestWorkspace.GetService<LspWorkspaceRegistrationService>().Deregister(GetManagerAccessor().GetLspMiscellaneousFilesWorkspace());
 
                // Some tests will manually call shutdown and exit, so attempting to call this during dispose
                // will fail as the server's jsonrpc instance will be disposed of.
                if (!LanguageServer.GetTestAccessor().HasShutdownStarted())
                {
                    await ShutdownTestServerAsync();
                    await ExitTestServerAsync();
                }
 
                // Wait for all the exit notifications to run to completion.
                await LanguageServer.WaitForExitAsync();
 
                TestWorkspace.Dispose();
                _clientRpc.Dispose();
            }
        }
    }
}