馃惢 minimal ui2 fuzzy finder for Neovim
codeberg.org/comfysage/artio.nvim
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 { buf = 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 { buf = vim.fn.bufadd(item[1]), pos = { item[2], 0 } }
139 end,
140 get_icon = config.get().opts.use_icons and function(item)
141 return require("mini.icons").get("file", item.v[1])
142 end or nil,
143 hl_item = utils.hl_qfitem,
144 actions = extend(
145 {},
146 utils.make_setqflistactions(function(item)
147 return { filename = item.v[1], lnum = item.v[2], col = item.v[3], text = item.text }
148 end)
149 ),
150 }, props))
151end
152
153local function find_oldfiles()
154 return vim
155 .iter(vim.v.oldfiles)
156 :filter(function(v)
157 return vim.uv.fs_stat(v) --[[@as boolean]]
158 end)
159 :totable()
160end
161
162builtins.oldfiles = function(props)
163 props = props or {}
164 local lst = find_oldfiles()
165
166 return artio.generic(
167 lst,
168 extend({
169 prompt = "oldfiles",
170 on_close = function(text, _)
171 vim.schedule(function()
172 vim.cmd.edit(text)
173 end)
174 end,
175 get_icon = config.get().opts.use_icons and function(item)
176 return require("mini.icons").get("file", item.v)
177 end or nil,
178 preview_item = function(item)
179 return { buf = vim.fn.bufadd(item) }
180 end,
181 actions = extend(
182 {},
183 utils.make_setqflistactions(function(item)
184 return { filename = item.v }
185 end)
186 ),
187 }, props)
188 )
189end
190
191builtins.buffergrep = function(props)
192 props = props or {}
193 local win = vim.api.nvim_get_current_win()
194 local buf = vim.api.nvim_win_get_buf(win)
195 local n = vim.api.nvim_buf_line_count(buf)
196 local lst = {} ---@type integer[]
197 for i = 1, n do
198 lst[#lst + 1] = i
199 end
200
201 local pad = #tostring(lst[#lst])
202
203 return artio.generic(
204 lst,
205 extend({
206 prompt = "buffergrep",
207 on_close = function(row, _)
208 vim.schedule(function()
209 vim.api.nvim_win_set_cursor(win, { row, 0 })
210 end)
211 end,
212 format_item = function(row)
213 return vim.api.nvim_buf_get_lines(buf, row - 1, row, true)[1]
214 end,
215 preview_item = function(row)
216 return { buf = buf, pos = { row, 0 } }
217 end,
218 get_icon = function(row)
219 local v = tostring(row.v)
220 return ("%s%s"):format((" "):rep(pad - #v), v)
221 end,
222 actions = extend(
223 {},
224 utils.make_setqflistactions(function(item)
225 return { filename = vim.api.nvim_buf_get_name(buf), lnum = item.v }
226 end)
227 ),
228 }, props)
229 )
230end
231
232local function find_helptags()
233 local buf = vim.api.nvim_create_buf(false, true)
234 vim.bo[buf].buftype = "help"
235 local tags = vim.api.nvim_buf_call(buf, function()
236 return vim.fn.taglist(".*")
237 end)
238 vim.api.nvim_buf_delete(buf, { force = true })
239 return vim.tbl_map(function(t)
240 return t.name
241 end, tags)
242end
243
244builtins.helptags = function(props)
245 props = props or {}
246 local lst = find_helptags()
247
248 return artio.generic(
249 lst,
250 extend({
251 prompt = "helptags",
252 on_close = function(text, _)
253 vim.schedule(function()
254 vim.cmd.help(text)
255 end)
256 end,
257 preview_item = function(tag)
258 local buf = vim.api.nvim_create_buf(false, true)
259
260 vim.bo[buf].bufhidden = "wipe"
261 vim.bo[buf].buftype = "help"
262
263 vim._with({ buf = buf }, function()
264 vim.cmd.help(tag)
265 end)
266
267 return { buf = buf }
268 end,
269 }, props)
270 )
271end
272
273local function find_buffers()
274 return vim
275 .iter(vim.api.nvim_list_bufs())
276 :filter(function(bufnr)
277 return vim.api.nvim_buf_is_valid(bufnr) and vim.bo[bufnr].buflisted
278 end)
279 :totable()
280end
281
282builtins.buffers = function(props)
283 props = props or {}
284 local lst = find_buffers()
285
286 return artio.generic(
287 lst,
288 vim.tbl_extend("force", {
289 prompt = "buffers",
290 format_item = function(bufnr)
291 return vim.api.nvim_buf_get_name(bufnr)
292 end,
293 on_close = function(bufnr, _)
294 vim.schedule(function()
295 vim.cmd.buffer(bufnr)
296 end)
297 end,
298 get_icon = config.get().opts.use_icons and function(item)
299 return require("mini.icons").get("file", vim.api.nvim_buf_get_name(item.v))
300 end or nil,
301 preview_item = function(item)
302 return { buf = item }
303 end,
304 }, props)
305 )
306end
307
308---@param currentfile string
309---@param item string
310---@return integer score
311local function matchproximity(currentfile, item)
312 currentfile = vim.fs.abspath(currentfile)
313 local parts = vim.split(currentfile, "/", { trimempty = true })
314 item = vim.fs.abspath(item)
315
316 return vim.iter(ipairs(vim.split(item, "/", { trimempty = true }))):fold(0, function(score, i, part)
317 if part == parts[i] then
318 return score + 50
319 end
320 return score
321 end)
322end
323
324--- uses the regular files picker as a base
325--- - boosts items in the bufferlist
326--- - proportionally boosts items that match closely to the current file in proximity within the filesystem
327builtins.smart = function(props)
328 props = props or {}
329 local currentbuf = vim.fn.bufnr("%")
330 local currentfile = vim.api.nvim_buf_get_name(currentbuf)
331 currentfile = vim.fs.abspath(currentfile)
332 local alternatebuf = vim.fn.bufnr("#")
333 local lbufnr = vim.fn.bufnr("$")
334 local bufnr_len = #(string.format("%d", lbufnr))
335
336 props.findprg = props.findprg or findprg
337 local base_dir = vim.fn.getcwd(0)
338 local lst = utils.make_cmd(props.findprg, {
339 cwd = base_dir,
340 })()
341
342 local recentlst = vim
343 .iter(find_buffers())
344 :map(function(buf)
345 local v = vim.api.nvim_buf_get_name(buf)
346 return { path = vim.fs.abspath(v), buf = buf }
347 end)
348 :totable()
349
350 local items = vim.list.unique(
351 vim
352 .iter({ recentlst, lst })
353 :flatten(1)
354 :map(function(x)
355 if type(x) == "string" then
356 x = { path = x }
357 end
358 return x
359 end)
360 :map(function(x)
361 if x.buf and x.buf == currentbuf then
362 x.current = true
363 elseif x.buf and x.buf == alternatebuf then
364 x.alt = true
365 end
366 return x
367 end)
368 :totable(),
369 function(x)
370 return x.path
371 end
372 )
373
374 return artio.pick(extend({
375 prompt = "smart",
376 items = items,
377 fn = artio.mergesorters(
378 "base",
379 -- use default sorter but only display buffer files if input is empty
380 function(l, input)
381 if #input == 0 then
382 return vim
383 .iter(l)
384 :map(function(item)
385 if not item.v.buf then
386 return
387 end
388 return { item.id, {}, 0 }
389 end)
390 :totable()
391 end
392
393 return artio.sorter(l, input)
394 end,
395 -- boost files that are open as buffers
396 function(l, _)
397 return vim
398 .iter(l)
399 :map(function(item)
400 if not item.v.buf then
401 return
402 end
403 return { item.id, {}, 100 }
404 end)
405 :totable()
406 end,
407 -- boost files that are close in proximity to the current file
408 function(l, _)
409 return vim
410 .iter(l)
411 :map(function(item)
412 return { item.id, {}, matchproximity(currentfile, item.v.path) }
413 end)
414 :totable()
415 end
416 ),
417 on_close = function(v, _)
418 vim.schedule(function()
419 vim.cmd.edit(v.path)
420 end)
421 end,
422 format_item = function(v)
423 local path = vim.fs.relpath(base_dir, v.path) or v.path
424 local ind = v.current and "%" or v.alt and "#" or " "
425 return v.buf and string.format("%s (%0" .. bufnr_len .. "d) %s", ind, v.buf, path)
426 or string.format("%s " .. string.rep(" ", bufnr_len + 2) .. " %s", ind, path)
427 end,
428 hl_item = function(_)
429 return {
430 { { 0, 2 }, "Special" },
431 { { 2, 3 }, "Delimiter" },
432 { { 3, 3 + bufnr_len }, "Number" },
433 { { 3 + bufnr_len, 3 + bufnr_len + 1 }, "NonText" },
434 }
435 end,
436 get_icon = config.get().opts.use_icons and function(item)
437 return require("mini.icons").get("file", item.v.path)
438 end or nil,
439 preview_item = function(v)
440 return { buf = vim.fn.bufadd(v.path) }
441 end,
442 actions = extend(
443 {},
444 utils.make_setqflistactions(function(item)
445 return { filename = item.v.path }
446 end)
447 ),
448 }, props))
449end
450
451builtins.colorschemes = function(props)
452 props = props or {}
453 local files = vim.api.nvim_get_runtime_file("colors/*.{vim,lua}", true)
454 local lst = vim.tbl_map(function(f)
455 return vim.fs.basename(f):gsub("%.[^.]+$", "")
456 end, files)
457
458 local current = vim.g.colors_name
459 local bg = vim.o.background
460
461 return artio.generic(
462 lst,
463 extend({
464 prompt = "colorschemes",
465 on_close = function(text, _)
466 vim.schedule(function()
467 vim.cmd.colorscheme(text)
468 end)
469 end,
470 on_quit = function()
471 -- reset colorscheme
472 vim.schedule(function()
473 if vim.g.colors_name ~= current then
474 vim.cmd.colorscheme(current)
475 end
476 end)
477 end,
478 preview_item = function(item)
479 vim.cmd.colorscheme(item)
480 vim.o.background = bg
481
482 return {}
483 end,
484 }, props)
485 )
486end
487
488builtins.highlights = function(props)
489 props = props or {}
490 local hlout = vim.split(vim.api.nvim_exec2([[ highlight ]], { output = true }).output, "\n", { trimempty = true })
491
492 local maxw = 0
493
494 local hls = vim
495 .iter(hlout)
496 :map(function(hl)
497 local sp = string.find(hl, "%s", 1)
498 maxw = sp > maxw and sp or maxw
499 return { hl:sub(1, sp - 1), hl }
500 end)
501 :fold({}, function(t, hl)
502 local pad = math.max(1, math.min(20, maxw) - #hl[1] + 1)
503 t[hl[1]] = string.gsub(hl[2], "%s+", (" "):rep(pad), 1)
504 return t
505 end)
506
507 return artio.generic(
508 vim.tbl_keys(hls),
509 extend({
510 prompt = "highlights",
511 on_close = function(hlname, _)
512 vim.schedule(function()
513 vim.cmd(("verbose hi %s"):format(hlname))
514 end)
515 end,
516 format_item = function(hlname)
517 return hls[hlname]
518 end,
519 hl_item = function(hlname)
520 local x_start, x_end = string.find(hlname.text, "%sxxx")
521
522 return {
523 { { 0, #hlname.v }, hlname.v },
524 { { x_start, x_end }, hlname.v },
525 }
526 end,
527 }, props)
528 )
529end
530
531---@private
532---@param severity vim.diagnostic.Severity
533---@return string
534local function get_severity_hl(severity)
535 if severity == vim.diagnostic.severity.ERROR then
536 return "DiagnosticError"
537 elseif severity == vim.diagnostic.severity.WARN then
538 return "DiagnosticWarn"
539 elseif severity == vim.diagnostic.severity.INFO then
540 return "DiagnosticInfo"
541 elseif severity == vim.diagnostic.severity.HINT then
542 return "DiagnosticHint"
543 end
544 return ""
545end
546
547---@class artio.picker.diagnostics.Props : artio.Picker.config
548---@field buf? integer defaults to workspace
549
550---@param props? artio.picker.diagnostics.Props
551builtins.diagnostics = function(props)
552 props = props or {}
553 local lst = vim.diagnostic.get(props.buf)
554
555 return artio.generic(
556 lst,
557 extend({
558 prompt = "diagnostics",
559 format_item = function(item)
560 local text = item.message
561 if item.code then
562 text = ("%s [%s]"):format(text, item.code)
563 end
564 return ("%d:%d :: %s"):format(item.end_lnum, item.end_col, text)
565 end,
566 on_close = function(item, _)
567 vim.schedule(function()
568 local win = vim.fn.bufwinid(item.bufnr)
569 if win < 0 then
570 vim.api.nvim_win_set_buf(0, item.bufnr)
571 win = 0
572 end
573 vim.api.nvim_set_current_win(win)
574 vim.api.nvim_win_set_cursor(win, { item.end_lnum + 1, item.end_col })
575 end)
576 end,
577 hl_item = function(item)
578 return {
579 { { 0, #item.text }, get_severity_hl(item.v.severity) },
580 }
581 end,
582 get_icon = function(item)
583 if item.v.severity == vim.diagnostic.severity.ERROR then
584 return "E", get_severity_hl(item.v.severity)
585 elseif item.v.severity == vim.diagnostic.severity.WARN then
586 return "W", get_severity_hl(item.v.severity)
587 elseif item.v.severity == vim.diagnostic.severity.INFO then
588 return "I", get_severity_hl(item.v.severity)
589 elseif item.v.severity == vim.diagnostic.severity.HINT then
590 return "H", get_severity_hl(item.v.severity)
591 end
592 return " "
593 end,
594 }, props)
595 )
596end
597
598---@param props? artio.picker.diagnostics.Props
599builtins.diagnostics_buffer = function(props)
600 props = props or {}
601 props.buf = props.buf or vim.api.nvim_get_current_buf()
602 return builtins.diagnostics(props)
603end
604
605---@class artio.picker.keymaps.Props : artio.Picker.config
606---@field modes? string[] defaults to all
607
608---@param props? artio.picker.keymaps.Props
609builtins.keymaps = function(props)
610 props = props or {}
611 props.modes = props.modes or { "n", "i", "c", "v", "x", "s", "o", "t", "l" }
612
613 ---@type vim.api.keyset.get_keymap[]
614 local lst = vim.iter(props.modes):fold({}, function(keymaps, mode)
615 vim.iter(vim.api.nvim_get_keymap(mode)):each(function(km)
616 keymaps[#keymaps + 1] = km
617 end)
618 return keymaps
619 end)
620
621 return artio.generic(
622 lst,
623 extend({
624 prompt = "keymaps",
625 format_item = function(km)
626 return ("%s %s %s | %s"):format(km.mode, km.lhs, km.rhs, km.desc)
627 end,
628 ---@param km vim.api.keyset.get_keymap
629 on_close = function(km, _)
630 vim.schedule(function()
631 local out = vim.api.nvim_cmd({
632 cmd = ("%smap"):format(km.mode),
633 args = { km.lhs },
634 }, {
635 output = true,
636 })
637 vim.print(out)
638 end)
639 end,
640 }, props)
641 )
642end
643
644builtins.commands = function(props)
645 props = props or {}
646 local lst = vim.api.nvim_get_commands({})
647
648 return artio.generic(
649 vim.tbl_values(lst),
650 extend({
651 prompt = "commands",
652 ---@param item vim.api.keyset.command_info
653 format_item = function(item)
654 return item.name
655 end,
656 ---@param cmd vim.api.keyset.command_info
657 on_close = function(cmd, _)
658 local nargs = vim.F.npcall(tonumber, cmd.nargs)
659 local fmt = (nargs and nargs > 0) and ":%s " or ":%s"
660
661 artio.schedule(function()
662 vim.api.nvim_feedkeys(string.format(fmt, cmd.name), "n", false)
663 end)
664 end,
665 hl_item = function(item)
666 return {
667 { { 0, #item.v.name }, "@function.macro.vim" },
668 }
669 end,
670 }, props)
671 )
672end
673
674builtins.quickfix = function(props)
675 props = props or {}
676
677 local qfid = vim.fn.getqflist({ id = 0 }).id
678 local qflist = vim.fn.getqflist({ id = qfid, items = 0 }).items
679
680 return artio.generic(
681 vim
682 .iter(ipairs(qflist))
683 :map(function(_, item)
684 item.name = vim.fn.bufname(item.bufnr)
685 return item
686 end)
687 :totable(),
688 extend({
689 prompt = "quickfix",
690 on_close = function(_, idx)
691 artio.schedule(function()
692 vim.cmd([[copen]])
693 local win = vim.fn.getqflist({ id = qfid, winid = 0 }).winid
694 vim.api.nvim_win_set_cursor(win, { idx, 0 })
695 end)
696 end,
697 format_item = function(item)
698 return string.format("%s:%d:%d:%s", item.name, item.lnum, item.col, item.text)
699 end,
700 preview_item = function(item)
701 return { buf = item.bufnr, pos = { item.lnum, item.col }, pos_end = { item.end_lnum, item.end_col } }
702 end,
703 get_icon = config.get().opts.use_icons and function(item)
704 return require("mini.icons").get("file", item.v.name)
705 end or nil,
706 hl_item = utils.hl_qfitem,
707 actions = extend(
708 {},
709 utils.make_setqflistactions(function(item)
710 return item.v
711 end)
712 ),
713 }, props)
714 )
715end
716
717builtins.document_symbols = function(props)
718 props = props or {}
719
720 local buf = vim.api.nvim_get_current_buf()
721 local win = vim.api.nvim_get_current_win()
722
723 vim.lsp.buf.document_symbol({
724 on_list = function(what)
725 local lst = what.items
726
727 return artio.select(lst, {
728 prompt = "document_symbols",
729 format_item = function(v)
730 return v.text
731 end,
732 preview_item = function(v)
733 return {
734 buf = buf,
735 pos = { v.lnum, v.col },
736 pos_end = { v.end_lnum, v.end_col },
737 }
738 end,
739 }, function(v, _)
740 if not v then
741 return
742 end
743
744 vim.api.nvim_win_set_cursor(win, { v.lnum, v.col })
745 end, props)
746 end,
747 })
748end
749
750return builtins