Built a language server for learning purposes

Educational project

I was always intrigued by how IDEs provide smart completions when you press certain keys, how the diagnostics hit right, and all the mechanics they provide behind the scenes.

So I decided to dive into the process behind it: the Language Server Protocol. I also wanted to learn Elixir and ended up building an educational language server in Elixir.

While this is educational, I have implemented some basic functionalities regarding English writing:

  1. Shows the definition of the word under the cursor by calling vim.lsp.buf.hover() (default to K in Neovim).
  2. Sends an error diagnostic if the document contains VS Code.
  3. Suggests a list of words for completion.
  4. Suggests some code actions: replace VS Code with Neovim or censor it with VS C*de.

Any feedback, especially on the Elixir part, is welcome. I’m looking to get better in that language.

Architecture

  1. There are two GenServer: InputServer and LSPServer. They are both initialized in the module Application by a Supervisor.
                                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” 
                               β”Œβ”€β”€β”€β–Ίβ”‚  InputServer β”‚ 
                               β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ 
                               β”‚                     
                               β”‚                     
                               β”‚                     
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  initialize  β”‚                     
  β”‚ Application β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€                     
  β”‚  Supervisor β”‚              β”‚                     
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚                     
                               β”‚                     
                               β”‚                     
                               β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    
                               └───►│ LSPServer β”‚    
                                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    
  1. This is where the life cycle starts: InputServer listens on stdio.
    2.1. InputServer sends that message to LSPServer.
    2.2. LSPServer processes it and sends back a result to InputServer.
                                     
                                     
           message                   
  β”Œβ”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   
  β”‚clientβ”œβ”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚ InputServer β”‚   
  β””β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”¬β”€β”€β”€β”€β–²β”€β”€β”€β”€β”˜   
                       β”‚    β”‚ result 
                       β”‚    β”‚        
                       β”‚    β”‚        
               convey  β”‚    β”‚        
                    β”Œβ”€β”€β–Όβ”€β”€β”€β”€β”΄β”€β”€β”€β”    
                    β”‚ LSPServer β”‚    
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    
                                     
  1. InputServer sends the result back to the client and then loops back to listening on stdio.
                                                               
                                                               
                                              ready to listen  
                                            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       
                                            β”‚          β”‚       
                                            β”‚          β”‚       
                                            β–Ό          β”‚       
                               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚       
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”  result          β”‚               β”œβ”€β”€β”€β”€β”€β”€β”€β”˜       
   β”‚ client │◄──────────────────  InputServer  β”‚               
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  β”‚               β”‚               
                               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               
                                                               
                                                               

Notes

I have encountered two challenges that I haven’t solved in the Elixir way.

When and how to properly shut down the application?

I have set two System.stop() at two different occasions, which I found redundant:

  • when the client sends the shutdown request,
  • when :eof is received after listening with IO.read(:stdio, :line).

Process still runs in the background if a crash happens

In the life cycle of InputServer, if a crash happens (example String.split(nil, "\n")),
the process is still alive even after having closed Neovim, probably a zombie?
The only fix I have found was finding the source of the issue by checking the logs and correcting it.

4 Likes