/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs
735 строк
43 KB
David Barbet
Refactor LSP server logging to remove singleton lsp instance (#84001)
16 июн 2026, 05:04
Не верифицирован
16 июн 2026, 05:04
82e81e5
Код
Авторство
О чём код?
// 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.LanguageServer.UnitTests.MiscellaneousFiles; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; using Roslyn.Test.Utilities.TestGenerators; using Roslyn.Utilities; using Xunit; using Xunit.Abstractions; namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.Workspaces; public sealed class LspWorkspaceManagerTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerProtocolTests(testOutputHelper) { [Theory, CombinatorialData] public async Task TestUsesLspTextOnOpenCloseAsync(bool mutatingLspWorkspace) { var markup = ""; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); var documentUri = testLspServer.GetCurrentSolution().Projects.First().Documents.First().GetURI(); await testLspServer.OpenDocumentAsync(documentUri, "LSP text"); var (_, lspDocument) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServer).ConfigureAwait(false); Assert.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); Assert.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); } [Theory, CombinatorialData] public async Task TestLspUsesWorkspaceInstanceOnChangesAsync(bool mutatingLspWorkspace) { var markupOne = "One"; var markupTwo = "Two"; await using var testLspServer = await CreateTestLspServerAsync([markupOne, markupTwo], mutatingLspWorkspace); 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); if (mutatingLspWorkspace) { // In the mutating case, adding/opening the second document will cause the workspace solution to actually // change. So it will point to a different document instance for firstDocument. The underlying state will // be the same though. Assert.NotSame(testLspServer.TestWorkspace.CurrentSolution.GetDocument(firstDocument.Id), firstDocument); Assert.Same(testLspServer.TestWorkspace.CurrentSolution.GetDocument(firstDocument.Id)?.State, firstDocument?.State); } else { // 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")); Assert.NotNull(firstDocument); 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); Assert.NotNull(firstDocumentWithChange); Assert.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); } [Theory, CombinatorialData] public async Task TestLspHasClosedDocumentChangesAsync(bool mutatingLspWorkspace) { var markupOne = "One"; var markupTwo = "Two"; await using var testLspServer = await CreateTestLspServerAsync([markupOne, markupTwo], mutatingLspWorkspace); 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); Assert.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); Assert.NotNull(secondDocument); Assert.Equal("Two is now three!", (await secondDocument.GetTextAsync()).ToString()); if (mutatingLspWorkspace) { // In the mutating case, opening the second doc pushes its changes through to the underlying workspace. So // the documents will be the same. Assert.Equal(testLspServer.TestWorkspace.CurrentSolution.GetDocument(secondDocument.Id), secondDocument); } else { Assert.NotEqual(testLspServer.TestWorkspace.CurrentSolution.GetDocument(secondDocument.Id), secondDocument); } } [Theory, CombinatorialData] public async Task TestLspHasProjectChangesAsync(bool mutatingLspWorkspace) { var markup = "One"; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); 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); Assert.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); } [Theory, CombinatorialData] public async Task TestLspHasProjectChangesWithForkedTextAsync(bool mutatingLspWorkspace) { var markup = "One"; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); 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); // In the mutating case, the changes are pushed through such that the solutions are the same. if (mutatingLspWorkspace) { Assert.Equal(testLspServer.TestWorkspace.CurrentSolution, openedDocument!.Project.Solution); } else { 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); Assert.NotNull(openedDocument); Assert.Equal("LSP text", (await openedDocument.GetTextAsync(CancellationToken.None)).ToString()); Assert.Equal("NewCSProj1", openedDocument.Project.AssemblyName); // In the mutating case, the changes are pushed through such that the solutions are the same. if (mutatingLspWorkspace) { Assert.Equal(testLspServer.TestWorkspace.CurrentSolution, openedDocument.Project.Solution); } else { Assert.NotEqual(testLspServer.TestWorkspace.CurrentSolution, openedDocument.Project.Solution); } } [Theory, CombinatorialData] public async Task TestLspFindsNewDocumentAsync(bool mutatingLspWorkspace) { var markup = "One"; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); 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); Assert.NotNull(lspDocument); Assert.Equal(testLspServer.TestWorkspace.CurrentSolution, lspDocument.Project.Solution); } [Theory, CombinatorialData] public async Task TestUsesRegisteredHostWorkspace(bool mutatingLspWorkspace) { 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, mutatingLspWorkspace); // Verify 1 workspace registered to start with. Assert.True(IsWorkspaceRegistered(testLspServer.TestWorkspace, testLspServer)); using var testWorkspaceTwo = TestWorkspace.Create( XElement.Parse(secondWorkspaceXml), exportProvider: testLspServer.TestWorkspace.ExportProvider, workspaceKind: "OtherWorkspaceKind"); // 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 lspWorkspaceRegistrationListener = testLspServer.TestWorkspace.ExportProvider.GetExportedValue<LspWorkspaceRegistrationEventListener>(); lspWorkspaceRegistrationListener.StartListening(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); Assert.NotNull(hostSolution); Assert.Equal("FirstWorkspaceProject", hostSolution.Projects.First().Name); } [Theory, CombinatorialData] public async Task TestWorkspaceRequestFailsWhenHostWorkspaceMissing(bool mutatingLspWorkspace) { 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, mutatingLspWorkspace, workspaceKind: WorkspaceKind.MiscellaneousFiles); // 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); } [Theory, CombinatorialData] public async Task TestLspUpdatesCorrectWorkspaceWithMultipleWorkspacesAsync(bool mutatingLspWorkspace) { var firstWorkspaceXml = $""" <Workspace> <Project Language="C#" CommonReferences="true" AssemblyName="FirstWorkspaceProject"> <Document FilePath="{TestHelpers.CreateAbsolutePath("FirstWorkspace.cs")}">FirstWorkspace</Document> </Project> </Workspace> """; await using var testLspServer = await CreateXmlTestLspServerAsync(firstWorkspaceXml, mutatingLspWorkspace); using var testWorkspaceTwo = await CreateWorkspaceAsync(options: null, WorkspaceKind.MSBuild, mutatingLspWorkspace); testWorkspaceTwo.InitializeDocuments(XElement.Parse($""" <Workspace> <Project Language="C#" CommonReferences="true" AssemblyName="SecondWorkspaceProject"> <Document FilePath="{TestHelpers.CreateAbsolutePath("SecondWorkspace.cs")}">SecondWorkspace</Document> </Project> </Workspace> """)); // 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 = CreateAbsoluteDocumentUri("FirstWorkspace.cs"); var secondWorkspaceDocumentUri = CreateAbsoluteDocumentUri("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); Assert.NotNull(firstDocument); Assert.Equal(firstWorkspaceDocumentUri, firstDocument.GetURI()); Assert.Equal(testLspServer.TestWorkspace, firstWorkspace); var (secondWorkspace, secondDocument) = await GetLspWorkspaceAndDocumentAsync(secondWorkspaceDocumentUri, testLspServer).ConfigureAwait(false); Assert.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); Assert.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); } [Theory, CombinatorialData] public async Task TestWorkspaceEventUpdatesCorrectWorkspaceWithMultipleWorkspacesAsync(bool mutatingLspWorkspace) { var firstWorkspaceXml = $""" <Workspace> <Project Language="C#" CommonReferences="true" AssemblyName="FirstWorkspaceProject"> <Document FilePath="{TestHelpers.CreateAbsolutePath("FirstWorkspace.cs")}">FirstWorkspace</Document> </Project> </Workspace> """; await using var testLspServer = await CreateXmlTestLspServerAsync(firstWorkspaceXml, mutatingLspWorkspace); using var testWorkspaceTwo = await CreateWorkspaceAsync(options: null, workspaceKind: WorkspaceKind.MSBuild, mutatingLspWorkspace); testWorkspaceTwo.InitializeDocuments(XElement.Parse($""" <Workspace> <Project Language="C#" CommonReferences="true" AssemblyName="SecondWorkspaceProject"> <Document FilePath="{TestHelpers.CreateAbsolutePath("SecondWorkspace.cs")}">SecondWorkspace</Document> </Project> </Workspace> """)); // Wait for workspace operations to complete for the second workspace. await WaitForWorkspaceOperationsAsync(testWorkspaceTwo); var firstWorkspaceDocumentUri = CreateAbsoluteDocumentUri("FirstWorkspace.cs"); var secondWorkspaceDocumentUri = CreateAbsoluteDocumentUri("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); Assert.NotNull(firstDocument); Assert.Equal(firstWorkspaceDocumentUri, firstDocument.GetURI()); Assert.Equal(testLspServer.TestWorkspace, firstWorkspace); var (secondWorkspace, secondDocument) = await GetLspWorkspaceAndDocumentAsync(secondWorkspaceDocumentUri, testLspServer).ConfigureAwait(false); Assert.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); Assert.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); } [Theory, CombinatorialData] public async Task TestSeparateWorkspaceManagerPerServerAsync(bool mutatingLspWorkspace) { using var testWorkspace = await CreateWorkspaceAsync(options: null, workspaceKind: null, mutatingLspWorkspace); testWorkspace.InitializeDocuments(XElement.Parse($""" <Workspace> <Project Language="C#" CommonReferences="true" AssemblyName="CSProj1"> <Document FilePath="C:\test1.cs">Original text</Document> </Project> </Workspace> """)); await using var testLspServerOne = await TestLspServer.CreateAsync(testWorkspace, new InitializationOptions(), TestOutputHelper); await using var testLspServerTwo = await TestLspServer.CreateAsync(testWorkspace, new InitializationOptions(), TestOutputHelper); 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 documentUri = testWorkspace.CurrentSolution.Projects.First().Documents.First().GetURI(); var documentServerOne = await OpenDocumentAndVerifyLspTextAsync(documentUri, testLspServerOne, "Server one text"); var (_, documentServerTwo) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServerTwo).ConfigureAwait(false); Assert.NotNull(documentServerTwo); if (mutatingLspWorkspace) { // If the underlying workspace is getting mutated, then given that the second lsp-server hasn't heard // anything about doc-1 yet, it will see the change pushed through by the first lsp-server. Assert.Equal("Server one text", (await documentServerTwo.GetTextAsync(CancellationToken.None)).ToString()); } else { 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); Assert.NotNull(documentServerOne); Assert.Equal(newAssemblyName, documentServerOne.Project.AssemblyName); (_, documentServerTwo) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServerTwo).ConfigureAwait(false); Assert.NotNull(documentServerTwo); Assert.Equal(newAssemblyName, documentServerTwo.Project.AssemblyName); } [Theory, CombinatorialData] public async Task TestDoesNotForkWhenDocumentOpenedInWorkspaceAsync(bool mutatingLspWorkspace) { var markup = "Text"; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); var document = testLspServer.GetCurrentSolution().Projects.First().Documents.First(); var documentUri = testLspServer.GetCurrentSolution().Projects.First().Documents.First().GetURI(); // Open document in the workspace. await testLspServer.OpenDocumentInWorkspaceAsync(document.Id, openAllLinkedDocuments: false); await testLspServer.OpenDocumentAsync(documentUri, "Text"); var (_, lspDocument) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServer).ConfigureAwait(false); Assert.NotNull(lspDocument); Assert.Equal("Text", (await lspDocument.GetTextAsync(CancellationToken.None)).ToString()); Assert.Same(testLspServer.TestWorkspace.CurrentSolution, lspDocument.Project.Solution); } [Fact] public async Task TestLspDocumentPreferredOverProjectSystemDocumentAddInMutatingWorkspace() { // This verifies that we will still see the lsp view of the document, even though it found out about a document // prior to a project system even telling it about a file, and even if the project system removes the file. // Start with an empty workspace and composition with misc files provider. var composition = this.Composition.AddParts(typeof(TestLspMiscellaneousFilesWorkspaceProviderFactory)); await using var testLspServer = await CreateTestLspServerAsync( [], mutatingLspWorkspace: true, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }, composition: composition); // Open the doc var filePath = TestHelpers.CreateAbsolutePath("\ue25b\ud86d\udeac.cs"); var documentUri = ProtocolConversions.CreateAbsoluteDocumentUri(filePath); await testLspServer.OpenDocumentAsync(documentUri, "Text"); // Initially the doc will be in the lsp misc workspace. var (workspace1, document1) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServer).ConfigureAwait(false); Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace1?.Kind); Assert.NotNull(document1); Assert.Equal("Text", (await document1.GetTextAsync(CancellationToken.None)).ToString()); // Now, add a project to the actual workspace containing Test1.cs with different content. await testLspServer.TestWorkspace.ChangeSolutionAsync( testLspServer.TestWorkspace.CurrentSolution.Projects.Single() .AddDocument(filePath, SourceText.From("ProjectSystemText"), filePath: filePath) .Project.Solution); // if we were to directly query the workspace right now, the contents won't match what lsp thinks it is: Assert.NotEqual( (await document1.GetTextAsync(CancellationToken.None)).ToString(), (await testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.Single().GetTextAsync()).ToString()); // However, if we retrieve from lsp now, we'll see the document moved into the real workspace and we'll see the // real workspace contents changed. (workspace1, document1) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServer).ConfigureAwait(false); Assert.Equal(WorkspaceKind.Host, workspace1?.Kind); Assert.NotNull(document1); Assert.Equal("Text", (await document1.GetTextAsync(CancellationToken.None)).ToString()); // Retrieving the document from lsp should push its content into the workspace. Assert.Equal( (await document1.GetTextAsync(CancellationToken.None)).ToString(), (await testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.Single().GetTextAsync()).ToString()); // The file should not be in the misc workspace. Assert.Empty(await testLspServer.GetManagerAccessor().GetMiscellaneousDocumentsAsync(static p => p.Documents).ToImmutableArrayAsync(CancellationToken.None)); // Now, if the project system removes the file, we will still see the lsp version of, but back to the misc workspace. await testLspServer.TestWorkspace.ChangeSolutionAsync( testLspServer.TestWorkspace.CurrentSolution.Projects.Single().RemoveDocument(document1.Id).Solution); (workspace1, document1) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServer).ConfigureAwait(false); Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace1?.Kind); Assert.NotNull(document1); Assert.Equal("Text", (await document1.GetTextAsync(CancellationToken.None)).ToString()); } [Fact] public async Task TestLspDocumentPreferredOverProjectSystemDocumentChangeInMutatingWorkspace() { // This verifies that we will still see the lsp view of the document, even if the project system feeds in // external information about the contents of the file. // Start with an empty workspace. await using var testLspServer = await CreateTestLspServerAsync( "Initial Disk Contents", mutatingLspWorkspace: true, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); var documentUri = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.Single().GetURI(); await testLspServer.OpenDocumentAsync(documentUri, "Text"); var (workspace, document) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServer).ConfigureAwait(false); Assert.Equal(WorkspaceKind.Host, workspace?.Kind); // We should see the contents lsp told us about. Assert.NotNull(document); Assert.Equal("Text", (await document.GetTextAsync(CancellationToken.None)).ToString()); // Now, explicitly update the workspace, simulating the project system externally changing it. await testLspServer.TestWorkspace.ChangeSolutionAsync( testLspServer.TestWorkspace.CurrentSolution.WithDocumentText(document.Id, SourceText.From("New Disk Contents"))); // Querying the workspace directly, prior to an LSP call will show the new contents. document = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.Single(); Assert.Equal("New Disk Contents", (await document.GetTextAsync()).ToString()); // However, once we retrieve from LSP, the lsp contents should be pushed back into the workspace. (workspace, document) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServer).ConfigureAwait(false); Assert.NotNull(document); Assert.Equal("Text", (await document.GetTextAsync(CancellationToken.None)).ToString()); document = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.Single(); Assert.Equal("Text", (await document.GetTextAsync()).ToString()); } [Fact] public async Task TestLspDocumentChangedInMutatingWorkspaceIncrementallyParses() { // This verifies that we will still see the lsp view of the document, even if the project system feeds in // external information about the contents of the file. // Start with an empty workspace. await using var testLspServer = await CreateTestLspServerAsync( "Initial Disk Contents", mutatingLspWorkspace: true, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); var initialContents = "namespace N { class C1 { void X() { } } class C2 { void Y() { } } class C3 { void Z() { } } }"; var documentUri = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.Single().GetURI(); await testLspServer.OpenDocumentAsync(documentUri, initialContents); var (workspace, originalDocument) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServer).ConfigureAwait(false); Assert.Equal(WorkspaceKind.Host, workspace?.Kind); // We should see the contents lsp told us about. Assert.NotNull(originalDocument); var originalSourceText = await originalDocument.GetTextAsync(CancellationToken.None); var originalRoot = await originalDocument.GetRequiredSyntaxRootAsync(CancellationToken.None); Assert.Equal(initialContents, originalSourceText.ToString()); // Change "C3 => D3" await testLspServer.ReplaceTextAsync(documentUri, (ProtocolConversions.TextSpanToRange(new TextSpan(initialContents.IndexOf("C3"), 1), originalSourceText), "D")); var (_, newDocument) = await GetLspWorkspaceAndDocumentAsync(documentUri, testLspServer).ConfigureAwait(false); Assert.NotNull(newDocument); var newSourceText = await newDocument.GetTextAsync(CancellationToken.None); var newRoot = await newDocument.GetRequiredSyntaxRootAsync(CancellationToken.None); Assert.Equal("namespace N { class C1 { void X() { } } class C2 { void Y() { } } class D3 { void Z() { } } }", newSourceText.ToString()); // Demonstrate that incremental parsing is working by showing that the first class is reused across edits, while // the last class is not. We don't test the second as that one may or may not be reused depending on how good // incremental parsing may be. For example, sometimes it will conservatively not reuse a node because that node // touches a node that is getting recreated. var syntaxFacts = originalDocument.GetRequiredLanguageService<ISyntaxFactsService>(); var oldClassDeclarations = originalRoot.DescendantNodes().WhereAsArray(n => syntaxFacts.IsClassDeclaration(n)); var newClassDeclarations = newRoot.DescendantNodes().WhereAsArray(n => syntaxFacts.IsClassDeclaration(n)); Assert.Equal(oldClassDeclarations.Length, newClassDeclarations.Length); Assert.Equal(3, oldClassDeclarations.Length); Assert.True(oldClassDeclarations[0].IsIncrementallyIdenticalTo(newClassDeclarations[0])); Assert.False(oldClassDeclarations[2].IsIncrementallyIdenticalTo(newClassDeclarations[2])); // All the methods will get reused. var oldMethodDeclarations = originalRoot.DescendantNodes().WhereAsArray(n => syntaxFacts.IsMethodLevelMember(n)); var newMethodDeclarations = newRoot.DescendantNodes().WhereAsArray(n => syntaxFacts.IsMethodLevelMember(n)); Assert.Equal(oldMethodDeclarations.Length, newMethodDeclarations.Length); Assert.Equal(3, oldMethodDeclarations.Length); Assert.True(oldMethodDeclarations[0].IsIncrementallyIdenticalTo(newMethodDeclarations[0])); Assert.True(oldMethodDeclarations[1].IsIncrementallyIdenticalTo(newMethodDeclarations[1])); Assert.True(oldMethodDeclarations[2].IsIncrementallyIdenticalTo(newMethodDeclarations[2])); } [Theory, CombinatorialData] public async Task TestDoesNotUseForkForUnchangedGeneratedFileAsync(bool mutatingLspWorkspace) { var generatorText = "// Hello World!"; await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace); await AddGeneratorAsync(new SingleFileTestGenerator(generatorText), testLspServer.TestWorkspace); var sourceGeneratedDocuments = await testLspServer.GetCurrentSolution().Projects.Single().GetSourceGeneratedDocumentsAsync(); var sourceGeneratedDocumentIdentity = sourceGeneratedDocuments.Single().Identity; var sourceGeneratorDocumentUri = SourceGeneratedDocumentUri.Create(sourceGeneratedDocumentIdentity); // The workspace manager calls WithFrozenSourceGeneratedDocuments with the current DateTime. If that date time // is the same as the generation date time, it does not fork. To ensure that it is not the same in tests, we explicitly // wait a second to ensure the request datetime does not match the generated datetime. await Task.Delay(TimeSpan.FromSeconds(1)); var sourceGeneratedDocument = await OpenDocumentAndVerifyLspTextAsync(sourceGeneratorDocumentUri, testLspServer, generatorText) as SourceGeneratedDocument; Assert.NotNull(sourceGeneratedDocument); Assert.Same(testLspServer.TestWorkspace.CurrentSolution, sourceGeneratedDocument.Project.Solution); } [Theory, CombinatorialData] public async Task TestForksWithDifferentGeneratedContentsAsync(bool mutatingLspWorkspace) { var lspGeneratedText = "// Hello LSP!"; await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace); await AddGeneratorAsync(new SingleFileTestGenerator("// Hello World!"), testLspServer.TestWorkspace); var sourceGeneratedDocuments = await testLspServer.GetCurrentSolution().Projects.Single().GetSourceGeneratedDocumentsAsync(); var sourceGeneratedDocumentIdentity = sourceGeneratedDocuments.Single().Identity; var sourceGeneratorDocumentUri = SourceGeneratedDocumentUri.Create(sourceGeneratedDocumentIdentity); var sourceGeneratedDocument = await OpenDocumentAndVerifyLspTextAsync(sourceGeneratorDocumentUri, testLspServer, lspGeneratedText) as SourceGeneratedDocument; Assert.NotNull(sourceGeneratedDocument); Assert.NotSame(testLspServer.TestWorkspace.CurrentSolution, sourceGeneratedDocument.Project.Solution); } [Theory, CombinatorialData] public async Task TestForksWithRemovedGeneratorAsync(bool mutatingLspWorkspace) { var generatorText = "// Hello World!"; await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace); var generatorReference = await AddGeneratorAsync(new SingleFileTestGenerator(generatorText), testLspServer.TestWorkspace); var sourceGeneratedDocuments = await testLspServer.GetCurrentSolution().Projects.Single().GetSourceGeneratedDocumentsAsync(); var sourceGeneratedDocumentIdentity = sourceGeneratedDocuments.Single().Identity; var sourceGeneratorDocumentUri = SourceGeneratedDocumentUri.Create(sourceGeneratedDocumentIdentity); var sourceGeneratedDocument = await OpenDocumentAndVerifyLspTextAsync(sourceGeneratorDocumentUri, testLspServer, generatorText) as SourceGeneratedDocument; Assert.NotNull(sourceGeneratedDocument); Assert.Same(testLspServer.TestWorkspace.CurrentSolution, sourceGeneratedDocument.Project.Solution); // Remove the generator and verify the document is forked. await RemoveGeneratorAsync(generatorReference, testLspServer.TestWorkspace); var (_, removedSourceGeneratorDocument) = await GetLspWorkspaceAndDocumentAsync(sourceGeneratorDocumentUri, testLspServer).ConfigureAwait(false); Assert.NotNull(sourceGeneratedDocument as SourceGeneratedDocument); Assert.Equal(generatorText, (await sourceGeneratedDocument.GetTextAsync(CancellationToken.None)).ToString()); Assert.NotSame(testLspServer.TestWorkspace.CurrentSolution, sourceGeneratedDocument.Project.Solution); } private static async Task<Document> OpenDocumentAndVerifyLspTextAsync(DocumentUri 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); Assert.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(DocumentUri uri, TestLspServer testLspServer) { var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(CreateTextDocumentIdentifier(uri), CancellationToken.None).ConfigureAwait(false); return (workspace, document as Document); } private static Task<(Workspace?, Solution?)> GetLspHostWorkspaceAndSolutionAsync(TestLspServer testLspServer) { return testLspServer.GetManager().GetLspSolutionInfoAsync(CancellationToken.None); } }