Neovim's built-in LSP with Ruby and Rails

06 July, 2021

Neovim 0.5 was released on Friday, to much fanfare. It includes a built-in support for Microsoft’s Language Server Protocol. LSP adds features like completion, go to definition, documentation on hover, format, rename and refactor. LSP standardises the protocol for this interaction between languages and editors and offers a number of advantages over ctags. One killer advantage is that LSP understands scope, so jump to definitions are verifiably accurate.

I couldn’t find a Ruby specific guide for setting up LSP in Neovim, so, after spending some time on it today, here it is. This should get you up and running with Neovim’s built-in LSP, configured for Solargraph, optimised for Rails, and using either Rubocop or StandardRB for linting.

Installation and configuration

Before starting, I removed any existing Neovim plugins for ctags, linting and autocomplete. Though, that’s not necessary and I will likely bring some of them back to compliment Neovim’s built-in LSP.

1. Ensure you’re running Neovim 0.5.0 or later.

You can confirm this in Neovim with the :version command. If you’re not up-to-date you check Installing Neovim.

2. Install Solargraph

gem install solargraph

Visit the Solargraph GitHub page for more information.

3. Install and configure LSP with the nvim-lspconfig plugin

Install the nvim-lspconfig Neovim plugin with your Neovim plugin manager. Note that this plugin doesn’t contain the LSP functionality, it’s already built in to Neovim, it just provides some sensible configuration to get started.

After following the nvim-lspconfig installation instructions, change the provided example configuration snippets to the following to enable Solargraph support. In init.vim:

lua << EOF
require'lspconfig'.solargraph.setup{}
EOF

To enable Solargraph and key bindings automatically add the following to init.vim:

lua << EOF
local nvim_lsp = require('lspconfig')

-- Use an on_attach function to only map the following keys
-- after the language server attaches to the current buffer
local on_attach = function(client, bufnr)
  local function buf_set_keymap(...) vim.api.nvim_buf_set_keymap(bufnr, ...) end
  local function buf_set_option(...) vim.api.nvim_buf_set_option(bufnr, ...) end

  --Enable completion triggered by <c-x><c-o>
  buf_set_option('omnifunc', 'v:lua.vim.lsp.omnifunc')

  -- Mappings.
  local opts = { noremap=true, silent=true }

  -- See `:help vim.lsp.*` for documentation on any of the below functions
  buf_set_keymap('n', 'gD', '<Cmd>lua vim.lsp.buf.declaration()<CR>', opts)
  buf_set_keymap('n', 'gd', '<Cmd>lua vim.lsp.buf.definition()<CR>', opts)
  buf_set_keymap('n', 'K', '<Cmd>lua vim.lsp.buf.hover()<CR>', opts)
  buf_set_keymap('n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<CR>', opts)
  buf_set_keymap('n', '<C-k>', '<cmd>lua vim.lsp.buf.signature_help()<CR>', opts)
  buf_set_keymap('n', '<space>wa', '<cmd>lua vim.lsp.buf.add_workspace_folder()<CR>', opts)
  buf_set_keymap('n', '<space>wr', '<cmd>lua vim.lsp.buf.remove_workspace_folder()<CR>', opts)
  buf_set_keymap('n', '<space>wl', '<cmd>lua print(vim.inspect(vim.lsp.buf.list_workspace_folders()))<CR>', opts)
  buf_set_keymap('n', '<space>D', '<cmd>lua vim.lsp.buf.type_definition()<CR>', opts)
  buf_set_keymap('n', '<space>rn', '<cmd>lua vim.lsp.buf.rename()<CR>', opts)
  buf_set_keymap('n', '<space>ca', '<cmd>lua vim.lsp.buf.code_action()<CR>', opts)
  buf_set_keymap('n', 'gr', '<cmd>lua vim.lsp.buf.references()<CR>', opts)
  buf_set_keymap('n', '<space>e', '<cmd>lua vim.lsp.diagnostic.show_line_diagnostics()<CR>', opts)
  buf_set_keymap('n', '[d', '<cmd>lua vim.lsp.diagnostic.goto_prev()<CR>', opts)
  buf_set_keymap('n', ']d', '<cmd>lua vim.lsp.diagnostic.goto_next()<CR>', opts)
  buf_set_keymap('n', '<space>q', '<cmd>lua vim.lsp.diagnostic.set_loclist()<CR>', opts)
  buf_set_keymap("n", "<space>f", "<cmd>lua vim.lsp.buf.formatting()<CR>", opts)

end

-- Use a loop to conveniently call 'setup' on multiple servers and
-- map buffer local keybindings when the language server attaches
local servers = { "solargraph" }
for _, lsp in ipairs(servers) do
  nvim_lsp[lsp].setup {
	on_attach = on_attach,
	flags = {
	  debounce_text_changes = 150,
	}
  }
end
EOF

After restarting Neovim, you should be able to open a Ruby file, and see an active LSP attachment with :LspInfo.

4. Configure Solargraph for use with Rails

Due to the “magic” in Rails, static analysis is difficult. There are a couple of steps recommended by Solargraph to improve IntelliSense features in Rails project.

From your Rails project’s root directory, generate YARD documentation:

solargraph bundle

Add YARD directives to help Solargraph understand the Rails app by adding the most recent version of this GitHub Gist file to your Rails application at config/definitions.rb. Since I have many Rails projects I have cloned the Gist and symlinked it to each project.

git clone https://gist.github.com/castwide/28b349566a223dfb439a337aea29713e ~/src/enhance-rails-intellisense-in-solargraph

And for each project:

ln -s ~/src/enhance-rails-intellisense-in-solargraph/rails.rb <project_root>/config/definitions.rb

That way if the gist is updated I can update all repos with:

git -C ~/src/enhance-rails-intellisense-in-solargraph pull

I’ve also added this file to my global .gitignore file.

5. Optionally use StandardRB instead of RuboCop for linting

I like to use StandardRB where possible, but the default Solargraph configuration automatically reaches for RuboCop. Thankfully the solargraph-standardrb gem came to the rescue, just follow their installation instructions, restart Neovim, and you’re good to go.

How to use it

Some of the LSP features and their key mappings are listed here. But there are many more. See :help LSP for the full list.

Key map Action
<c-x> <c-o> Complete
g d Jump to definition
K Show hover documentation
g r Open quickfix with all references to method
r n Rename method and update references

So far I haven’t been able to get StandardRB to auto-fix its violations, or formatting to do anything. I’ll update this post if I work it out. If you have any tips, or need a hand with your Ruby or Rails project, get in touch.


Written by Sheldon Johnson