Skip to content

Commit 1dd98de

Browse files
authored
[LSP] Semantic tokens: Utilize frozen partial compilations (#55976)
1 parent 4d48ca4 commit 1dd98de

File tree

7 files changed

+110
-21
lines changed

7 files changed

+110
-21
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
#nullable enable
6+
7+
using System.Runtime.Serialization;
8+
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
9+
10+
namespace Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens
11+
{
12+
internal sealed class RoslynSemanticTokens : LSP.SemanticTokens
13+
{
14+
/// <summary>
15+
/// True if the token set is complete, meaning it's generated using a full semantic
16+
/// model rather than a frozen one.
17+
/// </summary>
18+
/// <remarks>
19+
/// Certain clients such as Razor need to know whether we're returning partial
20+
/// (i.e. possibly inaccurate) results. This may occur if the full compilation
21+
/// is not yet available.
22+
/// </remarks>
23+
[DataMember(Name = "isFinalized")]
24+
public bool IsFinalized { get; set; }
25+
}
26+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
6+
using System.Runtime.Serialization;
7+
8+
namespace Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens
9+
{
10+
internal sealed class RoslynSemanticTokensDelta : LSP.SemanticTokensDelta
11+
{
12+
/// <summary>
13+
/// True if the token set is complete, meaning it's generated using a full semantic
14+
/// model rather than a frozen one.
15+
/// </summary>
16+
/// <remarks>
17+
/// Certain clients such as Razor need to know whether we're returning partial
18+
/// (i.e. possibly inaccurate) results. This may occur if the full compilation
19+
/// is not yet available.
20+
/// </remarks>
21+
[DataMember(Name = "isFinalized")]
22+
public bool IsFinalized { get; set; }
23+
}
24+
}

src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensEditsHandler.cs

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
namespace Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens
1818
{
1919
/// <summary>
20-
/// Computes the semantic tokens edits for a file. An edit request is received every 500ms,
20+
/// Computes the semantic tokens edits for a file. Clients may make edit requests on a timer,
2121
/// or every time an edit is made by the user.
2222
/// </summary>
2323
internal class SemanticTokensEditsHandler : IRequestHandler<LSP.SemanticTokensDeltaParams, SumType<LSP.SemanticTokens, LSP.SemanticTokensDelta>>
@@ -51,7 +51,7 @@ public SemanticTokensEditsHandler(SemanticTokensCache tokensCache)
5151

5252
// Even though we want to ultimately pass edits back to LSP, we still need to compute all semantic tokens,
5353
// both for caching purposes and in order to have a baseline comparison when computing the edits.
54-
var newSemanticTokensData = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
54+
var (newSemanticTokensData, isFinalized) = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
5555
context.Document, SemanticTokensCache.TokenTypeToIndex,
5656
range: null, cancellationToken).ConfigureAwait(false);
5757

@@ -64,28 +64,48 @@ public SemanticTokensEditsHandler(SemanticTokensCache tokensCache)
6464
if (oldSemanticTokensData == null)
6565
{
6666
var newResultId = _tokensCache.GetNextResultId();
67-
var updatedTokens = new LSP.SemanticTokens { ResultId = newResultId, Data = newSemanticTokensData };
68-
await _tokensCache.UpdateCacheAsync(
69-
request.TextDocument.Uri, updatedTokens, cancellationToken).ConfigureAwait(false);
70-
return new LSP.SemanticTokens { ResultId = newResultId, Data = newSemanticTokensData };
67+
var updatedTokens = new RoslynSemanticTokens
68+
{
69+
ResultId = newResultId,
70+
Data = newSemanticTokensData,
71+
IsFinalized = isFinalized,
72+
};
73+
74+
if (newSemanticTokensData.Length > 0)
75+
{
76+
await _tokensCache.UpdateCacheAsync(
77+
request.TextDocument.Uri, updatedTokens, cancellationToken).ConfigureAwait(false);
78+
}
79+
80+
return updatedTokens;
7181
}
7282

73-
var resultId = request.PreviousResultId;
7483
var editArray = ComputeSemanticTokensEdits(oldSemanticTokensData, newSemanticTokensData);
84+
var resultId = request.PreviousResultId;
7585

7686
// If we have edits, generate a new ResultId. Otherwise, re-use the previous one.
7787
if (editArray.Length != 0)
7888
{
7989
resultId = _tokensCache.GetNextResultId();
80-
var updatedTokens = new LSP.SemanticTokens { ResultId = resultId, Data = newSemanticTokensData };
81-
await _tokensCache.UpdateCacheAsync(
82-
request.TextDocument.Uri, updatedTokens, cancellationToken).ConfigureAwait(false);
90+
if (newSemanticTokensData.Length > 0)
91+
{
92+
var updatedTokens = new RoslynSemanticTokens
93+
{
94+
ResultId = resultId,
95+
Data = newSemanticTokensData,
96+
IsFinalized = isFinalized
97+
};
98+
99+
await _tokensCache.UpdateCacheAsync(
100+
request.TextDocument.Uri, updatedTokens, cancellationToken).ConfigureAwait(false);
101+
}
83102
}
84103

85-
var edits = new SemanticTokensDelta
104+
var edits = new RoslynSemanticTokensDelta
86105
{
106+
ResultId = resultId,
87107
Edits = editArray,
88-
ResultId = resultId
108+
IsFinalized = isFinalized
89109
};
90110

91111
return edits;

src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHandler.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,16 @@ public SemanticTokensHandler(SemanticTokensCache tokensCache)
4949
Contract.ThrowIfNull(context.Document, "Document is null.");
5050

5151
var resultId = _tokensCache.GetNextResultId();
52-
var tokensData = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
52+
var (tokensData, isFinalized) = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
5353
context.Document, SemanticTokensCache.TokenTypeToIndex,
5454
range: null, cancellationToken).ConfigureAwait(false);
5555

56-
var tokens = new LSP.SemanticTokens { ResultId = resultId, Data = tokensData };
57-
await _tokensCache.UpdateCacheAsync(request.TextDocument.Uri, tokens, cancellationToken).ConfigureAwait(false);
56+
var tokens = new RoslynSemanticTokens { ResultId = resultId, Data = tokensData, IsFinalized = isFinalized };
57+
if (tokensData.Length > 0)
58+
{
59+
await _tokensCache.UpdateCacheAsync(request.TextDocument.Uri, tokens, cancellationToken).ConfigureAwait(false);
60+
}
61+
5862
return tokens;
5963
}
6064
}

src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System;
56
using System.Collections.Generic;
67
using System.Linq;
78
using System.Threading;
@@ -105,7 +106,7 @@ internal class SemanticTokensHelpers
105106
/// <summary>
106107
/// Returns the semantic tokens data for a given document with an optional range.
107108
/// </summary>
108-
internal static async Task<int[]> ComputeSemanticTokensDataAsync(
109+
internal static async Task<(int[], bool isFinalized)> ComputeSemanticTokensDataAsync(
109110
Document document,
110111
Dictionary<string, int> tokenTypesToIndex,
111112
LSP.Range? range,
@@ -116,9 +117,17 @@ internal static async Task<int[]> ComputeSemanticTokensDataAsync(
116117

117118
// By default we calculate the tokens for the full document span, although the user
118119
// can pass in a range if they wish.
119-
var textSpan = range == null ? root.FullSpan : ProtocolConversions.RangeToTextSpan(range, text);
120+
var textSpan = range is null ? root.FullSpan : ProtocolConversions.RangeToTextSpan(range, text);
120121

121-
var classifiedSpans = await Classifier.GetClassifiedSpansAsync(document, textSpan, cancellationToken).ConfigureAwait(false);
122+
// If the full compilation is not yet available, we'll try getting a partial one. It may contain inaccurate
123+
// results but will speed up how quickly we can respond to the client's request.
124+
var frozenDocument = document.WithFrozenPartialSemantics(cancellationToken);
125+
var semanticModel = await frozenDocument.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
126+
Contract.ThrowIfNull(semanticModel);
127+
var isFinalized = document.Project.TryGetCompilation(out var compilation) && compilation == semanticModel.Compilation;
128+
document = frozenDocument;
129+
130+
var classifiedSpans = Classifier.GetClassifiedSpans(semanticModel, textSpan, document.Project.Solution.Workspace, cancellationToken);
122131
Contract.ThrowIfNull(classifiedSpans, "classifiedSpans is null");
123132

124133
// Multi-line tokens are not supported by VS (tracked by https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1265495).
@@ -127,7 +136,7 @@ internal static async Task<int[]> ComputeSemanticTokensDataAsync(
127136

128137
// TO-DO: We should implement support for streaming if LSP adds support for it:
129138
// https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1276300
130-
return ComputeTokens(text.Lines, updatedClassifiedSpans, tokenTypesToIndex);
139+
return (ComputeTokens(text.Lines, updatedClassifiedSpans, tokenTypesToIndex), isFinalized);
131140
}
132141

133142
private static ClassifiedSpan[] ConvertMultiLineToSingleLineSpans(SourceText text, ClassifiedSpan[] classifiedSpans)

src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensRangeHandler.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,11 @@ public SemanticTokensRangeHandler(SemanticTokensCache tokensCache)
5252
// partial token results. In addition, a range request is only ever called with a whole
5353
// document request, so caching range results is unnecessary since the whole document
5454
// handler will cache the results anyway.
55-
var tokensData = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
55+
var (tokensData, isFinalized) = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
5656
context.Document, SemanticTokensCache.TokenTypeToIndex,
5757
request.Range, cancellationToken).ConfigureAwait(false);
58-
return new LSP.SemanticTokens { ResultId = resultId, Data = tokensData };
58+
59+
return new RoslynSemanticTokens { ResultId = resultId, Data = tokensData, IsFinalized = isFinalized };
5960
}
6061
}
6162
}

src/Features/LanguageServer/ProtocolUnitTests/SemanticTokens/SemanticTokensTests.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ static class C { }
8080
await VerifyNoMultiLineTokens(testLspServer, rangeResults.Data!).ConfigureAwait(false);
8181
Assert.Equal(expectedRangeResults.Data, rangeResults.Data);
8282
Assert.Equal(expectedRangeResults.ResultId, rangeResults.ResultId);
83+
Assert.True(rangeResults is RoslynSemanticTokens);
8384

8485
// 2. Whole document handler
8586
var wholeDocResults = await RunGetSemanticTokensAsync(testLspServer, caretLocation);
@@ -101,6 +102,7 @@ static class C { }
101102
await VerifyNoMultiLineTokens(testLspServer, wholeDocResults.Data!).ConfigureAwait(false);
102103
Assert.Equal(expectedWholeDocResults.Data, wholeDocResults.Data);
103104
Assert.Equal(expectedWholeDocResults.ResultId, wholeDocResults.ResultId);
105+
Assert.True(wholeDocResults is RoslynSemanticTokens);
104106

105107
// 3. Edits handler - insert newline at beginning of file
106108
var newMarkup = @"
@@ -115,10 +117,12 @@ static class C { }
115117

116118
Assert.Equal(expectedEdit, ((LSP.SemanticTokensDelta)editResults).Edits.First());
117119
Assert.Equal("3", ((LSP.SemanticTokensDelta)editResults).ResultId);
120+
Assert.True((LSP.SemanticTokensDelta)editResults is RoslynSemanticTokensDelta);
118121

119122
// 4. Edits handler - no changes (ResultId should remain same)
120123
var editResultsNoChange = await RunGetSemanticTokensEditsAsync(testLspServer, caretLocation, previousResultId: "3");
121124
Assert.Equal("3", ((LSP.SemanticTokensDelta)editResultsNoChange).ResultId);
125+
Assert.True((LSP.SemanticTokensDelta)editResultsNoChange is RoslynSemanticTokensDelta);
122126

123127
// 5. Re-request whole document handler (may happen if LSP runs into an error)
124128
var wholeDocResults2 = await RunGetSemanticTokensAsync(testLspServer, caretLocation);
@@ -140,6 +144,7 @@ static class C { }
140144
await VerifyNoMultiLineTokens(testLspServer, wholeDocResults2.Data!).ConfigureAwait(false);
141145
Assert.Equal(expectedWholeDocResults2.Data, wholeDocResults2.Data);
142146
Assert.Equal(expectedWholeDocResults2.ResultId, wholeDocResults2.ResultId);
147+
Assert.True(wholeDocResults2 is RoslynSemanticTokens);
143148
}
144149

145150
[Fact]

0 commit comments

Comments
 (0)