Skip to content

Commit f440ed5

Browse files
committed
feat(session_picker): enable multi-selection for deletion
1 parent 9a2cb69 commit f440ed5

File tree

3 files changed

+107
-32
lines changed

3 files changed

+107
-32
lines changed

lua/opencode/ui/base_picker.lua

Lines changed: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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)

lua/opencode/ui/session_picker.lua

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,28 +47,33 @@ function M.pick(sessions, callback)
4747
delete = {
4848
key = config.keymap.session_picker.delete_session,
4949
label = 'delete',
50+
multi_selection = true,
5051
fn = function(selected, opts)
5152
local state = require('opencode.state')
5253

53-
local session_id_to_delete = selected.id
54+
local sessions_to_delete = type(selected) == 'table' and selected.id == nil and selected or { selected }
5455

55-
if state.active_session and state.active_session.id == selected.id then
56-
vim.notify('deleting current session, creating new session')
57-
state.active_session = require('opencode.core').create_new_session()
58-
end
56+
for _, session in ipairs(sessions_to_delete) do
57+
if state.active_session and state.active_session.id == session.id then
58+
vim.notify('deleting current session, creating new session')
59+
state.active_session = require('opencode.core').create_new_session()
60+
end
5961

60-
state.api_client:delete_session(session_id_to_delete):catch(function(err)
61-
vim.schedule(function()
62-
vim.notify('Failed to delete session: ' .. vim.inspect(err), vim.log.levels.ERROR)
62+
state.api_client:delete_session(session.id):catch(function(err)
63+
vim.schedule(function()
64+
vim.notify('Failed to delete session ' .. session.id .. ': ' .. vim.inspect(err), vim.log.levels.ERROR)
65+
end)
6366
end)
64-
end)
6567

66-
local idx = util.find_index_of(opts.items, function(item)
67-
return item.id == selected.id
68-
end)
69-
if idx > 0 then
70-
table.remove(opts.items, idx)
68+
local idx = util.find_index_of(opts.items, function(item)
69+
return item.id == session.id
70+
end)
71+
if idx > 0 then
72+
table.remove(opts.items, idx)
73+
end
7174
end
75+
76+
vim.notify('Deleted ' .. #sessions_to_delete .. ' session(s)', vim.log.levels.INFO)
7277
return opts.items
7378
end,
7479
reload = true,

lua/opencode/util.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@ function M.find_index_of(tbl, predicate)
120120
return nil
121121
end
122122

123+
function M.some(tbl, predicate)
124+
for _, v in ipairs(tbl) do
125+
if predicate(v) then
126+
return true
127+
end
128+
end
129+
return false
130+
end
131+
123132
local _is_git_project = nil
124133
function M.is_git_project()
125134
if _is_git_project ~= nil then

0 commit comments

Comments
 (0)