minimal extui fuzzy finder for neovim
at 8763f8aac5d2aa435f5dccb3c56bac81557df31e 178 lines 4.2 kB view raw
1local function lzrq(modname) 2 return setmetatable({}, { 3 __index = function(_, key) 4 return require(modname)[key] 5 end, 6 }) 7end 8 9local config = lzrq("artio.config") 10 11local artio = {} 12 13---@param cfg? artio.config 14artio.setup = function(cfg) 15 cfg = cfg or {} 16 config.set(cfg) 17end 18 19---@param a integer[] 20---@param ... integer[] 21---@return integer[] 22local function mergehl(a, ...) 23 local hl_lists = { a, ... } 24 25 local t = vim.iter(hl_lists):fold({}, function(hls, hl_list) 26 for i = 1, #hl_list do 27 hls[hl_list[i]] = true 28 end 29 return hls 30 end) 31 return vim.tbl_keys(t) 32end 33 34---@param a artio.Picker.match 35---@param b artio.Picker.match 36---@return artio.Picker.match 37local function mergematches(a, b) 38 return { a[1], mergehl(a[2], b[2]), a[3] + b[3] } 39end 40 41---@param strat 'combine'|'intersect'|'base' 42--- combine: 43--- a, b -> a + ab + b 44--- intersect: 45--- a, b -> ab 46--- base: 47--- a, b -> a + ab 48---@param a artio.Picker.sorter 49---@param ... artio.Picker.sorter 50---@return artio.Picker.sorter 51function artio.mergesorters(strat, a, ...) 52 local sorters = { a, ... } ---@type artio.Picker.sorter[] 53 54 ---@generic T 55 ---@param t T[] 56 ---@param cmp fun(T): boolean 57 ---@return integer? 58 local function findi(t, cmp) 59 for i = 1, #t do 60 if t[i] and cmp(t[i]) then 61 return i 62 end 63 end 64 end 65 66 return function(lst, input) 67 local it = 0 68 return vim.iter(sorters):fold({}, function(oldmatches, sorter) 69 it = it + 1 70 ---@type artio.Picker.match[] 71 local newmatches = sorter(lst, input) 72 73 return vim.iter(newmatches):fold(strat == "intersect" and {} or oldmatches, function(matches, newmatch) 74 local oldmatchidx = findi(oldmatches, function(v) 75 return v[1] == newmatch[1] 76 end) 77 78 if oldmatchidx then 79 local oldmatch = oldmatches[oldmatchidx] 80 local next = mergematches(oldmatch, newmatch) 81 if strat == "intersect" then 82 matches[#matches + 1] = next 83 else 84 matches[oldmatchidx] = next 85 end 86 elseif strat == "combine" or it == 1 then 87 matches[#matches + 1] = newmatch 88 end 89 return matches 90 end) 91 end) 92 end 93end 94 95---@type artio.Picker.sorter 96artio.sorter = function(lst, input) 97 if not lst or #lst == 0 then 98 return {} 99 end 100 101 if not input or #input == 0 then 102 return vim.tbl_map(function(v) 103 return { v.id, {}, 0 } 104 end, lst) 105 end 106 107 local matches = vim.fn.matchfuzzypos(lst, input, { key = "text" }) 108 109 local items = {} 110 for i = 1, #matches[1] do 111 items[#items + 1] = { matches[1][i].id, matches[2][i], matches[3][i] } 112 end 113 return items 114end 115 116---@generic T 117---@param items T[] Arbitrary items 118---@param opts vim.ui.select.Opts Additional options 119---@param on_choice fun(item: T|nil, idx: integer|nil) 120---@param start_opts? artio.Picker.config 121artio.select = function(items, opts, on_choice, start_opts) 122 return artio.generic( 123 items, 124 vim.tbl_deep_extend("force", { 125 prompt = opts.prompt, 126 on_close = function(_, idx) 127 return on_choice(items[idx], idx) 128 end, 129 format_item = opts.format_item and function(item) 130 return opts.format_item(item) 131 end or nil, 132 }, start_opts or {}) 133 ) 134end 135 136---@generic T 137---@param items T[] 138---@param props artio.Picker.config 139artio.generic = function(items, props) 140 return artio.pick(vim.tbl_deep_extend("force", { 141 fn = artio.sorter, 142 items = items, 143 }, props)) 144end 145 146---@param ... artio.Picker.config 147artio.pick = function(...) 148 local Picker = require("artio.picker") 149 return Picker:new(...):open() 150end 151 152---@param fn artio.Picker.action 153---@param scheduled_fn? artio.Picker.action 154artio.wrap = function(fn, scheduled_fn) 155 return function() 156 local Picker = require("artio.picker") 157 local current = Picker.active_picker 158 if not current or current.closed then 159 return 160 end 161 162 -- whether to accept key inputs 163 if coroutine.status(current.co) ~= "suspended" then 164 return 165 end 166 167 pcall(fn, current) 168 169 if scheduled_fn == nil then 170 return 171 end 172 vim.schedule(function() 173 pcall(scheduled_fn, current) 174 end) 175 end 176end 177 178return artio