馃惢 minimal ui2 fuzzy finder for Neovim codeberg.org/comfysage/artio.nvim
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 315 lines 7.4 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.matches table<integer, artio.Picker.match> id: match 6---@alias artio.Picker.sorter fun(lst: artio.Picker.item[], input: string): artio.Picker.matches 7---@alias artio.Picker.hl [[integer, integer], string] 8---@alias artio.Picker.action fun(self: artio.Picker) 9 10---@class artio.Picker.config 11---@field items artio.Picker.item[]|string[] 12---@field fn artio.Picker.sorter 13---@field on_close fun(text: string, idx: integer) 14---@field get_items? fun(input: string): artio.Picker.item[] 15---@field format_item? fun(item: any): string 16---@field preview_item? fun(item: any):{buf?:integer, pos?:[integer,integer], pos_end?:[integer,integer]} 17---@field get_icon? fun(item: artio.Picker.item): string, string 18---@field hl_item? fun(item: artio.Picker.item): artio.Picker.hl[] 19---@field on_quit? fun() 20---@field live? boolean 21---@field prompt? string 22---@field defaulttext? string 23---@field prompttext? string 24---@field opts? artio.config.opts 25---@field win? artio.config.win 26---@field actions? table<string, artio.Picker.action> 27---@field mappings? table<string, 'up'|'down'|'accept'|'cancel'|'togglepreview'|string> 28 29---@class artio.Picker : artio.Picker.config 30---@field co thread? 31---@field thread function 32---@field input string 33---@field liveinput? string 34---@field idx integer 1-indexed 35---@field matches artio.Picker.match[] 36---@field marked table<integer, true|nil> 37---@field live boolean 38local Picker = {} 39Picker.__index = Picker 40Picker.active_picker = nil 41 42---@param props artio.Picker.config 43function Picker:new(props) 44 vim.validate("Picker.items", props.items, "table") 45 vim.validate("Picker:fn", props.fn, "function") 46 vim.validate("Picker:on_close", props.on_close, "function") 47 48 local t = vim.tbl_deep_extend("force", { 49 closed = false, 50 prompt = "", 51 input = nil, 52 liveinput = nil, 53 idx = 0, 54 items = {}, 55 matches = {}, 56 marked = {}, 57 }, require("artio.config").get(), props) 58 59 if not t.prompttext then 60 t.prompttext = t.opts.prompt_title and ("%s %s"):format(t.prompt, t.opts.promptprefix) or t.opts.promptprefix 61 end 62 63 t.live = vim.F.if_nil(t.live, t.get_items ~= nil) 64 65 if t.live then 66 t.input = "" 67 t.liveinput = t.defaulttext or "" 68 else 69 t.input = t.defaulttext or "" 70 t.liveinput = "" 71 end 72 73 Picker.getitems(t, "") 74 75 return setmetatable(t, Picker) 76end 77 78---@enum artio.Picker.Action 79local action_enum = { 80 accept = 0, 81 cancel = 1, 82} 83 84function Picker:open() 85 if Picker.active_picker and Picker.active_picker ~= self then 86 Picker.active_picker:close(true) 87 end 88 Picker.active_picker = self 89 90 self.view = View:new(self) 91 92 self.thread = coroutine.wrap(function() 93 self.view:open() 94 95 self:initkeymaps() 96 97 self.co = coroutine.running() 98 99 vim.api.nvim_exec_autocmds("User", { pattern = "ArtioEnter" }) 100 101 local result = coroutine.yield() 102 103 self:close() 104 105 while true do 106 if result == action_enum.cancel or result ~= action_enum.accept then 107 if self.on_quit then 108 self.on_quit() 109 end 110 break 111 end 112 113 local current = self.matches[self.idx] and self.matches[self.idx][1] 114 if not current then 115 break 116 end 117 118 local item = self.items[current] 119 if item then 120 self.on_close(item.v, item.id) 121 end 122 123 break 124 end 125 126 vim.api.nvim_exec_autocmds("User", { pattern = "ArtioLeave" }) 127 end) 128 self.thread() 129end 130 131function Picker:resume() 132 if not self.closed then 133 return 134 end 135 self.closed = false 136 137 self:open() 138end 139 140---@param free? boolean 141function Picker:close(free) 142 if self.closed then 143 return 144 end 145 146 if self.view then 147 self.view:close() 148 end 149 150 self:delkeymaps() 151 152 self.closed = true 153 154 if free then 155 self:free() 156 end 157end 158 159function Picker:free() 160 if self == nil then 161 return 162 end 163 self.items = nil 164 self.matches = nil 165 self.marked = nil 166 self = nil 167 vim.schedule(function() 168 collectgarbage("collect") 169 end) 170end 171 172function Picker:initkeymaps() 173 local ui2 = require("vim._core.ui2") 174 175 ---@type vim.keymap.set.Opts 176 local opts = { buffer = ui2.bufs.cmd } 177 178 if self.actions then 179 vim.iter(pairs(self.actions)):each(function(k, v) 180 vim.keymap.set("i", ("<Plug>(artio-action-%s)"):format(k), v, opts) 181 end) 182 end 183 if self.mappings then 184 vim.iter(pairs(self.mappings)):each(function(k, v) 185 vim.keymap.set("i", k, ("<Plug>(artio-action-%s)"):format(v), opts) 186 end) 187 end 188end 189 190function Picker:delkeymaps() 191 local ui2 = require("vim._core.ui2") 192 193 local keymaps = vim.api.nvim_buf_get_keymap(ui2.bufs.cmd, "i") 194 195 vim.iter(ipairs(keymaps)):each(function(_, v) 196 if v.lhs:match("^<Plug>(artio-action-") or (v.rhs and v.rhs:match("^<Plug>(artio-action-")) then 197 vim.api.nvim_buf_del_keymap(ui2.bufs.cmd, "i", v.lhs) 198 end 199 end) 200end 201 202function Picker:accept() 203 self.thread(action_enum.accept) 204end 205 206function Picker:cancel() 207 self.thread(action_enum.cancel) 208end 209 210function Picker:fix() 211 self.idx = math.max(self.idx, self.opts.preselect and 1 or 0) 212 self.idx = math.min(self.idx, #self.matches) 213end 214 215local function item_is_structured(item) 216 return type(item) == "table" and item.id and item.v and item.text 217end 218 219function Picker:getitems(input) 220 if self.live then 221 self.items = self.get_items and self.get_items(input) or self.items 222 end 223 224 if #self.items > 0 and not item_is_structured(self.items[1]) then 225 self.items = vim 226 .iter(ipairs(self.items)) 227 :map(function(i, v) 228 local text 229 if self.format_item and vim.is_callable(self.format_item) then 230 text = self.format_item(v) 231 end 232 233 return { 234 id = i, 235 v = v, 236 text = text or v, 237 } 238 end) 239 :totable() 240 end 241end 242 243---@param input? string 244function Picker:getmatches(input) 245 if not input then 246 input = self.live and self.liveinput or self.input 247 end 248 self:getitems(input) 249 250 -- if live, ignore sorting 251 if self.live then 252 self.matches = self:getallmatches() 253 return 254 end 255 256 self.matches = vim.tbl_values(self.fn(self.items, input)) 257 table.sort(self.matches, function(a, b) 258 return a[3] > b[3] 259 end) 260end 261 262---@return artio.Picker.match[] 263function Picker:getallmatches() 264 return vim 265 .iter(ipairs(self.items)) 266 :map(function(_, v) 267 return { v.id, {}, 0 } 268 end) 269 :totable() 270end 271 272---@param idx integer 273---@param yes? boolean 274function Picker:mark(idx, yes) 275 self.marked[idx] = yes == nil and true or yes 276end 277 278---@return integer[] 279function Picker:getmarked() 280 return vim 281 .iter(pairs(self.marked)) 282 :map(function(k, v) 283 return v and k or nil 284 end) 285 :totable() 286end 287 288---@param idx? integer index in items 289---@return artio.Picker.item? 290function Picker:getcurrent(idx) 291 if not idx then 292 local i = self.idx 293 idx = self.matches[i] and self.matches[i][1] 294 end 295 if not idx then 296 return 297 end 298 299 return self.items[idx] 300end 301 302function Picker:togglelive() 303 -- check if live can be toggled 304 if not self.get_items then 305 return 306 end 307 308 -- reset fuzzy search when enabling live search 309 if not self.live then 310 self.input = "" 311 end 312 self.live = not self.live 313end 314 315return Picker