minimal extui fuzzy finder for neovim
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