How to Implement Smart Goto Definition for LSP in Neovim: Custom Handler Guide
Implement smart goto definition in Neovim by configuring a custom on_list callback in vim.lsp.buf.definition() that deduplicates LSP results using a filename-plus-line hash, automatically jumping to single matches or opening the location list when multiple unique definitions exist.
Navigating code efficiently requires eliminating noisy duplicate entries that appear when modules export functions through local variable assignments. This guide demonstrates how to implement smart goto definition for LSP in Neovim using a modular approach that filters redundant locations before presenting results, based on the configuration found in the jdhao/nvim-config repository.
How Smart Goto Definition Works in Neovim
Standard LSP goto definition often returns multiple entries for the same physical location when code uses export patterns like local M.my_fn = function(). The smart implementation intercepts these results through the on_list callback parameter, processing the location items before Neovim decides how to display them.
The Deduplication Strategy
Inside the custom handler, the code iterates over options.items and constructs a hash table using filename .. lnum as the unique key. This eliminates duplicate entries that point to identical file-line combinations while preserving distinct definitions across different files or lines. The filtered results replace the original list via options.items = unique_defs before being passed to vim.fn.setloclist().
Presentation Logic
After deduplication, the handler checks the count of unique definitions. If more than one location remains, the results populate the location list and open it with :lopen. When only a single unique definition exists, Neovim jumps directly to that location using silent! lfirst, providing seamless navigation without intermediate UI steps.
Implementing the Custom Handler in lua/config/lsp.lua
The core implementation resides in lua/config/lsp.lua, where the gd mapping invokes vim.lsp.buf.definition with the custom callback. This approach hooks into Neovim's native LSP API without requiring external plugins.
map("n", "gd", function()
vim.lsp.buf.definition {
on_list = function(options)
local unique_defs = {}
local def_loc_hash = {}
for _, def_location in pairs(options.items) do
local hash_key = def_location.filename .. def_location.lnum
if not def_loc_hash[hash_key] then
def_loc_hash[hash_key] = true
table.insert(unique_defs, def_location)
end
end
options.items = unique_defs
vim.fn.setloclist(0, {}, " ", options)
if #options.items > 1 then
vim.cmd.lopen()
else
vim.cmd([[silent! lfirst]])
end
end,
}
end, { desc = "go to definition" })
The map function creates a buffer-local keymap with silent and buffer options, ensuring the mapping only exists when an LSP client is active. This prevents conflicts in buffers without language server support.
Configuring Buffer-Local Keymaps
The smart definition mapping registers through an LspAttach autocmd defined in lua/config/lsp.lua. This ensures the keymap is created only when an LSP client successfully attaches to the buffer, maintaining clean separation between LSP and non-LSP environments.
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(args)
local bufnr = args.buf
local map = function(mode, lhs, rhs, opts)
opts = vim.tbl_extend("force", { silent = true, buffer = bufnr }, opts or {})
vim.keymap.set(mode, lhs, rhs, opts)
end
-- Smart definition mapping shown above
map("n", "gd", function() ... end, { desc = "go to definition" })
end,
})
Fallback to Default Behavior
When you need the original LSP behavior without deduplication—useful for debugging or examining all symbol references—the configuration provides a fallback mapping. The <C-]> key invokes vim.lsp.buf.definition directly without the on_list callback, preserving the default Neovim behavior.
map("n", "<C-]>", vim.lsp.buf.definition)
Shared LSP Configuration
The implementation leverages shared capabilities defined in lua/lsp_utils.lua to ensure consistent server behavior. The get_default_capabilities() function extends standard client capabilities with folding-range support required by plugins like nvim-ufo, applying these settings globally via vim.lsp.config("*").
local capabilities = require("lsp_utils").get_default_capabilities()
vim.lsp.config("*", { capabilities = capabilities })
This modular architecture separates capability configuration from keymap logic, making the smart goto definition handler portable across different Neovim distributions.
Summary
- Deduplication mechanism: Hash table using
filename .. lnumkeys eliminates duplicate entries caused by local variable exports inlua/config/lsp.lua. - Smart navigation: Single definitions jump directly via
:lfirst; multiple results open the location list with:lopen. - Native LSP integration: Uses
vim.lsp.buf.definition()with customon_listcallback, requiring no external dependencies. - Buffer-local scoping: Keymaps register via
LspAttachautocmd to ensure availability only when language servers are active. - Fallback option:
<C-]>provides unfiltered access to standard LSP definition behavior.
Frequently Asked Questions
What is smart goto definition in Neovim?
Smart goto definition is a custom handler that intercepts LSP location results before navigation occurs, removing duplicate entries that point to identical file-line combinations. This eliminates noise when a symbol appears multiple times in the results due to module export patterns, providing cleaner navigation than the default vim.lsp.buf.definition() behavior.
How does the deduplication logic work?
The handler iterates through options.items in the on_list callback and constructs a hash table where the key combines def_location.filename and def_location.lnum. Only the first occurrence of each unique key is inserted into the unique_defs table, effectively filtering redundant entries while preserving distinct definitions across different files or line numbers.
Can I use this with multiple LSP servers simultaneously?
Yes, the implementation in lua/config/lsp.lua applies the smart goto definition mapping through the LspAttach autocmd, which triggers for every language server that attaches to a buffer. The deduplication logic works regardless of which LSP server provides the results, as it processes the final location list uniformly before presentation.
How do I disable the smart behavior for specific filetypes?
To bypass the deduplication logic for specific filetypes, modify the LspAttach callback in lua/config/lsp.lua to check vim.bo[bufnr].filetype before creating the gd mapping. For targeted filetypes, map gd directly to vim.lsp.buf.definition without the on_list callback, or use the existing <C-]> fallback mapping which invokes the standard LSP behavior unconditionally.
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 →