feat: universal selector (#1877)
This commit is contained in:
55
lua/avante/ui/selector/init.lua
Normal file
55
lua/avante/ui/selector/init.lua
Normal file
@@ -0,0 +1,55 @@
|
||||
local Utils = require("avante.utils")
|
||||
|
||||
---@class avante.ui.SelectorItem
|
||||
---@field id string
|
||||
---@field title string
|
||||
|
||||
---@class avante.ui.SelectorOption
|
||||
---@field provider avante.SelectorProvider
|
||||
---@field title string
|
||||
---@field items avante.ui.SelectorItem[]
|
||||
---@field default_item_id string | nil
|
||||
---@field selected_item_ids string[] | nil
|
||||
---@field provider_opts table | nil
|
||||
---@field on_select fun(item_ids: string[] | nil)
|
||||
---@field get_preview_content fun(item_id: string): (string, string) | nil
|
||||
|
||||
---@class avante.ui.Selector
|
||||
---@field provider avante.SelectorProvider
|
||||
---@field title string
|
||||
---@field items avante.ui.SelectorItem[]
|
||||
---@field default_item_id string | nil
|
||||
---@field provider_opts table | nil
|
||||
---@field on_select fun(item_ids: string[] | nil)
|
||||
---@field selected_item_ids string[] | nil
|
||||
---@field get_preview_content fun(item_id: string): (string, string) | nil
|
||||
local Selector = {}
|
||||
Selector.__index = Selector
|
||||
|
||||
---@param opts avante.ui.SelectorOption
|
||||
function Selector:new(opts)
|
||||
local o = {}
|
||||
setmetatable(o, Selector)
|
||||
o.provider = opts.provider
|
||||
o.title = opts.title
|
||||
o.items = opts.items
|
||||
o.default_item_id = opts.default_item_id
|
||||
o.provider_opts = opts.provider_opts or {}
|
||||
o.on_select = opts.on_select
|
||||
o.selected_item_ids = opts.selected_item_ids or {}
|
||||
o.get_preview_content = opts.get_preview_content
|
||||
return o
|
||||
end
|
||||
|
||||
function Selector:open()
|
||||
if type(self.provider) == "function" then
|
||||
self.provider(self)
|
||||
return
|
||||
end
|
||||
|
||||
local ok, provider = pcall(require, "avante.ui.selector.providers." .. self.provider)
|
||||
if not ok then Utils.error("Unknown file selector provider: " .. self.provider) end
|
||||
provider.show(self)
|
||||
end
|
||||
|
||||
return Selector
|
||||
49
lua/avante/ui/selector/providers/fzf_lua.lua
Normal file
49
lua/avante/ui/selector/providers/fzf_lua.lua
Normal file
@@ -0,0 +1,49 @@
|
||||
local Utils = require("avante.utils")
|
||||
local M = {}
|
||||
|
||||
---@param selector avante.ui.Selector
|
||||
function M.show(selector)
|
||||
local success, fzf_lua = pcall(require, "fzf-lua")
|
||||
if not success then
|
||||
Utils.error("fzf-lua is not installed. Please install fzf-lua to use it as a file selector.")
|
||||
return
|
||||
end
|
||||
|
||||
local formated_items = vim.iter(selector.items):map(function(item) return item.title end):totable()
|
||||
local title_to_id = {}
|
||||
for _, item in ipairs(selector.items) do
|
||||
title_to_id[item.title] = item.id
|
||||
end
|
||||
|
||||
local function close_action() selector.on_select(nil) end
|
||||
fzf_lua.fzf_exec(
|
||||
formated_items,
|
||||
vim.tbl_deep_extend("force", {
|
||||
prompt = selector.title,
|
||||
preview = selector.get_preview_content and function(item)
|
||||
local id = title_to_id[item[1]]
|
||||
local content = selector.get_preview_content(id)
|
||||
return content
|
||||
end or nil,
|
||||
fzf_opts = {},
|
||||
git_icons = false,
|
||||
actions = {
|
||||
["default"] = function(selected)
|
||||
if not selected or #selected == 0 then return close_action() end
|
||||
---@type string[]
|
||||
local selections = {}
|
||||
for _, entry in ipairs(selected) do
|
||||
local id = title_to_id[entry]
|
||||
if id then table.insert(selections, id) end
|
||||
end
|
||||
|
||||
selector.on_select(selections)
|
||||
end,
|
||||
["esc"] = close_action,
|
||||
["ctrl-c"] = close_action,
|
||||
},
|
||||
}, selector.provider_opts)
|
||||
)
|
||||
end
|
||||
|
||||
return M
|
||||
37
lua/avante/ui/selector/providers/mini_pick.lua
Normal file
37
lua/avante/ui/selector/providers/mini_pick.lua
Normal file
@@ -0,0 +1,37 @@
|
||||
local Utils = require("avante.utils")
|
||||
local M = {}
|
||||
|
||||
---@param selector avante.ui.Selector
|
||||
function M.show(selector)
|
||||
-- luacheck: globals MiniPick
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
if not _G.MiniPick then
|
||||
Utils.error("mini.pick is not set up. Please install and set up mini.pick to use it as a file selector.")
|
||||
return
|
||||
end
|
||||
local items = {}
|
||||
local title_to_id = {}
|
||||
for _, item in ipairs(selector.items) do
|
||||
title_to_id[item.title] = item.id
|
||||
if not vim.list_contains(selector.selected_item_ids, item.id) then table.insert(items, item) end
|
||||
end
|
||||
local function choose(item)
|
||||
if not item then
|
||||
selector.on_select(nil)
|
||||
return
|
||||
end
|
||||
local item_ids = {}
|
||||
---item is not a list
|
||||
for _, item_ in pairs(item) do
|
||||
table.insert(item_ids, title_to_id[item_])
|
||||
end
|
||||
selector.on_select(item_ids)
|
||||
end
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
MiniPick.ui_select(items, {
|
||||
prompt = selector.title,
|
||||
format_item = function(item) return item.title end,
|
||||
}, choose)
|
||||
end
|
||||
|
||||
return M
|
||||
21
lua/avante/ui/selector/providers/native.lua
Normal file
21
lua/avante/ui/selector/providers/native.lua
Normal file
@@ -0,0 +1,21 @@
|
||||
local M = {}
|
||||
|
||||
---@param selector avante.ui.Selector
|
||||
function M.show(selector)
|
||||
local items = {}
|
||||
for _, item in ipairs(selector.items) do
|
||||
if not vim.list_contains(selector.selected_item_ids, item.id) then table.insert(items, item) end
|
||||
end
|
||||
vim.ui.select(items, {
|
||||
prompt = selector.title,
|
||||
format_item = function(item) return item.title end,
|
||||
}, function(item)
|
||||
if item then
|
||||
selector.on_select({ item.id })
|
||||
else
|
||||
selector.on_select(nil)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
58
lua/avante/ui/selector/providers/snacks.lua
Normal file
58
lua/avante/ui/selector/providers/snacks.lua
Normal file
@@ -0,0 +1,58 @@
|
||||
local Utils = require("avante.utils")
|
||||
local M = {}
|
||||
|
||||
---@param selector avante.ui.Selector
|
||||
function M.show(selector)
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
if not _G.Snacks then
|
||||
Utils.error("Snacks is not set up. Please install and set up Snacks to use it as a file selector.")
|
||||
return
|
||||
end
|
||||
local finder_items = {}
|
||||
for i, item in ipairs(selector.items) do
|
||||
if not vim.list_contains(selector.selected_item_ids, item.id) then
|
||||
table.insert(finder_items, {
|
||||
formatted = item.title,
|
||||
text = item.title,
|
||||
item = item,
|
||||
idx = i,
|
||||
preview = selector.get_preview_content and (function()
|
||||
local content, filetype = selector.get_preview_content(item.id)
|
||||
return {
|
||||
text = content,
|
||||
ft = filetype,
|
||||
}
|
||||
end)() or nil,
|
||||
})
|
||||
end
|
||||
end
|
||||
local completed = false
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
Snacks.picker.pick({
|
||||
source = "select",
|
||||
items = finder_items,
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
format = Snacks.picker.format.ui_select(nil, #finder_items),
|
||||
title = selector.title,
|
||||
preview = selector.get_preview_content and "preview" or nil,
|
||||
layout = {
|
||||
preset = "default",
|
||||
preview = selector.get_preview_content ~= nil,
|
||||
},
|
||||
confirm = function(picker)
|
||||
if completed then return end
|
||||
completed = true
|
||||
picker:close()
|
||||
local items = picker:selected({ fallback = true })
|
||||
local selected_item_ids = vim.tbl_map(function(item) return item.item.id end, items)
|
||||
selector.on_select(selected_item_ids)
|
||||
end,
|
||||
on_close = function()
|
||||
if completed then return end
|
||||
completed = true
|
||||
vim.schedule(function() selector.on_select(nil) end)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
92
lua/avante/ui/selector/providers/telescope.lua
Normal file
92
lua/avante/ui/selector/providers/telescope.lua
Normal file
@@ -0,0 +1,92 @@
|
||||
local Utils = require("avante.utils")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param selector avante.ui.Selector
|
||||
function M.show(selector)
|
||||
local success, _ = pcall(require, "telescope")
|
||||
if not success then
|
||||
Utils.error("telescope is not installed. Please install telescope to use it as a file selector.")
|
||||
return
|
||||
end
|
||||
|
||||
local pickers = require("telescope.pickers")
|
||||
local finders = require("telescope.finders")
|
||||
local conf = require("telescope.config").values
|
||||
local actions = require("telescope.actions")
|
||||
local action_state = require("telescope.actions.state")
|
||||
local previewers = require("telescope.previewers")
|
||||
|
||||
local items = {}
|
||||
for _, item in ipairs(selector.items) do
|
||||
if not vim.list_contains(selector.selected_item_ids, item.id) then table.insert(items, item) end
|
||||
end
|
||||
|
||||
pickers
|
||||
.new(
|
||||
{},
|
||||
vim.tbl_extend("force", {
|
||||
prompt_title = selector.title,
|
||||
finder = finders.new_table({
|
||||
results = items,
|
||||
entry_maker = function(entry)
|
||||
return {
|
||||
value = entry.id,
|
||||
display = entry.title,
|
||||
ordinal = entry.title,
|
||||
}
|
||||
end,
|
||||
}),
|
||||
sorter = conf.file_sorter(),
|
||||
previewer = selector.get_preview_content and previewers.new_buffer_previewer({
|
||||
title = "Preview",
|
||||
define_preview = function(self, entry)
|
||||
if not entry then return end
|
||||
local content, filetype = selector.get_preview_content(entry.value)
|
||||
local lines = vim.split(content or "", "\n")
|
||||
-- Ensure the buffer exists and is valid before setting lines
|
||||
if vim.api.nvim_buf_is_valid(self.state.bufnr) then
|
||||
vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, lines)
|
||||
-- Set filetype after content is loaded
|
||||
vim.api.nvim_set_option_value("filetype", filetype, { buf = self.state.bufnr })
|
||||
-- Ensure cursor is within bounds
|
||||
vim.schedule(function()
|
||||
if vim.api.nvim_buf_is_valid(self.state.bufnr) then
|
||||
local row = math.min(vim.api.nvim_buf_line_count(self.state.bufnr), 1)
|
||||
pcall(vim.api.nvim_win_set_cursor, self.state.winnr, { row, 0 })
|
||||
end
|
||||
end)
|
||||
end
|
||||
end,
|
||||
}),
|
||||
attach_mappings = function(prompt_bufnr, map)
|
||||
map("i", "<esc>", require("telescope.actions").close)
|
||||
actions.select_default:replace(function()
|
||||
local picker = action_state.get_current_picker(prompt_bufnr)
|
||||
|
||||
local selections
|
||||
local multi_selection = picker:get_multi_selection()
|
||||
if #multi_selection ~= 0 then
|
||||
selections = multi_selection
|
||||
else
|
||||
selections = action_state.get_selected_entry()
|
||||
selections = vim.islist(selections) and selections or { selections }
|
||||
end
|
||||
|
||||
local selected_item_ids = vim
|
||||
.iter(selections)
|
||||
:map(function(selection) return selection.value end)
|
||||
:totable()
|
||||
|
||||
selector.on_select(selected_item_ids)
|
||||
|
||||
actions.close(prompt_bufnr)
|
||||
end)
|
||||
return true
|
||||
end,
|
||||
}, selector.provider_opts)
|
||||
)
|
||||
:find()
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user