How to Configure Buffer-Local Keybindings for LSP in Neovim
Define an LspAttach autocmd that captures the buffer number from the event context and wrap vim.keymap.set to inject opts.buffer = bufnr, ensuring LSP mappings only exist in buffers with an active language server.
Configuring buffer-local keybindings for LSP in Neovim keeps your editor free of conflicting shortcuts in non-code files. The jdhao/nvim-config repository implements this pattern in lua/config/lsp.lua by hooking into the LspAttach event to dynamically scope mappings to individual buffers. This approach guarantees that commands like "go to definition" only trigger when a language server is actually attached.
Why Buffer-Local Mappings Matter for LSP
Global keybindings for LSP functions clutter your configuration and create conflicts in buffers that lack language server support. By defining mappings inside an LspAttach callback, you ensure that gd for "go to definition" or K for hover documentation only exist where they are valid. This eliminates the need for manual filetype checks and keeps your keymap state clean.
The LspAttach Autocmd Pattern
Neovim fires the LspAttach event immediately after a language server connects to a buffer. In lua/config/lsp.lua, an autocmd registered with vim.api.nvim_create_autocmd captures this event to set up buffer-local state.
vim.api.nvim_create_autocmd("LspAttach", {
group = vim.api.nvim_create_augroup("lsp_buf_conf", { clear = true }),
callback = function(event_context)
local client = vim.lsp.get_client_by_id(event_context.data.client_id)
if not client then return end
local bufnr = event_context.buf
-- Mappings will be defined here
end,
nested = true,
desc = "Configure buffer keymap and behavior based on LSP",
})
The event_context.buf field contains the exact buffer number where the server attached, which you must store for local mapping scoping.
Creating a Buffer-Local Mapping Helper
Inside the callback, a small wrapper function enforces buffer locality by mutating the options table before every vim.keymap.set call. This helper lives at lines 23–30 of lua/config/lsp.lua.
local function map(mode, lhs, rhs, opts)
opts = opts or {}
opts.silent = true
opts.buffer = bufnr -- Critical: scopes the map to this buffer only
vim.keymap.set(mode, lhs, rhs, opts)
end
By hardcoding opts.buffer = bufnr, all subsequent calls to map automatically create buffer-local keybindings that disappear when the buffer is deleted.
Essential LSP Keybindings to Configure
With the helper defined, you can register common LSP actions using the vim.lsp.buf API. The jdhao/nvim-config setup includes these standard mappings:
-- Navigation
map("n", "gd", function()
vim.lsp.buf.definition { on_list = function(opts)
-- Custom filtering logic for duplicate definitions
end }
end, { desc = "go to definition" })
map("n", "<C-]>", vim.lsp.buf.definition)
map("n", "gi", vim.lsp.buf.implementation)
-- Documentation and help
map("n", "K", function()
vim.lsp.buf.hover {
border = "single",
max_height = 20,
max_width = 130,
close_events = { "CursorMoved", "BufLeave", "WinLeave", "LSPDetach" }
}
end)
map("n", "<C-k>", vim.lsp.buf.signature_help)
-- Refactoring and actions
map("n", "<space>rn", vim.lsp.buf.rename, { desc = "variable rename" })
map("n", "<space>ca", vim.lsp.buf.code_action, { desc = "code action" })
-- Workspace management
map("n", "<space>wa", vim.lsp.buf.add_workspace_folder, { desc = "add workspace folder" })
map("n", "<space>wr", vim.lsp.buf.remove_workspace_folder, { desc = "remove workspace folder" })
map("n", "<space>wl", function()
vim.print(vim.lsp.buf.list_workspace_folders())
end, { desc = "list workspace folder" })
Because these are wrapped by the local map function, they only respond in buffers where the LspAttach event fired.
Conditional Server-Specific Tweaks
The callback receives the client object, allowing you to modify server capabilities per-buffer before setting up mappings. This prevents overlapping functionality when multiple servers attach to the same buffer.
-- Disable hover for ruff to avoid conflicts with Pyright
if client.name == "ruff" then
client.server_capabilities.hoverProvider = false
end
-- Enable inlay hints only if supported
if client.server_capabilities.inlayHintProvider then
vim.lsp.inlay_hint.enable(true, { buffer = bufnr })
end
You can also conditionally register mappings based on server capabilities to avoid binding keys to unsupported functions.
Extending With Custom Mappings
To add new buffer-local LSP keybindings, insert additional map calls inside the LspAttach callback. For example, to add formatting and visual-mode implementation jumping:
-- Format the entire buffer
map("n", "<leader>f", vim.lsp.buf.format, { desc = "format buffer" })
-- Visual mode mapping for implementation
map("v", "gi", function()
vim.lsp.buf.implementation()
end, { desc = "go to implementation" })
Since the helper automatically injects buffer = bufnr, these additions immediately become buffer-scoped without extra boilerplate.
Summary
- Use
LspAttach: Hook into theLspAttachevent to run code only when a language server attaches. - Capture
bufnr: Extractevent_context.bufto target the specific buffer. - Wrap
vim.keymap.set: Create a helper that setsopts.buffer = bufnrfor every mapping. - Modify capabilities: Use the
clientobject to disable clashing features (e.g.,hoverProvider) server-by-server. - Keep it local: All mappings defined this way vanish with the buffer, preventing global namespace pollution.
Frequently Asked Questions
What is the LspAttach event in Neovim?
LspAttach is an autocmd event triggered by Neovim’s built-in LSP client immediately after a language server successfully attaches to a buffer. It provides the buffer number and client ID, making it the ideal hook for configuring buffer-local settings.
How do I make a keymapping buffer-local in Neovim?
Pass buffer = <bufnr> inside the options table to vim.keymap.set. In an LspAttach callback, obtain the buffer number from event_context.buf and store it in a local variable. A wrapper function that automatically injects this field keeps your mapping definitions concise.
Can I disable specific LSP features for certain servers?
Yes. Inside the LspAttach callback, inspect client.name or client.server_capabilities and modify the capability flags directly. For example, set client.server_capabilities.hoverProvider = false to disable hover for a specific server like ruff while keeping it enabled for others.
Where should I place my LSP keybinding configuration?
Place the LspAttach autocmd and mapping definitions in a dedicated file such as lua/config/lsp.lua. This separates LSP-specific logic from global keymaps (which typically live in lua/mappings.lua) and server-specific settings (which belong in after/lsp/<server>.lua).
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →