From ffa28b8773a8d1142eae46aeaec1810c008d1082 Mon Sep 17 00:00:00 2001 From: David Barbet Date: Fri, 27 Mar 2020 15:44:52 -0700 Subject: [PATCH] Add tests and fix bugs --- ...odeAnalysis.LanguageServer.Protocol.csproj | 1 + .../LanguageClient/InProcLanguageServer.cs | 19 +- .../Test.Next/Services/LspDiagnosticsTests.cs | 470 ++++++++++++++++++ 3 files changed, 485 insertions(+), 5 deletions(-) create mode 100644 src/VisualStudio/Core/Test.Next/Services/LspDiagnosticsTests.cs diff --git a/src/Features/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj b/src/Features/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj index 10b56fa28fc..2edfabc7f20 100644 --- a/src/Features/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj +++ b/src/Features/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj @@ -34,6 +34,7 @@ + diff --git a/src/VisualStudio/Core/Def/Implementation/LanguageClient/InProcLanguageServer.cs b/src/VisualStudio/Core/Def/Implementation/LanguageClient/InProcLanguageServer.cs index de4d2ec7a01..d12944c324f 100644 --- a/src/VisualStudio/Core/Def/Implementation/LanguageClient/InProcLanguageServer.cs +++ b/src/VisualStudio/Core/Def/Implementation/LanguageClient/InProcLanguageServer.cs @@ -249,7 +249,7 @@ private async void DiagnosticService_DiagnosticsUpdated(object sender, Diagnosti /// private readonly Dictionary> _documentsToPublishedUris = new Dictionary>(); - private async Task PublishDiagnosticsAsync(Document document) + internal async Task PublishDiagnosticsAsync(Document document) { // Retrieve all diagnostics for the current document grouped by their actual file uri. var fileUriToDiagnostics = await GetDiagnosticsAsync(document, CancellationToken.None).ConfigureAwait(false); @@ -257,7 +257,7 @@ private async Task PublishDiagnosticsAsync(Document document) // Get the list of file uris with diagnostics (for the document). // We need to join the uris from current diagnostics with those previously published // so that we clear out any diagnostics in mapped files that are no longer a part - // of the current diagnostics set (because the diagnostics was fixed). + // of the current diagnostics set (because the diagnostics were fixed). var urisForCurrentDocument = GetOrValue(_documentsToPublishedUris, document.Id, ImmutableHashSet.Empty).Union(fileUriToDiagnostics.Keys); // Update the mapping for this document to be the uris we're about to publish diagnostics for. @@ -289,9 +289,18 @@ private async Task PublishDiagnosticsAsync(Document document) _documentsToPublishedUris[document.Id] = _documentsToPublishedUris[document.Id].Remove(fileUri); } - // Update the published diagnostics map to contain the new diagnostics for this document and mapped uri. - _publishedFileToDiagnostics.GetOrAdd(fileUri, - (_) => { return new Dictionary>(); })[document.Id] = diagnostics; + // Update the published diagnostics map to contain the new diagnostics contributed by this document and fileUri. + var documentsToPublishedDiagnostics = _publishedFileToDiagnostics.GetOrAdd(fileUri, (_) => + new Dictionary>()); + if (fileUriToDiagnostics.ContainsKey(fileUri)) + { + documentsToPublishedDiagnostics[document.Id] = fileUriToDiagnostics[fileUri]; + } + else + { + // There are no new diagnostics for this document and file uri, if we're tracking it we can stop. + documentsToPublishedDiagnostics.Remove(document.Id); + } } } diff --git a/src/VisualStudio/Core/Test.Next/Services/LspDiagnosticsTests.cs b/src/VisualStudio/Core/Test.Next/Services/LspDiagnosticsTests.cs new file mode 100644 index 00000000000..6332eff70cf --- /dev/null +++ b/src/VisualStudio/Core/Test.Next/Services/LspDiagnosticsTests.cs @@ -0,0 +1,470 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServices.Implementation.LanguageService; +using Moq; +using Nerdbank.Streams; +using Newtonsoft.Json.Linq; +using Roslyn.Test.Utilities; +using Roslyn.Utilities; +using StreamJsonRpc; +using Xunit; +using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Roslyn.VisualStudio.Next.UnitTests.Services +{ + [UseExportProvider] + public class LspDiagnosticsTests : AbstractLanguageServerProtocolTests + { + [Fact] + public async Task AddDiagnosticTestAsync() + { + using var workspace = CreateTestWorkspace("", out _); + var document = workspace.CurrentSolution.Projects.First().Documents.First(); + + var diagnosticsMock = new Mock(); + // Create a mock that returns a diagnostic for the document. + SetupMockWithDiagnostics(diagnosticsMock, document.Id, await CreateMockDiagnosticDataAsync(document, "id").ConfigureAwait(false)); + + // Publish one document change diagnostic notification -> + // 1. doc1 with id. + // + // We expect one publish diagnostic notification -> + // 1. from doc1 with id. + var results = await RunPublishDiagnosticsAsync(workspace, diagnosticsMock.Object, 1, document).ConfigureAwait(false); + + // We expect one publish notification for the diagnostic on the document. + Assert.Equal(new Uri(document.FilePath), results.Single().Uri); + Assert.Equal("id", results.Single().Diagnostics.Single().Code); + } + + [Fact] + public async Task AddDiagnosticWithMappedFilesTestAsync() + { + using var workspace = CreateTestWorkspace("", out _); + var document = workspace.CurrentSolution.Projects.First().Documents.First(); + + var diagnosticsMock = new Mock(); + // Create two mapped diagnostics for the document. + SetupMockWithDiagnostics(diagnosticsMock, document.Id, + await CreateMockDiagnosticDatasWithMappedLocationAsync(document, ("id1", document.FilePath + "m1"), ("id2", document.FilePath + "m2")).ConfigureAwait(false)); + + // Publish one document change diagnostic notification -> + // 1. doc1 with id1 = mapped file m1 and id2 = mapped file m2. + // + // We expect two publish diagnostic notifications -> + // 1. from m1 with id1 (from 1 above). + // 2. from m2 with id2 (from 1 above). + var results = await RunPublishDiagnosticsAsync(workspace, diagnosticsMock.Object, expectedNumberOfCallbacks: 2, document).ConfigureAwait(false); + + Assert.Equal(2, results.Count); + Assert.Equal("id1", results.Single(p => p.Uri == new Uri(document.FilePath + "m1")).Diagnostics.Single().Code); + Assert.Equal("id2", results.Single(p => p.Uri == new Uri(document.FilePath + "m2")).Diagnostics.Single().Code); + } + + [Fact] + public async Task AddDiagnosticWithMappedFileToManyDocumentsTestAsync() + { + using var workspace = CreateTestWorkspace(new string[] { "", "" }, out _); + var documents = workspace.CurrentSolution.Projects.First().Documents.ToImmutableArray(); + + var diagnosticsMock = new Mock(); + // Create diagnostic for the first document that has a mapped location. + var mappedFilePath = documents[0].FilePath + "m1"; + var documentOneDiagnostic = await CreateMockDiagnosticDatasWithMappedLocationAsync(documents[0], ("doc1Diagnostic", mappedFilePath)).ConfigureAwait(false); + // Create diagnostic for the second document that maps to the same location as the first document diagnostic. + var documentTwoDiagnostic = await CreateMockDiagnosticDatasWithMappedLocationAsync(documents[1], ("doc2Diagnostic", mappedFilePath)).ConfigureAwait(false); + + SetupMockWithDiagnostics(diagnosticsMock, documents[0].Id, documentOneDiagnostic); + SetupMockWithDiagnostics(diagnosticsMock, documents[1].Id, documentTwoDiagnostic); + + // Publish two document change diagnostic notifications -> + // 1. doc1 with doc1Diagnostic = mapped file m1. + // 2. doc2 with doc2Diagnostic = mapped file m1. + // + // We expect two publish diagnostic notifications -> + // 1. from m1 with doc1Diagnostic (from 1 above). + // 2. from m1 with doc1Diagnostic and doc2Diagnostic (from 2 above adding doc2Diagnostic to m1). + var results = await RunPublishDiagnosticsAsync(workspace, diagnosticsMock.Object, 2, documents[0], documents[1]).ConfigureAwait(false); + + Assert.Equal(2, results.Count); + var expectedUri = new Uri(mappedFilePath); + Assert.Equal(expectedUri, results[0].Uri); + Assert.Equal("doc1Diagnostic", results[0].Diagnostics.Single().Code); + + Assert.Equal(expectedUri, results[1].Uri); + Assert.Equal(2, results[1].Diagnostics.Length); + Assert.True(results[1].Diagnostics.Contains(d => d.Code == "doc1Diagnostic")); + Assert.True(results[1].Diagnostics.Contains(d => d.Code == "doc2Diagnostic")); + } + + [Fact] + public async Task RemoveDiagnosticTestAsync() + { + using var workspace = CreateTestWorkspace("", out _); + var document = workspace.CurrentSolution.Projects.First().Documents.First(); + + var diagnosticsMock = new Mock(); + // Setup the mock so the first call for a document returns a diagnostic, but the second returns empty. + SetupMockDiagnosticSequence(diagnosticsMock, document.Id, + await CreateMockDiagnosticDataAsync(document, "id").ConfigureAwait(false), + ImmutableArray.Empty); + + // Publish two document change diagnostic notifications -> + // 1. doc1 with id. + // 2. doc1 with empty. + // + // We expect two publish diagnostic notifications -> + // 1. from doc1 with id. + // 2. from doc1 with empty (from 2 above clearing out diagnostics from doc1). + var results = await RunPublishDiagnosticsAsync(workspace, diagnosticsMock.Object, 2, document, document).ConfigureAwait(false); + + Assert.Equal(2, results.Count); + Assert.Equal(new Uri(document.FilePath), results[0].Uri); + Assert.Equal("id", results[0].Diagnostics.Single().Code); + + Assert.Equal(new Uri(document.FilePath), results[1].Uri); + Assert.True(results[1].Diagnostics.IsEmpty()); + } + + [Fact] + public async Task RemoveDiagnosticForMappedFilesTestAsync() + { + using var workspace = CreateTestWorkspace("", out _); + var document = workspace.CurrentSolution.Projects.First().Documents.First(); + + var diagnosticsMock = new Mock(); + + var mappedFilePathM1 = document.FilePath + "m1"; + var mappedFilePathM2 = document.FilePath + "m2"; + // Create two mapped diagnostics for the document on first call. + // On the second call, return only the second mapped diagnostic for the document. + SetupMockDiagnosticSequence(diagnosticsMock, document.Id, + await CreateMockDiagnosticDatasWithMappedLocationAsync(document, ("id1", mappedFilePathM1), ("id2", mappedFilePathM2)).ConfigureAwait(false), + await CreateMockDiagnosticDatasWithMappedLocationAsync(document, ("id2", mappedFilePathM2)).ConfigureAwait(false)); + + // Publish three document change diagnostic notifications -> + // 1. doc1 with id1 = mapped file m1 and id2 = mapped file m2. + // 2. doc1 with just id2 = mapped file m2. + // + // We expect four publish diagnostic notifications -> + // 1. from m1 with id1 (from 1 above). + // 2. from m2 with id2 (from 1 above). + // 3. from m1 with empty (from 2 above clearing out diagnostics for m1). + // 4. from m2 with id2 (from 2 above clearing out diagnostics for m1). + var results = await RunPublishDiagnosticsAsync(workspace, diagnosticsMock.Object, 4, document, document).ConfigureAwait(false); + + var mappedFileURIM1 = new Uri(mappedFilePathM1); + var mappedFileURIM2 = new Uri(mappedFilePathM2); + + Assert.Equal(4, results.Count); + + // First document update. + Assert.Equal(mappedFileURIM1, results[1].Uri); + Assert.Equal("id1", results[1].Diagnostics.Single().Code); + + Assert.Equal(mappedFileURIM2, results[0].Uri); + Assert.Equal("id2", results[0].Diagnostics.Single().Code); + + // Second document update. + Assert.Equal(mappedFileURIM1, results[3].Uri); + Assert.True(results[3].Diagnostics.IsEmpty()); + + Assert.Equal(mappedFileURIM2, results[2].Uri); + Assert.Equal("id2", results[2].Diagnostics.Single().Code); + } + + [Fact] + public async Task RemoveDiagnosticForMappedFileToManyDocumentsTestAsync() + { + using var workspace = CreateTestWorkspace(new string[] { "", "" }, out _); + var documents = workspace.CurrentSolution.Projects.First().Documents.ToImmutableArray(); + + var diagnosticsMock = new Mock(); + // Create diagnostic for the first document that has a mapped location. + var mappedFilePath = documents[0].FilePath + "m1"; + var documentOneDiagnostic = await CreateMockDiagnosticDatasWithMappedLocationAsync(documents[0], ("doc1Diagnostic", mappedFilePath)).ConfigureAwait(false); + // Create diagnostic for the second document that maps to the same location as the first document diagnostic. + var documentTwoDiagnostic = await CreateMockDiagnosticDatasWithMappedLocationAsync(documents[1], ("doc2Diagnostic", mappedFilePath)).ConfigureAwait(false); + + // On the first call for this document, return the mapped diagnostic. On the second, return nothing. + SetupMockDiagnosticSequence(diagnosticsMock, documents[0].Id, documentOneDiagnostic, ImmutableArray.Empty); + // Always return the mapped diagnostic for this document. + SetupMockWithDiagnostics(diagnosticsMock, documents[1].Id, documentTwoDiagnostic); + + // Publish three document change diagnostic notifications -> + // 1. doc1 with doc1Diagnostic = mapped file path m1 + // 2. doc2 with doc2Diagnostic = mapped file path m1 + // 3. doc1 with empty. + // + // We expect three publish diagnostics -> + // 1. from m1 with with doc1Diagnostic (triggered by 1 above to add doc1Diagnostic). + // 2. from m1 with doc1Diagnostic and doc2Diagnostic (triggered by 2 above to add doc2Diagnostic). + // 3. from m1 with just doc2Diagnostic (triggered by 3 above to remove doc1Diagnostic). + var results = await RunPublishDiagnosticsAsync(workspace, diagnosticsMock.Object, 3, documents[0], documents[1], documents[0]).ConfigureAwait(false); + + Assert.Equal(3, results.Count); + var expectedUri = new Uri(mappedFilePath); + Assert.Equal(expectedUri, results[0].Uri); + Assert.Equal("doc1Diagnostic", results[0].Diagnostics.Single().Code); + + Assert.Equal(expectedUri, results[1].Uri); + Assert.Equal(2, results[1].Diagnostics.Length); + Assert.True(results[1].Diagnostics.Contains(d => d.Code == "doc1Diagnostic")); + Assert.True(results[1].Diagnostics.Contains(d => d.Code == "doc2Diagnostic")); + + Assert.Equal(expectedUri, results[2].Uri); + Assert.Equal(1, results[2].Diagnostics.Length); + Assert.True(results[2].Diagnostics.Contains(d => d.Code == "doc2Diagnostic")); + } + + [Fact] + public async Task ClearAllDiagnosticsForMappedFilesTestAsync() + { + using var workspace = CreateTestWorkspace("", out _); + var document = workspace.CurrentSolution.Projects.First().Documents.First(); + + var diagnosticsMock = new Mock(); + var mappedFilePathM1 = document.FilePath + "m1"; + var mappedFilePathM2 = document.FilePath + "m2"; + // Create two mapped diagnostics for the document on first call. + // On the second call, return only empty diagnostics. + SetupMockDiagnosticSequence(diagnosticsMock, document.Id, + await CreateMockDiagnosticDatasWithMappedLocationAsync(document, ("id1", mappedFilePathM1), ("id2", mappedFilePathM2)).ConfigureAwait(false), + ImmutableArray.Empty); + + // Publish two document change diagnostic notifications -> + // 1. doc1 with id1 = mapped file m1 and id2 = mapped file m2. + // 2. doc1 with empty. + // + // We expect four publish diagnostic notifications - the first two are the two mapped files from 1. + // The second two are the two mapped files being cleared by 2. + var results = await RunPublishDiagnosticsAsync(workspace, diagnosticsMock.Object, 4, document, document).ConfigureAwait(false); + + var mappedFileURIM1 = new Uri(document.FilePath + "m1"); + var mappedFileURIM2 = new Uri(document.FilePath + "m2"); + + Assert.Equal(4, results.Count); + + // Document's first update. + Assert.Equal(mappedFileURIM1, results[1].Uri); + Assert.Equal("id1", results[1].Diagnostics.Single().Code); + + Assert.Equal(mappedFileURIM2, results[0].Uri); + Assert.Equal("id2", results[0].Diagnostics.Single().Code); + + // Document's second update. + Assert.Equal(mappedFileURIM1, results[3].Uri); + Assert.True(results[3].Diagnostics.IsEmpty()); + + Assert.Equal(mappedFileURIM2, results[2].Uri); + Assert.True(results[2].Diagnostics.IsEmpty()); + } + + [Fact] + public async Task ClearAllDiagnosticsForMappedFileToManyDocumentsTestAsync() + { + using var workspace = CreateTestWorkspace(new string[] { "", "" }, out _); + var documents = workspace.CurrentSolution.Projects.First().Documents.ToImmutableArray(); + + var diagnosticsMock = new Mock(); + // Create diagnostic for the first document that has a mapped location. + var mappedFilePath = documents[0].FilePath + "m1"; + var documentOneDiagnostic = await CreateMockDiagnosticDatasWithMappedLocationAsync(documents[0], ("doc1Diagnostic", mappedFilePath)).ConfigureAwait(false); + // Create diagnostic for the second document that maps to the same location as the first document diagnostic. + var documentTwoDiagnostic = await CreateMockDiagnosticDatasWithMappedLocationAsync(documents[1], ("doc2Diagnostic", mappedFilePath)).ConfigureAwait(false); + + // On the first call for the documents, return the mapped diagnostic. On the second, return nothing. + SetupMockDiagnosticSequence(diagnosticsMock, documents[0].Id, documentOneDiagnostic, ImmutableArray.Empty); + SetupMockDiagnosticSequence(diagnosticsMock, documents[1].Id, documentTwoDiagnostic, ImmutableArray.Empty); + + // Publish four document change diagnostic notifications -> + // 1. doc1 with doc1Diagnostic = mapped file m1. + // 2. doc2 with doc2Diagnostic = mapped file m1. + // 3. doc1 with empty diagnostics. + // 4. doc2 with empty diagnostics. + // + // We expect four publish diagnostics -> + // 1. from URI m1 with doc1Diagnostic (triggered by 1 above to add doc1Diagnostic). + // 2. from URI m1 with doc1Diagnostic and doc2Diagnostic (triggered by 2 above to add doc2Diagnostic). + // 3. from URI m1 with just doc2Diagnostic (triggered by 3 above to clear doc1 diagnostic). + // 4. from URI m1 with empty (triggered by 4 above to also clear doc2 diagnostic). + var results = await RunPublishDiagnosticsAsync(workspace, diagnosticsMock.Object, 4, documents[0], documents[1], documents[0], documents[1]).ConfigureAwait(false); + + Assert.Equal(4, results.Count); + var expectedUri = new Uri(mappedFilePath); + Assert.Equal(expectedUri, results[0].Uri); + Assert.Equal("doc1Diagnostic", results[0].Diagnostics.Single().Code); + + Assert.Equal(expectedUri, results[1].Uri); + Assert.Equal(2, results[1].Diagnostics.Length); + Assert.True(results[1].Diagnostics.Contains(d => d.Code == "doc1Diagnostic")); + Assert.True(results[1].Diagnostics.Contains(d => d.Code == "doc2Diagnostic")); + + Assert.Equal(expectedUri, results[2].Uri); + Assert.Equal(1, results[2].Diagnostics.Length); + Assert.True(results[2].Diagnostics.Contains(d => d.Code == "doc2Diagnostic")); + + Assert.Equal(expectedUri, results[3].Uri); + Assert.True(results[3].Diagnostics.IsEmpty()); + } + + private async Task> RunPublishDiagnosticsAsync(Workspace workspace, IDiagnosticService diagnosticService, + int expectedNumberOfCallbacks, params Document[] documentsToPublish) + { + var (clientStream, serverStream) = FullDuplexStream.CreatePair(); + var languageServer = CreateLanguageServer(serverStream, serverStream, workspace, diagnosticService); + + var callback = new Callback(expectedNumberOfCallbacks); + using (var jsonRpc = JsonRpc.Attach(clientStream, callback)) + { + foreach (var document in documentsToPublish) + { + await languageServer.PublishDiagnosticsAsync(document).ConfigureAwait(false); + } + + await callback.CallbackCompletedTask.Task.ConfigureAwait(false); + + return callback.Results; + } + + static InProcLanguageServer CreateLanguageServer(Stream inputStream, Stream outputStream, Workspace workspace, IDiagnosticService mockDiagnosticService) + { + var protocol = ((TestWorkspace)workspace).ExportProvider.GetExportedValue(); + + var languageServer = new InProcLanguageServer(inputStream, outputStream, protocol, workspace, mockDiagnosticService, clientName: "RazorCSharp"); + return languageServer; + } + } + + private void SetupMockWithDiagnostics(Mock diagnosticServiceMock, DocumentId documentId, IEnumerable diagnostics) + { + diagnosticServiceMock.Setup(d => d.GetDiagnostics(It.IsAny(), It.IsAny(), documentId, + It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(diagnostics); + } + + private void SetupMockDiagnosticSequence(Mock diagnosticServiceMock, DocumentId documentId, + IEnumerable firstDiagnostics, IEnumerable secondDiagnostics) + { + diagnosticServiceMock.SetupSequence(d => d.GetDiagnostics(It.IsAny(), It.IsAny(), documentId, + It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(firstDiagnostics) + .Returns(secondDiagnostics); + } + + private async Task> CreateMockDiagnosticDataAsync(Document document, string id) + { + var descriptor = new DiagnosticDescriptor(id, "", "", "", DiagnosticSeverity.Error, true); + var location = Location.Create(await document.GetSyntaxTreeAsync().ConfigureAwait(false), new TextSpan()); + return new DiagnosticData[] { DiagnosticData.Create(Diagnostic.Create(descriptor, location), document) }; + } + + private async Task> CreateMockDiagnosticDatasWithMappedLocationAsync(Document document, params (string diagnosticId, string mappedFilePath)[] diagnostics) + { + var tree = await document.GetSyntaxTreeAsync().ConfigureAwait(false); + + return diagnostics.Select(d => CreateMockDiagnosticDataWithMappedLocation(document, tree, d.diagnosticId, d.mappedFilePath)).ToImmutableArray(); + + static DiagnosticData CreateMockDiagnosticDataWithMappedLocation(Document document, SyntaxTree tree, string id, string mappedFilePath) + { + var descriptor = new DiagnosticDescriptor(id, "", "", "", DiagnosticSeverity.Error, true); + var location = Location.Create(tree, new TextSpan()); + + var diagnostic = Diagnostic.Create(descriptor, location); + return new DiagnosticData(diagnostic.Id, + diagnostic.Descriptor.Category, + null, + null, + diagnostic.Severity, + diagnostic.DefaultSeverity, + diagnostic.Descriptor.IsEnabledByDefault, + diagnostic.WarningLevel, + diagnostic.Descriptor.CustomTags.AsImmutableOrEmpty(), + diagnostic.Properties, + document.Project.Id, + GetDataLocation(document, mappedFilePath), + null, + document.Project.Language, + diagnostic.Descriptor.Title.ToString(), + diagnostic.Descriptor.Description.ToString(), + null, + diagnostic.IsSuppressed); + } + + static DiagnosticDataLocation GetDataLocation(Document document, string mappedFilePath) + => new DiagnosticDataLocation(document.Id, originalFilePath: document.FilePath, mappedFilePath: mappedFilePath); + } + + private class Callback + { + /// + /// Task that can be awaited for the all callbacks to complete. + /// + public TaskCompletionSource CallbackCompletedTask { get; } = new TaskCompletionSource(); + + /// + /// Serialized results of all publish diagnostic notifications recieved by this callback. + /// + public List Results { get; } = new List(); + + /// + /// Lock to guard concurrent callbacks. + /// + private readonly object _lock = new object(); + + /// + /// The expected number of times this callback should be hit. + /// Used in conjunction with + /// to determine if the callbacks are complete. + /// + private readonly int _expectedNumberOfCallbacks; + + /// + /// The current number of callbacks that this callback has been hit. + /// + private int _currentNumberOfCallbacks; + + + public Callback(int expectedNumberOfCallbacks) + { + _expectedNumberOfCallbacks = expectedNumberOfCallbacks; + _currentNumberOfCallbacks = 0; + } + + [JsonRpcMethod(LSP.Methods.TextDocumentPublishDiagnosticsName)] + public Task OnDiagnosticsPublished(JToken input) + { + lock(_lock) + { + _currentNumberOfCallbacks++; + Contract.ThrowIfTrue(_currentNumberOfCallbacks > _expectedNumberOfCallbacks, "received too many callbacks"); + + var diagnosticParams = input.ToObject(); + Results.Add(diagnosticParams); + + if (_currentNumberOfCallbacks == _expectedNumberOfCallbacks) + { + CallbackCompletedTask.SetResult(new object()); + } + + return Task.CompletedTask; + } + } + } + } +} -- GitLab