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 artio = lzrq("artio")
10local config = lzrq("artio.config")
11local utils = lzrq("artio.utils")
12
13local function extend(t1, ...)
14 return vim.tbl_deep_extend("force", t1, ...)
15end
16
17local builtins = {}
18
19local findprg = "fd -H -p -t f --color=never"
20
21---@class artio.picker.files.Props : artio.Picker.config
22---@field findprg? string
23
24---@param props? artio.picker.files.Props
25builtins.files = function(props)
26 props = props or {}
27 props.findprg = props.findprg or findprg
28
29 local lst = utils.make_cmd(props.findprg)()
30
31 return artio.generic(
32 lst,
33 extend({
34 prompt = "files",
35 on_close = function(text, _)
36 vim.schedule(function()
37 vim.cmd.edit(text)
38 end)
39 end,
40 get_icon = config.get().opts.use_icons and function(item)
41 return require("mini.icons").get("file", item.v)
42 end or nil,
43 preview_item = function(item)
44 return vim.fn.bufadd(item)
45 end,
46 actions = extend(
47 {},
48 utils.make_setqflistactions(function(item)
49 return { filename = item.v }
50 end)
51 ),
52 }, props)
53 )
54end
55
56---@class artio.picker.grep.Props : artio.Picker.config
57---@field grepprg? string
58
59---@param props? artio.picker.grep.Props
60builtins.grep = function(props)
61 props = props or {}
62 props.grepprg = props.grepprg or vim.o.grepprg
63
64 local ext = require("vim._extui.shared")
65 local grepcmd = utils.make_cmd(props.grepprg)
66
67 return artio.pick(extend({
68 items = {},
69 prompt = "grep",
70 get_items = function(input)
71 if input == "" then
72 return {}
73 end
74
75 local lines = grepcmd(input)
76
77 vim.fn.setloclist(ext.wins.cmd, {}, " ", {
78 title = "grep[" .. input .. "]",
79 lines = lines,
80 efm = vim.o.grepformat,
81 nr = "$",
82 })
83
84 return vim
85 .iter(ipairs(vim.fn.getloclist(ext.wins.cmd)))
86 :map(function(i, locitem)
87 return {
88 id = i,
89 v = { vim.fn.bufname(locitem.bufnr), locitem.lnum, locitem.col },
90 text = locitem.text,
91 }
92 end)
93 :totable()
94 end,
95 fn = artio.sorter,
96 on_close = function(item, _)
97 vim.schedule(function()
98 vim.cmd.edit(item[1])
99 vim.api.nvim_win_set_cursor(0, { item[2], item[3] })
100 end)
101 end,
102 preview_item = function(item)
103 return vim.fn.bufadd(item[1]),
104 function(w)
105 vim.api.nvim_set_option_value("cursorline", true, { scope = "local", win = w })
106 vim.api.nvim_win_set_cursor(w, { item[2], 0 })
107 end
108 end,
109 get_icon = config.get().opts.use_icons and function(item)
110 return require("mini.icons").get("file", item.v[1])
111 end or nil,
112 actions = extend(
113 {},
114 utils.make_setqflistactions(function(item)
115 return { filename = item.v[1], lnum = item.v[2], col = item.v[3], text = item.text }
116 end)
117 ),
118 }, props))
119end
120
121local function find_oldfiles()
122 return vim
123 .iter(vim.v.oldfiles)
124 :filter(function(v)
125 return vim.uv.fs_stat(v) --[[@as boolean]]
126 end)
127 :totable()
128end
129
130builtins.oldfiles = function(props)
131 props = props or {}
132 local lst = find_oldfiles()
133
134 return artio.generic(
135 lst,
136 extend({
137 prompt = "oldfiles",
138 on_close = function(text, _)
139 vim.schedule(function()
140 vim.cmd.edit(text)
141 end)
142 end,
143 get_icon = config.get().opts.use_icons and function(item)
144 return require("mini.icons").get("file", item.v)
145 end or nil,
146 preview_item = function(item)
147 return vim.fn.bufadd(item)
148 end,
149 actions = extend(
150 {},
151 utils.make_setqflistactions(function(item)
152 return { filename = item.v }
153 end)
154 ),
155 }, props)
156 )
157end
158
159builtins.buffergrep = function(props)
160 props = props or {}
161 local win = vim.api.nvim_get_current_win()
162 local buf = vim.api.nvim_win_get_buf(win)
163 local n = vim.api.nvim_buf_line_count(buf)
164 local lst = {} ---@type integer[]
165 for i = 1, n do
166 lst[#lst + 1] = i
167 end
168
169 local pad = #tostring(lst[#lst])
170
171 return artio.generic(
172 lst,
173 extend({
174 prompt = "buffergrep",
175 on_close = function(row, _)
176 vim.schedule(function()
177 vim.api.nvim_win_set_cursor(win, { row, 0 })
178 end)
179 end,
180 format_item = function(row)
181 return vim.api.nvim_buf_get_lines(buf, row - 1, row, true)[1]
182 end,
183 preview_item = function(row)
184 return buf,
185 function(w)
186 vim.api.nvim_set_option_value("cursorline", true, { scope = "local", win = w })
187 vim.api.nvim_win_set_cursor(w, { row, 0 })
188 end
189 end,
190 get_icon = function(row)
191 local v = tostring(row.v)
192 return ("%s%s"):format((" "):rep(pad - #v), v)
193 end,
194 actions = extend(
195 {},
196 utils.make_setqflistactions(function(item)
197 return { filename = vim.api.nvim_buf_get_name(buf), lnum = item.v }
198 end)
199 ),
200 }, props)
201 )
202end
203
204local function find_helptags()
205 local buf = vim.api.nvim_create_buf(false, true)
206 vim.bo[buf].buftype = "help"
207 local tags = vim.api.nvim_buf_call(buf, function()
208 return vim.fn.taglist(".*")
209 end)
210 vim.api.nvim_buf_delete(buf, { force = true })
211 return vim.tbl_map(function(t)
212 return t.name
213 end, tags)
214end
215
216builtins.helptags = function(props)
217 props = props or {}
218 local lst = find_helptags()
219
220 return artio.generic(
221 lst,
222 extend({
223 prompt = "helptags",
224 on_close = function(text, _)
225 vim.schedule(function()
226 vim.cmd.help(text)
227 end)
228 end,
229 }, props)
230 )
231end
232
233local function find_buffers()
234 return vim
235 .iter(vim.api.nvim_list_bufs())
236 :filter(function(bufnr)
237 return vim.api.nvim_buf_is_valid(bufnr) and vim.bo[bufnr].buflisted
238 end)
239 :totable()
240end
241
242builtins.buffers = function(props)
243 props = props or {}
244 local lst = find_buffers()
245
246 return artio.select(lst, {
247 prompt = "buffers",
248 format_item = function(bufnr)
249 return vim.api.nvim_buf_get_name(bufnr)
250 end,
251 }, function(bufnr, _)
252 vim.schedule(function()
253 vim.cmd.buffer(bufnr)
254 end)
255 end, {
256 get_icon = config.get().opts.use_icons and function(item)
257 return require("mini.icons").get("file", vim.api.nvim_buf_get_name(item.v))
258 end or nil,
259 preview_item = function(item)
260 return item
261 end,
262 }, props)
263end
264
265---@param currentfile string
266---@param item string
267---@return integer score
268local function matchproximity(currentfile, item)
269 item = vim.fs.abspath(item)
270
271 return vim.iter(ipairs(vim.split(item, "/", { trimempty = true }))):fold(0, function(score, i, part)
272 if part == currentfile[i] then
273 return score + 50
274 end
275 return score
276 end)
277end
278
279--- uses the regular files picker as a base
280--- - boosts items in the bufferlist
281--- - proportionally boosts items that match closely to the current file in proximity within the filesystem
282builtins.smart = function(props)
283 props = props or {}
284 local currentfile = vim.api.nvim_buf_get_name(0)
285 currentfile = vim.fs.abspath(currentfile)
286
287 props.findprg = props.findprg or findprg
288 local lst = utils.make_cmd(props.findprg)()
289
290 local pwd = vim.fn.getcwd()
291 local recentlst = vim
292 .iter(find_buffers())
293 :map(function(buf)
294 local v = vim.api.nvim_buf_get_name(buf)
295 return vim.fs.relpath(pwd, v) or v
296 end)
297 :totable()
298
299 return artio.pick(extend({
300 prompt = "smart",
301 items = vim.tbl_keys(vim.iter({ lst, recentlst }):fold({}, function(items, l)
302 for i = 1, #l do
303 items[l[i]] = true
304 end
305 return items
306 end)),
307 fn = artio.mergesorters("base", artio.sorter, function(l, _)
308 return vim
309 .iter(l)
310 :map(function(v)
311 if not vim.tbl_contains(recentlst, v.text) then
312 return
313 end
314 return { v.id, {}, 100 }
315 end)
316 :totable()
317 end, function(l, _)
318 return vim
319 .iter(l)
320 :map(function(v)
321 return { v.id, {}, matchproximity(currentfile, v.text) }
322 end)
323 :totable()
324 end),
325 on_close = function(text, _)
326 vim.schedule(function()
327 vim.cmd.edit(text)
328 end)
329 end,
330 get_icon = config.get().opts.use_icons and function(item)
331 return require("mini.icons").get("file", item.v)
332 end or nil,
333 preview_item = function(item)
334 return vim.fn.bufadd(item)
335 end,
336 actions = extend(
337 {},
338 utils.make_setqflistactions(function(item)
339 return { filename = item.v }
340 end)
341 ),
342 }, props))
343end
344
345builtins.colorschemes = function(props)
346 props = props or {}
347 local files = vim.api.nvim_get_runtime_file("colors/*.{vim,lua}", true)
348 local lst = vim.tbl_map(function(f)
349 return vim.fs.basename(f):gsub("%.[^.]+$", "")
350 end, files)
351
352 return artio.generic(
353 lst,
354 extend({
355 prompt = "colorschemes",
356 on_close = function(text, _)
357 vim.schedule(function()
358 vim.cmd.colorscheme(text)
359 end)
360 end,
361 }, props)
362 )
363end
364
365builtins.highlights = function(props)
366 props = props or {}
367 local hlout = vim.split(vim.api.nvim_exec2([[ highlight ]], { output = true }).output, "\n", { trimempty = true })
368
369 local maxw = 0
370
371 local hls = vim
372 .iter(hlout)
373 :map(function(hl)
374 local sp = string.find(hl, "%s", 1)
375 maxw = sp > maxw and sp or maxw
376 return { hl:sub(1, sp - 1), hl }
377 end)
378 :fold({}, function(t, hl)
379 local pad = math.max(1, math.min(20, maxw) - #hl[1] + 1)
380 t[hl[1]] = string.gsub(hl[2], "%s+", (" "):rep(pad), 1)
381 return t
382 end)
383
384 return artio.generic(
385 vim.tbl_keys(hls),
386 extend({
387 prompt = "highlights",
388 on_close = function(line, _)
389 vim.schedule(function()
390 vim.print(line)
391 end)
392 end,
393 format_item = function(hlname)
394 return hls[hlname]
395 end,
396 hl_item = function(hlname)
397 local x_start, x_end = string.find(hlname.text, "%sxxx")
398
399 return {
400 { { 0, #hlname.v }, hlname.v },
401 { { x_start, x_end }, hlname.v },
402 }
403 end,
404 }, props)
405 )
406end
407
408return builtins