minimal extui fuzzy finder for neovim
at main 690 lines 18 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 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