Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a Razor language server cohosted in Roslyn #70819

Merged
merged 22 commits into from
Dec 12, 2023

Conversation

davidwengier
Copy link
Contributor

@davidwengier davidwengier commented Nov 15, 2023

Part of dotnet/razor#9519

This creates a basic LSP client/server for the "Razor" content type, inheriting from the existing Roslyn language server. Tried to keep the code as minimal as possible, as all of the endpoints will be in the Razor repo, but we need document sync in this repo, in order to access internals on RequestContext, and project context in this repo to match the data the RequestContext expects to receive. Some minor complexity in order to avoid exposing VS Protocol types in the API, to avoid dependency annoyances.

This also includes a very small portion of #70170, which is support for LSP requests against TextDocuments. Thank you @mgoertz-msft!

I've tested locally with local build Roslyn and Razor, but not used it in anger to try to implement a "real" endpoint in the Razor repo yet. Wanted to put this up early-ish in case there is a lot of feedback.

@davidwengier davidwengier requested a review from a team as a code owner November 15, 2023 07:09
@dotnet-issue-labeler dotnet-issue-labeler bot added Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead labels Nov 15, 2023
@davidwengier davidwengier requested review from dibarbet and a team November 15, 2023 07:10
@@ -35,19 +35,20 @@ public GetTextDocumentWithContextHandler()
Contract.ThrowIfNull(context.Workspace);
Contract.ThrowIfNull(context.Solution);

// We specifically don't use context.Document here because we want multiple
var documents = context.Solution.GetDocuments(request.TextDocument.Uri);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changing this to call GetTextDocuments seemed overly wasteful given only project info is needed, so I changed this handler a bit. Mainly wanted to avoid calling GetDocument and GetAdditionalDocument on every Id (and this endpoint is called a fair bit)

ExportProvider exportProvider,
[Import(AllowDefault = true)] IRazorCohostCapabilitiesProvider? razorCapabilitiesProvider = null,
[Import(AllowDefault = true)] IRazorCustomMessageTarget? razorCustomMessageTarget = null,
[Import(AllowDefault = true)] IRazorCohostLanguageClientActivationService? razorCohostLanguageClientActivationService = null)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Made all of these nullable to avoid dual insertions etc. and so nothing activates if the Razor bits aren't installed. Though hopefully this never activates in that case, due to the content type :)

@@ -31,7 +31,6 @@
<InternalsVisibleTo Include="Microsoft.VisualStudio.LanguageServices.Razor" Key="$(RazorKey)" />
<InternalsVisibleTo Include="Microsoft.VisualStudio.LanguageServerClient.Razor" Key="$(RazorKey)" />
<InternalsVisibleTo Include="Microsoft.VisualStudio.LanguageServerClient.Razor.Test" Key="$(RazorKey)" />
<InternalsVisibleTo Include="Microsoft.VisualStudio.Mac.LanguageServices.Razor" Key="$(RazorKey)" />
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Snuck this change in here, and the above repo uri change :)


namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;

internal abstract class AbstractRazorCohostRequestHandler<TRequestType, TResponseType> : ILspServiceRequestHandler<TRequestType, TResponseType>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Endpoints on the Razor side will inherit from one of these two abstract types.


protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(TextDocumentPositionParams request)
{
return new RazorTextDocumentIdentifier(request.TextDocument.Uri, (request.TextDocument as VSTextDocumentIdentifier)?.ProjectContext?.Id);
Copy link
Contributor Author

@davidwengier davidwengier Nov 15, 2023

Choose a reason for hiding this comment

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

All of the endpoints in the Razor repo will have to do this, in particular the second parameter, in order to ensure project context flows correctly. Not too bad, and I couldn't see a way around it. Will create a nice extension method on the Razor side for it anyway, so it would be return request.TextDocument.ToRazorTextDocumentIdentifier()

Copy link
Member

@dibarbet dibarbet left a comment

Choose a reason for hiding this comment

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

Generally fine with this

/// <summary>
/// Razor LSP server for Razor document requests (.razor and .cshtml files)
/// </summary>
RazorCohostServer,
Copy link
Member

Choose a reason for hiding this comment

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

So trying to understand the steps here

  1. Create this server to handle Razor files (instead of the current Razor server). Migrate Razor support to here.
  2. (maybe) Once the rest of Marco's work goes in, we can unify this Razor server with the RazorCSharp server, and have a single server that handles both
  3. Once dynamic registration goes in we merge this with the normal C# server

Does that sound right? Potentially we can also skip 2) entirely if requests about C# are converted to non-LSP entirely before 3?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Item 2 hadn't really occurred to me, but I guess we could do that. Without dynamic registration though, we probably can't do that behind a feature flag though, as it would just be controlled by the content type attribute on the server. Interesting idea though.

The rest is correct though. Essentially this lets us work on migration, find bugs, maybe get some benefit, but without being blocked on dynamic registration.

[Export(typeof(RazorLspServiceProvider)), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class RazorLspServiceProvider(
Copy link
Member

Choose a reason for hiding this comment

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

All of these should go away once we can merge this into the C# server (with dynamic registration) right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep! Thats why I tried to make them do as little as possible

@davidwengier
Copy link
Contributor Author

@CyrusNajmabadi I heard a rumour you wanted to review this too?

I imagine there will be follow ups regardless, as I start implementing the Razor side

return base.ActivateAsync(cancellationToken);
}

return Task.FromResult<Connection?>(null);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sadly, this doesn't work. The platform helpfully checks this for null and just immediately throws an exception 🤦‍♂️

https://dev.azure.com/devdiv/DevDiv/_git/VSLanguageServerClient?path=/src/product/RemoteLanguage/Impl/Infrastructure/MefClientProvider/RemoteLanguageClientInstance.cs&version=GBdevelop&line=528&lineEnd=529&lineStartColumn=1&lineEndColumn=1&lineStyle=plain&_a=contents

So options are ClientName, or just return no capabilities. Thoughts @dibarbet ?

I'm guessing ClientName, since activating a server to nothing isn't great, but there is a slight downside with that, that we'd be unable to do progressive moves to this server, it would be all or nothing. I don't know how important that will be though.

Copy link
Member

Choose a reason for hiding this comment

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

So I'm not super opposed to having a server with no capabilities. We'll just have to make sure that the server isn't trying to do anything (e.g. sending refresh notifications).

As you mention it would allow you to migrate features one at a time which I think is fairly valuable.

@davidwengier
Copy link
Contributor Author

I've addressed all of the feedback, and am using this fine from the Razor repo. Just to confirm @dibarbet is this good to merge? Or do you want to hold off for a specific version etc.?

@davidwengier
Copy link
Contributor Author

Made a few more updates to this for issues found when implementing the first endpoint from Razor (in dotnet/razor#9674). Would be good if you can take a look and re-approve @dibarbet, then I'll merge so I can get some packages going.

context.UpdateTrackedDocument(request.TextDocument.Uri, text);

// Razor can't handle this request because they don't have access to the RequestContext, but they might want to do something with it
if (didChangeHandler is not null)
Copy link
Member

Choose a reason for hiding this comment

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

Wondering here if we want a different mechanism to expose changes to Razor, if Razor needs to do special handling on didChange. For example similar to the IOnInitialized services that get called by the base InitializedHandler.

Especially when we unify with the Roslyn server- I'm not sure we want to allow separate didChange handlers per language.

Does the work Razor needs to do here require the queue to be blocked while its happening?

Same for open/close

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All good thoughts, and I see this as definitely a point-in-time thing.

  1. When we have dynamic registration, yes I would expect the "normal" text document sync handlers work for all languages and we'd remove these.
  2. When Razor can pull on the source generator for our .g.cs files then we won't need to be notified about changes or opens.

For now though, we need something here, as the Razor side that responds to this is effectively unchanged from our existing code, we kick off a compile of the razor file as it changes.

Copy link
Contributor Author

@davidwengier davidwengier Dec 12, 2023

Choose a reason for hiding this comment

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

Actually, even when we can use the source generator and even use the Roslyn API rather than LSP to ask questions about it etc, we'd still need some way to get notified of open and change of Razor files, and run code while the queue is blocked, in order to make sure the html virtual buffers are up to date. Maybe some way to hook into the LspWorkspaceManager? Haven't really thought about the specifics of that far ahead yet.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm. Probably depends on what needs to happen. I wouldn't be opposed to something like IOnInitialized where we expose a hook inside the handler. I think putting it there makes it more clear that its a blocking hook (than putting in LspWorkspaceManager0

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm missing something. Isn't that exactly what this is currently doing?

Copy link
Member

Choose a reason for hiding this comment

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

What I meant was putting code inside the main DidChangeHandler that allows for arbitrary additional callers, instead of duplicating teh handler for Razor and having a special Razor hook.
e.g. https://github.com/dotnet/roslyn/blob/main/src/Features/LanguageServer/Protocol/Handler/ServerLifetime/InitializedHandler.cs#L27

We'd have a generic hook that razor could implement. However I think you need to export a separate Razor didChange handler already - so I'm fine if we do that later when we merge the servers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh yep, can definitely follow up with that once we merge. The existing handlers won't activate in this server though, due to the export attribute being used.

@davidwengier
Copy link
Contributor Author

@dibarbet any reason not to merge this?

@davidwengier davidwengier merged commit aae160a into dotnet:main Dec 12, 2023
22 of 24 checks passed
@davidwengier davidwengier deleted the RazorCohost branch December 12, 2023 22:40
@ghost ghost added this to the Next milestone Dec 12, 2023
davidwengier added a commit to dotnet/razor that referenced this pull request Jan 4, 2024
First part of #9519
Razor side of dotnet/roslyn#70819

This PR consumes the new Cohost server from Roslyn, and when enabled,
moves DocumentColor requests from being handled by our existing Razor
Language Server, to instead be handled by the new server. There is
currently no sharing of project system data or anything like that yet. A
few TODOs for follow ups, ~can't be merged, and indeed won't build,
until a Roslyn package is available etc. but~ it's reviewable while we
keep working, and more important its shippable in its "off" state.

~Ignore the last commit, obviously won't be merged :)~
@Cosifne Cosifne modified the milestones: Next, 17.9 P3 Jan 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants