-
Notifications
You must be signed in to change notification settings - Fork 105
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
[info request] Hosting the language server code in Blazor? #456
Comments
I was thinking about this a week or two ago, in theory things should "just work" because the language server is just a .NET Standard library. This sounds fun, so I'll take a look at your code and see if I can get it running. |
If there is a message pump at play one of the challenges might be that pump using a dedicated thread. I don't think Blazor WASM supports threads yet. Certain BCL methods that interact with threading will "spin" and fry your CPU :) |
Thanks! If you need any pointers in getting it running, let me know. It should just work if you clone the repo and run:
I've stuck some haphazard logging in which should get written out to the browser console. |
For input the process scheduler runs on the thread pool, which I think should be fine. For output however... I think it by default runs a dedicated thread.
|
Okay here's a possible quick fix. When setting up the server... try this.
|
I theory that should kick the output handler to use the |
@ryanbrandenburg @NTaylorMullen @TylerLeonhardt thoughts, should I just move to use the task pool scheduler for handing input/output? At the time a dedicated thread "made sense" but honestly it probably doesn't matter. Input is already on the task pool and working fine. |
Having a dedicated thread has been risky because if something doesn't ConfigureAwait(false) and blocks you're doomed. We've actually encountered that issue once or twice in VS (as I'm sure you recall) so relying on the task pool scheduler doesn't sound awful. Are there any other drawbacks? All that said for extra background info, we run Razor's language server in-proc in VS today which I presume from the quick glance at this thread similar types of things are trying to be acheived. Here's where we create our own abstraction to start the spinup of the O# framework bits in VS: https://github.com/dotnet/aspnetcore-tooling/blob/feb060660bf14c9da3f284a72fe5f86390d3ab65/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/RazorLanguageServerClient.cs#L126-L129 And here's our actual abstraction that can rely on in-proc or out of proc streams: https://github.com/dotnet/aspnetcore-tooling/blob/feb060660bf14c9da3f284a72fe5f86390d3ab65/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs#L72-L75 |
I gave this a go, but didn't see any observable difference in behavior. I did notice that the Reactive.Wasm library I'm trying to use to replace the default scheduler doesn't appear to be doing what it's meant to in .NET 5 - in particular these checks no longer seem to work: In theory if they were working, I should be able to do:
I'll see if I can fix up the above checks locally and get that working. |
Thanks for the pointers! We have the language server running as a standalone exe, which we use for VSCode integration, but as an experiment, I'm trying to see if we can also host the language server fully in a web browser, using Blazor/WASM without a backend - I think that's where the complexity is mostly coming from. Is that something your team has attempted by any chance? |
Ah, ya I can definitely imagine that being difficult 😄. No we haven't tried that but I can just imagine how the threading models may make things more difficult in addition to things like file watchers |
@david-driscoll I just got an end-to-end working with a very hacky change here:
Instead of adding the message to the queue, I just sent it directly with:
So I think that definitely confirms that it's something to do with the scheduler. Interestingly, I noticed that in the version of the language server we're using (0.18.3) it is using I'm going to try and get a more solid PoC together by replacing IOutputHandler in the IoC container. |
interesting!
@anthony-c-martin are you on slack or msteams? |
So I'm running into an error Here's my branch for you reference from: Couple notes: I was building locally with the latest version of the library (0.19.0-beta.1) so the C# changes are the changes required based on the breaking changes I've documented. Also I was able to simplify the interop a little bit by using |
I've created this PR #458 so we can configure the schedulers specifically. |
This is AMAZING, thank you so much for your help! I ran into the same issue with the language client - looks like semantic support has only been added to a preview version, and that they haven't yet picked up the latest LSP spec. For now, since we've already implemented our own semantic token handler anyway, I've reverted back to using this for now until the language client has actual support for it. I've picked up a bunch of your changes and updated my branch: main...antmarti/experiment/monaco_lsp. I've pushed a demo of this here: https://bicepdemo.z22.web.core.windows.net/experiment/lsp/index.html |
This has me thinking of making a Blazor Component that uses the monaco editor... but to try to make as much as possible of it actually live in C# and use the Other than the annoying part of converting the monaco api into C#... ugh. |
Right now I don't think I have the bandwidth to tie monaco and blazor together. I might spike something out next weekend. I looked at https://github.com/microsoft/monaco-editor/blob/master/monaco.d.ts and while I'm sure I could... that's a lot of code to keep in sync, so I would want to build out some sort of tool to integrate the two together. I found this project, and posted an issue there canhorn/EventHorizon.Blazor.TypeScript.Interop.Generator#31 to see what might be needed to support generation interop with In the meantime there is recent activity on https://github.com/TypeFox/monaco-languageclient updating it to the latest version (that would include semantic tokens), you might be able to pin to the latest master branch and see if that works (I have not tried). |
I'm on Teams - [email protected] |
Shoot now I want to run the PowerShell language server in Blazor! |
I think you can, you'll just have to do something similar to the bicep solution using monaco + monaco-languageclient, it totally works, there might be some issues if you use the file system APIs but those can always be fixed. |
@TylerLeonhardt feel free to reach out if you'd like any pointers for the Bicep code! |
I'm skeptical the PowerShell API will "just work" in Blazor WASM but worth a shot. @anthony-c-martin how did you "start the language server" in Blazor WASM? I'd love to take a peak at how the language server is hooked up to Monaco Editor. |
For context, I've used the Monaco-languageclient before, but only their stdio option where the language server was running in a separate process on the machine. |
[credit goes to @david-driscoll for a lot of this code] Here's where the server is being initialized: Here's the C# method that the JS code invokes to send data from client to server: Here's where the C# code invokes the JS code to send data from server to client: Here's the JS code to setup the send/receive with the server via the Blazor methods/callbacks: On startup I'm initializing the Blazor code from JS and setting the If you follow through the TS code, you should be able to see how the above is hooked into monaco-languageclient. I'm probably going to try and refine this code at some point to see if I can clean up the use of globals, and also to see if I can use a webworker to run the Blazor code. |
Out of interest, what are the benefits of implementing the translation layer between LSP & monaco's "custom LSP" in C# vs relying on monaco-languageclient to do it? I quite like the clean separation of having the TS code handle the translation and communicating with the C# code via LSP. |
I just think it would be pretty cool to have a fully featured wrapper for monaco from the C# side. The added extra would make it it easier to consume using the client. |
Probably because then @david-driscoll could guarantee that the monaco language client was up-to-date on the LSP spec. |
Going to pin this issue for any passers by as it is truly a cool feature. |
FWIW I think we need one of dotnet/aspnetcore#17730 or dotnet/aspnetcore#5475 to really unlock the power of this, because at the moment synchronous dotnet code locks up the UI thread, which feels a little janky when typing. There's also this project which I haven't really investigated that might work as a stopgap: https://github.com/Tewr/BlazorWorker |
I think a web worker would be perfect. Your UI (TypeScript) starts the worker, and you interop with the worker using postmessage. The worker then just has to interop with the language server. |
Thank you guys, you helped me a lot to understand some ideas. I'm trying to build a small POC on blazor and monaco based C# code editor with code completion. However, I cannot get code completion to work. What I've done:
public class Interop
{
private LanguageServer languageServer;
private readonly IJSRuntime jsRuntime;
private readonly PipeWriter inputWriter;
private readonly PipeReader outputReader;
public Interop(IJSRuntime jsRuntime)
{
this.jsRuntime = jsRuntime;
var inputPipe = new Pipe();
var outputPipe = new Pipe();
inputWriter = inputPipe.Writer;
outputReader = outputPipe.Reader;
languageServer = LanguageServer.PreInit(opts =>
{
opts.WithInput(inputPipe.Reader);
opts.WithOutput(outputPipe.Writer);
opts.Services.AddSingleton<IScheduler>(ImmediateScheduler.Instance);
});
Task.Run(() => RunAsync(CancellationToken.None));
Task.Run(() => ProcessInputStreamAsync());
}
public async Task RunAsync(CancellationToken cancellationToken)
{
await languageServer.Initialize(cancellationToken);
await languageServer.WaitForExit;
}
[JSInvokable]
public async Task SendLspDataAsync(string jsonContent)
{
var cancelToken = CancellationToken.None;
Console.WriteLine("jsonContent");
Console.WriteLine(jsonContent);
await inputWriter.WriteAsync(Encoding.UTF8.GetBytes(jsonContent)).ConfigureAwait(false);
}
private async Task ProcessInputStreamAsync()
{
do
{
var result = await outputReader.ReadAsync(CancellationToken.None).ConfigureAwait(false);
var buffer = result.Buffer;
Console.WriteLine("ProcessInputStreamAsync");
await jsRuntime.InvokeVoidAsync("ReceiveLspData", Encoding.UTF8.GetString(buffer.Slice(buffer.Start, buffer.End)));
outputReader.AdvanceTo(buffer.End, buffer.End);
// Stop reading if there's no more data coming.
if (result.IsCompleted && buffer.IsEmpty)
{
break;
}
// TODO: Add cancellation token
} while (!CancellationToken.None.IsCancellationRequested);
}
} Code completion doesn't work, because I haven't registered CodeCompletionHandler. I don't understand which one to use, because in Bicep you use a custom completion handler, in my POC I would like to use O# completion handler. |
I've made a solution, that compiles C# project into single-file UMD library: https://github.com/Elringus/DotNetJS Tried to use the server with it, but not sure how to deal with input/output. Console.STD won't work, obviously. Can we somehow run the server via websocket? |
@elringus Nice, I'll check that library out! Here's how I've been doing things in my experimental branch - using a simple send/receive method to pass JSONRPC back and forth from JS <-> C#:
Not the most elegant/performant, but it works well enough for now. Being able to have client-side Blazor host a websocket would make this a lot nicer. Failing that, being able to hook up the JS streams / C# pipes to each other directly would avoid the serialization/deserialization step. |
@elringus I'm currently trying to use your library to get our OmniSharp-based Language Server to run in an VS Code Web extension. This sounds very similar to what you want to achieve. May I ask if you already managed to get that working? My current status is that I can run the language server in a Blazor project (thanks to the information in this thread), but in the web extension the server never finishes initialization. |
@Skleni I've switched to Microsoft's reference LSP implementation in JS (https://github.com/microsoft/vscode-languageserver-node), while reusing the existing language-specific C# code via DotNetJS: — this way we can get up-to-date LSP implementation and native webworker transport layer out of the box, while keeping all the handlers logic in C#. Regarding VS Code, there were 2 issues with this workflow, but they're both solved in insiders stream now and should become available in the main stream in February: |
We're using this library to provide LSP support for Bicep. We have an browser demo at https://aka.ms/bicepdemo which calls into the compiler code directly using Blazor/WASM, using Microsoft.JSInterop to compile, emit diagnostics, get semantic tokens, and pass the results back to the Monaco editor, but we're not using the language server for this - instead we've created a few custom functions for it.
The monaco-languageclient library can be used to hook monaco up to a language server which would provide much of the functionality that VSCode offers in a browser. It would be extremely cool to be able to simply use the LSP in a browser without the need for a back-end server.
I'm curious as to whether anyone has tried to run this server code via Blazor before. I've been experimenting with it and am able to get the
initialize
request/response negotiation to take place, but I don't see theclient/registerCapability
request come through from the server. I suspect there may be some sort of message pump that needs to run, but am not at all familiar with the Reactive library that's being used. Any pointers that you can give me would be awesome!Here's an example of the code changes I've been experimenting with to hook this up to monaco: main...antmarti/experiment/monaco_lsp
The text was updated successfully, but these errors were encountered: