Skip to content

[LSP] Add semantic token caching for solution open#58238

Merged
allisonchou merged 15 commits intodotnet:mainfrom
allisonchou:AddLSPCaching
Dec 16, 2021
Merged

[LSP] Add semantic token caching for solution open#58238
allisonchou merged 15 commits intodotnet:mainfrom
allisonchou:AddLSPCaching

Conversation

@allisonchou
Copy link
Copy Markdown
Contributor

  • 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.

@ghost ghost added the Area-IDE label Dec 10, 2021
// 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"))
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NTaylorMullen for thoughts.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@allisonchou allisonchou marked this pull request as ready for review December 10, 2021 00:47
@allisonchou allisonchou requested a review from a team as a code owner December 10, 2021 00:47

var solution = document.Project.Solution;

// Only cache classifications in non-LSP scenarios. LSP caching is handled separately.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copy link
Copy Markdown
Contributor Author

@allisonchou allisonchou Dec 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. It bases whether the document is open on the non-LSP workspace rather than the LSP workspace.
  2. 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.

@allisonchou
Copy link
Copy Markdown
Contributor Author

@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.

// 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));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@allisonchou
Copy link
Copy Markdown
Contributor Author

@CyrusNajmabadi thanks for your review! I've responded to your feedback. Would you mind taking a look again whenever you have time?

Copy link
Copy Markdown
Contributor

@ryanbrandenburg ryanbrandenburg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only complaint is the "secret return", but you probably want to wait for review from someone with more expertise in Roslyn.

}

[Fact]
public async Task TestGetSemanticTokensRange_PartialDocAsync()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did this test go missing? I'm not seeing a new test that verifies the partial (1,0) -> (2,0) positions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, you're totally right. Thanks! Added it back.

Copy link
Copy Markdown
Contributor

@CyrusNajmabadi CyrusNajmabadi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@allisonchou allisonchou merged commit 0bf2997 into dotnet:main Dec 16, 2021
@ghost ghost added this to the Next milestone Dec 16, 2021
@allisonchou allisonchou deleted the AddLSPCaching branch December 16, 2021 21:22
@allisonchou
Copy link
Copy Markdown
Contributor Author

Thanks everyone!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants