Language Tailored Plugins: An Experience Like No Other

NeovimConf 2022

Chris Kipp

@ckipp01

A little about me

Professionally

Scala Center

Language Tailored Plugins

Language Tailored Plugins

For example

  • scalameta/nvim-metals (Scala)
  • mfussenegger/nvim-jdtls (Java)
    • ​Special thanks to Mathias
  • simrat39/rust-tools.nvim (Rust)
  • akinsho/flutter-tools.nvim (Dart)
  1. a users perspective
  2. an authors perspective

From 2 perspectives

From a users perspective

When you're getting started

  • Look up your language in neovim/nvim-lspconfig
require("lspconfig").metals.setup{}

When you're getting started

  • Install your server
cs install metals

When you're getting started

  • Add some keybindings
vim.keymap.set("n", "K", vim.lsp.buf.hover)

And... that's sort of it

Unless your server has more to offer

Or requires more in-depth setup

LSP Extensions

We have the normal kids on the block

  • textDoment/hover
  • textDocument/publishDiagnostics
  • textDocument/references
  • etc....

metals/publishDecorations

or handle special

workspace/executeCommands

or complete custom protocols like TVP (Tree View Protocol)

and just allows for smoother integrations with tools like nvim-dap

Must be way harder to set up

use({'scalameta/nvim-metals', requires = { "nvim-lua/plenary.nvim" }})
vim.api.nvim_create_autocmd("FileType", {
  pattern = { "scala", "sbt", "java" },
  callback = function()
    require("metals").initialize_or_attach({})
  end,
  group = nvim_metals_group,
})

Want Debugging?

local metals_config = require("metals").bare_config()
metals_config.on_attach = function(client, bufnr)
  require("metals").setup_dap()
end

vim.api.nvim_create_autocmd("FileType", {
  pattern = { "scala", "sbt", "java" },
  callback = function()
    require("metals").initialize_or_attach(metals_config)
  end,
  group = nvim_metals_group,
})

So without much extra work... you're getting quite a bit more

From an authors perspective

Using vim.lsp.start()

local function initialize_or_attach(config)
  config = config or conf.get_config_cache()

  if invalid_scala_file() then
    return
  end

  add_commands()

  if in_disabled_mode(config) then
    conf.set_config_cache(config)
    return
  end

  local current_buf = api.nvim_get_current_buf()
  local valid_config = conf.validate_config(config, current_buf)

  if valid_config then
    lsp.start(valid_config)
  end
end

Full control over the install process

LSP Extensions

metals/publishDecorations

metals/publishDecorations

M["metals/publishDecorations"] = function(err, result)
  if err then
    log.error_and_show(
      "Server error while publishing decorations. Please see logs for details."
    )
    log.error(err.message)
  end

  local uri = result.uri
  local bufnr = vim.uri_to_bufnr(uri)
 
  -- Plus some extra sanity checks in here

  decoration.clear(bufnr)

  for _, deco in ipairs(result.options) do
    decoration.set_decoration(bufnr, deco)
  end
end

or handle special

workspace/executeCommands

or handle special

workspace/executeCommands

local function execute_command(command_params, callback)
  lsp.buf_request(
    0,
    "workspace/executeCommand",
    command_params,
    function(err, result, ctx)
      if callback then
        callback(err, ctx.method, result)
      elseif err then
        log.error_and_show(
          string.format("Could not execute command: %s", err.message)
        )
      end
    end)
end

or handle special

workspace/executeCommands

M.analyze_stacktrace = function()
  local trace = fn.getreg("*")
  if trace:len() > 0 then
    execute_command(
      { command = "metals.analyze-stacktrace", arguments = { trace } }
    )
  else
    log.warn_and_show("No text found in your register.")
  end
end

M.switch_bsp = function()
  execute_command({ command = "metals.bsp-switch" })
end

M.connect_build = function()
  execute_command({ command = "metals.build-connect" })
end

and just allows for smoother integrations with tools like nvim-dap

For languages like Scala, this is really important

Because it actually looks like this

So as an author

  • Way more control over setup
  • Easy way to add server commands
  • Easy way to add custom handlers
  • Ability to make setup a breeze
  • Allows for much tighter integrations
  • Which all lead to happier users

So if your language has dedicated plugin... go give it a try

If you're considering making one for your language...

go give it a try