File: Workspaces\LspWorkspaceManagerTests.cs
Web Access
Project: ..\..\..\src\Features\LanguageServer\ProtocolUnitTests\Microsoft.CodeAnalysis.LanguageServer.Protocol.UnitTests.csproj (Microsoft.CodeAnalysis.LanguageServer.Protocol.UnitTests)
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.LanguageServer.UnitTests.Completion;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;
 
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.Workspaces;
 
public class LspWorkspaceManagerTests : AbstractLanguageServerProtocolTests
{
    public LspWorkspaceManagerTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
    {
    }
 
    [Fact]
    public async Task TestUsesLspTextOnOpenCloseAsync()
    {
        var markup = "";
        await using var testLspServer = await CreateTestLspServerAsync(markup);
        var documentUri = testLspServer.GetCurrentSolution().Projects.First().Documents.First().GetURI();
 
        await testLspServer.OpenDocumentAsync(documentUri, "LSP text");
 
        var (_, lspDocument) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(lspDocument);
        Assert.Equal("LSP text", (await lspDocument.GetTextAsync(CancellationToken.None)).ToString());
 
        // Verify LSP text changes are reflected in the opened document.
        await testLspServer.InsertTextAsync(documentUri, (0, 0, "More text"));
        (_, lspDocument) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(lspDocument);
        Assert.Equal("More textLSP text", (await lspDocument.GetTextAsync(CancellationToken.None)).ToString());
 
        // Close the document in LSP and verify all LSP tracked changes are now gone.
        // The document should be reset to the workspace's state.
        await testLspServer.CloseDocumentAsync(documentUri);
        var (_, closedDocument) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServer).ConfigureAwait(false);
        Assert.Equal(testLspServer.GetCurrentSolution(), closedDocument!.Project.Solution);
    }
 
    [Fact]
    public async Task TestLspUsesWorkspaceInstanceOnChangesAsync()
    {
        var markupOne = "One";
        var markupTwo = "Two";
        await using var testLspServer = await CreateTestLspServerAsync(new string[] { markupOne, markupTwo });
        var firstDocumentUri = testLspServer.GetCurrentSolution().Projects.First().Documents.Single(d => d.FilePath!.Contains("test1")).GetURI();
        var secondDocumentUri = testLspServer.GetCurrentSolution().Projects.First().Documents.Single(d => d.FilePath!.Contains("test2")).GetURI();
 
        var firstDocument = await OpenDocumentAndVerifyLspTextAsync(firstDocumentUri, testLspServer, markupOne);
        var secondDocument = await OpenDocumentAndVerifyLspTextAsync(secondDocumentUri, testLspServer, markupTwo);
        var firstDocumentInitialVersion = await firstDocument.GetSyntaxVersionAsync(CancellationToken.None);
        var secondDocumentInitialVersion = await secondDocument.GetSyntaxVersionAsync(CancellationToken.None);
 
        // Verify the LSP documents are the same instance as the workspaces documents.
        Assert.Same(testLspServer.TestWorkspace.CurrentSolution.GetDocument(firstDocument.Id), firstDocument);
        Assert.Same(testLspServer.TestWorkspace.CurrentSolution.GetDocument(secondDocument.Id), secondDocument);
 
        // Make a text change in one of the opened documents in both LSP and the workspace.
        await testLspServer.InsertTextAsync(firstDocumentUri, (0, 0, "Some more text"));
        await testLspServer.TestWorkspace.ChangeDocumentAsync(firstDocument.Id, SourceText.From($"Some more text{markupOne}", System.Text.Encoding.UTF8, SourceHashAlgorithms.Default));
 
        var (_, firstDocumentWithChange) = await GetLspWorkspaceAndDocumentAsync(firstDocumentUri, testLspServer).ConfigureAwait(false);
        var (_, secondDocumentUnchanged) = await GetLspWorkspaceAndDocumentAsync(secondDocumentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(firstDocumentWithChange);
        AssertEx.NotNull(secondDocumentUnchanged);
 
        // Verify that the document that we inserted text into had a version change.
        Assert.NotEqual(firstDocumentInitialVersion, await firstDocumentWithChange.GetSyntaxVersionAsync(CancellationToken.None));
        Assert.Equal($"Some more text{markupOne}", (await firstDocumentWithChange.GetTextAsync(CancellationToken.None)).ToString());
 
        // Verify that the document that we did not change still has the same version.
        Assert.Equal(secondDocumentInitialVersion, await secondDocumentUnchanged.GetSyntaxVersionAsync(CancellationToken.None));
 
        // Verify the LSP documents are the same instance as the workspaces documents.
        Assert.Equal(testLspServer.TestWorkspace.CurrentSolution.GetDocument(firstDocumentWithChange.Id), firstDocumentWithChange);
        Assert.Equal(testLspServer.TestWorkspace.CurrentSolution.GetDocument(secondDocumentUnchanged.Id), secondDocumentUnchanged);
    }
 
    [Fact]
    public async Task TestLspHasClosedDocumentChangesAsync()
    {
        var markupOne = "One";
        var markupTwo = "Two";
        await using var testLspServer = await CreateTestLspServerAsync(new string[] { markupOne, markupTwo });
        var firstDocumentUri = testLspServer.GetCurrentSolution().Projects.First().Documents.Single(d => d.FilePath!.Contains("test1")).GetURI();
 
        var secondDocument = testLspServer.GetCurrentSolution().Projects.First().Documents.Single(d => d.FilePath!.Contains("test2"));
        var secondDocumentUri = secondDocument.GetURI();
 
        // Open one of the documents via LSP and verify we have created our LSP solution.
        await OpenDocumentAndVerifyLspTextAsync(firstDocumentUri, testLspServer);
 
        // Modify a closed document via the workspace.
        await testLspServer.TestWorkspace.ChangeDocumentAsync(secondDocument.Id, SourceText.From("Two is now three!", System.Text.Encoding.UTF8, SourceHashAlgorithms.Default));
 
        // Verify that the LSP solution has the LSP text from the open document.
        var (_, openedDocument) = await GetLspWorkspaceAndDocumentAsync(firstDocumentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(openedDocument);
        Assert.Equal("LSP text", (await openedDocument.GetTextAsync(CancellationToken.None)).ToString());
 
        // Verify that the LSP solution has the workspace text in the closed document.
        (_, secondDocument) = await GetLspWorkspaceAndDocumentAsync(secondDocumentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(secondDocument);
        Assert.Equal("Two is now three!", (await secondDocument.GetTextAsync()).ToString());
        Assert.NotEqual(testLspServer.TestWorkspace.CurrentSolution.GetDocument(secondDocument.Id), secondDocument);
    }
 
    [Fact]
    public async Task TestLspHasProjectChangesAsync()
    {
        var markup = "One";
        await using var testLspServer = await CreateTestLspServerAsync(markup);
        var documentUri = testLspServer.GetCurrentSolution().Projects.First().Documents.Single(d => d.FilePath!.Contains("test1")).GetURI();
 
        // Open the document via LSP and verify the initial project name.
        var openedDocument = await OpenDocumentAndVerifyLspTextAsync(documentUri, testLspServer, markup);
        Assert.Equal("Test", openedDocument?.Project.AssemblyName);
        Assert.Equal(testLspServer.TestWorkspace.CurrentSolution, openedDocument!.Project.Solution);
 
        // Modify the project via the workspace.
        var newProject = testLspServer.TestWorkspace.CurrentSolution.Projects.First().WithAssemblyName("NewCSProj1");
        await testLspServer.TestWorkspace.ChangeProjectAsync(newProject.Id, newProject.Solution);
 
        // Verify that the new LSP solution has the updated project info.
        (_, openedDocument) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(openedDocument);
        Assert.Equal(markup, (await openedDocument.GetTextAsync(CancellationToken.None)).ToString());
        Assert.Equal("NewCSProj1", openedDocument.Project.AssemblyName);
        Assert.Equal(testLspServer.TestWorkspace.CurrentSolution, openedDocument.Project.Solution);
    }
 
    [Fact]
    public async Task TestLspHasProjectChangesWithForkedTextAsync()
    {
        var markup = "One";
        await using var testLspServer = await CreateTestLspServerAsync(markup);
        var documentUri = testLspServer.GetCurrentSolution().Projects.First().Documents.Single(d => d.FilePath!.Contains("test1")).GetURI();
 
        // Open the document via LSP with different text from the workspace and verify the initial project name.
        var openedDocument = await OpenDocumentAndVerifyLspTextAsync(documentUri, testLspServer);
        Assert.Equal("Test", openedDocument?.Project.AssemblyName);
        Assert.NotEqual(testLspServer.TestWorkspace.CurrentSolution, openedDocument!.Project.Solution);
 
        // Modify the project via the workspace.
        var newProject = testLspServer.TestWorkspace.CurrentSolution.Projects.First().WithAssemblyName("NewCSProj1");
        await testLspServer.TestWorkspace.ChangeProjectAsync(newProject.Id, newProject.Solution);
 
        // Verify that the new LSP solution has the updated project info.
        (_, openedDocument) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(openedDocument);
        Assert.Equal("LSP text", (await openedDocument.GetTextAsync(CancellationToken.None)).ToString());
        Assert.Equal("NewCSProj1", openedDocument.Project.AssemblyName);
        Assert.NotEqual(testLspServer.TestWorkspace.CurrentSolution, openedDocument.Project.Solution);
    }
 
    [Fact]
    public async Task TestLspFindsNewDocumentAsync()
    {
        var markup = "One";
        await using var testLspServer = await CreateTestLspServerAsync(markup);
        var documentUri = testLspServer.GetCurrentSolution().Projects.First().Documents.Single(d => d.FilePath!.Contains("test1")).GetURI();
 
        // Open the document via LSP to create the initial LSP solution.
        await OpenDocumentAndVerifyLspTextAsync(documentUri, testLspServer, markup);
 
        // Add a new document to the workspace
        var newDocumentId = DocumentId.CreateNewId(testLspServer.TestWorkspace.CurrentSolution.ProjectIds[0]);
        var newSolution = testLspServer.TestWorkspace.CurrentSolution.AddDocument(newDocumentId, "NewDoc.cs", SourceText.From("New Doc", System.Text.Encoding.UTF8, SourceHashAlgorithms.Default), filePath: @"C:\NewDoc.cs");
        var newDocumentUri = newSolution.GetRequiredDocument(newDocumentId).GetURI();
        await testLspServer.TestWorkspace.ChangeSolutionAsync(newSolution);
 
        // Verify that the lsp server sees the workspace change and picks up the document in the correct workspace.
        await testLspServer.OpenDocumentAsync(newDocumentUri);
        var (_, lspDocument) = await GetLspWorkspaceAndDocumentAsync(newDocumentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(lspDocument);
        Assert.Equal(testLspServer.TestWorkspace.CurrentSolution, lspDocument.Project.Solution);
    }
 
    [Fact]
    public async Task TestLspTransfersDocumentToNewWorkspaceAsync()
    {
        var markup = "One";
 
        // Create a server that includes the LSP misc files workspace so we can test transfers to and from it.
        await using var testLspServer = await CreateTestLspServerAsync(markup, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer });
 
        // Create a new document, but do not update the workspace solution yet.
        var newDocumentId = DocumentId.CreateNewId(testLspServer.TestWorkspace.CurrentSolution.ProjectIds[0]);
        var newDocumentFilePath = @"C:/NewDoc.cs";
        var newDocumentInfo = DocumentInfo.Create(newDocumentId, "NewDoc.cs", filePath: newDocumentFilePath, loader: new TestTextLoader("New Doc"));
        var newDocumentUri = ProtocolConversions.GetUriFromFilePath(newDocumentFilePath);
 
        // Open the document via LSP before the workspace sees it.
        await testLspServer.OpenDocumentAsync(newDocumentUri, "LSP text");
 
        // Verify it is in the lsp misc workspace.
        var (miscWorkspace, miscDocument) = await GetLspWorkspaceAndDocumentAsync(newDocumentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(miscDocument);
        Assert.Equal(testLspServer.GetManagerAccessor().GetLspMiscellaneousFilesWorkspace(), miscWorkspace);
        Assert.Equal("LSP text", (await miscDocument.GetTextAsync(CancellationToken.None)).ToString());
 
        // Make a change and verify the misc document is updated.
        await testLspServer.InsertTextAsync(newDocumentUri, (0, 0, "More LSP text"));
        (_, miscDocument) = await GetLspWorkspaceAndDocumentAsync(newDocumentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(miscDocument);
        var miscText = await miscDocument.GetTextAsync(CancellationToken.None);
        Assert.Equal("More LSP textLSP text", miscText.ToString());
 
        // Update the registered workspace with the new document.
        await testLspServer.TestWorkspace.AddDocumentAsync(newDocumentInfo);
 
        // Verify that the newly added document in the registered workspace is returned.
        var (documentWorkspace, document) = await GetLspWorkspaceAndDocumentAsync(newDocumentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(document);
        Assert.Equal(testLspServer.TestWorkspace, documentWorkspace);
        Assert.Equal(newDocumentId, document.Id);
        // Verify we still are using the tracked LSP text for the document.
        var documentText = await document.GetTextAsync(CancellationToken.None);
        Assert.Equal("More LSP textLSP text", documentText.ToString());
    }
 
    [Fact]
    public async Task TestUsesRegisteredHostWorkspace()
    {
        var firstWorkspaceXml =
@$"<Workspace>
    <Project Language=""C#"" CommonReferences=""true"" AssemblyName=""FirstWorkspaceProject"">
        <Document FilePath=""C:\FirstWorkspace.cs"">FirstWorkspace</Document>
    </Project>
</Workspace>";
 
        var secondWorkspaceXml =
@$"<Workspace>
    <Project Language=""C#"" CommonReferences=""true"" AssemblyName=""SecondWorkspaceProject"">
        <Document FilePath=""C:\SecondWorkspace.cs"">SecondWorkspace</Document>
    </Project>
</Workspace>";
 
        await using var testLspServer = await CreateXmlTestLspServerAsync(firstWorkspaceXml);
        // Verify 1 workspace registered to start with.
        Assert.True(IsWorkspaceRegistered(testLspServer.TestWorkspace, testLspServer));
 
        using var testWorkspaceTwo = TestWorkspace.Create(
            XElement.Parse(secondWorkspaceXml),
            workspaceKind: "OtherWorkspaceKind",
            composition: testLspServer.TestWorkspace.Composition);
 
        // Wait for workspace creation operations for the second workspace to complete.
        await WaitForWorkspaceOperationsAsync(testWorkspaceTwo);
 
        // Manually register the workspace since the workspace listener does not listen for this workspace kind.
        var workspaceRegistrationService = testLspServer.TestWorkspace.GetService<LspWorkspaceRegistrationService>();
        workspaceRegistrationService.Register(testWorkspaceTwo);
 
        // Verify both workspaces registered.
        Assert.True(IsWorkspaceRegistered(testLspServer.TestWorkspace, testLspServer));
        Assert.True(IsWorkspaceRegistered(testWorkspaceTwo, testLspServer));
 
        // Verify the host workspace returned is the workspace with kind host.
        var (_, hostSolution) = await GetLspHostWorkspaceAndSolutionAsync(testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(hostSolution);
        Assert.Equal("FirstWorkspaceProject", hostSolution.Projects.First().Name);
    }
 
    [Fact]
    public async Task TestWorkspaceRequestFailsWhenHostWorkspaceMissing()
    {
        var firstWorkspaceXml =
@$"<Workspace>
    <Project Language=""C#"" CommonReferences=""true"" AssemblyName=""FirstWorkspaceProject"">
        <Document FilePath=""C:\FirstWorkspace.cs"">FirstWorkspace</Document>
    </Project>
</Workspace>";
 
        await using var testLspServer = await CreateXmlTestLspServerAsync(firstWorkspaceXml, workspaceKind: WorkspaceKind.MiscellaneousFiles);
        var exportProvider = testLspServer.TestWorkspace.ExportProvider;
 
        var workspaceRegistrationService = exportProvider.GetExport<LspWorkspaceRegistrationService>();
        Assert.Equal(WorkspaceKind.Host, workspaceRegistrationService.Value.GetHostWorkspaceKind());
 
        // Verify the workspace is registered.
        Assert.True(IsWorkspaceRegistered(testLspServer.TestWorkspace, testLspServer));
 
        // Verify there is not workspace matching the host workspace kind.
        var (_, solution) = await GetLspHostWorkspaceAndSolutionAsync(testLspServer).ConfigureAwait(false);
        Assert.Null(solution);
    }
 
    [Fact]
    public async Task TestLspUpdatesCorrectWorkspaceWithMultipleWorkspacesAsync()
    {
        var firstWorkspaceXml =
@$"<Workspace>
    <Project Language=""C#"" CommonReferences=""true"" AssemblyName=""FirstWorkspaceProject"">
        <Document FilePath=""C:\FirstWorkspace.cs"">FirstWorkspace</Document>
    </Project>
</Workspace>";
 
        var secondWorkspaceXml =
@$"<Workspace>
    <Project Language=""C#"" CommonReferences=""true"" AssemblyName=""SecondWorkspaceProject"">
        <Document FilePath=""C:\SecondWorkspace.cs"">SecondWorkspace</Document>
    </Project>
</Workspace>";
 
        await using var testLspServer = await CreateXmlTestLspServerAsync(firstWorkspaceXml);
 
        using var testWorkspaceTwo = CreateWorkspace(options: null, WorkspaceKind.MSBuild);
        testWorkspaceTwo.InitializeDocuments(XElement.Parse(secondWorkspaceXml));
 
        // Wait for workspace creation operations to complete for the second workspace.
        await WaitForWorkspaceOperationsAsync(testWorkspaceTwo);
 
        // Verify both workspaces registered.
        Assert.True(IsWorkspaceRegistered(testLspServer.TestWorkspace, testLspServer));
        Assert.True(IsWorkspaceRegistered(testWorkspaceTwo, testLspServer));
 
        var firstWorkspaceDocumentUri = ProtocolConversions.GetUriFromFilePath(@"C:\FirstWorkspace.cs");
        var secondWorkspaceDocumentUri = ProtocolConversions.GetUriFromFilePath(@"C:\SecondWorkspace.cs");
        await testLspServer.OpenDocumentAsync(firstWorkspaceDocumentUri);
 
        // Verify we can get both documents from their respective workspaces.
        var (firstWorkspace, firstDocument) = await GetLspWorkspaceAndDocumentAsync(firstWorkspaceDocumentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(firstDocument);
        Assert.Equal(firstWorkspaceDocumentUri, firstDocument.GetURI());
        Assert.Equal(testLspServer.TestWorkspace, firstWorkspace);
 
        var (secondWorkspace, secondDocument) = await GetLspWorkspaceAndDocumentAsync(secondWorkspaceDocumentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(secondDocument);
        Assert.Equal(secondWorkspaceDocumentUri, secondDocument.GetURI());
        Assert.Equal(testWorkspaceTwo, secondWorkspace);
 
        // Verify making an LSP change only changes the respective workspace and document.
        await testLspServer.InsertTextAsync(firstWorkspaceDocumentUri, (0, 0, "Change in first workspace"));
 
        // The first document should now different text.
        var (_, changedFirstDocument) = await GetLspWorkspaceAndDocumentAsync(firstWorkspaceDocumentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(changedFirstDocument);
        var changedFirstDocumentText = await changedFirstDocument.GetTextAsync(CancellationToken.None);
        var firstDocumentText = await firstDocument.GetTextAsync(CancellationToken.None);
        Assert.NotEqual(firstDocumentText, changedFirstDocumentText);
 
        // The second document should return the same document instance since it was not changed.
        var (_, unchangedSecondDocument) = await GetLspWorkspaceAndDocumentAsync(secondWorkspaceDocumentUri, testLspServer).ConfigureAwait(false);
        Assert.Equal(secondDocument, unchangedSecondDocument);
    }
 
    [Fact]
    public async Task TestWorkspaceEventUpdatesCorrectWorkspaceWithMultipleWorkspacesAsync()
    {
        var firstWorkspaceXml =
@$"<Workspace>
    <Project Language=""C#"" CommonReferences=""true"" AssemblyName=""FirstWorkspaceProject"">
        <Document FilePath=""C:\FirstWorkspace.cs"">FirstWorkspace</Document>
    </Project>
</Workspace>";
 
        var secondWorkspaceXml =
@$"<Workspace>
    <Project Language=""C#"" CommonReferences=""true"" AssemblyName=""SecondWorkspaceProject"">
        <Document FilePath=""C:\SecondWorkspace.cs"">SecondWorkspace</Document>
    </Project>
</Workspace>";
 
        await using var testLspServer = await CreateXmlTestLspServerAsync(firstWorkspaceXml);
 
        using var testWorkspaceTwo = CreateWorkspace(options: null, workspaceKind: WorkspaceKind.MSBuild);
        testWorkspaceTwo.InitializeDocuments(XElement.Parse(secondWorkspaceXml));
 
        // Wait for workspace operations to complete for the second workspace.
        await WaitForWorkspaceOperationsAsync(testWorkspaceTwo);
 
        var firstWorkspaceDocumentUri = ProtocolConversions.GetUriFromFilePath(@"C:\FirstWorkspace.cs");
        var secondWorkspaceDocumentUri = ProtocolConversions.GetUriFromFilePath(@"C:\SecondWorkspace.cs");
        await testLspServer.OpenDocumentAsync(firstWorkspaceDocumentUri);
 
        // Verify we can get both documents from their respective workspaces.
        var (firstWorkspace, firstDocument) = await GetLspWorkspaceAndDocumentAsync(firstWorkspaceDocumentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(firstDocument);
        Assert.Equal(firstWorkspaceDocumentUri, firstDocument.GetURI());
        Assert.Equal(testLspServer.TestWorkspace, firstWorkspace);
 
        var (secondWorkspace, secondDocument) = await GetLspWorkspaceAndDocumentAsync(secondWorkspaceDocumentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(secondDocument);
        Assert.Equal(secondWorkspaceDocumentUri, secondDocument.GetURI());
        Assert.Equal(testWorkspaceTwo, secondWorkspace);
 
        // Verify making a workspace change only changes the respective workspace.
        var newProjectWorkspaceTwo = testWorkspaceTwo.CurrentSolution.Projects.First().WithAssemblyName("NewCSProj1");
        await testWorkspaceTwo.ChangeProjectAsync(newProjectWorkspaceTwo.Id, newProjectWorkspaceTwo.Solution);
 
        // The second document should have an updated project assembly name.
        var (_, secondDocumentChangedProject) = await GetLspWorkspaceAndDocumentAsync(secondWorkspaceDocumentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(secondDocumentChangedProject);
        Assert.Equal("NewCSProj1", secondDocumentChangedProject.Project.AssemblyName);
        Assert.NotEqual(secondDocument, secondDocumentChangedProject);
 
        // The first document should be the same document as the last one since that workspace was not changed.
        var (_, lspDocument) = await GetLspWorkspaceAndDocumentAsync(firstWorkspaceDocumentUri, testLspServer).ConfigureAwait(false);
        Assert.Equal(firstDocument, lspDocument);
    }
 
    [Fact]
    public async Task TestSeparateWorkspaceManagerPerServerAsync()
    {
        var workspaceXml =
@$"<Workspace>
    <Project Language=""C#"" CommonReferences=""true"" AssemblyName=""CSProj1"">
        <Document FilePath=""C:\test1.cs"">Original text</Document>
    </Project>
</Workspace>";
 
        using var testWorkspace = CreateWorkspace(options: null, workspaceKind: null);
        testWorkspace.InitializeDocuments(XElement.Parse(workspaceXml));
 
        // Wait for workspace creation operations to complete.
        await WaitForWorkspaceOperationsAsync(testWorkspace);
 
        var documentUri = testWorkspace.CurrentSolution.Projects.First().Documents.First().GetURI();
 
        await using var testLspServerOne = await TestLspServer.CreateAsync(testWorkspace, new InitializationOptions(), TestOutputLspLogger);
        await using var testLspServerTwo = await TestLspServer.CreateAsync(testWorkspace, new InitializationOptions(), TestOutputLspLogger);
 
        Assert.NotEqual(testLspServerOne.GetManager(), testLspServerTwo.GetManager());
 
        // Verify workspace is registered with both servers.
        Assert.True(IsWorkspaceRegistered(testWorkspace, testLspServerOne));
        Assert.True(IsWorkspaceRegistered(testWorkspace, testLspServerTwo));
 
        // Verify that the LSP solution uses the correct text for each server.
        var documentServerOne = await OpenDocumentAndVerifyLspTextAsync(documentUri, testLspServerOne, "Server one text");
 
        var (_, documentServerTwo) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServerTwo).ConfigureAwait(false);
        AssertEx.NotNull(documentServerTwo);
        Assert.Equal("Original text", (await documentServerTwo.GetTextAsync(CancellationToken.None)).ToString());
 
        // Verify workspace updates are reflected in both servers.
        var newAssemblyName = "NewCSProj1";
        var newProject = testWorkspace.CurrentSolution.Projects.First().WithAssemblyName(newAssemblyName);
        await testWorkspace.ChangeProjectAsync(newProject.Id, newProject.Solution);
 
        // Verify LSP solution has the project changes.
        (_, documentServerOne) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServerOne).ConfigureAwait(false);
        AssertEx.NotNull(documentServerOne);
        Assert.Equal(newAssemblyName, documentServerOne.Project.AssemblyName);
        (_, documentServerTwo) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServerTwo).ConfigureAwait(false);
        AssertEx.NotNull(documentServerTwo);
        Assert.Equal(newAssemblyName, documentServerTwo.Project.AssemblyName);
    }
 
    [Fact]
    public async Task TestDoesNotForkWhenDocumentTextBufferOpenedAsync()
    {
        var markup = "Text";
        await using var testLspServer = await CreateTestLspServerAsync(markup);
        var documentUri = testLspServer.GetCurrentSolution().Projects.First().Documents.First().GetURI();
 
        // Calling get text buffer opens the document in the workspace.
        testLspServer.TestWorkspace.Documents.Single().GetTextBuffer();
 
        await testLspServer.OpenDocumentAsync(documentUri, "Text");
 
        var (_, lspDocument) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(lspDocument);
        Assert.Equal("Text", (await lspDocument.GetTextAsync(CancellationToken.None)).ToString());
 
        Assert.Same(testLspServer.TestWorkspace.CurrentSolution, lspDocument.Project.Solution);
    }
 
    private static async Task<Document> OpenDocumentAndVerifyLspTextAsync(Uri documentUri, TestLspServer testLspServer, string openText = "LSP text")
    {
        await testLspServer.OpenDocumentAsync(documentUri, openText);
 
        // Verify we can find the document with correct text in the new LSP solution.
        var (_, lspDocument) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServer).ConfigureAwait(false);
        AssertEx.NotNull(lspDocument);
        Assert.Equal(openText, (await lspDocument.GetTextAsync(CancellationToken.None)).ToString());
        return lspDocument;
    }
 
    private static bool IsWorkspaceRegistered(Workspace workspace, TestLspServer testLspServer)
    {
        return testLspServer.GetManagerAccessor().IsWorkspaceRegistered(workspace);
    }
 
    private static async Task<(Workspace? workspace, Document? document)> GetLspWorkspaceAndDocumentAsync(Uri uri, TestLspServer testLspServer)
    {
        var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(CreateTextDocumentIdentifier(uri), CancellationToken.None).ConfigureAwait(false);
        return (workspace, document);
    }
 
    private static Task<(Workspace?, Solution?)> GetLspHostWorkspaceAndSolutionAsync(TestLspServer testLspServer)
    {
        return testLspServer.GetManager().GetLspSolutionInfoAsync(CancellationToken.None);
    }
}