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);