minimal extui fuzzy finder for neovim
at ad99e16f04438f09e5c4ed8cdae1d39879f063be 201 lines 4.8 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.fuzzy_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---@type artio.Picker.sorter 117artio.pattern_sorter = function(lst, input) 118 local match = string.match(input, "^/[^/]*/") 119 local pattern = match and string.match(match, "^/([^/]*)/$") 120 121 return vim 122 .iter(lst) 123 :map(function(v) 124 if pattern and not string.match(v.text, pattern) then 125 return 126 end 127 128 return { v.id, {}, 0 } 129 end) 130 :totable() 131end 132 133---@type artio.Picker.sorter 134artio.sorter = artio.mergesorters("intersect", artio.pattern_sorter, function(lst, input) 135 input = string.gsub(input, "^/[^/]*/", "") 136 return artio.fuzzy_sorter(lst, input) 137end) 138 139---@generic T 140---@param items T[] Arbitrary items 141---@param opts vim.ui.select.Opts Additional options 142---@param on_choice fun(item: T|nil, idx: integer|nil) 143---@param start_opts? artio.Picker.config 144artio.select = function(items, opts, on_choice, start_opts) 145 return artio.generic( 146 items, 147 vim.tbl_deep_extend("force", { 148 prompt = opts.prompt, 149 on_close = function(_, idx) 150 return on_choice(items[idx], idx) 151 end, 152 format_item = opts.format_item and function(item) 153 return opts.format_item(item) 154 end or nil, 155 }, start_opts or {}) 156 ) 157end 158 159---@generic T 160---@param items T[] 161---@param props artio.Picker.config 162artio.generic = function(items, props) 163 return artio.pick(vim.tbl_deep_extend("force", { 164 fn = artio.sorter, 165 items = items, 166 }, props)) 167end 168 169---@param ... artio.Picker.config 170artio.pick = function(...) 171 local Picker = require("artio.picker") 172 return Picker:new(...):open() 173end 174 175---@param fn artio.Picker.action 176---@param scheduled_fn? artio.Picker.action 177artio.wrap = function(fn, scheduled_fn) 178 return function() 179 local Picker = require("artio.picker") 180 local current = Picker.active_picker 181 if not current or current.closed then 182 return 183 end 184 185 -- whether to accept key inputs 186 if coroutine.status(current.co) ~= "suspended" then 187 return 188 end 189 190 pcall(fn, current) 191 192 if scheduled_fn == nil then 193 return 194 end 195 vim.schedule(function() 196 pcall(scheduled_fn, current) 197 end) 198 end 199end 200 201return artio