refer.nvim
August 5, 2026 · View on GitHub
Introduction
Gallery
Commands
https://github.com/user-attachments/assets/d441844b-12ef-45a8-b2d4-560f8d7e6f10
Files (fd) and Quickfix
https://github.com/user-attachments/assets/2f91247d-5db0-42e6-b487-b97abb5e8b86
LSP
https://github.com/user-attachments/assets/3d31ddb8-abda-43c9-b62e-4b2d5b3ec890
Macros
https://github.com/user-attachments/assets/5b7be64e-e3f1-43c3-83d7-27c69b796cd1
About
refer.nvim is a minimalist picker for Neovim.
It is designed to:
- Be intuitive: It shouldn't pull you out of your current context and meld in seamlessly with your workflow.
- Be clean: Minimalist UI without floating windows and noise, a lot like Emacs minibuffers.
- Integrate: With other plugins (use the fuzzy sorter used in your at-point completion in your selecter).
- Functionally hackable: While there is limited flexibility in the picker's aesthetics, it is functionally hackable in every way.
It is not designed to:
- Be "blazingly fast": Speed is relative. This is, in essense, a picker plugin. I am not developing a super-fast fuzzy sorter.
Features
This plugin only provides you with a picker. I am not spending any energy to develop an optimised fuzzy sorter. There are already implementations out there that you can use. This plugin provides you with the ability to register these fuzzy sorter implementations.
There are already some sorters registered:
- Blink: Rust-based, extremely fast (Default for static lists) (Requires
blink.cmpinstalled. Or else it will download just the library from GitHub.). - Native: Vim's
matchfuzzy(Vim is generous...:h matchfuzzy().). - Mini: Support for
mini.fuzzyif installed (This supports strings with spaces in them, unlike the above two options.). - Lua: Pure Lua fallback (Default for async file lists) (I kept this for posterity's sake. Its by no means the most optimal option. Also supports strings with spaces in them.).
Requirements
- Neovim 0.10+
- curl (Required to download the pre-built Blink fuzzy matcher binary).
- fd (Required for the
Filespicker). - ripgrep (Required for the
Greppicker).
Installation
Using vim.pack:
vim.pack.add { src = "https://github.com/juniorsundar/refer.nvim" }
-- Optional
-- vim.pack.add { src = "https://github.com/saghen/blink.cmp" }
-- OR
-- vim.pack.add { src = "https://github.com/nvim-mini/mini.fuzzy" }
require("refer").setup (
-- opts
)
Using lazy.nvim:
{
"juniorsundar/refer.nvim",
dependencies = {
-- Optional:
-- "saghen/blink.cmp",
-- "nvim-mini/mini.fuzzy",
},
config = function()
-- The plugin autoloads, but you can pass opts to setup.
require("refer").setup(
-- opts
)
end
}
Commands
Use :Refer <subcommand> to launch pickers:
| Command | Description |
|---|---|
Files | Fuzzy find files using fd (Async) |
Grep | Live grep using ripgrep (Async) |
Selection | Search for word under cursor or visual selection |
Lines | Filter lines in the current buffer |
Buffers | Switch between open buffers |
OldFiles | Browse recently opened files |
Commands | Execute Vim commands interactively |
Help | Search Vim help tags and jump to documentation |
References | List LSP references for symbol under cursor |
Definitions | Go to LSP definition for symbol under cursor |
Implementations | Go to LSP implementation for symbol under cursor |
Declarations | Go to LSP declaration for symbol under cursor |
Symbols | List LSP document symbols for current buffer |
LspServers | Manage LSP servers (start/stop) |
Macros | Edit and preview Vim registers/macro content |
Resume | Resume the last picker, restoring its previous query (session-only) |
Extras FindFile | Emacs-style filesystem picker (requires extras.find_file = true in setup) |
Tutorials & Advanced Usage
- Replacing
vim.ui.select - Custom Keymaps
- Bring Your Own Fuzzy (Custom Sorters)
- Custom Parsers
- Customizing File Search
- Customizing Grep
- Creating Custom Pickers
- Creating Extensions
- Enabling Built-in Extras
Replacing vim.ui.select
Use refer as the interface for vim.ui.select (used by code actions and
plugins):
require("refer").setup_ui_select()
Custom Keymaps
You can customize key bindings inside the picker window.
require("refer").setup({
keymaps = {
-- Bind to a built-in action name
["<C-x>"] = "close",
-- Bind to a built-in action with a description
["<C-z>"] = { action = "close", desc = "Close picker" },
-- Or use a custom function (desc is optional)
["<C-y>"] = function(selection, builtin)
print("You selected: " .. selection)
builtin.actions.close()
end
}
})
Default Keymaps:
| Key | Action | Description |
|---|---|---|
<Tab> | complete_selection | Complete selection |
<CR> | select_input | Confirm selection |
<C-n> / <Down> | next_item | Next item |
<C-p> / <Up> | prev_item | Previous item |
<C-v> | toggle_preview | Toggle preview |
<C-u> | scroll_preview_up | Scroll preview up |
<C-d> | scroll_preview_down | Scroll preview down |
<C-s> | cycle_sorter | Cycle sorter |
<C-q> | send_to_qf | Send to quickfix |
<C-g> | send_to_grep | Send to grep buffer (experimental) |
<M-a> | select_all | Select all |
<M-d> | deselect_all | Deselect all |
<M-t> | toggle_all | Toggle all marks |
<Esc> / <C-c> | close | Close picker |
| — | edit_entry | Open selected item in current window |
| — | split_entry | Open selected item in a horizontal split |
| — | vsplit_entry | Open selected item in a vertical split |
| — | tab_entry | Open selected item in a new tab |
| — | select_entry | Call on_select with item and its attached data |
Bring Your Own Fuzzy (Custom Sorters)
You can define custom sorting algorithms. For example, a simple prefix matcher:
require("refer").setup({
custom_sorters = {
my_prefix_sorter = function(items, query)
local matches = {}
for _, item in ipairs(items) do
if vim.startswith(item, query) then
table.insert(matches, item)
end
end
return matches
end,
},
-- Add to available sorters to allow cycling to it with <C-s>
available_sorters = { "blink", "my_prefix_sorter", "lua" },
})
Custom Parsers
Teach refer how to parse specific text formats to enable file preview and
navigation. This is useful if you are piping custom logs or tool output into
refer.
Scenario: You have input lines formatted like: src/main.lua [Line 10, Col 5].
require("refer").setup({
custom_parsers = {
my_log_format = {
-- Lua pattern with capture groups
pattern = "^(.-)%s+%[Line (%d+), Col (%d+)%]",
-- Map capture groups to keys ("filename", "lnum", "col", "content")
keys = { "filename", "lnum", "col" },
-- Optional type conversion
types = { lnum = tonumber, col = tonumber },
},
}
})
Customizing File Search (Using find)
By default, refer uses fd. If you prefer standard find, you can provide a
custom command generator function.
require("refer").setup({
providers = {
files = {
-- Return the command as a table of strings
find_command = function(query)
return { "find", ".", "-type", "f", "-name", "*" .. query .. "*" }
end
}
}
})
Customizing Grep (Using grep)
By default, refer uses rg (ripgrep). If you prefer standard grep, you can
provide a custom command generator function.
require("refer").setup({
providers = {
grep = {
-- Return the command as a table of strings
grep_command = function(query)
return { "grep", "-rnI", query, "." }
end
}
}
})
Creating Custom Pickers
Static List Picker
local refer = require("refer")
refer.pick(
{ "Option A", "Option B", "Option C" },
function(item)
print("You picked: " .. item)
end,
{
prompt = "Pick one > ",
-- Custom keymaps for this picker
keymaps = {
["<C-d>"] = function(selection, builtin)
print("Deleted: " .. selection)
builtin.actions.close()
end
}
}
)
Structured Items (ReferItem)
Items passed to pick() can be plain strings or structured ReferItem
tables of the form { text = string, data = any }. When a structured item is
selected, data is passed directly to your on_select callback — no parser
needed.
Plain strings continue to work unchanged; they are normalized automatically.
local refer = require("refer")
refer.pick(
{
{ text = "src/main.lua", data = { path = "src/main.lua", lnum = 1 } },
{ text = "src/util.lua", data = { path = "src/util.lua", lnum = 1 } },
},
function(selection, data)
-- `selection` is the display text; `data` is the attached table
vim.cmd("edit " .. data.path)
vim.api.nvim_win_set_cursor(0, { data.lnum, 0 })
end,
{ prompt = "Jump > " }
)
Async Command Picker
Create a picker that runs a shell command based on your query (e.g., locate).
local refer = require("refer")
refer.pick_async(
function(query)
-- Return the command to run as a table of strings
-- Return nil to stop/wait (e.g. if query is too short)
if #query < 3 then return nil end
return { "locate", query }
end,
function(selection)
vim.cmd("edit " .. selection)
end,
{
prompt = "Locate > ",
debounce_ms = 200,
}
)
Enabling Previews for Custom Items
Create a picker with custom string formats and teach refer how to parse them
so the built-in file previewer works.
local refer = require("refer")
refer.pick(
{
-- Alternate format as col:lnum:filename
"10:5:lua/refer/picker.lua",
"20:1:README.md",
},
function(selection, data)
if data and data.filename then
vim.cmd("edit " .. data.filename)
vim.api.nvim_win_set_cursor(0, {data.lnum, data.col - 1})
end
end,
{
prompt = "Navigate > ",
preview = { enabled = true },
-- Custom parser for "row:col:filename"
parser = function(selection)
local lnum, col, filename = selection:match("^(%d+):(%d+):(.+)$")
if filename then
return {
filename = filename,
lnum = tonumber(lnum),
col = tonumber(col)
}
end
return nil
end
}
)
Decoupling Choices from Preview
Sometimes you want the preview to correspond to something unrelated to the choice currently under selection. For example, you want to create a picker to move through the headings of a markdown file. You want the selections to be the heading text, but you want the preview to be where in the markdown file the heading is found.
local refer = require("refer")
local api = vim.api
if vim.bo.filetype ~= "markdown" then
return
end
local bufnr = api.nvim_get_current_buf()
local filename = api.nvim_buf_get_name(bufnr)
local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false)
local choices = {}
local lookup = {}
for lnum, line in ipairs(lines) do
local hashes, title = line:match("^(#+)%s+(.+)$")
if hashes then
local level = #hashes
local indent = string.rep(" ", level - 1)
local display_text = indent .. title
if not lookup[display_text] then
table.insert(choices, display_text)
lookup[display_text] = lnum
end
end
end
refer.pick(
choices,
function(selection)
local lnum = lookup[selection]
if lnum then
api.nvim_win_set_cursor(0, {lnum, 0})
end
end,
{
prompt = "Outline > ",
preview = { enabled = true },
parser = function(selection)
local lnum = lookup[selection]
if lnum then
return {
filename = filename,
lnum = lnum,
col = 1
}
end
return nil
end
}
)
Enabling Built-in Extras
refer.nvim ships with optional extras that are disabled by default. Enable
them via setup():
require("refer").setup({
extras = {
find_file = true,
},
})
extras.find_file
Inspired by João Paulo.
Link to config.
Registers :Refer Extras FindFile — an Emacs-style filesystem picker. It
opens at your current working directory and lets you navigate your filesystem
incrementally: selecting a directory descends into it, selecting a file opens
it.
| Key | Action |
|---|---|
<CR> | Descend into directory or open file |
<Esc> / <C-c> | Close picker |
Creating Extensions
You can register new :Refer subcommands from your init.lua or from
third-party plugins.
local refer = require("refer")
refer.add_command("MyPicker", function(opts)
refer.pick({ "Choice A", "Choice B" }, function(selection)
print("Selected: " .. selection)
end, {
prompt = "My Picker > "
})
end)
Now you can run :Refer MyPicker and it will appear in tab completion.
Configuration Reference
The default configuration with all available options:
require("refer").setup({
-- General Settings
max_height_percent = 0.4, -- Window height (0.1 - 1.0)
min_height = 1, -- Minimum lines
-- Async Settings
debounce_ms = 100, -- Delay for async searching
min_query_len = 2, -- Min chars to start async search
-- Sorting
available_sorters = { "blink", "mini", "native", "lua" },
default_sorter = "blink",
-- Preview Settings
preview = {
enabled = true,
max_lines = 1000,
},
-- UI Customization
ui = {
mark_char = "●",
mark_hl = "String",
input_position = "top", -- "top" or "bottom"
reverse_result = false,
winhighlight = "Normal:Normal,FloatBorder:Normal,WinSeparator:Normal,StatusLine:Normal,StatusLineNC:Normal",
highlights = {
prompt = "Title",
selection = "Visual",
header = "WarningMsg",
},
},
-- Provider Configuration
providers = {
files = {
ignored_dirs = { ".git", ".jj", "node_modules", ".cache" },
find_command = { "fd", "-H", "--type", "f", "--color", "never" },
},
grep = {
grep_command = { "rg", "--vimgrep", "--smart-case" },
},
},
-- Extras (all disabled by default)
extras = {
find_file = false, -- set to true to register :Refer Extras FindFile
},
-- See "Tutorials" section for keymaps, custom_sorters, and custom_parsers
})