minimal extui fuzzy finder for neovim
at 4ce3bdd97032e5a4ffa6ea3c32d6047bb29c473c 170 lines 4.5 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---@alias artio.Picker.action fun(self: artio.Picker, co: thread) 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): integer, fun(win: 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 actions? table<string, artio.Picker.action> 20---@field prompt? string 21---@field defaulttext? string 22---@field prompttext? string 23---@field opts? artio.config.opts 24---@field win? artio.config.win 25---@field mappings? table<string, 'up'|'down'|'accept'|'cancel'|'togglepreview'|string> 26 27---@class artio.Picker : artio.Picker.config 28---@field idx integer 1-indexed 29---@field matches artio.Picker.match[] 30---@field actions? artio.Actions 31local Picker = {} 32Picker.__index = Picker 33Picker.active_picker = nil 34 35local action_enum = { 36 accept = 0, 37 cancel = 1, 38} 39 40---@type table<string, fun(self: artio.Picker, co: thread)> 41local default_actions = { 42 down = function(self, _) 43 self.idx = self.idx + 1 44 self.view:showmatches() -- adjust for scrolling 45 self.view:hlselect() 46 end, 47 up = function(self, _) 48 self.idx = self.idx - 1 49 self.view:showmatches() -- adjust for scrolling 50 self.view:hlselect() 51 end, 52 accept = function(_, co) 53 coroutine.resume(co, action_enum.accept) 54 end, 55 cancel = function(_, co) 56 coroutine.resume(co, action_enum.cancel) 57 end, 58 togglepreview = function(self, _) 59 self.view:togglepreview() 60 end, 61} 62 63---@param props artio.Picker.config 64function Picker:new(props) 65 vim.validate("Picker.items", props.items, "table") 66 vim.validate("Picker:fn", props.fn, "function") 67 vim.validate("Picker:on_close", props.on_close, "function") 68 69 local t = vim.tbl_deep_extend("force", { 70 closed = false, 71 prompt = "", 72 idx = 0, 73 items = {}, 74 matches = {}, 75 }, require("artio.config").get(), props) 76 77 if not t.prompttext then 78 t.prompttext = t.opts.prompt_title and ("%s %s"):format(t.prompt, t.opts.promptprefix) or t.opts.promptprefix 79 end 80 81 Picker.getitems(t, "") 82 83 t.actions = Actions:new({ 84 actions = vim.tbl_extend("force", default_actions, t.actions or {}), 85 }) 86 87 return setmetatable(t, Picker) 88end 89 90function Picker:open() 91 if Picker.active_picker then 92 Picker.active_picker:close() 93 end 94 Picker.active_picker = self 95 96 self.view = View:new(self) 97 98 coroutine.wrap(function() 99 self.view:open() 100 101 local result = self.actions:init(self) 102 103 self:close() 104 105 if result == action_enum.cancel or result ~= action_enum.accept then 106 return 107 end 108 109 local current = self.matches[self.idx] and self.matches[self.idx][1] 110 if not current then 111 return 112 end 113 114 local item = self.items[current] 115 if item then 116 self.on_close(item.v, item.id) 117 end 118 end)() 119end 120 121function Picker:close() 122 if self.closed then 123 return 124 end 125 126 if self.view then 127 self.view:close() 128 end 129 130 self.closed = true 131end 132 133function Picker:fix() 134 self.idx = math.max(self.idx, self.opts.preselect and 1 or 0) 135 self.idx = math.min(self.idx, #self.matches) 136end 137 138function Picker:getitems(input) 139 self.items = self.get_items and self.get_items(input) or self.items 140 if 141 #self.items > 0 142 and (type(self.items[1]) ~= "table" or not (self.items[1].v and self.items[1].id and self.items[1].text)) 143 then 144 self.items = vim 145 .iter(ipairs(self.items)) 146 :map(function(i, v) 147 local text 148 if self.format_item and vim.is_callable(self.format_item) then 149 text = self.format_item(v) 150 end 151 152 return { 153 id = i, 154 v = v, 155 text = text or v, 156 } 157 end) 158 :totable() 159 end 160end 161 162function Picker:getmatches(input) 163 self:getitems(input) 164 self.matches = self.fn(self.items, input) 165 table.sort(self.matches, function(a, b) 166 return a[3] > b[3] 167 end) 168end 169 170return Picker