There was a recent post on Reddit asking why there wasn’t an LSP for Crystal. This is a copy / paste of what I said in response, repeated for visibility and further discussion.
There are a number of things that make writing a language server for Crystal difficult.
- There is no fault tolerant parser, which is necessary to provide functionality with “broken” code (well, there was before the tree sitter got a lot of improvements, at 99.07% of the stdlib parsing without errors now)
- Crystal’s complex syntax makes it incredibly difficult to make a fault tolerant parser
- There is no semantic analysis done (by the compiler) to methods that aren’t used
- A full semantic analysis of the stdlib and all files is required (due to the nature of the language) every time a change is made
- Type inference means that the types of variables and parameters depends on how they’re used, requiring semantic analysis of everything to ensure types are accurately captured
- Everything is in a global namespace, and analysis relies on one or multiple entrypoints that have to be specified explicitly
Crystalline has been around for a few years and it does the best for what it has, re-using a lot of the semantic analysis that the compiler uses. It works for some people, especially on small to medium sized projects. I’d recommend giving it a try to see how well it works for you.
I’ve been working on a language server since last June, very much far from done but here’s a list of what I have so far:
- Auto-generated bindings to the language server protocol based on Microsoft’s own generated bindings for Rust / Python, see GitHub - nobodywasishere/lsprotocol-crystal: Code generator and generated types for Language Server Protocol & Crystal lang
- Basic framework for a language server to be implemented, pulling a lot of inspiration from Crystalline, named larimar
- Started doing a lot of research into fault-tolerant parsers and language server architectures (Roslyn, Rust, etc), and even started implementing my own fault-tolerant parser, but this is an immense task that may not be the best approach
- Worked with a group of amazing people to put a lot of work into the tree sitter parser for Crystal GitHub - crystal-lang-tools/tree-sitter-crystal: tree sitter parser for crystal lang . This is especially useful for syntax highlighting in helix / neovim / zed
- Began to put a lot of work into improving ameba, a linter for Crystal that I find very useful and should be used more widely by the community
- I’ve integrated both ameba and the tree sitter back into larimar to provide linting, syntax errors, and syntax highlighting in an editor-independent way. It’s not 100% yet for those but it’s a starting point
- I intend to keep working on ameba, adding semantic analysis to allow for better linting. I intend to take some of the code that’s used for that and re-use it in larimar. How exactly that will work i have some ideas but that’s still WIP. See Expand ameba's functionality with semantic information · Issue #513 · crystal-ameba/ameba · GitHub
My current approach that some people may not like, is that to get LSP-like features, methods / parameters / variables need to be explicitly typed. It’s the only way to avoid needing a full semantic analysis to resolve types.
Having spent the past year and a half working on tooling surrounding Crystal, I’ve become extremely familiar with the limitations of the language, and how it’s seemingly designed to be as hard as possible to have good tooling for. I know a lot of people in the community may be dissatisfied or frustrated with the current state of tooling, but the limitations of Crystalline are the limitations of the language itself, and it’s very hard to work around / outside of those without some compromise.
Progress is being made, but it needs to be done carefully and will take time.