minimal extui fuzzy finder for neovim
at 3ccbd8cfef81e04868a4df1d155fed08bf870c3d 147 lines 3.6 kB view raw
1local Actions = require("artio.actions") 2local View = require("artio.view") 3 4---@alias artio.Picker.item { id: integer, v: any, text: string, icon?: string, icon_hl?: string, hls?: artio.Picker.hl[] } 5---@alias artio.Picker.match [integer, integer[], integer] [item, pos[], score] 6---@alias artio.Picker.sorter fun(lst: artio.Picker.item[], input: string): artio.Picker.match[] 7---@alias artio.Picker.hl [[integer, integer], string] 8 9---@class artio.Picker.config : artio.config 10---@field items artio.Picker.item[] 11---@field fn artio.Picker.sorter 12---@field on_close fun(text: string, idx: integer) 13---@field format_item? fun(item: any): string 14---@field preview_item? fun(item: any): integer, fun(win: integer) 15---@field get_icon? fun(item: artio.Picker.item): string, string 16---@field hl_item? fun(item: artio.Picker.item): artio.Picker.hl[] 17---@field actions? artio.Actions 18---@field prompt? string 19---@field defaulttext? string 20---@field prompttext? string 21 22---@class artio.Picker : artio.Picker.config 23---@field idx integer 1-indexed 24---@field matches artio.Picker.match[] 25local Picker = {} 26Picker.__index = Picker 27 28local action_enum = { 29 accept = 0, 30 cancel = 1, 31} 32 33---@type table<string, fun(self: artio.Picker, co: thread)> 34local default_actions = { 35 down = function(self, _) 36 self.idx = self.idx + 1 37 self.view:showmatches() 38 self.view:hlselect() 39 end, 40 up = function(self, _) 41 self.idx = self.idx - 1 42 self.view:showmatches() 43 self.view:hlselect() 44 end, 45 accept = function(_, co) 46 coroutine.resume(co, action_enum.accept) 47 end, 48 cancel = function(_, co) 49 coroutine.resume(co, action_enum.cancel) 50 end, 51 togglepreview = function(self, _) 52 self.view:togglepreview() 53 end, 54} 55 56---@param props artio.Picker.config 57function Picker:new(props) 58 vim.validate("Picker.items", props.items, "table") 59 vim.validate("Picker:fn", props.fn, "function") 60 vim.validate("Picker:on_close", props.on_close, "function") 61 62 local t = vim.tbl_deep_extend("force", { 63 closed = false, 64 prompt = "", 65 idx = 0, 66 items = {}, 67 matches = {}, 68 }, require("artio.config").get(), props) 69 70 if not t.prompttext then 71 t.prompttext = t.opts.prompt_title and ("%s %s"):format(t.prompt, t.opts.promptprefix) or t.opts.promptprefix 72 end 73 74 t.items = vim 75 .iter(ipairs(t.items)) 76 :map(function(i, v) 77 local text 78 if t.format_item and vim.is_callable(t.format_item) then 79 text = t.format_item(v) 80 end 81 82 return { 83 id = i, 84 v = v, 85 text = text or v, 86 } 87 end) 88 :totable() 89 90 t.actions = t.actions or Actions:new({ 91 actions = default_actions, 92 }) 93 94 return setmetatable(t, Picker) 95end 96 97function Picker:open() 98 self.view = View:new(self) 99 100 coroutine.wrap(function() 101 self.view:open() 102 103 local result = self.actions:init(self) 104 105 self:close() 106 107 if result == action_enum.cancel or result ~= action_enum.accept then 108 return 109 end 110 111 local current = self.matches[self.idx] and self.matches[self.idx][1] 112 if not current then 113 return 114 end 115 116 local item = self.items[current] 117 if item then 118 self.on_close(item.v, item.id) 119 end 120 end)() 121end 122 123function Picker:close() 124 if self.closed then 125 return 126 end 127 128 if self.view then 129 self.view:close() 130 end 131 132 self.closed = true 133end 134 135function Picker:fix() 136 self.idx = math.max(self.idx, self.opts.preselect and 1 or 0) 137 self.idx = math.min(self.idx, #self.matches) 138end 139 140function Picker:getmatches(input) 141 self.matches = self.fn(self.items, input) 142 table.sort(self.matches, function(a, b) 143 return a[3] > b[3] 144 end) 145end 146 147return Picker