minimal extui fuzzy finder for neovim
at main 217 lines 5.4 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 return function(lst, input) 55 return vim.iter(ipairs(sorters)):fold({}, function(oldmatches, it, sorter) 56 ---@type artio.Picker.matches 57 local newmatches = sorter(lst, input) 58 59 return vim 60 .iter(pairs(newmatches)) 61 :fold(strat == "intersect" and {} or oldmatches, function(matches, idx, newmatch) 62 local oldmatch = oldmatches[idx] 63 if oldmatch then 64 local next = mergematches(oldmatch, newmatch) 65 matches[idx] = next 66 elseif strat == "combine" or it == 1 then 67 matches[idx] = newmatch 68 end 69 return matches 70 end) 71 end) 72 end 73end 74 75---@type artio.Picker.sorter 76artio.fuzzy_sorter = function(lst, input) 77 if not lst or #lst == 0 then 78 return {} 79 end 80 81 if not input or #input == 0 then 82 return vim.iter(lst):fold({}, function(acc, v) 83 acc[v.id] = { v.id, {}, 0 } 84 return acc 85 end) 86 end 87 88 local matches = vim.fn.matchfuzzypos(lst, input, { key = "text" }) 89 90 local items = {} 91 for i = 1, #matches[1] do 92 items[matches[1][i].id] = { matches[1][i].id, matches[2][i], matches[3][i] } 93 end 94 return items 95end 96 97---@type artio.Picker.sorter 98artio.pattern_sorter = function(lst, input) 99 local match = string.match(input, "^/[^/]*/") 100 local pattern = match and string.match(match, "^/([^/]*)/$") 101 102 return vim.iter(lst):fold({}, function(acc, v) 103 if pattern and not string.match(v.text, pattern) then 104 return acc 105 end 106 107 acc[v.id] = { v.id, {}, 0 } 108 return acc 109 end) 110end 111 112--- the default sorter provides support for pattern matching. a `/.../` match 113--- at the start of the input will limit the fuzzy sorter to items matching the 114--- pattern. if you want to use `/.../` in your fuzzy matches, make sure to 115--- escape it by starting the input with an empty space (` /.../`). fuzzy 116--- sorting will be done on the input with the pattern removed. 117---@type artio.Picker.sorter 118artio.sorter = artio.mergesorters("intersect", artio.pattern_sorter, function(lst, input) 119 input = string.gsub(input, "^/[^/]*/", "") 120 return artio.fuzzy_sorter(lst, input) 121end) 122 123---@generic T 124---@param items T[] Arbitrary items 125---@param opts vim.ui.select.Opts Additional options 126---@param on_choice fun(item: T|nil, idx: integer|nil) 127---@param start_opts? artio.Picker.config 128artio.select = function(items, opts, on_choice, start_opts) 129 return artio.generic( 130 items, 131 vim.tbl_deep_extend( 132 "force", 133 { 134 on_close = function(_, idx) 135 artio.schedule(function() 136 on_choice(items[idx], idx) 137 end) 138 end, 139 on_quit = function() 140 artio.schedule(function() 141 on_choice(nil, nil) 142 end) 143 end, 144 }, 145 opts or {}, -- opts.prompt, opts.format_item 146 start_opts or {} 147 ) 148 ) 149end 150 151---@generic T 152---@param items T[] 153---@param props artio.Picker.config 154artio.generic = function(items, props) 155 return artio.pick(vim.tbl_deep_extend("force", { 156 fn = artio.sorter, 157 items = items, 158 }, props)) 159end 160 161---@param ... artio.Picker.config 162artio.pick = function(...) 163 local Picker = require("artio.picker") 164 return Picker:new(...):open() 165end 166 167---@param f fun(): any 168artio.schedule = function(f) 169 local Picker = require("artio.picker") 170 local current = Picker.active_picker 171 if not current or not current.closed then 172 return 173 end 174 175 vim.schedule(function() 176 vim.defer_fn(f, 10) 177 vim.wait(1000, function() 178 return coroutine.status(current.co) == "dead" 179 end) 180 end) 181end 182 183artio.resume = function() 184 local picker = require("artio.picker").active_picker 185 if not picker or not picker.closed then 186 return 187 end 188 picker:resume() 189end 190 191---@param fn artio.Picker.action 192---@param scheduled_fn? artio.Picker.action 193artio.wrap = function(fn, scheduled_fn) 194 return function() 195 local Picker = require("artio.picker") 196 local current = Picker.active_picker 197 if not current or current.closed then 198 return 199 end 200 201 -- whether to accept key inputs 202 if coroutine.status(current.co) ~= "suspended" then 203 return 204 end 205 206 pcall(fn, current) 207 208 if scheduled_fn == nil then 209 return 210 end 211 artio.schedule(function() 212 pcall(scheduled_fn, current) 213 end) 214 end 215end 216 217return artio