minimal extui fuzzy finder for neovim
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