A fast, signal-aware fuzzy picker for neovim, rendered in plain splits (a prompt window + a results window) anchored to the top of the tabpage. Your own windows are never taken over — needle opens its own full-width splits and restores the previous layout when it closes. It ranks files not just by fuzzy match score but by weighted signals about how relevant a file is to what you're currently doing.
Install with neovim's native plugin manager, vim.pack (Neovim 0.12+):
vim.pack.add({ "https://github.com/dlants/needle" })
require("needle").setup()needle has no required external dependencies and only requires Neovim 0.12+
(for vim.pack). The files picker prefers fd
and falls back to rg, then to find — so it works out of the box, but fd
or rg is recommended for the best file listing.
| Command | Function | What it lists |
|---|---|---|
:Needle [dir] |
M.files(opts) |
files under the search root (fd/rg/find driven) |
:NeedleBuffers |
M.buffers() |
the buffer list |
:NeedleHelp |
M.help() |
help tags |
The search root is picked from the current buffer: cwd if the buffer is under
it, else the nearest git root, else the buffer's directory. :Needle accepts an
optional directory argument.
Files are scored by a fuzzy match (needle/score.lua) plus weighted signals,
shown as a blamg flag prefix column:
- in buffer list
- adjacent dir — directory proximity to open buffers (0..1)
- recent access — exponential decay against access half-life
- recent mtime
- git dirty
Access history is persisted to stdpath("data")/needle/state.json and updated
on BufWinEnter (throttled).
| Key | Action |
|---|---|
<C-j> / <C-n> / <Down> |
next result |
<C-k> / <C-p> / <Up> |
previous result |
<C-d> / <C-u> |
jump 10 down / up (<C-u> in insert mode clears the prompt) |
<CR> |
open |
<C-x> |
open in split |
<C-v> |
open in vsplit |
<C-t> |
open in tab |
<C-h> |
toggle unrestricted (--no-ignore) file listing |
? (normal mode) |
show keymap help in a floating window |
<Esc> / <C-c> |
close |
require("needle").setup({
-- files_command = nil, -- override: a list, or function(unrestricted) -> list
-- exclude_dirs = {}, -- extra dir names to skip (.git/node_modules always skipped)
weights = {
in_buffer = 60,
adjacent_dir = 50,
recent_access = 50,
recent_mtime = 5,
git_dirty = 8,
},
access_half_life_s = 86400, -- 1 day
mtime_half_life_s = 604800, -- 1 week
max_render = 80,
debounce_ms = 25,
height = 0.3, -- picker height as a fraction of vim.o.lines (min 6 rows)
})Suggested keymaps:
vim.keymap.set("n", "<leader>f", function() require("needle").files() end)
vim.keymap.set("n", "<leader>b", function() require("needle").buffers() end)
vim.keymap.set("n", "<leader>h", function() require("needle").help() end)Other neovim plugins by dlants:
- magenta.nvim — transparent tools for agentic AI workflows.
- shuck — a streamed shell-command picker (live-grep replacement).
- glean — a git diff reviewer in a single foldable buffer.