# How to Configure Buffer-Local Keybindings for LSP in Neovim

> Learn to configure buffer-local keybindings for LSP in Neovim. Ensure LSP mappings only exist in buffers with an active language server using LspAttach autocmds.

- Repository: [jdhao/nvim-config](https://github.com/jdhao/nvim-config)
- Tags: how-to-guide
- Published: 2026-03-04

---

**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`](https://github.com/jdhao/nvim-config/blob/main/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`](https://github.com/jdhao/nvim-config/blob/main/lua/config/lsp.lua), an autocmd registered with `vim.api.nvim_create_autocmd` captures this event to set up buffer-local state.

```lua
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`](https://github.com/jdhao/nvim-config/blob/main/lua/config/lsp.lua).

```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:

```lua
-- 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.

```lua
-- 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:

```lua
-- 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`](https://github.com/jdhao/nvim-config/blob/main/lua/config/lsp.lua). This separates LSP-specific logic from global keymaps (which typically live in [`lua/mappings.lua`](https://github.com/jdhao/nvim-config/blob/main/lua/mappings.lua)) and server-specific settings (which belong in `after/lsp/<server>.lua`).