diff --git a/src/Stampeded.RoslynLsp/RoslynLspServer.cs b/src/Stampeded.RoslynLsp/RoslynLspServer.cs index 80761aa..d9d971d 100644 --- a/src/Stampeded.RoslynLsp/RoslynLspServer.cs +++ b/src/Stampeded.RoslynLsp/RoslynLspServer.cs @@ -107,7 +107,7 @@ async Task HandleNotificationAsync(string method, JsonElement parameters, Cancel case "textDocument/didChange": // The client sends what the review is showing, which may be a revision that // is not on disk. An overlay is how the workspace is told. - Overlay(parameters); + await OverlayAsync(parameters, ct); break; case "initialized": case "$/cancelRequest": @@ -206,8 +206,14 @@ void WatchParent(JsonElement parameters) }); } + // An event handler cannot await, and the client only needs the notification to arrive + // eventually: the write lock queues waiters in order, so states are still sent in the + // order they were reached. void ReportState() - => Notify("stampeded/state", new { state = head.State.ToString(), detail = head.StateDetail }); + => NotifyAsync("stampeded/state", new { state = head.State.ToString(), detail = head.StateDetail }) + .ContinueWith( + t => CliLog.Write("roslyn-lsp", $"state notification failed: {t.Exception?.GetBaseException().Message}"), + TaskContinuationOptions.OnlyOnFaulted); /// Derives the base-side workspace from the head one, given the texts of the /// revision being compared against. @@ -242,7 +248,7 @@ static Dictionary ReadTexts(JsonElement parameters, string name) return texts; } - void Overlay(JsonElement parameters) + async Task OverlayAsync(JsonElement parameters, CancellationToken ct) { if (!parameters.TryGetProperty("textDocument", out var document) || !document.TryGetProperty("uri", out var uri)) @@ -261,7 +267,7 @@ void Overlay(JsonElement parameters) // Only when it differs from what the workspace has: the client opens a document to // be able to ask about it at all, and re-stating the file on disk would throw away // the compilation that already knows it. - if (target.Service.GetDocumentTextAsync(target.RelPath, CancellationToken.None).Result is { } current + if (await target.Service.GetDocumentTextAsync(target.RelPath, ct) is { } current && string.Equals(current.ReplaceLineEndings("\n"), text.ReplaceLineEndings("\n"), StringComparison.Ordinal)) { return; @@ -619,11 +625,11 @@ async Task RespondAsync(JsonElement id, object? result) await SendAsync(payload); } - void Notify(string method, object parameters) + Task NotifyAsync(string method, object parameters) { var payload = JsonSerializer.SerializeToUtf8Bytes( new { jsonrpc = "2.0", method, @params = parameters }, Json); - SendAsync(payload).GetAwaiter().GetResult(); + return SendAsync(payload); } async Task SendAsync(byte[] payload) diff --git a/tests/Stampeded.Core.Tests/GeneratedSourcesTests.cs b/tests/Stampeded.Core.Tests/GeneratedSourcesTests.cs index b7eb357..080e3e8 100644 --- a/tests/Stampeded.Core.Tests/GeneratedSourcesTests.cs +++ b/tests/Stampeded.Core.Tests/GeneratedSourcesTests.cs @@ -54,13 +54,13 @@ public void KeepsOutputOfTheSameGeneratorInDifferentProjectsApart() } [Test] - public void PairsTheTwoSidesEvenWhenTheyWereBuiltDifferently() + public async Task PairsTheTwoSidesEvenWhenTheyWereBuiltDifferently() { string baseTree = NewDirectory(), headTree = NewDirectory(); Write(baseTree, "src/Lib/obj/Debug/net10.0/generated/G/E/Thing.g.cs", "one\ntwo\n"); Write(headTree, "src/Lib/obj/Release/net11.0/generated/G/E/Thing.g.cs", "one\ntwo changed\n"); - var files = GeneratedSources.DiffAsync(baseTree, headTree).GetAwaiter().GetResult(); + var files = await GeneratedSources.DiffAsync(baseTree, headTree); Assert.That(files, Has.Count.EqualTo(1)); Assert.That(files[0].Kind, Is.EqualTo(FileChangeKind.Modified)); @@ -69,13 +69,13 @@ public void PairsTheTwoSidesEvenWhenTheyWereBuiltDifferently() } [Test] - public void ReportsWhatOnlyOneSideGenerated() + public async Task ReportsWhatOnlyOneSideGenerated() { string baseTree = NewDirectory(), headTree = NewDirectory(); Write(baseTree, "src/Lib/obj/Debug/net10.0/generated/G/E/Gone.g.cs", "was here\n"); Write(headTree, "src/Lib/obj/Debug/net10.0/generated/G/E/New.g.cs", "is here\n"); - var files = GeneratedSources.DiffAsync(baseTree, headTree).GetAwaiter().GetResult(); + var files = await GeneratedSources.DiffAsync(baseTree, headTree); Assert.That(files.Select(f => (f.Path, f.Kind)), Is.EquivalentTo(new[] { ("src/Lib/generated/G/E/Gone.g.cs", FileChangeKind.Deleted), @@ -84,24 +84,24 @@ public void ReportsWhatOnlyOneSideGenerated() } [Test] - public void LeavesOutGeneratedFilesTheChangeDidNotMove() + public async Task LeavesOutGeneratedFilesTheChangeDidNotMove() { string baseTree = NewDirectory(), headTree = NewDirectory(); Write(baseTree, "src/Lib/obj/Debug/net10.0/generated/G/E/Same.g.cs", "unchanged\n"); Write(headTree, "src/Lib/obj/Debug/net10.0/generated/G/E/Same.g.cs", "unchanged\n"); - Assert.That(GeneratedSources.DiffAsync(baseTree, headTree).GetAwaiter().GetResult(), Is.Empty, + Assert.That(await GeneratedSources.DiffAsync(baseTree, headTree), Is.Empty, "a generator whose output stands still is not part of the change"); } [Test] - public void CarriesWhereEachSideCanBeReadFrom() + public async Task CarriesWhereEachSideCanBeReadFrom() { string baseTree = NewDirectory(), headTree = NewDirectory(); Write(baseTree, "src/Lib/obj/Debug/net10.0/generated/G/E/Thing.g.cs", "before\n"); Write(headTree, "src/Lib/obj/Debug/net10.0/generated/G/E/Thing.g.cs", "after\n"); - var file = GeneratedSources.DiffAsync(baseTree, headTree).GetAwaiter().GetResult().Single(); + var file = (await GeneratedSources.DiffAsync(baseTree, headTree)).Single(); // Nothing can read these out of a commit, so the diff has to say where they are. Assert.That(file.IsGenerated, Is.True);