Using ReasonML with Vim / Neovim

Here’s my attempts to get ReasonML working within Vim and the journey it took me on to understand what language servers are. If you’re using Vim this is essentially step 2 of the ‘quick start’ guide for reason: editor plugins.

Warning to all who enter here… this took me 10+ hours to fix. It’s because my setup doesn’t mesh exactly with their expected setup. If you want an easy life just use VS Code. Failing that if you want an easy life with Vim, just use the following setup:

This assumes that you can install the LanguageServer which you have to do for VS Code as well.

Vim support

By default for using linting I have the following setup:

  • Vim 8
  • Vundle
  • ALE (Asynchronous Lint Engine)

This has managed to work ‘OK’ for Javascript with a bit of linting. But I never got fully fledged Language Server Protocol (LSP) integration working, I don’t really know why.

My initial attempts with my default setup were a total failure. Mostly because I find the LSP concept hard to understand. ALE should be a LSP ‘thing’. So it should be able to act as a Language Client and talk to Language Servers.

The repeated issue I come across is that to get LSP working you need the vim-plug Vim plugin manager. I don’t particularly know why, I guess it works better for these more complex plugins.

Looking through my .vimrc file it looks like I tried to get ALE to work as a LSP.

I switched to Neovim as part of this to minimise the number of plugins that I’m installing.

Switching from Vundle to vim-plug

Until now I’ve always used Vundle, it’s a good basic plugin. But I keep hitting plugins that don’t have instructions for Vundle and just vim-plug. vim-plug seems as good as Vundle, so let’s try it and see how much work the conversion is.

It’s actually ridiculously easy, much respect for vim-plug, and probably Vundle for both having very similar and easy to replace syntax. I replaced the Vundle lines at the start:

-filetype off                  " required
-set rtp+=~/.vim/bundle/Vundle.vim
-call vundle#begin()
-
-Plugin 'VundleVim/Vundle.vim'
+call plug#begin('~/.vim/bundle')

Then I replace all Plugin with Plug and then at the end:

-call vundle#end()
-filetype plugin indent on    " required
+call plug#end()

Then I ran :source % and :PlugInstall and it magically installed all my 31 plugins in 10s and they all seem to be magically working. If you use the .vim/bundle installation directory then vim-plug doesn’t even need to install anything.

There’s some useful instructions in the vim-plug wiki on Migrating from Vundle.

Magically also my installation of deoplete worked correctly. So now deoplete pops up all the time as I’m typing – I’m assuming it’s possible to make it less in your face…

if has('nvim')
  Plug 'Shougo/deoplete.nvim', { 'do': ':UpdateRemotePlugins' }
else
  Plug 'Shougo/deoplete.nvim'
  Plug 'roxma/nvim-yarp'
  Plug 'roxma/vim-hug-neovim-rpc'
endif

Because I’ve switched to neovim it simplifies that installation as well.

But I’ve now fallen into the neovim only trap. Because my installation for deoplete only works with neovim, then I get an error if I try to use vim now. Perhaps I can just comment the whole block for if deoplete installed out. This works, so now vim-plug just doesn’t use deoplete if I’m in Vim.

Note that Deoplete should start working immediately after you install it and you should start seeing a popup box as you type.

Installing the language server

This was just downloading the zip from the language server releases page and unzipping it to ~/rls-linux.

Figuring out how to get ALE to work with reason-language-server

I’m using ALE rather than the recommended autozimu/LanguageClient-neovim. ALE is also a Language Client and so should also work fine.

Install via:

Plug 'dense-analysis/ale'

ALE needs to know about the existance of the reason-language-server and thankfully that support has been added. You can go to :help ale-reasonml-ols and it tells you the correct config:

  let g:ale_reason_ls_executable = '~/rls-linux/reason-language-server'

After this I expected to restart Vim and it to magically work which it didn’t.

The path has to be absolute (as noted in the config information for the LanguageClient-neovim in the README):

  let g:ale_reason_ls_executable = '/home/ian/rls-linux/reason-language-server'

You can also use the magic of vimscripts expand to handle it for you too:

  let g:ale_reason_ls_executable = expand('~/rls-linux/reason-language-server')

This then showed promise. Completion would work nicely and you get useful information coming up as you type, this also integrated with ALE via:

call deoplete#custom#option('sources', {
\ '_': ['ale'],
\})

It took a while to see if all the functionality was there. It pickedup linting errors which are put into the location list (:lopen) and would give useful info with :ALEHover

I compared it to VS Code, which does manage to implement this better. The error messages get formatted properly, for some reason the line breaks in the Vim error messages don’t get applied. Also you get the ‘Hover’ (equivalent to ALEHover) info showing up as you type in context.

I then tried to format the code. ALE has a :ALEFix command that I know works from eslint. It helpfully suggests that you need to configure the correct ‘fixer’ in .vimrc. However once that has been configured running ALEFix does nothing. I installed VS Code to check that using that along with the reason-language-server does correctly format the code – which it does. So there appears to be some problem with ALE.

Time to try another plugin…

So I can either try the Rust made LanguageClient, or the Typescript COC.

I’ve heard about COC a couple of times and it’s designed to be a VS Code matching ‘LSPy thing’. So you should be able to configure Language Clients almost exactly as they do with VS Code but inside Vim.

But hey, I prefer Rust so let’s try that first…

Trying LanguageClient-neovim instead of ALE

Now that I’ve switched to vim-plug installation of LanguageClient-neovim is easy because it includes the manual install step in the .vimrc code.

Then also the configuration was easy because it’s included in the vim-reason-plus README:

let g:LanguageClient_serverCommands = {
    \ 'reason': ['/absolute/path/to/reason-language-server.exe'],
    \ }

Note again here, that it mentions ‘absolute path’, so you have to use /home/ian instead of ~, although I still just use expand('~/path')

Now once I had installed that and restarted NVim then it all worked pretty smoothly.

Firstly the errors show up in the quickfix list :copen as well as to the right of the code. It’s not quite formatted as nicely with the line breaks as VS Code but is at least a consistent block of text, not spaced out with extra padding where the line breaks should be. I suspect though that using the location list is better as it won’t overwrite any searches that I’ve done.

Interstingly the ‘quickfix list’ is actually supposed to show errors according to :help :copen. So perhaps my searches should be in the location list. This can be changed via:

    let g:LanguageClient_diagnosticsList = 'Location'

The commands for the LanguageClient are more confusing though.

:ALEHover vs call LanguageClient#textDocument_hover()<cr>
:ALEFix vs call LanguageClient#textDocument_formatting()<cr>

But you can fix that through the vimrc mappings, but now happily the formatting did work which is what I want. Shit works without too much hassle.

Try getting it to work using COC

One nice property of COC is that it doesn’t use any advanced features of vim-plug, so this will probably all still work with Vundle. Also it looks like it combines the Auto-completion and LSP in one plugin.

Install nodejs – you can follow the instructions in https://github.com/neoclide/coc.nvim/wiki/Install-coc.nvim.
Remove Deoplete and LanguageClient-neovim and any settings from your .vimrc file. Then add COC:

Plug 'neoclide/coc.nvim', {'branch': 'release'}

Then reload the vimrc with :source % and run :PlugInstall. Warning you’ll get errors if you haven’t cleared all the deoplete settings. You might still need to restart Vim.

Install the reason extension

COC works similarly to VS Code in that it requires you install extensions to get it to work with certain language servers.

Install the reason extension, which is assuming that you are using the reason-language-server rather than the OCaml or Merlin LSP:

:CocInstall coc-reason

I ran this and it froze my Neovim. Closing and re-opening the terminal got it working again, but not good.

Now you’ll need to configure it as you always need to specify the absolute path to the reason-language-server. Handily there is a configuration section for Reason

Run :CocConfig and if it’s empty (because this is the first time of using it), then you first need to insert an empty root object:

{
}

Then put the following config inside the root object:

  "languageserver": {
     "reason": {
      "command": "/absolute/path/to/reason-language-server",
      "filetypes": ["reason"]
     }
  }
Troubleshooting

Don’t forget to put the correct absolute path for the command. Here now you can’t use the vimscript expand, so just use /home/[username].

e.g.: "/home/ian/rls-linux/reason-language-server"

Otherwise you’ll get:

[coc.nvim] Server languageserver.reason failed to start: Command “reason-language-server” of languageserver.reason is not executable
: Error: not found: reason-language-server

As soon as you set the correct path you should immediately start getting auto-completion and LSP goodies in your reason files.

Another possible error you can get is:

[coc.nvim] Server languageserver.reason failed to start: Cannot read property ‘reader’

This is a connected error that means you have the command path wrong. In my case I’d just written "/home/rls-linux/reason-language-server"

Further I tested it on a basic reason file created in an empty directory which gives this error:

[coc.nvim] No root directory found

This appears to be a reason-language-server issue #334. You need to initialise the directory as per the installation page.

The auto-completion seems to work very nicely, but I noticed that the error messages are severely truncted, for example:

src/Index.re|7 col 34 error| [undefined] Error: This expression has type [E]
src/Index.re|7 col 59 error| [undefined] Error: The function applied to this argument has type [E]

Actually you can get the full error, using :call CocAction('diagnosticInfo'), or :call CocAction('diagnosticPreview').

This gives a better error than I got with ALE or LanguageClient-neovim:

[reason] [E] Error: This expression has type
(~message: string) =>
ReasonReact.componentSpec(ReasonReact.stateless,
ReasonReact.stateless,
ReasonReact.noRetainedProps,
ReasonReact.noRetainedProps,
ReasonReact.actionless)
but an expression was expected of type
ReasonReact.component(‘a, ‘b, ‘c) =
ReasonReact.componentSpec(‘a, ‘a, ‘b, ‘b, ‘c)

To get code formatting working you need to run: :call CocAction('format'). So its similar to the LanguageClient-neovim in that you’ll probably want to create a whole bunch of vimrc shortcuts. But at least it does format which is a step up from ALE.

Hover info is through :call CocAction('doHover').

One nice thing about this plugin is that it combines the Complete and LSP plugins and so the Plug config is:

Plug 'neoclide/coc.nvim', {'branch': 'release'}

Instead of:

Plug 'autozimu/LanguageClient-neovim', {
    \ 'branch': 'next',
    \ 'do': 'bash install.sh',
    \ }

" for neovim
if has('nvim')
  Plug 'Shougo/deoplete.nvim', { 'do': ':UpdateRemotePlugins' }
" for vim 8 with python
else
  Plug 'Shougo/deoplete.nvim'
  Plug 'roxma/nvim-yarp'
  Plug 'roxma/vim-hug-neovim-rpc'
  " the path to python3 is obtained through executing `:echo exepath('python3')` in vim
  let g:python3_host_prog = "/absolute/path/to/python3"
endif

So your choices are…

Note these are recommendations, you probably can get Vim/Vundle to work with these, but I’ve simplified my life to match the instructions that maintainers give out.

  1. Neovim, vim-plug, ALE, deoplete: format doesn’t appear to work, error messages are very ugly, deoplete has some nice integrations with fzf
  2. Neovim, vim-plug, LanguageClient-neovim, deoplete: has the most nice touches and seems to work the best with little config, format works, but error messages are still pushed into one long line that can be too long for the location list
  3. Vim/Neovim, vim-plug, COC: simplest plugin setup, but took a while to understand how to configure, doesn’t work so nicely out of the box, format works, appears to give the best formatted error messages – when you look for them. It’s easier if they just appear in the location/quickfix list. However I guess that’s the problem, location/quickfix don’t allow for multiline error messages. Also in this seems the most light weight of the plugins as it doesn’t bundle all possible language server configurations, you install them as extensions.

Futher work with COC

I’ve since had further thoughts with COC. Things that are actually fairly magical.

  1. If you’re a Javascript developer then working with eslint is very common. The coc-eslint plugin magically works with eslint straight off. I had all sorts of problems with eslint and ALE, which required eslint_d and neomake for reasons that I can’t quite remember.
  2. By default the errors in COC don’t show until you switch from insert mode to normal mode. This is actually a better experience in my opinion as it reduces the amount of constant information that you’re getting. There’s no need to display an error just because you haven’t typed something yet.
  3. So it means that I can replace, ALE, Neomake, Deoplete, LanguageServer-neovim with COC. COC requires node to be installed but beyond that there’s no specific vimrc config, so should allow using Vundle – although it does require a specific branch which I don’t think Vundle can handle. Also without Deoplete there’s no difference between using Vim or Neovim which is great.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.