Something went wrong. Try again.
🐻 minimal ui2 fuzzy finder for Neovim codeberg.org/comfysage/artio.nvim
Something went wrong. Try again.
7.9 kB · 330 lines
Lua
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331local View = require("artio.view")
---@alias artio.Picker.item { id: integer, v: any, text: string, icon?: string, icon_hl?: string, hls?: artio.Picker.hl[] }---@alias artio.Picker.match { [1]: integer, [2]: integer[], [3]: integer } item, positions, score---@alias artio.Picker.matches table<integer, artio.Picker.match> id: match---@alias artio.Picker.sorter fun(lst: artio.Picker.item[], input: string): artio.Picker.matches---@alias artio.Picker.hl [[integer, integer], string]---@alias artio.Picker.action fun(self: artio.Picker)
---@class artio.Picker.config---@field items artio.Picker.item[]|string[]---@field fn artio.Picker.sorter---@field on_close fun(text: string, idx: integer)---@field get_items? fun(input: string): artio.Picker.item[]---@field format_item? fun(item: any): string---@field preview_item? fun(item: any):{buf?:integer, pos?:[integer,integer], pos_end?:[integer,integer]}---@field get_icon? fun(item: artio.Picker.item): string, string---@field hl_item? fun(item: artio.Picker.item): artio.Picker.hl[]---@field on_quit? fun()---@field live? boolean---@field prompt? string---@field defaulttext? string---@field prompttext? string---@field opts? artio.config.opts---@field win? artio.config.win---@field actions? table<string, artio.Picker.action>---@field mappings? table<string, 'up'|'down'|'accept'|'cancel'|'togglepreview'|string>
---@class artio.Picker : artio.Picker.config---@field co thread?---@field thread function---@field input string---@field liveinput? string---@field idx integer 1-indexed---@field matches artio.Picker.match[]---@field marked table<integer, true|nil>---@field live boolean---@field opts artio.config.opts---@field win artio.config.winlocal Picker = {}Picker.__index = Picker
---@type artio.Picker?Picker.active_picker = nil
---@param props artio.Picker.configfunction Picker:new(props) vim.validate("Picker.items", props.items, function(v) if not v then return props.get_items and type(props.get_items) == "function" end return type(v) == "table" and vim.islist(v) end) vim.validate("Picker:fn", props.fn, "function") vim.validate("Picker:on_close", props.on_close, "function")
local t = vim.tbl_deep_extend("force", { closed = false, prompt = "", input = nil, liveinput = nil, idx = 0, matches = {}, marked = {}, }, require("artio.config").get(), props)
if not t.prompttext then t.prompttext = t.opts.prompt_title and ("%s %s"):format(t.prompt, t.opts.promptprefix) or t.opts.promptprefix end
t.live = vim.F.if_nil(t.live, t.get_items ~= nil)
if t.live then t.input = "" t.liveinput = t.defaulttext or "" else t.input = t.defaulttext or "" t.liveinput = "" end
return setmetatable(t, Picker)end
---@enum artio.Picker.Actionlocal action_enum = { accept = 0, cancel = 1,}
function Picker:open() local ui2 = require("vim._core.ui2")
if Picker.active_picker and Picker.active_picker ~= self then Picker.active_picker:close(true) end Picker.active_picker = self
self.view = View:new(self)
ui2.check_targets() self.thread = coroutine.wrap(function() -- self:getitems("") -- self.items = self.items or {}
self.view:open()
self:initkeymaps()
self.co = coroutine.running()
vim.api.nvim_exec_autocmds("User", { pattern = "ArtioEnter" })
local result = coroutine.yield()
self:close()
while true do if result == action_enum.cancel or result ~= action_enum.accept then if self.on_quit then self.on_quit() end break end
local current = self.matches[self.idx] and self.matches[self.idx][1] if not current then break end
local item = self.items[current] if item then self.on_close(item.v, item.id) end
break end
vim.api.nvim_exec_autocmds("User", { pattern = "ArtioLeave" }) end) self.thread()end
function Picker:resume() if not self.closed then return end self.closed = false
self:open()end
---@param free? booleanfunction Picker:close(free) if self.closed then return end
if self.view then self.view:close() end
self:delkeymaps()
self.closed = true
if free then self:free() endend
function Picker:free() if self == nil then return end self.items = nil self.matches = nil self.marked = nil self = nil vim.schedule(function() collectgarbage("collect") end)end
function Picker:initkeymaps() local ui2 = require("vim._core.ui2")
---@type vim.keymap.set.Opts local opts = { buf = ui2.bufs.cmd }
if self.actions then vim.iter(pairs(self.actions)):each(function(k, v) vim.keymap.set("i", ("<Plug>(artio-action-%s)"):format(k), v, opts) end) end if self.mappings then vim.iter(pairs(self.mappings)):each(function(k, v) vim.keymap.set("i", k, ("<Plug>(artio-action-%s)"):format(v), opts) end) endend
function Picker:delkeymaps() local ui2 = require("vim._core.ui2")
local keymaps = vim.api.nvim_buf_get_keymap(ui2.bufs.cmd, "i")
vim.iter(ipairs(keymaps)):each(function(_, v) if v.lhs:match("^<Plug>(artio-action-") or (v.rhs and v.rhs:match("^<Plug>(artio-action-")) then vim.api.nvim_buf_del_keymap(ui2.bufs.cmd, "i", v.lhs) end end)end
function Picker:accept() self.thread(action_enum.accept)end
function Picker:cancel() self.thread(action_enum.cancel)end
function Picker:fix() self.idx = math.max(self.idx, self.opts.preselect and 1 or 0) self.idx = math.min(self.idx, #self.matches)end
local function item_is_structured(item) return type(item) == "table" and item.id and item.v and item.textend
function Picker:getitems(input) if (self.items == nil or self.live) and self.get_items then self.items = self.get_items(input) end
if #self.items > 0 and not item_is_structured(self.items[1]) then self.items = vim .iter(ipairs(self.items)) :map(function(i, v) local text if self.format_item and vim.is_callable(self.format_item) then text = self.format_item(v) end
return { id = i, v = v, text = text or v, } end) :totable() endend
---@param input? stringfunction Picker:getmatches(input) assert(coroutine.running(), "Picker:getmatches needs to be called from a coroutine")
if not input then input = self.live and self.liveinput or self.input end self:getitems(input) assert(self.items ~= nil, "Picker.items cannot be nil after Picker:getitems")
-- if live, ignore sorting if self.live then self.matches = self:getallmatches() return end
self.matches = vim.tbl_values(self.fn(self.items, input)) table.sort(self.matches, function(a, b) return a[3] > b[3] end)end
---@return artio.Picker.match[]function Picker:getallmatches() return vim .iter(ipairs(self.items)) :map(function(_, v) return { v.id, {}, 0 } end) :totable()end
---@param idx integer---@param yes? booleanfunction Picker:mark(idx, yes) self.marked[idx] = yes == nil and true or yesend
---@return integer[]function Picker:getmarked() return vim .iter(pairs(self.marked)) :map(function(k, v) return v and k or nil end) :totable()end
---@param idx? integer index in items---@return artio.Picker.item?function Picker:getcurrent(idx) if not idx then local i = self.idx idx = self.matches[i] and self.matches[i][1] end if not idx then return end
return self.items[idx]end
function Picker:togglelive() -- check if live can be toggled if not self.get_items then return end
-- reset fuzzy search when enabling live search if not self.live then self.input = "" end self.live = not self.liveend
return Picker