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
19builtins.builtins = function(props)
20 props = props or {}
21
22 return artio.generic(
23 vim.tbl_keys(builtins),
24 extend({
25 prompt = "builtins",
26 on_close = function(fname, _)
27 if not builtins[fname] then
28 return
29 end
30
31 artio.schedule(builtins[fname])
32 end,
33 }, props)
34 )
35end
36
37---@class artio.picker.generic.fs.Props : artio.Picker.config
38---@field base_dir? string
39
40local findprg = vim.fn.executable("fd") == 1 and "fd -H -p -a -t f --color=never --"
41 or "find . -type f -iregex '.*$*.*'"
42
43---@class artio.picker.files.Props : artio.picker.generic.fs.Props
44---@field findprg? string
45
46---@param props? artio.picker.files.Props
47builtins.files = function(props)
48 props = props or {}
49 props.findprg = props.findprg or findprg
50
51 local base_dir = props.base_dir or vim.fn.getcwd(0)
52 local lst = utils.make_cmd(props.findprg, {
53 cwd = base_dir,
54 })()
55
56 return artio.generic(
57 lst,
58 extend({
59 prompt = "files",
60 on_close = function(text, _)
61 vim.schedule(function()
62 vim.cmd.edit(text)
63 end)
64 end,
65 format_item = function(item)
66 return vim.fs.relpath(base_dir, item) or item
67 end,
68 get_icon = config.get().opts.use_icons and function(item)
69 return require("mini.icons").get("file", item.v)
70 end or nil,
71 preview_item = function(item)
72 return vim.fn.bufadd(item)
73 end,
74 actions = extend(
75 {},
76 utils.make_setqflistactions(function(item)
77 return { filename = item.v }
78 end),
79 utils.make_fileactions(function(item)
80 return vim.fn.bufnr(item.v, true)
81 end)
82 ),
83 }, props)
84 )
85end
86
87---@class artio.picker.grep.Props : artio.picker.generic.fs.Props
88---@field grepprg? string
89
90---@param props? artio.picker.grep.Props
91builtins.grep = function(props)
92 props = props or {}
93 props.grepprg = props.grepprg or vim.o.grepprg
94
95 local base_dir = props.base_dir or vim.fn.getcwd(0)
96 local ui2 = require("vim._core.ui2")
97 local grepcmd = utils.make_cmd(props.grepprg, {
98 cwd = base_dir,
99 })
100
101 return artio.pick(extend({
102 items = {},
103 prompt = "grep",
104 get_items = function(input)
105 if input == "" then
106 return {}
107 end
108
109 local lines = grepcmd(input)
110
111 vim.fn.setloclist(ui2.wins.cmd, {}, " ", {
112 title = "grep[" .. input .. "]",
113 lines = lines,
114 efm = vim.o.grepformat,
115 nr = "$",
116 })
117
118 return vim
119 .iter(ipairs(vim.fn.getloclist(ui2.wins.cmd)))
120 :map(function(i, locitem)
121 local name = vim.fs.abspath(vim.fn.bufname(locitem.bufnr))
122 return {
123 id = i,
124 v = { name, locitem.lnum, locitem.col },
125 text = ("%s:%d:%d:%s"):format(vim.fs.relpath(base_dir, name), locitem.lnum, locitem.col, locitem.text),
126 }
127 end)
128 :totable()
129 end,
130 fn = artio.sorter,
131 on_close = function(item, _)
132 vim.schedule(function()
133 vim.cmd.edit(item[1])
134 vim.api.nvim_win_set_cursor(0, { item[2], item[3] })
135 end)
136 end,
137 preview_item = function(item)
138 return vim.fn.bufadd(item[1]),
139 function(w)
140 vim.api.nvim_set_option_value("cursorline", true, { scope = "local", win = w })
141 vim.api.nvim_win_set_cursor(w, { item[2], 0 })
142 end
143 end,
144 get_icon = config.get().opts.use_icons and function(item)
145 return require("mini.icons").get("file", item.v[1])
146 end or nil,
147 hl_item = utils.hl_qfitem,
148 actions = extend(
149 {},
150 utils.make_setqflistactions(function(item)
151 return { filename = item.v[1], lnum = item.v[2], col = item.v[3], text = item.text }
152 end)
153 ),
154 }, props))
155end
156
157local function find_oldfiles()
158 return vim
159 .iter(vim.v.oldfiles)
160 :filter(function(v)
161 return vim.uv.fs_stat(v) --[[@as boolean]]
162 end)
163 :totable()
164end
165
166builtins.oldfiles = function(props)
167 props = props or {}
168 local lst = find_oldfiles()
169
170 return artio.generic(
171 lst,
172 extend({
173 prompt = "oldfiles",
174 on_close = function(text, _)
175 vim.schedule(function()
176 vim.cmd.edit(text)
177 end)
178 end,
179 get_icon = config.get().opts.use_icons and function(item)
180 return require("mini.icons").get("file", item.v)
181 end or nil,
182 preview_item = function(item)
183 return vim.fn.bufadd(item)
184 end,
185 actions = extend(
186 {},
187 utils.make_setqflistactions(function(item)
188 return { filename = item.v }
189 end)
190 ),
191 }, props)
192 )
193end
194
195builtins.buffergrep = function(props)
196 props = props or {}
197 local win = vim.api.nvim_get_current_win()
198 local buf = vim.api.nvim_win_get_buf(win)
199 local n = vim.api.nvim_buf_line_count(buf)
200 local lst = {} ---@type integer[]
201 for i = 1, n do
202 lst[#lst + 1] = i
203 end
204
205 local pad = #tostring(lst[#lst])
206
207 return artio.generic(
208 lst,
209 extend({
210 prompt = "buffergrep",
211 on_close = function(row, _)
212 vim.schedule(function()
213 vim.api.nvim_win_set_cursor(win, { row, 0 })
214 end)
215 end,
216 format_item = function(row)
217 return vim.api.nvim_buf_get_lines(buf, row - 1, row, true)[1]
218 end,
219 preview_item = function(row)
220 return buf,
221 function(w)
222 vim.api.nvim_set_option_value("cursorline", true, { scope = "local", win = w })
223 vim.api.nvim_win_set_cursor(w, { row, 0 })
224 end
225 end,
226 get_icon = function(row)
227 local v = tostring(row.v)
228 return ("%s%s"):format((" "):rep(pad - #v), v)
229 end,
230 actions = extend(
231 {},
232 utils.make_setqflistactions(function(item)
233 return { filename = vim.api.nvim_buf_get_name(buf), lnum = item.v }
234 end)
235 ),
236 }, props)
237 )
238end
239
240local function find_helptags()
241 local buf = vim.api.nvim_create_buf(false, true)
242 vim.bo[buf].buftype = "help"
243 local tags = vim.api.nvim_buf_call(buf, function()
244 return vim.fn.taglist(".*")
245 end)
246 vim.api.nvim_buf_delete(buf, { force = true })
247 return vim.tbl_map(function(t)
248 return t.name
249 end, tags)
250end
251
252builtins.helptags = function(props)
253 props = props or {}
254 local lst = find_helptags()
255
256 return artio.generic(
257 lst,
258 extend({
259 prompt = "helptags",
260 on_close = function(text, _)
261 vim.schedule(function()
262 vim.cmd.help(text)
263 end)
264 end,
265 preview_item = function(tag)
266 return vim.api.nvim_create_buf(false, true),
267 function(w)
268 local buf = vim.api.nvim_win_get_buf(w)
269 vim.bo[buf].bufhidden = "wipe"
270 vim.bo[buf].buftype = "help"
271
272 vim._with({ buf = buf }, function()
273 vim.cmd.help(tag)
274 end)
275 end
276 end,
277 }, props)
278 )
279end
280
281local function find_buffers()
282 return vim
283 .iter(vim.api.nvim_list_bufs())
284 :filter(function(bufnr)
285 return vim.api.nvim_buf_is_valid(bufnr) and vim.bo[bufnr].buflisted
286 end)
287 :totable()
288end
289
290builtins.buffers = function(props)
291 props = props or {}
292 local lst = find_buffers()
293
294 return artio.generic(
295 lst,
296 vim.tbl_extend("force", {
297 prompt = "buffers",
298 format_item = function(bufnr)
299 return vim.api.nvim_buf_get_name(bufnr)
300 end,
301 on_close = function(bufnr, _)
302 vim.schedule(function()
303 vim.cmd.buffer(bufnr)
304 end)
305 end,
306 get_icon = config.get().opts.use_icons and function(item)
307 return require("mini.icons").get("file", vim.api.nvim_buf_get_name(item.v))
308 end or nil,
309 preview_item = function(item)
310 return item
311 end,
312 }, props)
313 )
314end
315
316---@param currentfile string
317---@param item string
318---@return integer score
319local function matchproximity(currentfile, item)
320 item = vim.fs.abspath(item)
321
322 return vim.iter(ipairs(vim.split(item, "/", { trimempty = true }))):fold(0, function(score, i, part)
323 if part == currentfile[i] then
324 return score + 50
325 end
326 return score
327 end)
328end
329
330--- uses the regular files picker as a base
331--- - boosts items in the bufferlist
332--- - proportionally boosts items that match closely to the current file in proximity within the filesystem
333builtins.smart = function(props)
334 props = props or {}
335 local currentfile = vim.api.nvim_buf_get_name(0)
336 currentfile = vim.fs.abspath(currentfile)
337
338 props.findprg = props.findprg or findprg
339 local base_dir = vim.fn.getcwd(0)
340 local lst = utils.make_cmd(props.findprg, {
341 cwd = base_dir,
342 })()
343
344 local recentlst = vim
345 .iter(find_buffers())
346 :map(function(buf)
347 local v = vim.api.nvim_buf_get_name(buf)
348 return vim.fs.abspath(v)
349 end)
350 :totable()
351
352 local items = vim.list.unique(vim.iter({ lst, recentlst }):flatten(1):totable())
353
354 return artio.pick(extend({
355 prompt = "smart",
356 items = items,
357 fn = artio.mergesorters("base", function(l, input)
358 if #input == 0 then
359 return vim
360 .iter(l)
361 :map(function(v)
362 if not vim.tbl_contains(recentlst, v.v) then
363 return
364 end
365 return { v.id, {}, 0 }
366 end)
367 :totable()
368 end
369
370 return artio.sorter(l, input)
371 end, function(l, _)
372 return vim
373 .iter(l)
374 :map(function(v)
375 if not vim.tbl_contains(recentlst, v.v) then
376 return
377 end
378 return { v.id, {}, 100 }
379 end)
380 :totable()
381 end, function(l, _)
382 return vim
383 .iter(l)
384 :map(function(v)
385 return { v.id, {}, matchproximity(currentfile, v.v) }
386 end)
387 :totable()
388 end),
389 on_close = function(text, _)
390 vim.schedule(function()
391 vim.cmd.edit(text)
392 end)
393 end,
394 format_item = function(item)
395 return vim.fs.relpath(base_dir, item) or item
396 end,
397 get_icon = config.get().opts.use_icons and function(item)
398 return require("mini.icons").get("file", item.v)
399 end or nil,
400 preview_item = function(item)
401 return vim.fn.bufadd(item)
402 end,
403 actions = extend(
404 {},
405 utils.make_setqflistactions(function(item)
406 return { filename = item.v }
407 end)
408 ),
409 }, props))
410end
411
412builtins.colorschemes = function(props)
413 props = props or {}
414 local files = vim.api.nvim_get_runtime_file("colors/*.{vim,lua}", true)
415 local lst = vim.tbl_map(function(f)
416 return vim.fs.basename(f):gsub("%.[^.]+$", "")
417 end, files)
418
419 local current = vim.g.colors_name
420 local bg = vim.o.background
421
422 return artio.generic(
423 lst,
424 extend({
425 prompt = "colorschemes",
426 on_close = function(text, _)
427 vim.schedule(function()
428 vim.cmd.colorscheme(text)
429 end)
430 end,
431 on_quit = function()
432 -- reset colorscheme
433 vim.schedule(function()
434 if vim.g.colors_name ~= current then
435 vim.cmd.colorscheme(current)
436 end
437 end)
438 end,
439 preview_item = function(item)
440 return vim.api.nvim_create_buf(false, true),
441 function(w)
442 local buf = vim.api.nvim_win_get_buf(w)
443 vim.bo[buf].bufhidden = "wipe"
444 vim.bo[buf].buftype = "nofile"
445
446 vim.api.nvim_buf_set_lines(buf, 0, -1, false, {})
447 vim.api.nvim_win_set_config(w, { hide = true })
448
449 vim.cmd.colorscheme(item)
450 vim.o.background = bg
451 end
452 end,
453 }, props)
454 )
455end
456
457builtins.highlights = function(props)
458 props = props or {}
459 local hlout = vim.split(vim.api.nvim_exec2([[ highlight ]], { output = true }).output, "\n", { trimempty = true })
460
461 local maxw = 0
462
463 local hls = vim
464 .iter(hlout)
465 :map(function(hl)
466 local sp = string.find(hl, "%s", 1)
467 maxw = sp > maxw and sp or maxw
468 return { hl:sub(1, sp - 1), hl }
469 end)
470 :fold({}, function(t, hl)
471 local pad = math.max(1, math.min(20, maxw) - #hl[1] + 1)
472 t[hl[1]] = string.gsub(hl[2], "%s+", (" "):rep(pad), 1)
473 return t
474 end)
475
476 return artio.generic(
477 vim.tbl_keys(hls),
478 extend({
479 prompt = "highlights",
480 on_close = function(hlname, _)
481 vim.schedule(function()
482 vim.cmd(("verbose hi %s"):format(hlname))
483 end)
484 end,
485 format_item = function(hlname)
486 return hls[hlname]
487 end,
488 hl_item = function(hlname)
489 local x_start, x_end = string.find(hlname.text, "%sxxx")
490
491 return {
492 { { 0, #hlname.v }, hlname.v },
493 { { x_start, x_end }, hlname.v },
494 }
495 end,
496 }, props)
497 )
498end
499
500---@private
501---@param severity vim.diagnostic.Severity
502---@return string
503local function get_severity_hl(severity)
504 if severity == vim.diagnostic.severity.ERROR then
505 return "DiagnosticError"
506 elseif severity == vim.diagnostic.severity.WARN then
507 return "DiagnosticWarn"
508 elseif severity == vim.diagnostic.severity.INFO then
509 return "DiagnosticInfo"
510 elseif severity == vim.diagnostic.severity.HINT then
511 return "DiagnosticHint"
512 end
513 return ""
514end
515
516---@class artio.picker.diagnostics.Props : artio.Picker.config
517---@field buf? integer defaults to workspace
518
519---@param props? artio.picker.diagnostics.Props
520builtins.diagnostics = function(props)
521 props = props or {}
522 local lst = vim.diagnostic.get(props.buf)
523
524 return artio.generic(
525 lst,
526 extend({
527 prompt = "diagnostics",
528 format_item = function(item)
529 local text = item.message
530 if item.code then
531 text = ("%s [%s]"):format(text, item.code)
532 end
533 return ("%d:%d :: %s"):format(item.end_lnum, item.end_col, text)
534 end,
535 on_close = function(item, _)
536 vim.schedule(function()
537 local win = vim.fn.bufwinid(item.bufnr)
538 if win < 0 then
539 vim.api.nvim_win_set_buf(0, item.bufnr)
540 win = 0
541 end
542 vim.api.nvim_set_current_win(win)
543 vim.api.nvim_win_set_cursor(win, { item.end_lnum + 1, item.end_col })
544 end)
545 end,
546 hl_item = function(item)
547 return {
548 { { 0, #item.text }, get_severity_hl(item.v.severity) },
549 }
550 end,
551 get_icon = function(item)
552 if item.v.severity == vim.diagnostic.severity.ERROR then
553 return "E", get_severity_hl(item.v.severity)
554 elseif item.v.severity == vim.diagnostic.severity.WARN then
555 return "W", get_severity_hl(item.v.severity)
556 elseif item.v.severity == vim.diagnostic.severity.INFO then
557 return "I", get_severity_hl(item.v.severity)
558 elseif item.v.severity == vim.diagnostic.severity.HINT then
559 return "H", get_severity_hl(item.v.severity)
560 end
561 return " "
562 end,
563 }, props)
564 )
565end
566
567---@param props? artio.picker.diagnostics.Props
568builtins.diagnostics_buffer = function(props)
569 props = props or {}
570 props.buf = props.buf or vim.api.nvim_get_current_buf()
571 return builtins.diagnostics(props)
572end
573
574---@class artio.picker.keymaps.Props : artio.Picker.config
575---@field modes? string[] defaults to all
576
577---@param props? artio.picker.keymaps.Props
578builtins.keymaps = function(props)
579 props = props or {}
580 props.modes = props.modes or { "n", "i", "c", "v", "x", "s", "o", "t", "l" }
581
582 ---@type vim.api.keyset.get_keymap[]
583 local lst = vim.iter(props.modes):fold({}, function(keymaps, mode)
584 vim.iter(vim.api.nvim_get_keymap(mode)):each(function(km)
585 keymaps[#keymaps + 1] = km
586 end)
587 return keymaps
588 end)
589
590 return artio.generic(
591 lst,
592 extend({
593 prompt = "keymaps",
594 format_item = function(km)
595 return ("%s %s %s | %s"):format(km.mode, km.lhs, km.rhs, km.desc)
596 end,
597 ---@param km vim.api.keyset.get_keymap
598 on_close = function(km, _)
599 vim.schedule(function()
600 local out = vim.api.nvim_cmd({
601 cmd = ("%smap"):format(km.mode),
602 args = { km.lhs },
603 }, {
604 output = true,
605 })
606 vim.print(out)
607 end)
608 end,
609 }, props)
610 )
611end
612
613builtins.commands = function(props)
614 props = props or {}
615 local lst = vim.api.nvim_get_commands({})
616
617 return artio.generic(
618 vim.tbl_values(lst),
619 extend({
620 prompt = "commands",
621 ---@param item vim.api.keyset.command_info
622 format_item = function(item)
623 return item.name
624 end,
625 ---@param cmd vim.api.keyset.command_info
626 on_close = function(cmd, _)
627 local nargs = vim.F.npcall(tonumber, cmd.nargs)
628 local fmt = (nargs and nargs > 0) and ":%s " or ":%s"
629
630 artio.schedule(function()
631 vim.api.nvim_feedkeys(string.format(fmt, cmd.name), "n", false)
632 end)
633 end,
634 hl_item = function(item)
635 return {
636 { { 0, #item.v.name }, "@function.macro.vim" },
637 }
638 end,
639 }, props)
640 )
641end
642
643builtins.quickfix = function(props)
644 props = props or {}
645
646 local qfid = vim.fn.getqflist({ id = 0 }).id
647 local qflist = vim.fn.getqflist({ id = qfid, items = 0 }).items
648
649 return artio.generic(
650 vim
651 .iter(ipairs(qflist))
652 :map(function(_, item)
653 item.name = vim.fn.bufname(item.bufnr)
654 return item
655 end)
656 :totable(),
657 extend({
658 prompt = "quickfix",
659 on_close = function(_, idx)
660 artio.schedule(function()
661 vim.cmd([[copen]])
662 local win = vim.fn.getqflist({ id = qfid, winid = 0 }).winid
663 vim.api.nvim_win_set_cursor(win, { idx, 0 })
664 end)
665 end,
666 format_item = function(item)
667 return string.format("%s:%d:%d:%s", item.name, item.lnum, item.col, item.text)
668 end,
669 preview_item = function(item)
670 return item.bufnr,
671 function(w)
672 vim.api.nvim_set_option_value("cursorline", true, { scope = "local", win = w })
673 vim.api.nvim_win_set_cursor(w, { item.lnum, item.col })
674 end
675 end,
676 get_icon = config.get().opts.use_icons and function(item)
677 return require("mini.icons").get("file", item.v.name)
678 end or nil,
679 hl_item = utils.hl_qfitem,
680 actions = extend(
681 {},
682 utils.make_setqflistactions(function(item)
683 return item.v
684 end)
685 ),
686 }, props)
687 )
688end
689
690return builtins