picker.lua
Branches > main > latest > files / nvim / lua / picker.lua
local api = vim.api
local fn = vim.fn
local ns = api.nvim_create_namespace("dev.grahp.picker")
local function float(row, height)
local buf = api.nvim_create_buf(false, true)
vim.bo[buf].bufhidden = "wipe"
local win = api.nvim_open_win(buf, false, {
relative = "editor",
row = row,
col = 0,
width = vim.o.columns,
height = height,
style = "minimal",
border = "none"
})
return win, buf
end
--- Fullscreen fuzzy picker, prompt at the bottom and matches going up from it
--- on_choice gets the picked string, or nil if cancelled
local function pick(items, on_choice)
local orig_win = api.nvim_get_current_win()
local orig_cursor = api.nvim_win_get_cursor(orig_win)
local h = vim.o.lines - 1
local list_win, list_buf = float(0, h)
local prompt_win, prompt_buf = float(h, 1)
vim.wo[list_win].cursorline = true
api.nvim_set_current_win(prompt_win)
local p = {items = items, query = "", matches = {}}
local done = false
local function query() return api.nvim_buf_get_lines(prompt_buf, 0, 1, false)[1] end
local function refilter()
p.query = query()
local res = p.query == "" and {vim.list_slice(p.items, 1, h), {}}
or fn.matchfuzzypos(p.items, p.query, {limit = h})
p.matches = res[1]
local lines = {}
for row = 1, h do lines[row] = p.matches[h - row + 1] or "" end
api.nvim_buf_set_lines(list_buf, 0, -1, false, lines)
api.nvim_buf_clear_namespace(list_buf, ns, 0, -1)
for i, positions in ipairs(res[2]) do
for _, pos in ipairs(positions) do
local col = vim.str_byteindex(p.matches[i], "utf-32", pos, false)
api.nvim_buf_set_extmark(list_buf, ns, h - i, col, {end_col = col + 1, hl_group = "PickerMatch"})
end
end
api.nvim_win_set_cursor(list_win, {h, 0})
end
local function close(choice)
if done then return end
done = true
vim.cmd("stopinsert")
pcall(api.nvim_win_close, prompt_win, true)
pcall(api.nvim_win_close, list_win, true)
vim.schedule(function()
--- Leaving insert mode nudges the cursor left, code actions care about where it is
pcall(api.nvim_win_set_cursor, orig_win, orig_cursor)
on_choice(choice)
end)
end
local function accept()
--- TextChangedI waits for typeahead, so typing fast then <CR> can be stale
if query() ~= p.query then refilter() end
close(p.matches[h - api.nvim_win_get_cursor(list_win)[1] + 1])
end
local function move(d)
return function()
local row = api.nvim_win_get_cursor(list_win)[1] + d
if row <= h and row > h - #p.matches then api.nvim_win_set_cursor(list_win, {row, 0}) end
end
end
local keys = {["<CR>"] = accept,
["<Esc>"] = function() close() end,
["<C-c>"] = function() close() end,
["<Down>"] = move(1), ["<C-j>"] = move(1), ["<C-n>"] = move(1), ["<Tab>"] = move(1),
["<Up>"] = move(-1), ["<C-k>"] = move(-1), ["<C-p>"] = move(-1), ["<S-Tab>"] = move(-1)}
for lhs, f in pairs(keys) do
vim.keymap.set({"i", "n"}, lhs, f, {buffer = prompt_buf, nowait = true})
end
api.nvim_create_autocmd({"TextChangedI", "TextChanged"}, {buffer = prompt_buf, callback = refilter})
api.nvim_create_autocmd("WinLeave", {buffer = prompt_buf, callback = function() close() end})
function p.set_items(new)
if not done then p.items = new; refilter() end
end
refilter()
vim.cmd("startinsert")
return p
end
local function lines_of(cmd, cb)
vim.system(cmd, {text = true}, vim.schedule_wrap(function(res)
cb(res.code == 0 and vim.split(res.stdout, "\n", {trimempty = true}) or nil)
end))
end
--- Opens instantly, the file list fills in once git (or rg outside a repo) is done
local function files()
local p = pick({}, function(file)
if file then vim.cmd("edit " .. fn.fnameescape(file)) end
end)
lines_of({"git", "ls-files", "--cached", "--others", "--exclude-standard"}, function(found)
if found then
p.set_items(found)
elseif fn.executable("rg") == 1 then
lines_of({"rg", "--files", "--hidden", "--glob", "!.git"}, function(rg) p.set_items(rg or {}) end)
end
end)
end
--- Drop in for vim.ui.select (code actions and such)
local function select(items, opts, on_choice)
local format = (opts or {}).format_item or tostring
local texts = vim.tbl_map(function(item) return (format(item):gsub("\n", " ")) end, items)
pick(texts, function(text)
local idx = text and fn.index(texts, text) + 1
on_choice(idx and items[idx], idx)
end)
end
return {files = files, select = select}