minimal extui fuzzy finder for neovim
at 8763f8aac5d2aa435f5dccb3c56bac81557df31e 198 lines 5.1 kB view raw
1local View = require("artio.view") 2 3---@alias artio.Picker.item { id: integer, v: any, text: string, icon?: string, icon_hl?: string, hls?: artio.Picker.hl[] } 4---@alias artio.Picker.match [integer, integer[], integer] [item, pos[], score] 5---@alias artio.Picker.sorter fun(lst: artio.Picker.item[], input: string): artio.Picker.match[] 6---@alias artio.Picker.hl [[integer, integer], string] 7---@alias artio.Picker.action fun(self: artio.Picker) 8 9---@class artio.Picker.config 10---@field items artio.Picker.item[]|string[] 11---@field fn artio.Picker.sorter 12---@field on_close fun(text: string, idx: integer) 13---@field get_items? fun(input: string): artio.Picker.item[] 14---@field format_item? fun(item: any): string 15---@field preview_item? fun(item: any): integer, fun(win: integer) 16---@field get_icon? fun(item: artio.Picker.item): string, string 17---@field hl_item? fun(item: artio.Picker.item): artio.Picker.hl[] 18---@field prompt? string 19---@field defaulttext? string 20---@field prompttext? string 21---@field opts? artio.config.opts 22---@field win? artio.config.win 23---@field actions? table<string, artio.Picker.action> 24---@field mappings? table<string, 'up'|'down'|'accept'|'cancel'|'togglepreview'|string> 25 26---@class artio.Picker : artio.Picker.config 27---@field co thread|nil 28---@field idx integer 1-indexed 29---@field matches artio.Picker.match[] 30---@field marked table<integer, true|nil> 31local Picker = {} 32Picker.__index = Picker 33Picker.active_picker = nil 34 35---@param props artio.Picker.config 36function Picker:new(props) 37 vim.validate("Picker.items", props.items, "table") 38 vim.validate("Picker:fn", props.fn, "function") 39 vim.validate("Picker:on_close", props.on_close, "function") 40 41 local t = vim.tbl_deep_extend("force", { 42 closed = false, 43 prompt = "", 44 idx = 0, 45 items = {}, 46 matches = {}, 47 marked = {}, 48 }, require("artio.config").get(), props) 49 50 if not t.prompttext then 51 t.prompttext = t.opts.prompt_title and ("%s %s"):format(t.prompt, t.opts.promptprefix) or t.opts.promptprefix 52 end 53 54 Picker.getitems(t, "") 55 56 return setmetatable(t, Picker) 57end 58 59local action_enum = { 60 accept = 0, 61 cancel = 1, 62} 63 64function Picker:open() 65 if Picker.active_picker then 66 Picker.active_picker:close() 67 end 68 Picker.active_picker = self 69 70 self.view = View:new(self) 71 72 coroutine.wrap(function() 73 self.view:open() 74 75 self:initkeymaps() 76 77 local co, ismain = coroutine.running() 78 assert(not ismain, "must be called from a coroutine") 79 self.co = co 80 81 local result = coroutine.yield() 82 83 self:close() 84 85 if result == action_enum.cancel or result ~= action_enum.accept then 86 return 87 end 88 89 local current = self.matches[self.idx] and self.matches[self.idx][1] 90 if not current then 91 return 92 end 93 94 local item = self.items[current] 95 if item then 96 self.on_close(item.v, item.id) 97 end 98 end)() 99end 100 101function Picker:close() 102 if self.closed then 103 return 104 end 105 106 if self.view then 107 self.view:close() 108 end 109 110 self:delkeymaps() 111 112 self.closed = true 113end 114 115function Picker:initkeymaps() 116 local ext = require("vim._extui.shared") 117 118 ---@type vim.keymap.set.Opts 119 local opts = { buffer = ext.bufs.cmd } 120 121 if self.actions then 122 vim.iter(pairs(self.actions)):each(function(k, v) 123 vim.keymap.set("i", ("<Plug>(artio-action-%s)"):format(k), v, opts) 124 end) 125 end 126 if self.mappings then 127 vim.iter(pairs(self.mappings)):each(function(k, v) 128 vim.keymap.set("i", k, ("<Plug>(artio-action-%s)"):format(v), opts) 129 end) 130 end 131end 132 133function Picker:delkeymaps() 134 local ext = require("vim._extui.shared") 135 136 local keymaps = vim.api.nvim_buf_get_keymap(ext.bufs.cmd, "i") 137 138 vim.iter(ipairs(keymaps)):each(function(_, v) 139 if v.lhs:match("^<Plug>(artio-action-") or (v.rhs and v.rhs:match("^<Plug>(artio-action-")) then 140 vim.api.nvim_buf_del_keymap(ext.bufs.cmd, "i", v.lhs) 141 end 142 end) 143end 144 145function Picker:fix() 146 self.idx = math.max(self.idx, self.opts.preselect and 1 or 0) 147 self.idx = math.min(self.idx, #self.matches) 148end 149 150function Picker:getitems(input) 151 self.items = self.get_items and self.get_items(input) or self.items 152 if 153 #self.items > 0 154 and (type(self.items[1]) ~= "table" or not (self.items[1].v and self.items[1].id and self.items[1].text)) 155 then 156 self.items = vim 157 .iter(ipairs(self.items)) 158 :map(function(i, v) 159 local text 160 if self.format_item and vim.is_callable(self.format_item) then 161 text = self.format_item(v) 162 end 163 164 return { 165 id = i, 166 v = v, 167 text = text or v, 168 } 169 end) 170 :totable() 171 end 172end 173 174function Picker:getmatches(input) 175 self:getitems(input) 176 self.matches = self.fn(self.items, input) 177 table.sort(self.matches, function(a, b) 178 return a[3] > b[3] 179 end) 180end 181 182---@param idx integer 183---@param yes? boolean 184function Picker:mark(idx, yes) 185 self.marked[idx] = yes == nil and true or yes 186end 187 188---@return integer[] 189function Picker:getmarked() 190 return vim 191 .iter(pairs(self.marked)) 192 :map(function(k, v) 193 return v and k or nil 194 end) 195 :totable() 196end 197 198return Picker