Fine‑Tuning Neovim’s LSP: Async Jobs, Floating Diagnostics, and Lua Hooks
Learn how to use Neovim’s Lua‑based LSP client and async job control to create responsive, floating diagnostics and run external formatters without blocking the editor. The article covers a concrete example, trade‑offs, and best‑practice checks.
11 Sept 2026, 22:49 UTC

The Problem: Slow LSP Startup and Cluttered UI
When you open a large project in Neovim, the built‑in LSP client often blocks the editor for several seconds. Even after the server is ready, diagnostics can flood the command line or the status line, making it hard to focus on code. Many developers resort to external tools or plugins that add their own UI layers, which can be fragile or hard to maintain.
Why Lua‑Based LSP Matters
Neovim’s LSP client is written in Lua, not C or a compiled extension. That means you can tweak it at runtime, add callbacks, and compose new UI elements without rebuilding Neovim. The nvim_lsp API exposes on_attach, capabilities, and dynamic negotiation, allowing you to decide exactly what the server can do and how you want to display its output.
Async Job Control in Action
Neovim’s job_start function lets you launch external commands asynchronously. Combined with chan_filter and chan_total, you can stream output into a buffer or a floating window while keeping the editor responsive. Below is a minimal example that runs prettier on the current buffer and shows the formatted result in a floating window.
-- Use job_start to format the current buffer asynchronously
local function format_prettier()
local bufnr = vim.api.nvim_get_current_buf()
local cmd = {"prettier", "--stdin-filepath", vim.api.nvim_buf_get_name(bufnr)}
local stdout = {}
local job_id = vim.fn.job_start(cmd, {
stdout_buffered = true,
on_stdout = function(_, data, _)
if data then
vim.list_extend(stdout, data)
end
end,
on_exit = function(_, exit_code, _)
if exit_code == 0 then
local formatted = table.concat(stdout, "\n")
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, vim.split(formatted, "\n"))
vim.api.nvim_echo({{"Formatted with prettier", "None"}}, false, {})
else
vim.api.nvim_echo({{"prettier failed", "ErrorMsg"}}, false, {})
end
end,
})
end
-- Map the function to a keybinding
vim.api.nvim_set_keymap("n", "\&f", "\&:lua format_prettier()", {silent = true})
Because the job runs in the background, the editor stays responsive. If you need to cancel the job, you can call vim.fn.job_stop(job_id).
Custom Floating Diagnostics
Neovim’s LSP diagnostics can be rendered in a floating window instead of the usual virtual text or status line. This keeps the code clean and lets you inspect errors on demand. The following on_attach callback sets up a keybinding to toggle diagnostics in a floating window.
local function on_attach(client, bufnr)
-- Disable virtual text for a cleaner buffer
client.config.flags = { allow_incremental_sync = true }
-- Keybinding to open diagnostics in a floating window
vim.api.nvim_buf_set_keymap(bufnr, "n", "\&e", "\&:lua vim.diagnostic.open_float()", {silent = true, noremap = true})
end
require('nvim_lsp').pyright.setup({
on_attach = on_attach,
capabilities = require('cmp_nvim_lsp').default_capabilities(),
})
Run :LspInfo to verify that the server is attached and that the on_attach callback has executed. The floating window will appear when you press \&e.
Trade‑offs and Pitfalls
- Version Compatibility: The
job_startandon_attachfeatures are fully available only in Neovim 0.10+. Check:checkhealth lspto confirm your version supports the API you use. - Overriding Defaults: A blanket
on_attachthat setsclient.config.flagscan unintentionally disable useful features like incremental sync. Test the callback in isolation before merging into your config. - Resource Usage: Running many asynchronous jobs concurrently can exhaust file descriptors or CPU. Use
job_stopto clean up or throttle job launches.
Takeaway
By leveraging Neovim’s Lua‑based LSP client and async job control, you can build a lightweight, responsive development environment. Custom floating diagnostics keep code uncluttered, while job_start lets you run external formatters or linters without blocking the UI. Remember to test on the exact Neovim version you ship to, and keep on_attach scoped to avoid overriding built‑in defaults.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.