diff --git a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs
index 379a7864f883a..d0f5b1b8f6046 100644
--- a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs
+++ b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs
@@ -130,7 +130,7 @@ static SemanticTokensHelpers()
///
/// Returns the semantic tokens data for a given document with an optional range.
///
- internal static async Task<(int[], bool isFinalized)> ComputeSemanticTokensDataAsync(
+ internal static async Task ComputeSemanticTokensDataAsync(
Document document,
Dictionary tokenTypesToIndex,
LSP.Range? range,
@@ -147,10 +147,8 @@ static SemanticTokensHelpers()
// If the full compilation is not yet available, we'll try getting a partial one. It may contain inaccurate
// results but will speed up how quickly we can respond to the client's request.
- var frozenDocument = document.WithFrozenPartialSemantics(cancellationToken);
- var semanticModel = await frozenDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
- var isFinalized = document.Project.TryGetCompilation(out var compilation) && compilation == semanticModel.Compilation;
- document = frozenDocument;
+ document = document.WithFrozenPartialSemantics(cancellationToken);
+ options = options with { ForceFrozenPartialSemanticsForCrossProcessOperations = true };
var classifiedSpans = await GetClassifiedSpansForDocumentAsync(
document, textSpan, options, includeSyntacticClassifications, cancellationToken).ConfigureAwait(false);
@@ -161,7 +159,7 @@ static SemanticTokensHelpers()
// TO-DO: We should implement support for streaming if LSP adds support for it:
// https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1276300
- return (ComputeTokens(text.Lines, updatedClassifiedSpans, tokenTypesToIndex), isFinalized);
+ return ComputeTokens(text.Lines, updatedClassifiedSpans, tokenTypesToIndex);
}
private static async Task GetClassifiedSpansForDocumentAsync(
diff --git a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensRangeHandler.cs b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensRangeHandler.cs
index bdd4c460a84e1..ec482cb835713 100644
--- a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensRangeHandler.cs
+++ b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensRangeHandler.cs
@@ -3,10 +3,12 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Collections.Generic;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification;
+using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.LanguageServer.Protocol;
@@ -24,6 +26,10 @@ internal class SemanticTokensRangeHandler : AbstractStatelessRequestHandler> _projectIdToCompilation = new();
+
public override bool MutatesSolutionState => false;
public override bool RequiresLSPSolution => true;
@@ -48,13 +54,20 @@ public SemanticTokensRangeHandler(IGlobalOptionService globalOptions)
Contract.ThrowIfNull(request.TextDocument, "TextDocument is null.");
Contract.ThrowIfNull(context.Document, "Document is null.");
- var options = _globalOptions.GetClassificationOptions(context.Document.Project.Language);
+ var project = context.Document.Project;
+ var options = _globalOptions.GetClassificationOptions(project.Language);
+
+ // Razor uses isFinalized to determine whether to cache tokens. We should be able to
+ // remove it altogether once Roslyn implements workspace/semanticTokens/refresh:
+ // https://github.com/dotnet/roslyn/issues/60441
+ var isFinalized = !context.Document.IsRazorDocument() ||
+ await IsDataFinalizedAsync(project, cancellationToken).ConfigureAwait(false);
// The results from the range handler should not be cached since we don't want to cache
// partial token results. In addition, a range request is only ever called with a whole
// document request, so caching range results is unnecessary since the whole document
// handler will cache the results anyway.
- var (tokensData, isFinalized) = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
+ var tokensData = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
context.Document,
SemanticTokensHelpers.TokenTypeToIndex,
request.Range,
@@ -64,5 +77,42 @@ public SemanticTokensRangeHandler(IGlobalOptionService globalOptions)
return new RoslynSemanticTokens { Data = tokensData, IsFinalized = isFinalized };
}
+
+ private async Task IsDataFinalizedAsync(Project project, CancellationToken cancellationToken)
+ {
+ // We use a combination of IsFullyLoaded + the completed project compilation as the metric
+ // for isFinalized. It may not be completely accurate but this is only a a temporary fix until
+ // workspace/semanticTokens/refresh is implemented.
+ lock (_lock)
+ {
+ if (_projectIdToCompilation.TryGetValue(project.Id, out var compilationTask) && compilationTask.IsCompleted)
+ {
+ // We don't want to hang on to the compilation since this can be very expensive,
+ // but we do want to mark the compilation as being successfully retrieved.
+ if (compilationTask.Result is not null)
+ {
+ _projectIdToCompilation[project.Id] = Task.FromResult(null);
+ }
+
+ return true;
+ }
+ }
+
+ var workspaceStatusService = project.Solution.Workspace.Services.GetRequiredService();
+ var isFullyLoaded = await workspaceStatusService.IsFullyLoadedAsync(cancellationToken).ConfigureAwait(false);
+
+ lock (_lock)
+ {
+ if (isFullyLoaded && !_projectIdToCompilation.ContainsKey(project.Id))
+ {
+ // If the project's compilation isn't yet available, kick off a task in the background to
+ // hopefully make it available faster since we'll need it later to compute tokens.
+ var newCompilationTask = project.GetCompilationAsync(cancellationToken);
+ _projectIdToCompilation.Add(project.Id, newCompilationTask);
+ }
+ }
+
+ return false;
+ }
}
}
diff --git a/src/Features/LanguageServer/ProtocolUnitTests/SemanticTokens/SemanticTokensRangeTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/SemanticTokens/SemanticTokensRangeTests.cs
index c2c827c6e6e6e..ae0f70de56044 100644
--- a/src/Features/LanguageServer/ProtocolUnitTests/SemanticTokens/SemanticTokensRangeTests.cs
+++ b/src/Features/LanguageServer/ProtocolUnitTests/SemanticTokens/SemanticTokensRangeTests.cs
@@ -53,7 +53,7 @@ static class C { }
var range = new LSP.Range { Start = new Position(0, 0), End = new Position(2, 0) };
var options = ClassificationOptions.Default;
- var (results, _) = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
+ var results = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
document, SemanticTokensHelpers.TokenTypeToIndex, range, options, includeSyntacticClassifications: true, CancellationToken.None);
var expectedResults = new LSP.SemanticTokens
@@ -87,7 +87,7 @@ static class C { }
var document = testLspServer.GetCurrentSolution().Projects.First().Documents.First();
var range = new LSP.Range { Start = new Position(1, 0), End = new Position(2, 0) };
var options = ClassificationOptions.Default;
- var (results, _) = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
+ var results = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
document, SemanticTokensHelpers.TokenTypeToIndex, range, options, includeSyntacticClassifications: true, CancellationToken.None);
var expectedResults = new LSP.SemanticTokens
@@ -122,7 +122,7 @@ public async Task TestGetSemanticTokensRange_MultiLineComment_RazorAsync()
var document = testLspServer.GetCurrentSolution().Projects.First().Documents.First();
var range = new LSP.Range { Start = new Position(0, 0), End = new Position(4, 0) };
var options = ClassificationOptions.Default;
- var (results, _) = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
+ var results = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
document, SemanticTokensHelpers.TokenTypeToIndex, range, options, includeSyntacticClassifications: true, CancellationToken.None);
var expectedResults = new LSP.SemanticTokens
@@ -197,7 +197,7 @@ void M()
var document = testLspServer.GetCurrentSolution().Projects.First().Documents.First();
var range = new LSP.Range { Start = new Position(0, 0), End = new Position(9, 0) };
var options = ClassificationOptions.Default;
- var (results, _) = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
+ var results = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
document, SemanticTokensHelpers.TokenTypeToIndex, range, options, includeSyntacticClassifications: true, CancellationToken.None);
var expectedResults = new LSP.SemanticTokens
@@ -250,7 +250,7 @@ void M()
var document = testLspServer.GetCurrentSolution().Projects.First().Documents.First();
var range = new LSP.Range { Start = new Position(0, 0), End = new Position(9, 0) };
var options = ClassificationOptions.Default;
- var (results, _) = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
+ var results = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
document, SemanticTokensHelpers.TokenTypeToIndex, range, options, includeSyntacticClassifications: true, CancellationToken.None);
var expectedResults = new LSP.SemanticTokens