Something went wrong. Try again.
🐻 minimal ui2 fuzzy finder for Neovim codeberg.org/comfysage/artio.nvim
Something went wrong. Try again.
21 kB · 773 lines
Lua
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774local function lzrq(modname) return setmetatable({}, { __index = function(_, key) return require(modname)[key] end, })end
local artio = lzrq("artio")local config = lzrq("artio.config")local utils = lzrq("artio.utils")
local function extend(t1, ...) return vim.tbl_deep_extend("force", t1, ...)end
local builtins = {}
builtins.builtins = function(props) props = props or {}
return artio.generic( vim.tbl_keys(builtins), extend({ prompt = "builtins", on_close = function(fname, _) if not builtins[fname] then return end
artio.schedule(builtins[fname]) end, }, props) )end
---@class artio.picker.generic.fs.Props : artio.Picker.config---@field base_dir? string
local findprg = vim.fn.executable("fd") == 1 and "fd -H -p -a -t f --color=never --" or "find . -type f -iregex '.*$*.*'"
---@class artio.picker.files.Props : artio.picker.generic.fs.Props---@field findprg? string
---@param props? artio.picker.files.Propsbuiltins.files = function(props) props = props or {} props.findprg = props.findprg or findprg
local base_dir = props.base_dir or vim.fn.getcwd(0)
return artio.pick(extend({ prompt = "files", live = false, get_items = utils.make_cmd(props.findprg, { cwd = base_dir, }), fn = artio.sorter, on_close = function(text, _) artio.schedule(function() utils.edit(text) end) end, format_item = function(item) return vim.fs.relpath(base_dir, item) or item end, get_icon = config.get().opts.use_icons and function(item) return require("mini.icons").get("file", item.v) end or nil, preview_item = function(item) return { buf = vim.fn.bufadd(item) } end, actions = extend( {}, utils.make_fileactions(function(item) return vim.fn.bufnr(item.v, true) end), utils.make_setqflistactions(function(item) return { filename = item.v } end) ), }, props))end
---@class artio.picker.grep.Props : artio.picker.generic.fs.Props---@field grepprg? string
---@param props? artio.picker.grep.Propsbuiltins.grep = function(props) props = props or {} props.grepprg = props.grepprg or vim.o.grepprg
local base_dir = props.base_dir or vim.fn.getcwd(0) local ui2 = require("vim._core.ui2") local grepcmd = utils.make_cmd(props.grepprg, { cwd = base_dir, })
return artio.pick(extend({ prompt = "grep", live = true, get_items = function(input) if input == "" then return {} end
local lines, err = grepcmd(input) if err then return vim .iter(ipairs(lines)) :map(function(i, l) return { id = i, v = {}, text = l, } end) :totable() end
local co = coroutine.running() assert(co)
coroutine.wrap(vim.schedule_wrap(function() vim.fn.setloclist(ui2.wins.cmd, {}, " ", { title = "grep[" .. input .. "]", lines = lines, efm = vim.o.grepformat, nr = "$", }) local results = vim .iter(ipairs(vim.fn.getloclist(ui2.wins.cmd))) :map(function(i, locitem) local name = vim.fs.abspath(vim.fn.bufname(locitem.bufnr)) return { id = i, v = { name, locitem.lnum, locitem.col }, text = ("%s:%d:%d:%s"):format(vim.fs.relpath(base_dir, name), locitem.lnum, locitem.col, locitem.text), } end) :totable() coroutine.resume(co, results) end))()
return coroutine.yield() end, fn = artio.sorter, on_close = function(item, _) artio.schedule(function() local win = vim.api.nvim_get_current_win() utils.edit(item[1], { win = win }) vim.api.nvim_win_set_cursor(win, { item[2], item[3] }) end) end, preview_item = function(item) return { buf = vim.fn.bufadd(item[1]), pos = { item[2], 0 } } end, get_icon = config.get().opts.use_icons and function(item) return item.v[1] and require("mini.icons").get("file", item.v[1]) or nil end or nil, hl_item = utils.hl_qfitem, actions = extend( {}, utils.make_setqflistactions(function(item) return { filename = item.v[1], lnum = item.v[2], col = item.v[3], text = item.text } end) ), }, props))end
local function find_oldfiles() return vim .iter(vim.v.oldfiles) :filter(function(v) return vim.uv.fs_stat(v) --[[@as boolean]] end) :totable()end
builtins.oldfiles = function(props) props = props or {} local lst = find_oldfiles()
return artio.generic( lst, extend({ prompt = "oldfiles", on_close = function(text, _) artio.schedule(function() utils.edit(text) end) end, get_icon = config.get().opts.use_icons and function(item) return require("mini.icons").get("file", item.v) end or nil, preview_item = function(item) return { buf = vim.fn.bufadd(item) } end, actions = extend( {}, utils.make_setqflistactions(function(item) return { filename = item.v } end) ), }, props) )end
builtins.buffergrep = function(props) props = props or {} local win = vim.api.nvim_get_current_win() local buf = vim.api.nvim_win_get_buf(win) local n = vim.api.nvim_buf_line_count(buf) local lst = {} ---@type integer[] for i = 1, n do lst[#lst + 1] = i end
local pad = #tostring(lst[#lst])
return artio.generic( lst, extend({ prompt = "buffergrep", on_close = function(row, _) vim.schedule(function() vim.api.nvim_win_set_cursor(win, { row, 0 }) end) end, format_item = function(row) return vim.api.nvim_buf_get_lines(buf, row - 1, row, true)[1] end, preview_item = function(row) return { buf = buf, pos = { row, 0 } } end, get_icon = function(row) local v = tostring(row.v) return ("%s%s"):format((" "):rep(pad - #v), v) end, actions = extend( {}, utils.make_setqflistactions(function(item) return { filename = vim.api.nvim_buf_get_name(buf), lnum = item.v } end) ), }, props) )end
local function find_helptags() local buf = vim.api.nvim_create_buf(false, true) vim.bo[buf].buftype = "help" local tags = vim.api.nvim_buf_call(buf, function() return vim.fn.taglist(".*") end) vim.api.nvim_buf_delete(buf, { force = true }) return vim.tbl_map(function(t) return t.name end, tags)end
builtins.helptags = function(props) props = props or {} local lst = find_helptags()
return artio.generic( lst, extend({ prompt = "helptags", on_close = function(text, _) vim.schedule(function() vim.cmd.help(text) end) end, preview_item = function(tag) local buf = vim.api.nvim_create_buf(false, true)
vim.bo[buf].bufhidden = "wipe" vim.bo[buf].buftype = "help"
vim._with({ buf = buf }, function() vim.cmd.help(tag) end)
return { buf = buf } end, }, props) )end
local function find_buffers() return vim .iter(vim.api.nvim_list_bufs()) :filter(function(bufnr) return vim.api.nvim_buf_is_valid(bufnr) and vim.bo[bufnr].buflisted end) :totable()end
builtins.buffers = function(props) props = props or {} local lst = find_buffers()
return artio.generic( lst, vim.tbl_extend("force", { prompt = "buffers", format_item = function(bufnr) return vim.api.nvim_buf_get_name(bufnr) end, on_close = function(bufnr, _) vim.schedule(function() vim.cmd.buffer(bufnr) end) end, get_icon = config.get().opts.use_icons and function(item) return require("mini.icons").get("file", vim.api.nvim_buf_get_name(item.v)) end or nil, preview_item = function(item) return { buf = item } end, }, props) )end
---@param currentfile string---@param item string---@return integer scorelocal function matchproximity(currentfile, item) currentfile = vim.fs.abspath(currentfile) local parts = vim.split(currentfile, "/", { trimempty = true }) item = vim.fs.abspath(item)
return vim.iter(ipairs(vim.split(item, "/", { trimempty = true }))):fold(0, function(score, i, part) if part == parts[i] then return score + 50 end return score end)end
--- uses the regular files picker as a base--- - boosts items in the bufferlist--- - proportionally boosts items that match closely to the current file in proximity within the filesystembuiltins.smart = function(props) props = props or {} local currentbuf = vim.fn.bufnr("%") local currentfile = vim.api.nvim_buf_get_name(currentbuf) currentfile = vim.fs.abspath(currentfile) local alternatebuf = vim.fn.bufnr("#") local lbufnr = vim.fn.bufnr("$") local bufnr_len = #(string.format("%d", lbufnr))
props.findprg = props.findprg or findprg local base_dir = vim.fn.getcwd(0)
local recentlst = vim .iter(find_buffers()) :map(function(buf) local v = vim.api.nvim_buf_get_name(buf) return { path = vim.fs.abspath(v), buf = buf } end) :totable()
return artio.pick(extend({ prompt = "smart", live = false, get_items = function(_) local lst = utils.make_cmd(props.findprg, { cwd = base_dir, })()
local results = vim .iter({ recentlst, lst }) :flatten(1) :map(function(x) if type(x) == "string" then x = { path = x } end return x end) :map(function(x) if x.buf and x.buf == currentbuf then x.current = true elseif x.buf and x.buf == alternatebuf then x.alt = true end return x end) :totable()
return vim.list.unique(results, function(x) return x.path end) end, fn = artio.mergesorters( "base", -- use default sorter but only display buffer files if input is empty function(l, input) if #input == 0 then return vim .iter(l) :map(function(item) if not item.v.buf then return end return { item.id, {}, 0 } end) :totable() end
return artio.sorter(l, input) end, -- boost files that are open as buffers function(l, _) return vim .iter(l) :map(function(item) if not item.v.buf then return end return { item.id, {}, 100 } end) :totable() end, -- boost files that are close in proximity to the current file function(l, _) return vim .iter(l) :map(function(item) return { item.id, {}, matchproximity(currentfile, item.v.path) } end) :totable() end ), on_close = function(v, _) artio.schedule(function() utils.edit(v.path) end) end, format_item = function(v) local path = vim.fs.relpath(base_dir, v.path) or v.path local ind = v.current and "%" or v.alt and "#" or " " return v.buf and string.format("%s (%0" .. bufnr_len .. "d) %s", ind, v.buf, path) or string.format("%s " .. string.rep(" ", bufnr_len + 2) .. " %s", ind, path) end, hl_item = function(_) return { { { 0, 2 }, "Special" }, { { 2, 3 }, "Delimiter" }, { { 3, 3 + bufnr_len }, "Number" }, { { 3 + bufnr_len, 3 + bufnr_len + 1 }, "NonText" }, } end, get_icon = config.get().opts.use_icons and function(item) return require("mini.icons").get("file", item.v.path) end or nil, preview_item = function(v) return { buf = vim.fn.bufadd(v.path) } end, actions = extend( {}, utils.make_fileactions(function(item) return vim.fn.bufnr(item.v.path, true) end), utils.make_setqflistactions(function(item) return { filename = item.v.path } end) ), }, props))end
builtins.colorschemes = function(props) props = props or {} local files = vim.api.nvim_get_runtime_file("colors/*.{vim,lua}", true) local lst = vim.tbl_map(function(f) return vim.fs.basename(f):gsub("%.[^.]+$", "") end, files)
local current = vim.g.colors_name local bg = vim.o.background
return artio.generic( lst, extend({ prompt = "colorschemes", on_close = function(text, _) vim.schedule(function() vim.cmd.colorscheme(text) end) end, on_quit = function() -- reset colorscheme vim.schedule(function() if vim.g.colors_name ~= current then vim.cmd.colorscheme(current) end end) end, preview_item = function(item) vim.cmd.colorscheme(item) vim.o.background = bg
return {} end, }, props) )end
builtins.highlights = function(props) props = props or {} local hlout = vim.split(vim.api.nvim_exec2([[ highlight ]], { output = true }).output, "\n", { trimempty = true })
local maxw = 0
local hls = vim .iter(hlout) :map(function(hl) local sp = string.find(hl, "%s", 1) maxw = sp > maxw and sp or maxw return { hl:sub(1, sp - 1), hl } end) :fold({}, function(t, hl) local pad = math.max(1, math.min(20, maxw) - #hl[1] + 1) t[hl[1]] = string.gsub(hl[2], "%s+", (" "):rep(pad), 1) return t end)
return artio.generic( vim.tbl_keys(hls), extend({ prompt = "highlights", on_close = function(hlname, _) vim.schedule(function() vim.cmd(("verbose hi %s"):format(hlname)) end) end, format_item = function(hlname) return hls[hlname] end, hl_item = function(hlname) local x_start, x_end = string.find(hlname.text, "%sxxx")
return { { { 0, #hlname.v }, hlname.v }, { { x_start, x_end }, hlname.v }, } end, }, props) )end
---@private---@param severity vim.diagnostic.Severity---@return stringlocal function get_severity_hl(severity) if severity == vim.diagnostic.severity.ERROR then return "DiagnosticError" elseif severity == vim.diagnostic.severity.WARN then return "DiagnosticWarn" elseif severity == vim.diagnostic.severity.INFO then return "DiagnosticInfo" elseif severity == vim.diagnostic.severity.HINT then return "DiagnosticHint" end return ""end
---@class artio.picker.diagnostics.Props : artio.Picker.config---@field buf? integer defaults to workspace
---@param props? artio.picker.diagnostics.Propsbuiltins.diagnostics = function(props) props = props or {} local lst = vim.diagnostic.get(props.buf)
return artio.generic( lst, extend({ prompt = "diagnostics", format_item = function(item) local text = item.message if item.code then text = ("%s [%s]"):format(text, item.code) end return ("%d:%d :: %s"):format(item.end_lnum, item.end_col, text) end, on_close = function(item, _) vim.schedule(function() local win = vim.fn.bufwinid(item.bufnr) if win < 0 then vim.api.nvim_win_set_buf(0, item.bufnr) win = 0 end vim.api.nvim_set_current_win(win) vim.api.nvim_win_set_cursor(win, { item.end_lnum + 1, item.end_col }) end) end, hl_item = function(item) return { { { 0, #item.text }, get_severity_hl(item.v.severity) }, } end, get_icon = function(item) if item.v.severity == vim.diagnostic.severity.ERROR then return "E", get_severity_hl(item.v.severity) elseif item.v.severity == vim.diagnostic.severity.WARN then return "W", get_severity_hl(item.v.severity) elseif item.v.severity == vim.diagnostic.severity.INFO then return "I", get_severity_hl(item.v.severity) elseif item.v.severity == vim.diagnostic.severity.HINT then return "H", get_severity_hl(item.v.severity) end return " " end, }, props) )end
---@param props? artio.picker.diagnostics.Propsbuiltins.diagnostics_buffer = function(props) props = props or {} props.buf = props.buf or vim.api.nvim_get_current_buf() return builtins.diagnostics(props)end
---@class artio.picker.keymaps.Props : artio.Picker.config---@field modes? string[] defaults to all
---@param props? artio.picker.keymaps.Propsbuiltins.keymaps = function(props) props = props or {} props.modes = props.modes or { "n", "i", "c", "v", "x", "s", "o", "t", "l" }
---@type vim.api.keyset.get_keymap[] local lst = vim.iter(props.modes):fold({}, function(keymaps, mode) vim.iter(vim.api.nvim_get_keymap(mode)):each(function(km) keymaps[#keymaps + 1] = km end) return keymaps end)
return artio.generic( lst, extend({ prompt = "keymaps", format_item = function(km) return ("%s %s %s | %s"):format(km.mode, km.lhs, km.rhs, km.desc) end, ---@param km vim.api.keyset.get_keymap on_close = function(km, _) vim.schedule(function() local out = vim.api.nvim_cmd({ cmd = ("%smap"):format(km.mode), args = { km.lhs }, }, { output = true, }) vim.print(out) end) end, }, props) )end
builtins.commands = function(props) props = props or {} local lst = vim.api.nvim_get_commands({})
return artio.generic( vim.tbl_values(lst), extend({ prompt = "commands", ---@param item vim.api.keyset.command_info format_item = function(item) return item.name end, ---@param cmd vim.api.keyset.command_info on_close = function(cmd, _) local nargs = vim.F.npcall(tonumber, cmd.nargs) local fmt = (nargs and nargs > 0) and ":%s " or ":%s"
artio.schedule(function() vim.api.nvim_feedkeys(string.format(fmt, cmd.name), "n", false) end) end, hl_item = function(item) return { { { 0, #item.v.name }, "@function.macro.vim" }, } end, }, props) )end
builtins.quickfix = function(props) props = props or {}
local qfid = vim.fn.getqflist({ id = 0 }).id local qflist = vim.fn.getqflist({ id = qfid, items = 0 }).items
return artio.generic( vim .iter(ipairs(qflist)) :map(function(_, item) item.name = vim.fn.bufname(item.bufnr) return item end) :totable(), extend({ prompt = "quickfix", on_close = function(_, idx) artio.schedule(function() vim.cmd([[copen]]) local win = vim.fn.getqflist({ id = qfid, winid = 0 }).winid vim.api.nvim_win_set_cursor(win, { idx, 0 }) end) end, format_item = function(item) return string.format("%s:%d:%d:%s", item.name, item.lnum, item.col, item.text) end, preview_item = function(item) return { buf = item.bufnr, pos = { item.lnum, item.col }, pos_end = { item.end_lnum, item.end_col } } end, get_icon = config.get().opts.use_icons and function(item) return require("mini.icons").get("file", item.v.name) end or nil, hl_item = utils.hl_qfitem, actions = extend( {}, utils.make_setqflistactions(function(item) return item.v end) ), }, props) )end
builtins.document_symbols = function(props) props = props or {}
local buf = vim.api.nvim_get_current_buf() local win = vim.api.nvim_get_current_win()
vim.lsp.buf.document_symbol({ on_list = function(what) local lst = what.items
return artio.select(lst, { prompt = "document_symbols", format_item = function(v) return v.text end, preview_item = function(v) return { buf = buf, pos = { v.lnum, v.col }, pos_end = { v.end_lnum, v.end_col }, } end, }, function(v, _) if not v then return end
vim.api.nvim_win_set_cursor(win, { v.lnum, v.col }) end, props) end, })end
return builtins