Skip to content

How I Change Ruby LSPs Per Project

Published: at 12:32 AM

Some Ruby projects that I work on have Rubocop with a bunch of custom or agreed upon rules and some are just a collection of Ruby files. In both cases, I still want to have LSP functionality in Neovim. So here is how I switch from Rubocop’s LSP to Shopify’s Ruby LSP dynamically.

First thing’s first. I use rbenv to sandbox Ruby versions and rbenv-gemset files to sandbox project dependencies. If you use other toolsets to manage this then you’ll want to change things accordingly. I typically setup a base global Ruby version to the latest release and then install Shopify’s Ruby LSP for that version of Ruby:

rbenv install 3.4.1
rbnev global 3.4.1
gem install ruby-lsp

🚨 WARNING 🚨 Do not install LSPs with mason-lsp-config as it will not sandbox Ruby gems correctly.

Now that we have that out of the way, we’ll need to install some NeoVim plugins; specifically nvim-lspconfig and cmp-nvim-lsp. I have the latter defined in file for completions.lua and the former in a plugin file called lsp-config.lua. For cmp-nvim-lsp, I just have it defined as a plugin for Lazy to load. For my lsp-config.lua file I have the following:

  {
    'neovim/nvim-lspconfig',
    config = function()
      local capabilities = require('cmp_nvim_lsp').default_capabilities()
      local lspconfig = require('lspconfig')
      local util = require('lspconfig.util')

      local function choose_ruby_lsp()
        local project_root = util.root_pattern("Gemfile", ".rubocop.yml", ".ruby-version")(vim.fn.getcwd())

        if project_root and vim.fn.filereadable(project_root .. "/.rubocop.yml") == 1 then
          return "rubocop"
        else
          return "ruby-lsp"
        end
      end

      local function find_rbenv_executable(executable)
        local path = vim.fn.system("rbenv which " .. executable):gsub("\n", "")
        if vim.fn.filereadable(path) == 1 then
          return path
        else
          print("Warning: " .. executable .. " not found via rbenv")
          return nil
        end
      end

      local selected_lsp = choose_ruby_lsp()

      if selected_lsp == "rubocop" then
        local rubocop_path = find_rbenv_executable("rubocop")
        if rubocop_path then
          lspconfig.rubocop.setup({
            cmd = { "bundle", "exec", rubocop_path, "--lsp" },
            filetypes = { "ruby" },
            flags = { debounce_text_changes = 100 },
            root_dir = util.root_pattern(".rubocop.yml", "Gemfile"),
            capabilities = capabilities,
          })
        end
      else
        local ruby_lsp_path = find_rbenv_executable("ruby-lsp")
        if ruby_lsp_path then
          lspconfig.ruby_lsp.setup({
            cmd = { ruby_lsp_path },
            filetypes = { "ruby" },
            flags = { debounce_text_changes = 100 },
            root_dir = util.root_pattern("Gemfile", ".ruby-version"),
            capabilities = capabilities
          })
        end
      end

      lspconfig.lua_ls.setup({
        capabilities = capabilities
      })
    end
  },

The first thing that we do is check to see if there is a .rubocop.yml file in our project. If there is we set the local variable selected_lsp to rubocop. If not then we set it to ruby-lsp. If the selected_lsp is Rubocop then the command that we run is bundle exec <rubocop_path> --lsp. If it’s not Rubocop then we just get the path to the ruby-lsp binary.

That’s pretty much it. I use Lua to inspect the filesytem and change things accordingly.


Next Post
ugaterm - A Straightforward Terminal Integration for Neovim