[LSP] Add semantic token caching for solution open#58238
[LSP] Add semantic token caching for solution open#58238allisonchou merged 15 commits intodotnet:mainfrom
Conversation
allisonchou
commented
Dec 10, 2021
- In local C#/VB, semantic tokens are cached in a SQL database that is queried on solution open. This allows us to instantly colorize the solution without performing any token computations. The goal of this PR is to accomplish something similar in LSP scenarios.
- This PR adds the caching described above to Roslyn LSP. It also makes it so we now only return semantic classifications, whereas before we returned both semantic + syntactic classifications. We are able to do this because the C# syntactic classifier runs on the client, so we save ourselves lots of work by only returning semantic classifications.
- We have to special case Razor since Razor requires us to return both syntactic and semantic classifications. This is due to the C# syntactic classifier not running on the client in Razor scenarios. Ideally, Razor would eventually run the classifier in some way (tracked by Investigate having C# syntactic classifier run in Razor razor#5850) so we can get rid of this special casing. For now, we cache and return both types of tokens.
- Updated + added tests.
src/EditorFeatures/Core/Implementation/Classification/SemanticClassificationUtilities.cs
Outdated
Show resolved
Hide resolved
src/EditorFeatures/Core/Implementation/Classification/SemanticClassificationUtilities.cs
Outdated
Show resolved
Hide resolved
src/Features/Core/Portable/SemanticClassificationCache/SemanticClassificationCacheUtilities.cs
Outdated
Show resolved
Hide resolved
| // classifications since the client doesn't run the C# classifier in Razor LSP scenarios. | ||
| // Ideally, Razor will eventually run the C# syntactic classifier on their end so we can remove this | ||
| // special casing: https://github.com/dotnet/razor-tooling/issues/5850 | ||
| if (document.FilePath is not null && document.FilePath.EndsWith(".g.cs")) |
There was a problem hiding this comment.
.IsRazorDocument() doesn't work in OOP scenarios, so as a workaround I added a check for .EndsWith(".g.cs") instead which seems to do the trick
There was a problem hiding this comment.
note: This feels weird as well. somethign about the entire razor space here seems somewhat broken (necessitating these extra strange codepaths at the feature level). my preference would be that if razor is currently doing something different/weird, that we just not have this optimization/feature for them. and, if we can then get razor docs/scenarios to work like normal scenarios, then we reenable, with no specialized code in the feature to handle razor.
There was a problem hiding this comment.
Most of this special casing is due to Razor not running the C# syntax classifier on their end. However, I believe adding support is a pretty big work item and could possibly also negatively impact Razor perf, so the effects would have to be investigated.
While this special casing doesn't look good, I think it will result in pretty good gains for Razor semantic tokens which imo might be an appropriate trade-off since the Razor LSP editor is on by default for users.
src/Features/Core/Portable/SemanticClassificationCache/SemanticClassificationCacheUtilities.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensRangeHandler.cs
Outdated
Show resolved
Hide resolved
|
|
||
| var solution = document.Project.Solution; | ||
|
|
||
| // Only cache classifications in non-LSP scenarios. LSP caching is handled separately. |
There was a problem hiding this comment.
@dibarbet here as well...
interesting. why is this @allisonchou ? why not have the normal incremental analyzer handle this. note: if this is the right way to go, then this comment needs beefing up. Specifically, this is an example of a comment htat just repeats what the code is doing but doesn't provide clarity as to 'why'. I cannot look at this code/comment and say: ah, yes, that makes sense, we def should be doing things in this fashion.
There was a problem hiding this comment.
The main issue with using the default logic is the method's call to document.IsOpen(). This call is problematic in LSP for two reasons:
- It bases whether the document is open on the non-LSP workspace rather than the LSP workspace.
- Razor back-end generated documents won't be considered open even if the front-end document is open. This means we would have to special case Razor (via exposing a new document service, workspace service or something else).
Putting the caching logic in the LSP handler addresses both 1) and 2) by ensuring we only cache open files in the LSP workspace, since we only get semantic token requests for the LSP file(s) currently open + we get requests for Razor generated documents. In an ideal world, I think we'd have the IsOpen check correspond to the LSP workspace and that workspace can then specially check for open Razor documents, but I believe that is a much bigger work item.
|
@CyrusNajmabadi whenever you have time, this should be ready to review again. I've addressed your feedback + responded to the changes from your other PRs. |
src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs
Show resolved
Hide resolved
src/Workspaces/Core/Portable/Classification/ClassifierHelper.cs
Outdated
Show resolved
Hide resolved
| // See if we can find a semantic part to replace this syntax part. | ||
| var replacementIndex = semanticParts.FindIndex( | ||
| lastReplacementIndex, t => t.TextSpan == syntaxPartAndSpan.TextSpan); | ||
| lastReplacementIndex, t => t.TextSpan.OverlapsWith(syntaxPartAndSpan.TextSpan)); |
There was a problem hiding this comment.
I believe this is an existing bug in the logic related to the comment I added below:
// There may be multiple semantic parts corresponding to a single
// syntactic part, so we might need to go through a syntactic part
// multiple times to verify. For example, this is the case with
// verbatim string literals containing string escape characters.
Essentially, there's a chance the TextSpan of the semantic part will not exactly match the TextSpan of the syntactic part. This occurs with string literals containing string escape characters. The string escape character will not have the same exact span as the syntactic part and therefore will not be considered. I believe instead calling OverlapsWith mitigates the problem.
src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs
Show resolved
Hide resolved
|
@CyrusNajmabadi thanks for your review! I've responded to your feedback. Would you mind taking a look again whenever you have time? |
ryanbrandenburg
left a comment
There was a problem hiding this comment.
Only complaint is the "secret return", but you probably want to wait for review from someone with more expertise in Roslyn.
src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensRangeHandler.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/ProtocolUnitTests/SemanticTokens/SemanticTokensRangeTests.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs
Show resolved
Hide resolved
| } | ||
|
|
||
| [Fact] | ||
| public async Task TestGetSemanticTokensRange_PartialDocAsync() |
There was a problem hiding this comment.
did this test go missing? I'm not seeing a new test that verifies the partial (1,0) -> (2,0) positions
There was a problem hiding this comment.
Whoops, you're totally right. Thanks! Added it back.
src/Workspaces/Core/Portable/Classification/ClassifierHelper.cs
Outdated
Show resolved
Hide resolved
|
Thanks everyone! |