Considering the ongoing rewrite of the compiler in Zig, I was wondering if there were any plans to rewrite the LSP too. I was looking at the LSP TODOs and, having a bit of experience with writing LSP, I thought I could contribute as well. Though being more interested in Zig over Rust I thought it might be worth starting a Zig rewrite on the side and work from there. Would that be worthwhile or is it better to just contribute to the existing Rust code?
Zig for sure, and actually in the compiler code base! :smiley:
I want to bake a lot of this functionality into the compiler, and in the future expose it in a way that can be used for more than just LSP. But as a starting point we can target LSP directly.
happy to chat about it if you'd like some direction getting started!
Thats perfect! I'd love to chat and start tackling this. I'll definitely make a fork of the compiler repo and work on something from there in the same codebase. How should I get started? By the way Im gonna be offline for a little while, Im on some messed up reverse sleep schedule today
Hi Etienne, I'd be very interested in lending a hand with the LSP if you'd be down. I have a vested interest in the NeoVim Roc LSP experience bahaha.
Sounds great! I could definitely use the help ahah I actually have the scaffolding committed on my fork at the moment. It has the transport layer, the data structure and a very basic server with none of the capabilities yet apart from initialized and exit. I will likely make a pull request soon so we can start integrating some functionalities one by one. Im just doing a bit more thorough testing and documenting. It does currently get detected by Neovim though with :LspInfo
Sounds great! Would you have time for a quick call in the next couple days just so I can get on the same page as you? Wanted to hack away on something since I have a long weekend next weekend haha. Very excited!
For sure! I should have time tomorrow during daytime EST if that works with you. If not, better even for me is Thursday evening.
Kind of a messy schedule here, juggling between 40hr/week software dev job, full time university and a girlfriend
Yeah, lemme know if you wanna do around 12 at lunch or maybe 5 PM, I'm also EST. If you're too busy tho that's fine, you can post your PR/post when you merge and I'll pull and start looking at everything. I'm also working so yeah async communication may be easiest to coordinate on the LSP.
12 is good! I might have to make the PR happen early in the morning though. Just got off a double shift so my brain is fried and theres some technical tweaks I want to do. Gonna sleep it off and wake up at around 4am so I'll get something out way before 12
By the way, we might want to take this convo into the DM. Some people might have other things to add here though
If you could just share any plans or updates once you've figured out the details in this thread that would be most appreciated -- it will help others contribute.
I'm really excited to have an LSP for the new syntax :smiley:
I just made the pull request. The request itself contains information about the implementation, but I also added a README.md in the new src/lsp folder alongside the other files.
Simply put the transport.zig and protocol.zig have been abstracted in a way that they shouldn't require change. When it comes to adding new functionnalities it'll be as simple as adding the new handler to the handlers directory and implementing them the same way as the current basic initialize and exit methods. Every handlers takes the ServerType which holds the state of the project as well as the functions to communicate back to the editor.
Also I suggest using the --debug-transport flag when connecting to the LSP with your editor. Since the LSP takes control over the stdin and stdout you can have all the requests and responses mirrored to a temporary log file. That way you can just watch it with tail -f while youre testing things out.
Currently the LSP is started with roc experimental-lsp, should we put the LSP in a separate binary to avoid that people start relying on it being included in the long term @Richard Feldman?
Actually he told me himself to have it be executed like that with roc experimental-lsp
yeah I think for now this will be the easiest way to get everything up and running, and "experimental" should convey "expect the way this works to change!"
Working on some change currently on the base structure of the LSP as I'm figuring out on the implementation of didChange and didOpen notifications. Wouldn't make sense to group them with requests such as initialized and shutdown as those expect a response immediately. Anyway in the LSP specifications they're separated between notification and request. Also working on a DocumentStore to handle the project different documents and store them in a StringHashMap. Could probably implement some caching later on to not have to reload every documents whenever the editor close. Particularly with neovim as it restart an lsp session every time you close all your buffers.
Also working on a DocumentStore to handle the project different documents and store them in a StringHashMap
I'm not 100% on the use case here ... but maybe it would help for you to checkout our cache manager implementation if you haven't seen that yet
We also have a file watcher implementation that is cross-platform
The cache manager is used to store the serialised modules into the roc cache
The StringHashMap is only to store the plain text of the files. Its really just meant to have the current buffer in memory so that we can do some parsing on it for things like syntax check
The file watcher and cache manager might definitely be useful though
Might be able to get a simple LSP going for AoC with the new compiler. Its coupled well with the BuildEnv so it works perfectly with the new syntax. People using the nightly build and the old syntax might still need to use the old LSP though. It doesnt play super well with the platform import, but its something for now.
lsp-demo.gif
Hey I haven't contributed to the compiler before but I wanted to help out with this since I've worked on other similar LSP/compiler type projects before. One thing I noticed is that it looks like the way this is implemented, we are reusing all the machinery/plumbing that is being used for roc check. One thing I ran into with that is https://github.com/roc-lang/roc/issues/8435 which means theoretically if we can implement the fix for that issue in roc check we also get type checking in the lsp which would be pretty nice. I started looking into it a bit https://github.com/roc-lang/roc/pull/8582 but ran into an issue with the import resolution (looks like roc check doesn't process platform imports the same way as roc run). I was able to get things hooked up and see results though
Screenshot 2025-12-06 at 10.25.57 PM.png
@Ameen yeah the type checking seems a bit inconsistent right now. As you can see in the gif I posted a bit higher up it is able to tell that String is not a correct type, but after updating the compiler further some of these checks seems to have been gone
@Richard Feldman has a plan and a WIP PR to rip the gpa out of ModuleEnv which means we can unify a lot of the logic between roc run and roc check -- I hope I explained that right...
it has drifted a lot, might be easier to start over on that one - I'll get back to it after we get to the end of Advent of Code :smile:
So I actually dug deeper into the issue because of @Ameen insights. It seems that what was causing the issue is due to how the LSP runs the build into single threaded mode. The run loop exits once the remaining_modules reaches zero. The problem is that the platform is actually being discovered late and the platform injector gets new task, but since theres no global queue and the package loop has already stopped the platform never reaches the Done state, so the ImportResolver isReady stays false, hence why its stuck on WaitingOnImports. The fix by @Ameen does force the platform scheduling to run, but it doesnt resolve all WaitingOnImports. This whole thing could be an issue even in the regular multithreaded mode as the waitForIdle() doesnt check that modules with external imports are not still waiting on those dependencies. So even though the build might finish, no type check is gonna get done on those imports. Im working on a step to unblock these later down the build pipeline.
I thought we sorted the module dependencies in strongly connected components and always compiled the leaf nodes first to avoid any deadlocks
I'm not sure if we fully implemented that or if it was just a plan
@Luke Boswell It seems like in the current build the modules are being added to the queue as they are being discovered, but they are not being woken up. So there doesn't seem to be any SCC type scheduling that guarantees the leaf nodes are compiled first.
Basically, the tryUnblock checks the external imports, but at this point their state might not be isReady . So they're stuck on WaitingOnImports, the function returns and they're just left there
yeah the new compiler's import system is very basic right now :smile:
Yeah, I've been digging my head in the build system for a moment and because the roc check command doesnt actually resolve imports the LSP also doesn't. I tried to see if we could use the build pipeline itself with the LSP, but it doesn't seem as simple to do in memory as to not add some huge overhead at every change in the editor. Right now I made a small patch that triggers the imports to be checked again later down the pipeline so theres not WaitingOnImports and as a bandaid I suppressed the diagnostics regarding undefined variable, but only for those regarding imports. Theres just no check thats gonna be done on those imports, but at least the rest of the program is gonna get type checked normally and no error is gonna show up for the imported functions that are valid.
Last updated: Dec 21 2025 at 12:15 UTC