minimal extui fuzzy finder for neovim

refactor: rename _extui => _core.ui2

https://github.com/neovim/neovim/pull/37692

fix: https://github.com/comfysage/artio.nvim/issues/13

+56 -57
+1 -1
.nvim.lua
··· 5 6 vim.cmd([[ set rtp^=. ]]) 7 8 - require('vim._extui').enable({}) 9 10 vim.api.nvim_create_autocmd("UIEnter", { 11 callback = function()
··· 5 6 vim.cmd([[ set rtp^=. ]]) 7 8 + require("vim._core.ui2").enable({}) 9 10 vim.api.nvim_create_autocmd("UIEnter", { 11 callback = function()
+5 -5
README.md
··· 1 # artio.nvim 2 3 - A minimal, nature-infused file picker for Neovim using the new extui window. 4 Inspired by forest spirits and the calm intuition of hunting, Artio helps you gently select files without the weight of heavy fuzzy-finder dependencies. 5 6 ![preview](./assets/preview.png) ··· 9 10 Requires Neovim `>= 0.12` 11 12 - - Lightweight picker window built on Neovim's extui 13 - Prompt + list UI components - minimal and focused 14 - Fuzzy filtering using matchfuzzy (built-in) 15 - Icon support for common filetypes through [mini.icons](https://github.com/echasnovski/mini.nvim) _(optional)_ 16 - No heavy dependencies - pure Lua 17 18 - ### extui 19 20 - artio requires the extui to be enabled. 21 22 an example of how to set this up is: 23 24 ```lua 25 - require("vim._extui").enable({ enable = true, msg = { 26 target = "msg", 27 } }) 28 ```
··· 1 # artio.nvim 2 3 + A minimal, nature-infused file picker for Neovim using ui2. 4 Inspired by forest spirits and the calm intuition of hunting, Artio helps you gently select files without the weight of heavy fuzzy-finder dependencies. 5 6 ![preview](./assets/preview.png) ··· 9 10 Requires Neovim `>= 0.12` 11 12 + - Lightweight picker window built on Neovim's ui2 13 - Prompt + list UI components - minimal and focused 14 - Fuzzy filtering using matchfuzzy (built-in) 15 - Icon support for common filetypes through [mini.icons](https://github.com/echasnovski/mini.nvim) _(optional)_ 16 - No heavy dependencies - pure Lua 17 18 + ### ui2 19 20 + artio requires ui2 to be enabled. 21 22 an example of how to set this up is: 23 24 ```lua 25 + require("vim._core.ui2").enable({ enable = true, msg = { 26 target = "msg", 27 } }) 28 ```
+5 -6
doc/artio.txt
··· 1 - *artio.txt* a minimal, nature-infused file picker for neovim using the new 2 - extui window 3 4 ============================================================================== 5 ··· 7 8 Requires Neovim `>= 0.12` 9 10 - - Lightweight picker window built on Neovim's extui 11 - Prompt + list UI components - minimal and focused 12 - Fuzzy filtering using matchfuzzy (built-in) 13 - Optional icon support for common filetypes through mini.icons 14 (https://github.com/echasnovski/mini.nvim) 15 - No heavy dependencies - pure Lua 16 17 - EXTUI 18 19 - artio requires the extui to be enabled. 20 21 an example of how to set this up is: 22 23 >lua 24 - require("vim._extui").enable({ enable = true, msg = { 25 target = "msg", 26 } }) 27 <
··· 1 + *artio.txt* a minimal, nature-infused file picker for neovim using ui2 2 3 ============================================================================== 4 ··· 6 7 Requires Neovim `>= 0.12` 8 9 + - Lightweight picker window built on Neovim's ui2 10 - Prompt + list UI components - minimal and focused 11 - Fuzzy filtering using matchfuzzy (built-in) 12 - Optional icon support for common filetypes through mini.icons 13 (https://github.com/echasnovski/mini.nvim) 14 - No heavy dependencies - pure Lua 15 16 + UI2 17 18 + artio requires ui2 to be enabled. 19 20 an example of how to set this up is: 21 22 >lua 23 + require("vim._core.ui2").enable({ enable = true, msg = { 24 target = "msg", 25 } }) 26 <
+3 -3
lua/artio/builtins.lua
··· 90 props.grepprg = props.grepprg or vim.o.grepprg 91 92 local base_dir = vim.fn.getcwd(0) 93 - local ext = require("vim._extui.shared") 94 local grepcmd = utils.make_cmd(props.grepprg, { 95 cwd = base_dir, 96 }) ··· 105 106 local lines = grepcmd(input) 107 108 - vim.fn.setloclist(ext.wins.cmd, {}, " ", { 109 title = "grep[" .. input .. "]", 110 lines = lines, 111 efm = vim.o.grepformat, ··· 113 }) 114 115 return vim 116 - .iter(ipairs(vim.fn.getloclist(ext.wins.cmd))) 117 :map(function(i, locitem) 118 local name = vim.fs.abspath(vim.fn.bufname(locitem.bufnr)) 119 return {
··· 90 props.grepprg = props.grepprg or vim.o.grepprg 91 92 local base_dir = vim.fn.getcwd(0) 93 + local ui2 = require("vim._core.ui2") 94 local grepcmd = utils.make_cmd(props.grepprg, { 95 cwd = base_dir, 96 }) ··· 105 106 local lines = grepcmd(input) 107 108 + vim.fn.setloclist(ui2.wins.cmd, {}, " ", { 109 title = "grep[" .. input .. "]", 110 lines = lines, 111 efm = vim.o.grepformat, ··· 113 }) 114 115 return vim 116 + .iter(ipairs(vim.fn.getloclist(ui2.wins.cmd))) 117 :map(function(i, locitem) 118 local name = vim.fs.abspath(vim.fn.bufname(locitem.bufnr)) 119 return {
+2 -2
lua/artio/health.lua
··· 7 vim.health.error("artio.nvim not loaded") 8 end 9 10 - if not vim.tbl_get(require("vim._extui.shared") or {}, "cfg", "enable") then 11 - vim.health.error("extui not enabled") 12 end 13 14 if _G["MiniIcons"] then
··· 7 vim.health.error("artio.nvim not loaded") 8 end 9 10 + if not vim.tbl_get(require("vim._core.ui2") or {}, "cfg", "enable") then 11 + vim.health.error("ui2 not enabled") 12 end 13 14 if _G["MiniIcons"] then
+5 -5
lua/artio/picker.lua
··· 169 end 170 171 function Picker:initkeymaps() 172 - local ext = require("vim._extui.shared") 173 174 ---@type vim.keymap.set.Opts 175 - local opts = { buffer = ext.bufs.cmd } 176 177 if self.actions then 178 vim.iter(pairs(self.actions)):each(function(k, v) ··· 187 end 188 189 function Picker:delkeymaps() 190 - local ext = require("vim._extui.shared") 191 192 - local keymaps = vim.api.nvim_buf_get_keymap(ext.bufs.cmd, "i") 193 194 vim.iter(ipairs(keymaps)):each(function(_, v) 195 if v.lhs:match("^<Plug>(artio-action-") or (v.rhs and v.rhs:match("^<Plug>(artio-action-")) then 196 - vim.api.nvim_buf_del_keymap(ext.bufs.cmd, "i", v.lhs) 197 end 198 end) 199 end
··· 169 end 170 171 function Picker:initkeymaps() 172 + local ui2 = require("vim._core.ui2") 173 174 ---@type vim.keymap.set.Opts 175 + local opts = { buffer = ui2.bufs.cmd } 176 177 if self.actions then 178 vim.iter(pairs(self.actions)):each(function(k, v) ··· 187 end 188 189 function Picker:delkeymaps() 190 + local ui2 = require("vim._core.ui2") 191 192 + local keymaps = vim.api.nvim_buf_get_keymap(ui2.bufs.cmd, "i") 193 194 vim.iter(ipairs(keymaps)):each(function(_, v) 195 if v.lhs:match("^<Plug>(artio-action-") or (v.rhs and v.rhs:match("^<Plug>(artio-action-")) then 196 + vim.api.nvim_buf_del_keymap(ui2.bufs.cmd, "i", v.lhs) 197 end 198 end) 199 end
+35 -35
lua/artio/view.lua
··· 1 - local cmdline = require("vim._extui.cmdline") 2 - local ext = require("vim._extui.shared") 3 4 local _log = {} 5 local _loglevel = vim.log.levels.ERROR ··· 38 ---@param hide boolean Whether to hide or show the window. 39 ---@param height integer (Text)height of the cmdline window. 40 local function win_config(win, hide, height) 41 - if ext.cmdheight == 0 and vim.api.nvim_win_get_config(win).hide ~= hide then 42 vim.api.nvim_win_set_config(win, { hide = hide, height = not hide and height or nil }) 43 elseif vim.api.nvim_win_get_height(win) ~= height then 44 vim.api.nvim_win_set_height(win, height) ··· 49 vim._with({ noautocmd = true, o = { splitkeep = "screen" } }, function() 50 vim.o.cmdheight = height 51 end) 52 - ext.msg.set_pos() 53 end 54 end 55 ··· 92 --- gets updated after changedtick event 93 local last_draw_tick = 0 94 local function get_changedtick() 95 - return vim.api.nvim_buf_get_changedtick(ext.bufs.cmd) 96 end 97 98 local cmdbuff = "" ---@type string Stored cmdline used to calculate translation offset. ··· 119 120 self:promptpos() 121 self:setlines(promptidx, promptidx + 1, lines) 122 - vim.fn.prompt_setprompt(ext.bufs.cmd, promptstr) 123 vim.schedule(function() 124 - local ok, result = pcall(vim.api.nvim_buf_set_mark, ext.bufs.cmd, ":", promptidx + 1, promptlen, {}) 125 if not ok then 126 logerror(("Failed to set mark %d:%d\n\t%s"):format(promptidx, promptlen, result)) 127 return ··· 141 function View:show(content, pos, firstc, prompt, indent, level, hl_id) 142 cmdline.level, cmdline.indent = level, indent 143 if cmdline.highlighter and cmdline.highlighter.active then 144 - cmdline.highlighter.active[ext.bufs.cmd] = nil 145 end 146 - if ext.msg.cmd.msg_row ~= -1 then 147 - ext.msg.msg_clear() 148 end 149 - ext.msg.virt.last = { {}, {}, {}, {} } 150 151 self:clear() 152 prompthl_id = hl_id ··· 169 170 ---@param predicted? integer The predicted height of the cmdline window 171 function View:updatewinheight(predicted) 172 - local height = math.max(1, predicted or vim.api.nvim_win_text_height(ext.wins.cmd, {}).all) 173 height = math.min(height, self.win.height) 174 - win_config(ext.wins.cmd, false, height) 175 end 176 177 function View:saveview() ··· 211 self.opts[level] = self.opts[level] or {} 212 local props = { 213 scope = level == "g" and "global" or "local", 214 - buf = level == "buf" and ext.bufs.cmd or nil, 215 - win = level == "win" and ext.wins.cmd or nil, 216 } 217 218 for name, value in pairs(o) do ··· 248 _log = nil 249 _log = {} 250 251 - ext.check_targets() 252 253 vim.schedule(function() 254 self.augroup = vim.api.nvim_create_augroup("artio:group", { clear = true }) ··· 286 287 vim.api.nvim_create_autocmd("TextChangedI", { 288 group = self.augroup, 289 - buffer = ext.bufs.cmd, 290 callback = function() 291 self:update() 292 end, ··· 294 295 vim.api.nvim_create_autocmd("CursorMovedI", { 296 group = self.augroup, 297 - buffer = ext.bufs.cmd, 298 callback = function() 299 self:updatecursor() 300 end, ··· 312 self:trigger_show() 313 314 vim._with({ noautocmd = true }, function() 315 - vim.api.nvim_set_current_win(ext.wins.cmd) 316 end) 317 318 self:setopts() ··· 325 326 -- trigger after registering events 327 vim.schedule(function() 328 - vim._with({ win = ext.wins.cmd, wo = { eventignorewin = "" } }, function() 329 vim.api.nvim_exec_autocmds("WinEnter", {}) 330 end) 331 end) ··· 338 self:closepreview() 339 vim.schedule(function() 340 pcall(vim.api.nvim_del_augroup_by_id, self.augroup) 341 - pcall(vim.api.nvim_buf_detach, ext.bufs.cmd) 342 343 vim.cmd.stopinsert() 344 ··· 364 end 365 366 function View:hide() 367 - vim.fn.clearmatches(ext.wins.cmd) -- Clear matchparen highlights. 368 - vim.api.nvim_win_set_cursor(ext.wins.cmd, { 1, 0 }) 369 - vim.api.nvim_buf_set_lines(ext.bufs.cmd, 0, -1, false, {}) 370 371 local clear = vim.schedule_wrap(function(was_prompt) 372 -- Avoid clearing prompt window when it is re-entered before the next event 373 -- loop iteration. E.g. when a non-choice confirm button is pressed. 374 if was_prompt and not cmdline.prompt then 375 pcall(function() 376 - vim.api.nvim_buf_set_lines(ext.bufs.cmd, 0, -1, false, {}) 377 - vim.api.nvim_buf_set_lines(ext.bufs.dialog, 0, -1, false, {}) 378 - vim.api.nvim_win_set_config(ext.wins.dialog, { hide = true }) 379 - vim.on_key(nil, ext.msg.dialog_on_key) 380 end) 381 end 382 -- Messages emitted as a result of a typed command are treated specially: ··· 389 clear(cmdline.prompt) 390 391 cmdline.prompt, cmdline.level = false, 0 392 - win_config(ext.wins.cmd, true, ext.cmdheight) 393 end 394 395 function View:trigger_show() ··· 441 self:promptpos() 442 443 if not pos or pos < 0 then 444 - local cursorpos = vim.api.nvim_win_get_cursor(ext.wins.cmd) 445 pos = cursorpos[2] - promptlen 446 end 447 ··· 459 curpos[1], curpos[2] = promptidx + 1, promptlen + pos 460 461 vim._with({ noautocmd = true }, function() 462 - local ok, _ = pcall(vim.api.nvim_win_set_cursor, ext.wins.cmd, curpos) 463 if not ok then 464 logerror(("Failed to set cursor %d:%d"):format(curpos[1], curpos[2])) 465 end ··· 482 -- update winheight to prevent wrong scroll when increasing from 1 483 local diff = #lines - (posend - posstart) 484 if diff ~= 0 then 485 - local height = vim.api.nvim_win_text_height(ext.wins.cmd, {}).all 486 local predicted = height + diff 487 self:updatewinheight(predicted) 488 end 489 490 before_draw_tick = get_changedtick() 491 - vim.api.nvim_buf_set_lines(ext.bufs.cmd, posstart, posend, false, lines) 492 last_draw_tick = get_changedtick() 493 end 494 ··· 511 function View:mark(id, line, col, opts) 512 if id and self.marks[id] then 513 vim._with({ noautocmd = true }, function() 514 - vim.api.nvim_buf_del_extmark(ext.bufs.cmd, view_ns, self.marks[id]) 515 end) 516 self.marks[id] = nil 517 end ··· 521 522 local ok, result 523 vim._with({ noautocmd = true }, function() 524 - ok, result = pcall(vim.api.nvim_buf_set_extmark, ext.bufs.cmd, view_ns, line, col, opts) 525 end) 526 if not ok then 527 logerror(("Failed to add extmark %d:%d\n\t%s"):format(line, col, result))
··· 1 + local cmdline = require("vim._core.ui2.cmdline") 2 + local ui2 = require("vim._core.ui2") 3 4 local _log = {} 5 local _loglevel = vim.log.levels.ERROR ··· 38 ---@param hide boolean Whether to hide or show the window. 39 ---@param height integer (Text)height of the cmdline window. 40 local function win_config(win, hide, height) 41 + if ui2.cmdheight == 0 and vim.api.nvim_win_get_config(win).hide ~= hide then 42 vim.api.nvim_win_set_config(win, { hide = hide, height = not hide and height or nil }) 43 elseif vim.api.nvim_win_get_height(win) ~= height then 44 vim.api.nvim_win_set_height(win, height) ··· 49 vim._with({ noautocmd = true, o = { splitkeep = "screen" } }, function() 50 vim.o.cmdheight = height 51 end) 52 + ui2.msg.set_pos() 53 end 54 end 55 ··· 92 --- gets updated after changedtick event 93 local last_draw_tick = 0 94 local function get_changedtick() 95 + return vim.api.nvim_buf_get_changedtick(ui2.bufs.cmd) 96 end 97 98 local cmdbuff = "" ---@type string Stored cmdline used to calculate translation offset. ··· 119 120 self:promptpos() 121 self:setlines(promptidx, promptidx + 1, lines) 122 + vim.fn.prompt_setprompt(ui2.bufs.cmd, promptstr) 123 vim.schedule(function() 124 + local ok, result = pcall(vim.api.nvim_buf_set_mark, ui2.bufs.cmd, ":", promptidx + 1, promptlen, {}) 125 if not ok then 126 logerror(("Failed to set mark %d:%d\n\t%s"):format(promptidx, promptlen, result)) 127 return ··· 141 function View:show(content, pos, firstc, prompt, indent, level, hl_id) 142 cmdline.level, cmdline.indent = level, indent 143 if cmdline.highlighter and cmdline.highlighter.active then 144 + cmdline.highlighter.active[ui2.bufs.cmd] = nil 145 end 146 + if ui2.msg.cmd.msg_row ~= -1 then 147 + ui2.msg.msg_clear() 148 end 149 + ui2.msg.virt.last = { {}, {}, {}, {} } 150 151 self:clear() 152 prompthl_id = hl_id ··· 169 170 ---@param predicted? integer The predicted height of the cmdline window 171 function View:updatewinheight(predicted) 172 + local height = math.max(1, predicted or vim.api.nvim_win_text_height(ui2.wins.cmd, {}).all) 173 height = math.min(height, self.win.height) 174 + win_config(ui2.wins.cmd, false, height) 175 end 176 177 function View:saveview() ··· 211 self.opts[level] = self.opts[level] or {} 212 local props = { 213 scope = level == "g" and "global" or "local", 214 + buf = level == "buf" and ui2.bufs.cmd or nil, 215 + win = level == "win" and ui2.wins.cmd or nil, 216 } 217 218 for name, value in pairs(o) do ··· 248 _log = nil 249 _log = {} 250 251 + ui2.check_targets() 252 253 vim.schedule(function() 254 self.augroup = vim.api.nvim_create_augroup("artio:group", { clear = true }) ··· 286 287 vim.api.nvim_create_autocmd("TextChangedI", { 288 group = self.augroup, 289 + buffer = ui2.bufs.cmd, 290 callback = function() 291 self:update() 292 end, ··· 294 295 vim.api.nvim_create_autocmd("CursorMovedI", { 296 group = self.augroup, 297 + buffer = ui2.bufs.cmd, 298 callback = function() 299 self:updatecursor() 300 end, ··· 312 self:trigger_show() 313 314 vim._with({ noautocmd = true }, function() 315 + vim.api.nvim_set_current_win(ui2.wins.cmd) 316 end) 317 318 self:setopts() ··· 325 326 -- trigger after registering events 327 vim.schedule(function() 328 + vim._with({ win = ui2.wins.cmd, wo = { eventignorewin = "" } }, function() 329 vim.api.nvim_exec_autocmds("WinEnter", {}) 330 end) 331 end) ··· 338 self:closepreview() 339 vim.schedule(function() 340 pcall(vim.api.nvim_del_augroup_by_id, self.augroup) 341 + pcall(vim.api.nvim_buf_detach, ui2.bufs.cmd) 342 343 vim.cmd.stopinsert() 344 ··· 364 end 365 366 function View:hide() 367 + vim.fn.clearmatches(ui2.wins.cmd) -- Clear matchparen highlights. 368 + vim.api.nvim_win_set_cursor(ui2.wins.cmd, { 1, 0 }) 369 + vim.api.nvim_buf_set_lines(ui2.bufs.cmd, 0, -1, false, {}) 370 371 local clear = vim.schedule_wrap(function(was_prompt) 372 -- Avoid clearing prompt window when it is re-entered before the next event 373 -- loop iteration. E.g. when a non-choice confirm button is pressed. 374 if was_prompt and not cmdline.prompt then 375 pcall(function() 376 + vim.api.nvim_buf_set_lines(ui2.bufs.cmd, 0, -1, false, {}) 377 + vim.api.nvim_buf_set_lines(ui2.bufs.dialog, 0, -1, false, {}) 378 + vim.api.nvim_win_set_config(ui2.wins.dialog, { hide = true }) 379 + vim.on_key(nil, ui2.msg.dialog_on_key) 380 end) 381 end 382 -- Messages emitted as a result of a typed command are treated specially: ··· 389 clear(cmdline.prompt) 390 391 cmdline.prompt, cmdline.level = false, 0 392 + win_config(ui2.wins.cmd, true, ui2.cmdheight) 393 end 394 395 function View:trigger_show() ··· 441 self:promptpos() 442 443 if not pos or pos < 0 then 444 + local cursorpos = vim.api.nvim_win_get_cursor(ui2.wins.cmd) 445 pos = cursorpos[2] - promptlen 446 end 447 ··· 459 curpos[1], curpos[2] = promptidx + 1, promptlen + pos 460 461 vim._with({ noautocmd = true }, function() 462 + local ok, _ = pcall(vim.api.nvim_win_set_cursor, ui2.wins.cmd, curpos) 463 if not ok then 464 logerror(("Failed to set cursor %d:%d"):format(curpos[1], curpos[2])) 465 end ··· 482 -- update winheight to prevent wrong scroll when increasing from 1 483 local diff = #lines - (posend - posstart) 484 if diff ~= 0 then 485 + local height = vim.api.nvim_win_text_height(ui2.wins.cmd, {}).all 486 local predicted = height + diff 487 self:updatewinheight(predicted) 488 end 489 490 before_draw_tick = get_changedtick() 491 + vim.api.nvim_buf_set_lines(ui2.bufs.cmd, posstart, posend, false, lines) 492 last_draw_tick = get_changedtick() 493 end 494 ··· 511 function View:mark(id, line, col, opts) 512 if id and self.marks[id] then 513 vim._with({ noautocmd = true }, function() 514 + vim.api.nvim_buf_del_extmark(ui2.bufs.cmd, view_ns, self.marks[id]) 515 end) 516 self.marks[id] = nil 517 end ··· 521 522 local ok, result 523 vim._with({ noautocmd = true }, function() 524 + ok, result = pcall(vim.api.nvim_buf_set_extmark, ui2.bufs.cmd, view_ns, line, col, opts) 525 end) 526 if not ok then 527 logerror(("Failed to add extmark %d:%d\n\t%s"):format(line, col, result))