How To Integrate Esbonio with 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, otherwiseLook 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:
Automatic python environment discovery (using the example logic in lsp-nvim-python-discovery)
Live preview and synchronized scrolling
A VSCode style log output window (using toggleterm and lsp-devtools)
Notifications and progress updates via fidget
“Standard” neovim plugins including telescope
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.