minimal extui fuzzy finder for neovim
at 8763f8aac5d2aa435f5dccb3c56bac81557df31e 408 lines 10 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 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