diff --git a/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensCache.cs b/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensCache.cs
index 3b57c01808d99..5ae1c03dad1c5 100644
--- a/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensCache.cs
+++ b/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensCache.cs
@@ -27,7 +27,6 @@ public CodeLensCache() : base(maxCacheSize: 3)
/// Cached data need to resolve a specific code lens item
///
/// the list of nodes and locations for codelens members
- /// the lsp document they came from
/// the syntax version the codelenses were calculated against (to validate the resolve request)
- internal record CodeLensCacheEntry(ImmutableArray CodeLensMembers, TextDocumentIdentifier TextDocumentIdentifier, VersionStamp SyntaxVersion);
+ internal record CodeLensCacheEntry(ImmutableArray CodeLensMembers, VersionStamp SyntaxVersion);
}
diff --git a/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensHandler.cs b/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensHandler.cs
index 4baeffcfe88b8..5afc072fbb3ed 100644
--- a/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensHandler.cs
+++ b/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensHandler.cs
@@ -49,7 +49,7 @@ public LSP.TextDocumentIdentifier GetTextDocumentIdentifier(LSP.CodeLensParams r
// Store the members in the resolve cache so that when we get a resolve request for a particular
// member we can re-use the syntax node and span we already computed here.
- var resultId = codeLensCache.UpdateCache(new CodeLensCache.CodeLensCacheEntry(members, request.TextDocument, syntaxVersion));
+ var resultId = codeLensCache.UpdateCache(new CodeLensCache.CodeLensCacheEntry(members, syntaxVersion));
// TODO - Code lenses need to be refreshed by the server when we detect solution/project wide changes.
// See https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1730462
@@ -63,7 +63,7 @@ public LSP.TextDocumentIdentifier GetTextDocumentIdentifier(LSP.CodeLensParams r
{
Range = range,
Command = null,
- Data = new CodeLensResolveData(resultId, i)
+ Data = new CodeLensResolveData(resultId, i, request.TextDocument)
};
codeLenses.Add(codeLens);
diff --git a/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensResolveData.cs b/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensResolveData.cs
index b9f9d6fd96dc6..fe5277d58c335 100644
--- a/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensResolveData.cs
+++ b/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensResolveData.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.CodeLens;
///
@@ -9,4 +11,5 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.CodeLens;
///
/// the resultId associated with the code lens list created on original request.
/// the index of the specific code lens item in the original list.
-internal sealed record CodeLensResolveData(long ResultId, int ListIndex);
+/// the text document associated with the code lens to resolve.
+internal sealed record CodeLensResolveData(long ResultId, int ListIndex, TextDocumentIdentifier TextDocument);
diff --git a/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensResolveHandler.cs b/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensResolveHandler.cs
index b6346e3b7fb39..f9e3e410005e5 100644
--- a/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensResolveHandler.cs
+++ b/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensResolveHandler.cs
@@ -33,12 +33,13 @@ public CodeLensResolveHandler(CodeLensCache codeLensCache)
public bool RequiresLSPSolution => true;
public LSP.TextDocumentIdentifier GetTextDocumentIdentifier(LSP.CodeLens request)
- => GetCacheEntry(request).CacheEntry.TextDocumentIdentifier;
+ => GetCodeLensResolveData(request).TextDocument;
public async Task HandleRequestAsync(LSP.CodeLens request, RequestContext context, CancellationToken cancellationToken)
{
var document = context.GetRequiredDocument();
- var (cacheEntry, memberToResolve) = GetCacheEntry(request);
+ var resolveData = GetCodeLensResolveData(request);
+ var (cacheEntry, memberToResolve) = GetCacheEntry(resolveData);
var currentSyntaxVersion = await document.GetSyntaxVersionAsync(cancellationToken).ConfigureAwait(false);
var cachedSyntaxVersion = cacheEntry.SyntaxVersion;
@@ -61,7 +62,7 @@ public LSP.TextDocumentIdentifier GetTextDocumentIdentifier(LSP.CodeLens request
CommandIdentifier = ClientReferencesCommand,
Arguments = new object[]
{
- cacheEntry.TextDocumentIdentifier.Uri,
+ resolveData.TextDocument.Uri,
request.Range.Start
}
};
@@ -71,14 +72,18 @@ public LSP.TextDocumentIdentifier GetTextDocumentIdentifier(LSP.CodeLens request
return request;
}
- private (CodeLensCache.CodeLensCacheEntry CacheEntry, CodeLensMember MemberToResolve) GetCacheEntry(LSP.CodeLens request)
+ private (CodeLensCache.CodeLensCacheEntry CacheEntry, CodeLensMember MemberToResolve) GetCacheEntry(CodeLensResolveData resolveData)
{
- var resolveData = (request.Data as JToken)?.ToObject();
- Contract.ThrowIfNull(resolveData, "Missing data for code lens resolve request");
-
var cacheEntry = _codeLensCache.GetCachedEntry(resolveData.ResultId);
Contract.ThrowIfNull(cacheEntry, "Missing cache entry for code lens resolve request");
return (cacheEntry, cacheEntry.CodeLensMembers[resolveData.ListIndex]);
}
+
+ private static CodeLensResolveData GetCodeLensResolveData(LSP.CodeLens codeLens)
+ {
+ var resolveData = (codeLens.Data as JToken)?.ToObject();
+ Contract.ThrowIfNull(resolveData, "Missing data for code lens resolve request");
+ return resolveData;
+ }
}
diff --git a/src/Features/LanguageServer/ProtocolUnitTests/CodeLens/CSharpCodeLensTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/CodeLens/CSharpCodeLensTests.cs
index 0daa914e9cdba..847daa07110ba 100644
--- a/src/Features/LanguageServer/ProtocolUnitTests/CodeLens/CSharpCodeLensTests.cs
+++ b/src/Features/LanguageServer/ProtocolUnitTests/CodeLens/CSharpCodeLensTests.cs
@@ -5,7 +5,10 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.LanguageServer.Handler.CodeLens;
+using Newtonsoft.Json;
using Roslyn.Test.Utilities;
+using StreamJsonRpc;
using Xunit;
using Xunit.Abstractions;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
@@ -190,4 +193,54 @@ public async Task TestRecordDeclarationAsync(bool lspMutatingWorkspace)
await using var testLspServer = await CreateTestLspServerAsync(markup, lspMutatingWorkspace, CapabilitiesWithVSExtensions);
await VerifyCodeLensAsync(testLspServer, expectedNumberOfReferences: 0);
}
+
+ [Theory, CombinatorialData]
+ public async Task TestDoesNotShutdownServerIfCacheEntryMissing(bool mutatingLspWorkspace)
+ {
+ var markup =
+@"class A
+{
+ void {|codeLens:M|}()
+ {
+ }
+
+ void UseM()
+ {
+ M();
+ }
+}";
+ await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
+
+ var textDocument = CreateTextDocumentIdentifier(testLspServer.GetCurrentSolution().Projects.Single().Documents.Single().GetURI());
+ var codeLensParams = new LSP.CodeLensParams
+ {
+ TextDocument = textDocument
+ };
+
+ var actualCodeLenses = await testLspServer.ExecuteRequestAsync(LSP.Methods.TextDocumentCodeLensName, codeLensParams, CancellationToken.None);
+ var firstCodeLens = actualCodeLenses.First();
+ var data = JsonConvert.DeserializeObject(firstCodeLens.Data!.ToString());
+ AssertEx.NotNull(data);
+ var firstResultId = data.ResultId;
+
+ // Verify the code lens 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.TextDocumentCodeLensName, codeLensParams, CancellationToken.None);
+ await testLspServer.ExecuteRequestAsync(LSP.Methods.TextDocumentCodeLensName, codeLensParams, CancellationToken.None);
+ var lastCodeLenses = await testLspServer.ExecuteRequestAsync(LSP.Methods.TextDocumentCodeLensName, codeLensParams, CancellationToken.None);
+ Assert.True(lastCodeLenses.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.CodeLensResolveName, firstCodeLens, CancellationToken.None));
+
+ // Assert that the server did not shutdown and that we can resolve the latest codelens request we made.
+ var lastCodeLens = await testLspServer.ExecuteRequestAsync(LSP.Methods.CodeLensResolveName, lastCodeLenses.First(), CancellationToken.None);
+ Assert.NotNull(lastCodeLens?.Command);
+ }
}