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