@@ -5,8 +5,9 @@ local Promise = require('opencode.promise')
55--- @class PickerAction
66--- @field key ? OpencodeKeymapEntry | string The key binding for this action
77--- @field label string The display label for this action
8- --- @field fn fun ( selected : any , opts : PickerOptions ): any[] | Promise<any[]> ? The action function
8+ --- @field fn fun ( selected : any | any[] , opts : PickerOptions ): any[] | Promise<any[]> ? The action function
99--- @field reload ? boolean Whether to reload the picker after action
10+ --- @field multi_selection ? boolean Whether this action supports multi-selection
1011
1112--- @class PickerOptions
1213--- @field items any[] The list of items to pick from
@@ -15,6 +16,7 @@ local Promise = require('opencode.promise')
1516--- @field callback fun ( selected : any ?) Callback when item is selected
1617--- @field title string | fun (): string The picker title
1718--- @field width ? number Optional width for the picker (defaults to config or current window width )
19+ --- @field multi_selection ? table<string , boolean> Actions that support multi-selection
1820
1921--- @class TelescopeEntry
2022--- @field value any
@@ -53,12 +55,14 @@ local picker = require('opencode.ui.picker')
5355--- Build title with action legend
5456--- @param base_title string The base title
5557--- @param actions table<string , PickerAction> The available actions
58+ --- @param support_multi ? boolean Whether multi-selection is supported
5659--- @return string title The formatted title with action legend
57- local function build_title (base_title , actions )
60+ local function build_title (base_title , actions , support_multi )
5861 local legend = {}
5962 for _ , action in pairs (actions ) do
6063 if action .key and action .key [1 ] then
61- table.insert (legend , action .key [1 ] .. ' ' .. action .label )
64+ local label = action .label .. (action .multi_selection and support_multi ~= false and ' (multi)' or ' ' )
65+ table.insert (legend , action .key [1 ] .. ' ' .. label )
6266 end
6367 end
6468 return base_title .. (# legend > 0 and ' | ' .. table.concat (legend , ' | ' ) or ' ' )
@@ -72,6 +76,7 @@ local function telescope_ui(opts)
7276 local conf = require (' telescope.config' ).values
7377 local actions = require (' telescope.actions' )
7478 local action_state = require (' telescope.actions.state' )
79+ local action_utils = require (' telescope.actions.utils' )
7580 local entry_display = require (' telescope.pickers.entry_display' )
7681 local displayer = entry_display .create ({
7782 separator = ' ' ,
@@ -87,7 +92,8 @@ local function telescope_ui(opts)
8792 return {
8893 value = item ,
8994 display = function (entry )
90- return displayer (opts .format_fn (entry .value ):to_formatted_text ())
95+ local formatted = opts .format_fn (entry .value ):to_formatted_text ()
96+ return displayer (formatted )
9197 end ,
9298 ordinal = opts .format_fn (item ):to_string (),
9399 }
@@ -125,9 +131,27 @@ local function telescope_ui(opts)
125131 end
126132
127133 local action_fn = function ()
128- local selection = action_state .get_selected_entry ()
129- if selection then
130- local new_items = action .fn (selection .value , opts )
134+ local items_to_process
135+
136+ if action .multi_selection then
137+ local multi_selection = {}
138+ action_utils .map_selections (prompt_bufnr , function (entry , index )
139+ table.insert (multi_selection , entry .value )
140+ end )
141+
142+ if # multi_selection > 0 then
143+ items_to_process = multi_selection
144+ else
145+ local selection = action_state .get_selected_entry ()
146+ items_to_process = selection and selection .value or nil
147+ end
148+ else
149+ local selection = action_state .get_selected_entry ()
150+ items_to_process = selection and selection .value or nil
151+ end
152+
153+ if items_to_process then
154+ local new_items = action .fn (items_to_process , opts )
131155 Promise .wrap (new_items ):and_then (function (resolved_items )
132156 if action .reload and resolved_items then
133157 opts .items = resolved_items
@@ -156,11 +180,18 @@ local function fzf_ui(opts)
156180 local fzf_lua = require (' fzf-lua' )
157181
158182 local function create_fzf_config ()
183+ local has_multi_action = util .some (opts .actions , function (action )
184+ return action .multi_selection
185+ end )
186+
159187 return {
160188 winopts = opts .width and {
161189 width = opts .width + 8 , -- extra space for fzf UI
162190 } or nil ,
163- fzf_opts = { [' --prompt' ] = opts .title .. ' > ' },
191+ fzf_opts = {
192+ [' --prompt' ] = opts .title .. ' > ' ,
193+ [' --multi' ] = has_multi_action and true or nil ,
194+ },
164195 _headers = { ' actions' },
165196 fn_fzf_index = function (line )
166197 for i , item in ipairs (opts .items ) do
@@ -209,9 +240,25 @@ local function fzf_ui(opts)
209240 if not selected or # selected == 0 then
210241 return
211242 end
212- local idx = fzf_opts .fn_fzf_index (selected [1 ] --[[ @as string]] )
213- if idx and opts .items [idx ] then
214- local new_items = action .fn (opts .items [idx ], opts )
243+
244+ local items_to_process
245+ if action .multi_selection and # selected > 1 then
246+ items_to_process = {}
247+ for _ , sel in ipairs (selected ) do
248+ local idx = fzf_opts .fn_fzf_index (sel --[[ @as string]] )
249+ if idx and opts .items [idx ] then
250+ table.insert (items_to_process , opts .items [idx ])
251+ end
252+ end
253+ else
254+ local idx = fzf_opts .fn_fzf_index (selected [1 ] --[[ @as string]] )
255+ if idx and opts .items [idx ] then
256+ items_to_process = opts .items [idx ]
257+ end
258+ end
259+
260+ if items_to_process then
261+ local new_items = action .fn (items_to_process , opts )
215262 Promise .wrap (new_items ):and_then (function (resolved_items )
216263 if action .reload and resolved_items then
217264 --- @cast resolved_items any[]
@@ -250,9 +297,10 @@ local function mini_pick_ui(opts)
250297 mappings [action_name ] = {
251298 char = action .key [1 ],
252299 func = function ()
253- local selected = mini_pick .get_picker_matches ().current
254- if selected and selected .item then
255- local new_items = action .fn (selected .item , opts )
300+ local current = mini_pick .get_picker_matches ().current
301+ if current and current .item then
302+ -- Mini.pick doesn't have native multi-selection, we fallback single selection
303+ local new_items = action .fn (current .item , opts )
256304 Promise .wrap (new_items ):and_then (function (resolved_items )
257305 if action .reload and resolved_items then
258306 opts .items = resolved_items
@@ -330,19 +378,29 @@ local function snacks_picker_ui(opts)
330378 },
331379 }
332380
381+ snack_opts .win = snack_opts .win or {}
382+ snack_opts .win .input = snack_opts .win .input or { keys = {} }
383+
333384 for action_name , action in pairs (opts .actions ) do
334385 if action .key and action .key [1 ] then
335- snack_opts .win = snack_opts .win or {}
336- snack_opts .win .input = snack_opts .win .input or { keys = {} }
337386 snack_opts .win .input .keys [action .key [1 ]] = { action_name , mode = action .key .mode or ' i' }
338387
339388 snack_opts .actions [action_name ] = function (_picker , item )
340389 if item then
341390 vim .schedule (function ()
342- local new_items = action .fn (item , opts )
391+ local items_to_process
392+ if action .multi_selection then
393+ local selected_items = _picker :selected ({ fallback = true })
394+ items_to_process = # selected_items > 1 and selected_items or item
395+ else
396+ items_to_process = item
397+ end
398+
399+ local new_items = action .fn (items_to_process , opts )
343400 Promise .wrap (new_items ):and_then (function (resolved_items )
344401 if action .reload and resolved_items then
345402 opts .items = resolved_items
403+ _picker :refresh ()
346404 _picker :find ()
347405 end
348406 end )
@@ -429,16 +487,19 @@ function M.pick(opts)
429487 end
430488
431489 local title_str = type (opts .title ) == ' function' and opts .title () or opts .title --[[ @as string]]
432- opts .title = build_title (title_str , opts .actions )
433490
434491 vim .schedule (function ()
435492 if picker_type == ' telescope' then
493+ opts .title = build_title (title_str , opts .actions )
436494 telescope_ui (opts )
437495 elseif picker_type == ' fzf' then
496+ opts .title = title_str
438497 fzf_ui (opts )
439498 elseif picker_type == ' mini.pick' then
499+ opts .title = build_title (title_str , opts .actions , false )
440500 mini_pick_ui (opts )
441501 elseif picker_type == ' snacks' then
502+ opts .title = build_title (title_str , opts .actions )
442503 snacks_picker_ui (opts )
443504 else
444505 opts .callback (nil )
0 commit comments