diff --git a/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintCache.cs b/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintCache.cs index 9875aa41296e2..aa98addfc4f9b 100644 --- a/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintCache.cs +++ b/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintCache.cs @@ -18,5 +18,5 @@ public InlayHintCache() : base(maxCacheSize: 3) /// /// Cached data need to resolve a specific inlay hint item. /// - internal record InlayHintCacheEntry(ImmutableArray InlayHintMembers, TextDocumentIdentifier TextDocumentIdentifier, VersionStamp SyntaxVersion); + internal record InlayHintCacheEntry(ImmutableArray InlayHintMembers, VersionStamp SyntaxVersion); } diff --git a/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintHandler.cs b/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintHandler.cs index 41d61987595ed..680a680b6845b 100644 --- a/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintHandler.cs @@ -56,7 +56,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(InlayHintParams request) // Store the members in the resolve cache so that when we get a resolve request for a particular // member we can re-use the inline hint. - var resultId = inlayHintCache.UpdateCache(new InlayHintCache.InlayHintCacheEntry(hints, request.TextDocument, syntaxVersion)); + var resultId = inlayHintCache.UpdateCache(new InlayHintCache.InlayHintCacheEntry(hints, syntaxVersion)); for (var i = 0; i < hints.Length; i++) { @@ -85,7 +85,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(InlayHintParams request) ToolTip = null, PaddingLeft = leftPadding, PaddingRight = rightPadding, - Data = new InlayHintResolveData(resultId, i) + Data = new InlayHintResolveData(resultId, i, request.TextDocument) }; inlayHints.Add(inlayHint); diff --git a/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintResolveData.cs b/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintResolveData.cs index 70546bd0bd83f..ea549b4facc50 100644 --- a/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintResolveData.cs +++ b/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintResolveData.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.VisualStudio.LanguageServer.Protocol; + namespace Microsoft.CodeAnalysis.LanguageServer.Handler.InlayHint; /// @@ -9,4 +11,5 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.InlayHint; /// /// the resultId associated with the inlay hint created on original request. /// the index of the specific inlay hint item in the original list. -internal sealed record InlayHintResolveData(long ResultId, int ListIndex); +/// /// the text document associated with the inlay hint to resolve. +internal sealed record InlayHintResolveData(long ResultId, int ListIndex, TextDocumentIdentifier TextDocument); diff --git a/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintResolveHandler.cs b/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintResolveHandler.cs index 5c627f36bf7af..cad8944a8c64f 100644 --- a/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintResolveHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintResolveHandler.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.InlineHints; +using Microsoft.CodeAnalysis.LanguageServer.Handler.CodeLens; using Microsoft.VisualStudio.LanguageServer.Protocol; using Newtonsoft.Json.Linq; using Roslyn.Utilities; @@ -32,12 +33,13 @@ public InlayHintResolveHandler(InlayHintCache inlayHintCache) public bool RequiresLSPSolution => true; public TextDocumentIdentifier GetTextDocumentIdentifier(LSP.InlayHint request) - => GetCacheEntry(request).CacheEntry.TextDocumentIdentifier; + => GetInlayHintResolveData(request).TextDocument; public async Task HandleRequestAsync(LSP.InlayHint request, RequestContext context, CancellationToken cancellationToken) { var document = context.GetRequiredDocument(); - var (cacheEntry, inlineHintToResolve) = GetCacheEntry(request); + var resolveData = GetInlayHintResolveData(request); + var (cacheEntry, inlineHintToResolve) = GetCacheEntry(resolveData); var currentSyntaxVersion = await document.GetSyntaxVersionAsync(cancellationToken).ConfigureAwait(false); var cachedSyntaxVersion = cacheEntry.SyntaxVersion; @@ -56,14 +58,18 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(LSP.InlayHint request) return request; } - private (InlayHintCache.InlayHintCacheEntry CacheEntry, InlineHint InlineHintToResolve) GetCacheEntry(LSP.InlayHint request) + private (InlayHintCache.InlayHintCacheEntry CacheEntry, InlineHint InlineHintToResolve) GetCacheEntry(InlayHintResolveData resolveData) { - var resolveData = (request.Data as JToken)?.ToObject(); - Contract.ThrowIfNull(resolveData, "Missing data for inlay hint resolve request"); - var cacheEntry = _inlayHintCache.GetCachedEntry(resolveData.ResultId); Contract.ThrowIfNull(cacheEntry, "Missing cache entry for inlay hint resolve request"); return (cacheEntry, cacheEntry.InlayHintMembers[resolveData.ListIndex]); } + + private static InlayHintResolveData GetInlayHintResolveData(LSP.InlayHint inlayHint) + { + var resolveData = (inlayHint.Data as JToken)?.ToObject(); + Contract.ThrowIfNull(resolveData, "Missing data for inlay hint resolve request"); + return resolveData; + } } } diff --git a/src/Features/LanguageServer/ProtocolUnitTests/InlayHint/CSharpInlayHintTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/InlayHint/CSharpInlayHintTests.cs index 3093ea047200d..345c243e74a9b 100644 --- a/src/Features/LanguageServer/ProtocolUnitTests/InlayHint/CSharpInlayHintTests.cs +++ b/src/Features/LanguageServer/ProtocolUnitTests/InlayHint/CSharpInlayHintTests.cs @@ -6,10 +6,17 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.InlineHints; +using Microsoft.CodeAnalysis.LanguageServer.Handler.InlayHint; +using Microsoft.CodeAnalysis.Text; +using Newtonsoft.Json; +using Roslyn.Test.Utilities; +using StreamJsonRpc; using Xunit; using Xunit.Abstractions; +using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.InlayHint { @@ -99,6 +106,58 @@ void X((int, bool) d) await RunVerifyInlayHintAsync(markup, mutatingLspWorkspace, hasTextEdits: false); } + [Theory, CombinatorialData] + public async Task TestDoesNotShutdownServerIfCacheEntryMissing(bool mutatingLspWorkspace) + { + var markup = +@"class A +{ + void M() + { + var {|int:|}x = 5; + } +}"; + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, CapabilitiesWithVSExtensions); + testLspServer.TestWorkspace.GlobalOptions.SetGlobalOption(InlineHintsOptionsStorage.EnabledForParameters, LanguageNames.CSharp, true); + testLspServer.TestWorkspace.GlobalOptions.SetGlobalOption(InlineHintsOptionsStorage.EnabledForTypes, LanguageNames.CSharp, true); + var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single(); + var textDocument = CreateTextDocumentIdentifier(document.GetURI()); + var sourceText = await document.GetTextAsync(); + var span = TextSpan.FromBounds(0, sourceText.Length); + + var inlayHintParams = new LSP.InlayHintParams + { + TextDocument = textDocument, + Range = ProtocolConversions.TextSpanToRange(span, sourceText) + }; + + var actualInlayHints = await testLspServer.ExecuteRequestAsync(LSP.Methods.TextDocumentInlayHintName, inlayHintParams, CancellationToken.None); + var firstInlayHint = actualInlayHints.First(); + var data = JsonConvert.DeserializeObject(firstInlayHint.Data!.ToString()); + AssertEx.NotNull(data); + var firstResultId = data.ResultId; + + // Verify the inlay hint item is in the cache. + var cache = testLspServer.GetRequiredLspService(); + Assert.NotNull(cache.GetCachedEntry(firstResultId)); + + // Execute a few more requests to ensure the first request is removed from the cache. + await testLspServer.ExecuteRequestAsync(LSP.Methods.TextDocumentInlayHintName, inlayHintParams, CancellationToken.None); + await testLspServer.ExecuteRequestAsync(LSP.Methods.TextDocumentInlayHintName, inlayHintParams, CancellationToken.None); + var lastInlayHints = await testLspServer.ExecuteRequestAsync(LSP.Methods.TextDocumentInlayHintName, inlayHintParams, CancellationToken.None); + Assert.True(lastInlayHints.Any()); + + // Assert that the first result id is no longer in the cache. + Assert.Null(cache.GetCachedEntry(firstResultId)); + + // Assert that the request throws because the item no longer exists in the cache. + await Assert.ThrowsAsync(async () => await testLspServer.ExecuteRequestAsync(LSP.Methods.InlayHintResolveName, firstInlayHint, CancellationToken.None)); + + // Assert that the server did not shutdown and that we can resolve the latest inlay hint request we made. + var lastInlayHint = await testLspServer.ExecuteRequestAsync(LSP.Methods.InlayHintResolveName, lastInlayHints.First(), CancellationToken.None); + Assert.NotNull(lastInlayHint?.ToolTip); + } + private async Task RunVerifyInlayHintAsync(string markup, bool mutatingLspWorkspace, bool hasTextEdits = true) { await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, CapabilitiesWithVSExtensions);