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 the LspAttach event to run code only when a language server attaches.
  • Capture bufnr: Extract event_context.buf to target the specific buffer.
  • Wrap vim.keymap.set: Create a helper that sets opts.buffer = bufnr for every mapping.
  • Modify capabilities: Use the client object 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →