How To use Esbonio in Neovim

This guide covers how to setup esbonio with Neovim’s built-in language client.

Installation

Install the language server using pipx:

pipx install esbonio

Configuration

The nvim-lspconfig plugin provides a base configuration for the language server.

It’s recommeded that any configuration settings specific to your project (such as your sphinx-build command) are stored in your project’s pyproject.toml file. Settings specific to you (such as your Python environment) are provided via the settings table passed to lspconfig.esbonio.setup {}.

lspconfig.esbonio.setup {
  settings = {
    sphinx = {
      pythonCommand = { "/path/to/project/.venv/bin/python" },
    }
  }
}

Important

You must provide a value for esbonio.sphinx.pythonCommand so that esbonio can build your documentation correctly.

See Configuration for a complete reference of all configuration options supported by the server.

Python Discovery

The most important setting to get right is to give esbonio the correct Python environment to use when building your documentation.

The simplest option is to hardcode the right environment into your configuration as the example above shows. However, if you change between projects often, constantly updating this value in your configuration is going to get tedious very quickly.

Another option is to include a function in your configuration that will automatically choose the correct one for you. For example the find_venv function below implements the following discovery rules.

  • If nvim is launched with a virtual environment active, use it, otherwise

  • Look for a virtual environment located within the project’s git repository

local function find_venv()

  -- If there is an active virtual env, use that
  if vim.env.VIRTUAL_ENV then
    return { vim.env.VIRTUAL_ENV .. "/bin/python" }
  end

  -- Search within the current git repo to see if we can find a virtual env to use.
  local repo = util.find_git_ancestor(vim.fn.getcwd())
  if not repo then
    return nil
  end

  local candidates = vim.fs.find("pyvenv.cfg", { path = repo })
  if #candidates == 0 then
    return nil
  end

  return { vim.fn.resolve(candidates[1] .. "./../bin/python") }
end

Be sure to pass the result of such a function to the server

lspconfig.esbonio.setup {
  settings = {
    sphinx = { pythonCommand = find_venv() }
  }
}

Example

Do you use Nix?

If you have the Nix package manager on your machine you can try out our example configuration with the following command:

nix run github:swyddfa/esbonio#nvim

There is an opionated, ready out of the box example configuration you can try, or at least get inspiration from. This configuration includes:

Show Example Config

You can also download this file

" vim: et ts=2 sw=2
set expandtab
set tabstop=3
set softtabstop=3
set shiftwidth=3

let mapleader=' '

colorscheme everforest
lua << EOF
local lspconfig = require('lspconfig')
local util = require('lspconfig.util')

local LSP_DEVTOOLS_PORT = '91234'

-- The helm/vertico/etc of the nvim world
local telescope = require('telescope.builtin')
local keymap_opts = { noremap = true, silent = true}
vim.keymap.set('n', '<leader>ff', telescope.find_files, {})
vim.keymap.set('n', '<leader>fg', telescope.live_grep, {})
vim.keymap.set('n', '<leader>fb', telescope.buffers, {})
vim.keymap.set('n', '<leader>fh', telescope.help_tags, {})

vim.keymap.set('n', '<leader>ds', telescope.lsp_document_symbols, keymap_opts)
vim.keymap.set('n', '<leader>ws', telescope.lsp_workspace_symbols, keymap_opts)
vim.keymap.set('n', '<leader>e', vim.diagnostic.open_float, keymap_opts)
vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, keymap_opts)
vim.keymap.set('n', ']d', vim.diagnostic.goto_next, keymap_opts)
vim.keymap.set('n', '<leader>q', vim.diagnostic.setloclist, keymap_opts)

vim.lsp.set_log_level("info")

local function scroll_view(ev)
  local esbonio = vim.lsp.get_active_clients({bufnr = 0, name = "esbonio"})[1]
  local view = vim.fn.winsaveview()

  local params = { line = view.topline }
  esbonio.notify("view/scroll", params)
end

local function preview_file()
  local params = {
    command = "esbonio.server.previewFile",
    arguments = {
      { uri = vim.uri_from_bufnr(0), show = false },
    }
  }
  local result = vim.lsp.buf.execute_command(params)
  print(vim.inspect(result))

  -- Setup sync scrolling
  local augroup = vim.api.nvim_create_augroup("EsbonioSyncScroll", { clear = true })
  vim.api.nvim_create_autocmd({"WinScrolled"}, {
    callback = scroll_view,
    group = augroup,
    buffer = 0,
  })
end

-- Attempt to find a virtualenv that the server can use to build the docs.

local function find_venv()

  -- If there is an active virtual env, use that
  if vim.env.VIRTUAL_ENV then
    return { vim.env.VIRTUAL_ENV .. "/bin/python" }
  end

  -- Search within the current git repo to see if we can find a virtual env to use.
  local repo = util.find_git_ancestor(vim.fn.getcwd())
  if not repo then
    return nil
  end

  local candidates = vim.fs.find("pyvenv.cfg", { path = repo })
  if #candidates == 0 then
    return nil
  end

  return { vim.fn.resolve(candidates[1] .. "./../bin/python") }
end

lspconfig.esbonio.setup {
  -- Wrap server with the lsp-devtools agent so that we can create out own
  -- VSCode style output window.
  cmd = { 'lsp-devtools', 'agent', '--port', LSP_DEVTOOLS_PORT, '--', 'esbonio' },
  init_options = {
    logging = {
      level = 'debug',
      -- Redirect logging output to window/logMessage notifications so that lsp-devtools can capture it.
      stderr = false,
      window = true,
    }
  },
  settings = {
    esbonio = {
      sphinx = {
        pythonCommand = find_venv(),
      }
    }
  },
  handlers = {
    ["editor/scroll"] = function(err, result, ctx, config)
      vim.cmd('normal '.. result.line .. 'Gzt')
    end
  },
  on_attach = function(client, bufnr)
    vim.api.nvim_buf_set_option(bufnr, 'omnifunc', 'v:lua.vim.lsp.omnifunc')

    local bufopts = { noremap=true, silent=true, buffer=bufnr }
    vim.keymap.set('n', 'gd', vim.lsp.buf.definition, bufopts)
    vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, bufopts)
    vim.keymap.set('n', 'gh', vim.lsp.buf.hover, bufopts)
    vim.keymap.set('n', '<leader>ca', vim.lsp.buf.code_action, bufopts)

    vim.api.nvim_create_user_command("EsbonioPreviewFile", preview_file, { desc = "Preview file" })
  end
}

-- UI for $/progress and other notifications
require('fidget').setup {
  notification = {
    override_vim_notify = true,
  }
}

-- smooth scrolling
require('neoscroll').setup {}

-- statusline
require('lualine').setup { theme = "everforest" }

-- VSCode-style output window
local Terminal  = require('toggleterm.terminal').Terminal
local log_output = Terminal:new({
  cmd = "lsp-devtools record --port " .. LSP_DEVTOOLS_PORT .. " -f '{.params.message}'",
  hidden = false,
  direction = 'horizontal',
  auto_scroll = true,
})
-- Ensure that the terminal is launched, so that it can connect to the server.
log_output:spawn()

function _log_output_toggle()
  log_output:toggle()
end

vim.api.nvim_set_keymap("n", "<leader>wl", "<cmd>lua _log_output_toggle()<CR>", keymap_opts)

EOF

Troubleshooting

You will also have to increase the LSP logging level in Neovim itself.

lua << EOF
vim.lsp.set_log_level("debug")
EOF

You can then open the log file with the command :LspLog. See here for more details.