diff --git a/README.md b/README.md index 7a08ec1..839f18f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ return { -- "nvim-telescope/telescope.nvim" -- }, opts = { - picker_integration = true, + -- Picker integration: "auto" | "snacks" | "telescope" | false + picker_integration = "auto", }, } ``` @@ -129,13 +130,25 @@ When using `:UVRunFunction` or `xf`, the plugin: 3. Creates a proper module import context for the function 4. Captures and displays return values -### Integration with Snacks.nvim +### Picker Integration -This plugin integrates with [Snacks.nvim](https://github.com/folke/snacks.nvim) for UI components: +This plugin supports multiple UI picker integrations. Configure with the `picker_integration` option: -- Command picker -- Environment management -- Function selection +```lua +require('uv').setup({ + -- Options: "auto" | "snacks" | "telescope" | false + picker_integration = "auto", -- default +}) +``` + +| Value | Description | +|-------|-------------| +| `"auto"` | Automatically detect available picker (tries Snacks first, then Telescope) | +| `"snacks"` | Explicitly use [Snacks.nvim](https://github.com/folke/snacks.nvim) picker | +| `"telescope"` | Explicitly use [Telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) picker | +| `false` | Disable picker integration | + +**Note:** `true` is still supported for backwards compatibility (treated as `"auto"`). ## API @@ -271,8 +284,9 @@ require('uv').setup({ -- Auto commands for directory changes auto_commands = true, - -- Integration with snacks picker - picker_integration = true, + -- Picker integration: "auto" | "snacks" | "telescope" | false + -- "auto" tries snacks first, then telescope + picker_integration = "auto", -- Keymaps to register (set to false to disable) keymaps = { diff --git a/lua/uv/init.lua b/lua/uv/init.lua index e3c93c2..825e788 100644 --- a/lua/uv/init.lua +++ b/lua/uv/init.lua @@ -21,11 +21,13 @@ ---@field sync boolean ---@field sync_all boolean +---@alias PickerIntegration "auto"|"snacks"|"telescope"|"fzf-lua"|boolean + ---@class UVConfig ---@field auto_activate_venv boolean ---@field notify_activate_venv boolean ---@field auto_commands boolean ----@field picker_integration boolean +---@field picker_integration PickerIntegration ---@field keymaps UVKeymapsConfig|false ---@field execution UVExecutionConfig @@ -43,8 +45,9 @@ M.config = { -- Auto commands for directory changes auto_commands = true, - -- Integration with picker (like Telescope or other UI components) - picker_integration = true, + -- Picker integration: "auto" | "snacks" | "telescope" | "fzf-lua" | false + -- "auto" tries snacks first, then telescope (backwards compatible with true) + picker_integration = "auto", -- Keymaps to register (set to false to disable) keymaps = { @@ -510,268 +513,29 @@ end -- Set up command pickers for integration with UI plugins function M.setup_pickers() - -- Snacks - if _G.Snacks and _G.Snacks.picker then - Snacks.picker.sources.uv_commands = { - finder = function() - return { - { text = "Run current file", desc = "Run current file with Python", is_run_current = true }, - { text = "Run selection", desc = "Run selected Python code", is_run_selection = true }, - { text = "Run function", desc = "Run specific Python function", is_run_function = true }, - { text = "uv add [package]", desc = "Install a package" }, - { text = "uv sync", desc = "Sync packages from lockfile" }, - { - text = "uv sync --all-extras --all-packages --all-groups", - desc = "Sync all extras, groups and packages", - }, - { text = "uv remove [package]", desc = "Remove a package" }, - { text = "uv init", desc = "Initialize a new project" }, - } - end, - preview = function(ctx) - local cmd = ctx.item.text:match("^(uv %a+)") - if cmd then - Snacks.picker.preview.cmd(cmd .. " --help", ctx) - else - ctx.preview:set_lines({}) - end - end, - format = function(item) - return { { item.text .. " - " .. item.desc } } - end, - confirm = function(picker, item) - if item then - picker:close() - if item.is_run_current then - M.run_file() - return - elseif item.is_run_selection then - local mode = vim.fn.mode() - if mode == "v" or mode == "V" or mode == "" then - vim.cmd("normal! \27") - vim.defer_fn(function() - M.run_python_selection() - end, 100) - else - vim.notify( - "Please select text first. Enter visual mode (v) and select code to run.", - vim.log.levels.INFO - ) - vim.api.nvim_create_autocmd("ModeChanged", { - pattern = "[vV\x16]*:n", - callback = function(_) - M.run_python_selection() - return true - end, - once = true, - }) - end - return - elseif item.is_run_function then - M.run_python_function() - return - end - - local cmd = item.text - if cmd:match("%[(.-)%]") then - local param_name = cmd:match("%[(.-)%]") - vim.ui.input({ prompt = "Enter " .. param_name .. ": " }, function(input) - if not input or input == "" then - vim.notify("Cancelled", vim.log.levels.INFO) - return - end - local actual_cmd = cmd:gsub("%[" .. param_name .. "%]", input) - M.run_command(actual_cmd) - end) - else - M.run_command(cmd) - end - end - end, - } - - Snacks.picker.sources.uv_venv = { - finder = function() - local venvs = {} - if vim.fn.isdirectory(".venv") == 1 then - table.insert(venvs, { - text = ".venv", - path = vim.fn.getcwd() .. "/.venv", - is_current = vim.env.VIRTUAL_ENV and vim.env.VIRTUAL_ENV:match(".venv$") ~= nil, - }) - end - if #venvs == 0 then - table.insert(venvs, { - text = "Create new virtual environment (uv venv)", - is_create = true, - }) - end - return venvs - end, - format = function(item) - if item.is_create then - return { { "+ " .. item.text } } - else - local icon = item.is_current and "● " or "○ " - return { { icon .. item.text .. " (Activate)" } } - end - end, - confirm = function(picker, item) - picker:close() - if item then - if item.is_create then - M.run_command("uv venv") - else - M.activate_venv(item.path) - end - end - end, - } - end - - -- Telescope - local has_telescope, telescope = pcall(require, "telescope") - if has_telescope and telescope then - local pickers = require("telescope.pickers") - local finders = require("telescope.finders") - local sorters = require("telescope.sorters") - local actions = require("telescope.actions") - local action_state = require("telescope.actions.state") - - function M.pick_uv_commands() - local items = { - { text = "Run current file", is_run_current = true }, - { text = "Run selection", is_run_selection = true }, - { text = "Run function", is_run_function = true }, - { text = "uv add [package]", cmd = "uv add ", needs_input = true }, - { text = "uv sync", cmd = "uv sync" }, - { - text = "uv sync --all-extras --all-packages --all-groups", - cmd = "uv sync --all-extras --all-packages --all-groups", - }, - { text = "uv remove [package]", cmd = "uv remove ", needs_input = true }, - { text = "uv init", cmd = "uv init" }, - } - - pickers - .new({}, { - prompt_title = "UV Commands", - finder = finders.new_table({ - results = items, - entry_maker = function(entry) - return { - value = entry, - display = entry.text, - ordinal = entry.text, - } - end, - }), - sorter = sorters.get_generic_fuzzy_sorter(), - attach_mappings = function(prompt_bufnr, map) - local function on_select() - local selection = action_state.get_selected_entry().value - actions.close(prompt_bufnr) - if selection.is_run_current then - M.run_file() - elseif selection.is_run_selection then - local mode = vim.fn.mode() - if mode == "v" or mode == "V" or mode == "" then - vim.cmd("normal! \27") - vim.defer_fn(function() - M.run_python_selection() - end, 100) - else - vim.notify( - "Please select text first. Enter visual mode (v) and select code to run.", - vim.log.levels.INFO - ) - vim.api.nvim_create_autocmd("ModeChanged", { - pattern = "[vV\x16]*:n", - callback = function() - M.run_python_selection() - return true - end, - once = true, - }) - end - elseif selection.is_run_function then - M.run_python_function() - else - if selection.needs_input then - local placeholder = selection.text:match("%[(.-)%]") - vim.ui.input( - { prompt = "Enter " .. (placeholder or "value") .. ": " }, - function(input) - if input and input ~= "" then - local cmd = selection.cmd .. input - M.run_command(cmd) - else - vim.notify("Cancelled", vim.log.levels.INFO) - end - end - ) - else - M.run_command(selection.cmd) - end - end - end - - map("i", "", on_select) - map("n", "", on_select) - return true - end, - }) - :find() - end + local picker = require("uv.picker") - function M.pick_uv_venv() - local items = {} - if vim.fn.isdirectory(".venv") == 1 then - table.insert(items, { - text = ".venv", - path = vim.fn.getcwd() .. "/.venv", - is_current = vim.env.VIRTUAL_ENV and vim.env.VIRTUAL_ENV:match(".venv$") ~= nil, - }) - end - if #items == 0 then - table.insert(items, { text = "Create new virtual environment (uv venv)", is_create = true }) - end + local callbacks = { + run_file = M.run_file, + run_python_selection = M.run_python_selection, + run_python_function = M.run_python_function, + run_command = M.run_command, + activate_venv = M.activate_venv, + } - pickers - .new({}, { - prompt_title = "UV Virtual Environments", - finder = finders.new_table({ - results = items, - entry_maker = function(entry) - local display = entry.is_create and "+ " .. entry.text - or ((entry.is_current and "● " or "○ ") .. entry.text .. " (Activate)") - return { - value = entry, - display = display, - ordinal = display, - } - end, - }), - sorter = sorters.get_generic_fuzzy_sorter(), - attach_mappings = function(prompt_bufnr, map) - local function on_select() - local selection = action_state.get_selected_entry().value - actions.close(prompt_bufnr) - if selection.is_create then - M.run_command("uv venv") - else - M.activate_venv(selection.path) - end - end - - map("i", "", on_select) - map("n", "", on_select) - return true - end, - }) - :find() - end + local success = picker.setup(M.config.picker_integration, callbacks) + + -- Expose telescope picker functions on M for backwards compatibility + local telescope_pickers = picker.get_commands_picker() + if telescope_pickers then + M.pick_uv_commands = telescope_pickers + end + local telescope_venv = picker.get_venv_picker() + if telescope_venv then + M.pick_uv_venv = telescope_venv end + + return success end -- Set up user commands @@ -818,37 +582,17 @@ function M.setup_keymaps() end local prefix = keymaps.prefix or "x" + local picker = require("uv.picker") -- Main UV command menu if keymaps.commands then - if _G.Snacks and _G.Snacks.picker then - vim.api.nvim_set_keymap( - "n", - prefix, - "lua Snacks.picker.pick('uv_commands')", - { noremap = true, silent = true, desc = "UV Commands" } - ) - vim.api.nvim_set_keymap( - "v", - prefix, - ":lua Snacks.picker.pick('uv_commands')", - { noremap = true, silent = true, desc = "UV Commands" } - ) - end - local has_telescope = pcall(require, "telescope") - if has_telescope then - vim.api.nvim_set_keymap( - "n", - prefix, - "lua require('uv').pick_uv_commands()", - { noremap = true, silent = true, desc = "UV Commands (Telescope)" } - ) - vim.api.nvim_set_keymap( - "v", - prefix, - ":lua require('uv').pick_uv_commands()", - { noremap = true, silent = true, desc = "UV Commands (Telescope)" } - ) + local cmd_keymap, picker_name = picker.get_commands_keymap(M.config.picker_integration) + if cmd_keymap then + local desc = picker_name == "snacks" and "UV Commands" or "UV Commands (Telescope)" + vim.api.nvim_set_keymap("n", prefix, cmd_keymap, { noremap = true, silent = true, desc = desc }) + -- Visual mode version + local vcmd = cmd_keymap:gsub("^", ":") + vim.api.nvim_set_keymap("v", prefix, vcmd, { noremap = true, silent = true, desc = desc }) end end @@ -884,22 +628,10 @@ function M.setup_keymaps() -- Environment management if keymaps.venv then - if _G.Snacks and _G.Snacks.picker then - vim.api.nvim_set_keymap( - "n", - prefix .. "e", - "lua Snacks.picker.pick('uv_venv')", - { noremap = true, silent = true, desc = "UV Environment" } - ) - end - local has_telescope_venv = pcall(require, "telescope") - if has_telescope_venv then - vim.api.nvim_set_keymap( - "n", - prefix .. "e", - "lua require('uv').pick_uv_venv()", - { noremap = true, silent = true, desc = "UV Environment (Telescope)" } - ) + local venv_keymap, picker_name = picker.get_venv_keymap(M.config.picker_integration) + if venv_keymap then + local desc = picker_name == "snacks" and "UV Environment" or "UV Environment (Telescope)" + vim.api.nvim_set_keymap("n", prefix .. "e", venv_keymap, { noremap = true, silent = true, desc = desc }) end end @@ -989,8 +721,8 @@ function M.setup(opts) M.setup_autocommands() end - -- Set up pickers if integration is enabled - if M.config.picker_integration then + -- Set up pickers if integration is enabled (false disables, anything else enables) + if M.config.picker_integration ~= false then M.setup_pickers() end diff --git a/lua/uv/picker/config.lua b/lua/uv/picker/config.lua new file mode 100644 index 0000000..62c147b --- /dev/null +++ b/lua/uv/picker/config.lua @@ -0,0 +1,74 @@ +-- Picker configuration module +-- Handles validation and normalization of picker_integration config + +local M = {} + +---@alias PickerType "auto"|"snacks"|"telescope"|"fzf-lua"|false + +-- Valid picker values +local VALID_PICKERS = { + ["auto"] = true, + ["snacks"] = true, + ["telescope"] = true, + ["fzf-lua"] = true, +} + +---Check if a value is a valid picker configuration +---@param value any +---@return boolean +function M.is_valid(value) + if value == false then + return true + end + if value == true then + return true -- backwards compatibility + end + if type(value) ~= "string" then + return false + end + return VALID_PICKERS[value] == true +end + +---Normalize picker configuration value +---Converts legacy boolean true to "auto", handles invalid values +---@param value any +---@return PickerType +function M.normalize(value) + if value == false then + return false + end + if value == true then + return "auto" -- backwards compatibility + end + if type(value) == "string" and VALID_PICKERS[value] then + return value + end + return "auto" -- default for invalid values +end + +---Check which pickers are available in the current environment +---@return table +function M.get_available_pickers() + local available = {} + + -- Check for Snacks + if _G.Snacks and _G.Snacks.picker then + available.snacks = true + end + + -- Check for Telescope + local has_telescope = pcall(require, "telescope") + if has_telescope then + available.telescope = true + end + + -- Check for fzf-lua + local has_fzf = pcall(require, "fzf-lua") + if has_fzf then + available["fzf-lua"] = true + end + + return available +end + +return M diff --git a/lua/uv/picker/fzf-lua.lua b/lua/uv/picker/fzf-lua.lua new file mode 100644 index 0000000..a17e962 --- /dev/null +++ b/lua/uv/picker/fzf-lua.lua @@ -0,0 +1,26 @@ +-- fzf-lua picker integration for uv.nvim +-- This is a stub for future implementation +local M = {} + +---Check if fzf-lua is available +---@return boolean +function M.is_available() + local has_fzf = pcall(require, "fzf-lua") + return has_fzf +end + +---Setup fzf-lua picker (not yet implemented) +---@param callbacks table Table with callback functions +---@return boolean success +function M.setup(callbacks) + if not M.is_available() then + return false + end + + -- TODO: Implement fzf-lua picker support + -- This would register custom pickers similar to telescope/snacks + vim.notify("fzf-lua picker support is not yet implemented. Using fallback.", vim.log.levels.WARN) + return false +end + +return M diff --git a/lua/uv/picker/init.lua b/lua/uv/picker/init.lua new file mode 100644 index 0000000..add183d --- /dev/null +++ b/lua/uv/picker/init.lua @@ -0,0 +1,121 @@ +-- Picker module for uv.nvim +-- Provides a unified interface for picker integrations (Snacks, Telescope, fzf-lua) + +local config = require("uv.picker.config") +local snacks = require("uv.picker.snacks") +local telescope = require("uv.picker.telescope") +local fzf_lua = require("uv.picker.fzf-lua") + +local M = {} + +---@type table|nil Stored telescope picker functions +M._telescope_pickers = nil + +---Resolve which picker to use based on config and availability +---@param picker_config PickerType +---@return string|false The resolved picker name or false if disabled +function M.resolve_picker(picker_config) + local normalized = config.normalize(picker_config) + + if normalized == false then + return false + end + + if normalized ~= "auto" then + -- User explicitly requested a specific picker + return normalized + end + + -- Auto mode: try pickers in priority order + if snacks.is_available() then + return "snacks" + end + + if telescope.is_available() then + return "telescope" + end + + -- No picker available + return false +end + +---Setup pickers based on configuration +---@param picker_config PickerType +---@param callbacks table Callback functions for picker actions +---@return boolean success +function M.setup(picker_config, callbacks) + local picker = M.resolve_picker(picker_config) + + if picker == false then + return false + end + + if picker == "snacks" then + return snacks.setup(callbacks) + end + + if picker == "telescope" then + M._telescope_pickers = telescope.setup(callbacks) + return M._telescope_pickers ~= nil + end + + if picker == "fzf-lua" then + return fzf_lua.setup(callbacks) + end + + return false +end + +---Get the commands picker function for the resolved picker +---@return function|nil +function M.get_commands_picker() + if M._telescope_pickers then + return M._telescope_pickers.pick_uv_commands + end + return nil +end + +---Get the venv picker function for the resolved picker +---@return function|nil +function M.get_venv_picker() + if M._telescope_pickers then + return M._telescope_pickers.pick_uv_venv + end + return nil +end + +---Get keymap command for the commands picker +---@param picker_config PickerType +---@return string|nil keymap_cmd, string|nil picker_name +function M.get_commands_keymap(picker_config) + local picker = M.resolve_picker(picker_config) + + if picker == "snacks" and snacks.is_available() then + return "lua Snacks.picker.pick('uv_commands')", "snacks" + end + + if picker == "telescope" and telescope.is_available() then + return "lua require('uv').pick_uv_commands()", "telescope" + end + + return nil, nil +end + +---Get keymap command for the venv picker +---@param picker_config PickerType +---@return string|nil keymap_cmd, string|nil picker_name +function M.get_venv_keymap(picker_config) + local picker = M.resolve_picker(picker_config) + + if picker == "snacks" and snacks.is_available() then + return "lua Snacks.picker.pick('uv_venv')", "snacks" + end + + if picker == "telescope" and telescope.is_available() then + return "lua require('uv').pick_uv_venv()", "telescope" + end + + return nil, nil +end + +return M diff --git a/lua/uv/picker/snacks.lua b/lua/uv/picker/snacks.lua new file mode 100644 index 0000000..6f91129 --- /dev/null +++ b/lua/uv/picker/snacks.lua @@ -0,0 +1,164 @@ +-- Snacks picker integration for uv.nvim +local M = {} + +---Check if Snacks picker is available +---@return boolean +function M.is_available() + return _G.Snacks ~= nil and _G.Snacks.picker ~= nil +end + +---Get the commands source configuration for Snacks picker +---@param callbacks table Table with run_file, run_python_selection, run_python_function, run_command functions +---@return table +function M.get_commands_source(callbacks) + return { + finder = function() + return { + { text = "Run current file", desc = "Run current file with Python", is_run_current = true }, + { text = "Run selection", desc = "Run selected Python code", is_run_selection = true }, + { text = "Run function", desc = "Run specific Python function", is_run_function = true }, + { text = "uv add [package]", desc = "Install a package" }, + { text = "uv sync", desc = "Sync packages from lockfile" }, + { + text = "uv sync --all-extras --all-packages --all-groups", + desc = "Sync all extras, groups and packages", + }, + { text = "uv remove [package]", desc = "Remove a package" }, + { text = "uv init", desc = "Initialize a new project" }, + } + end, + preview = function(ctx) + local cmd = ctx.item.text:match("^(uv %a+)") + if cmd then + Snacks.picker.preview.cmd(cmd .. " --help", ctx) + else + ctx.preview:set_lines({}) + end + end, + format = function(item) + return { { item.text .. " - " .. item.desc } } + end, + confirm = function(picker, item) + if item then + picker:close() + if item.is_run_current then + if callbacks.run_file then + callbacks.run_file() + end + return + elseif item.is_run_selection then + local mode = vim.fn.mode() + if mode == "v" or mode == "V" or mode == "" then + vim.cmd("normal! \27") + vim.defer_fn(function() + if callbacks.run_python_selection then + callbacks.run_python_selection() + end + end, 100) + else + vim.notify( + "Please select text first. Enter visual mode (v) and select code to run.", + vim.log.levels.INFO + ) + vim.api.nvim_create_autocmd("ModeChanged", { + pattern = "[vV\x16]*:n", + callback = function(_) + if callbacks.run_python_selection then + callbacks.run_python_selection() + end + return true + end, + once = true, + }) + end + return + elseif item.is_run_function then + if callbacks.run_python_function then + callbacks.run_python_function() + end + return + end + + local cmd = item.text + if cmd:match("%[(.-)%]") then + local param_name = cmd:match("%[(.-)%]") + vim.ui.input({ prompt = "Enter " .. param_name .. ": " }, function(input) + if not input or input == "" then + vim.notify("Cancelled", vim.log.levels.INFO) + return + end + local actual_cmd = cmd:gsub("%[" .. param_name .. "%]", input) + if callbacks.run_command then + callbacks.run_command(actual_cmd) + end + end) + else + if callbacks.run_command then + callbacks.run_command(cmd) + end + end + end + end, + } +end + +---Get the venv source configuration for Snacks picker +---@param callbacks table Table with activate_venv, run_command functions +---@return table +function M.get_venv_source(callbacks) + return { + finder = function() + local venvs = {} + if vim.fn.isdirectory(".venv") == 1 then + table.insert(venvs, { + text = ".venv", + path = vim.fn.getcwd() .. "/.venv", + is_current = vim.env.VIRTUAL_ENV and vim.env.VIRTUAL_ENV:match(".venv$") ~= nil, + }) + end + if #venvs == 0 then + table.insert(venvs, { + text = "Create new virtual environment (uv venv)", + is_create = true, + }) + end + return venvs + end, + format = function(item) + if item.is_create then + return { { "+ " .. item.text } } + else + local icon = item.is_current and "● " or "○ " + return { { icon .. item.text .. " (Activate)" } } + end + end, + confirm = function(picker, item) + picker:close() + if item then + if item.is_create then + if callbacks.run_command then + callbacks.run_command("uv venv") + end + else + if callbacks.activate_venv then + callbacks.activate_venv(item.path) + end + end + end + end, + } +end + +---Setup Snacks picker sources +---@param callbacks table Table with callback functions +function M.setup(callbacks) + if not M.is_available() then + return false + end + + Snacks.picker.sources.uv_commands = M.get_commands_source(callbacks) + Snacks.picker.sources.uv_venv = M.get_venv_source(callbacks) + return true +end + +return M diff --git a/lua/uv/picker/telescope.lua b/lua/uv/picker/telescope.lua new file mode 100644 index 0000000..fb9f70a --- /dev/null +++ b/lua/uv/picker/telescope.lua @@ -0,0 +1,194 @@ +-- Telescope picker integration for uv.nvim +local M = {} + +---Check if Telescope is available +---@return boolean +function M.is_available() + local has_telescope = pcall(require, "telescope") + return has_telescope +end + +---Create UV commands picker for Telescope +---@param callbacks table Table with run_file, run_python_selection, run_python_function, run_command functions +---@return function +function M.pick_uv_commands(callbacks) + return function() + local pickers = require("telescope.pickers") + local finders = require("telescope.finders") + local sorters = require("telescope.sorters") + local actions = require("telescope.actions") + local action_state = require("telescope.actions.state") + + local items = { + { text = "Run current file", is_run_current = true }, + { text = "Run selection", is_run_selection = true }, + { text = "Run function", is_run_function = true }, + { text = "uv add [package]", cmd = "uv add ", needs_input = true }, + { text = "uv sync", cmd = "uv sync" }, + { + text = "uv sync --all-extras --all-packages --all-groups", + cmd = "uv sync --all-extras --all-packages --all-groups", + }, + { text = "uv remove [package]", cmd = "uv remove ", needs_input = true }, + { text = "uv init", cmd = "uv init" }, + } + + pickers + .new({}, { + prompt_title = "UV Commands", + finder = finders.new_table({ + results = items, + entry_maker = function(entry) + return { + value = entry, + display = entry.text, + ordinal = entry.text, + } + end, + }), + sorter = sorters.get_generic_fuzzy_sorter(), + attach_mappings = function(prompt_bufnr, map) + local function on_select() + local selection = action_state.get_selected_entry().value + actions.close(prompt_bufnr) + if selection.is_run_current then + if callbacks.run_file then + callbacks.run_file() + end + elseif selection.is_run_selection then + local mode = vim.fn.mode() + if mode == "v" or mode == "V" or mode == "" then + vim.cmd("normal! \27") + vim.defer_fn(function() + if callbacks.run_python_selection then + callbacks.run_python_selection() + end + end, 100) + else + vim.notify( + "Please select text first. Enter visual mode (v) and select code to run.", + vim.log.levels.INFO + ) + vim.api.nvim_create_autocmd("ModeChanged", { + pattern = "[vV\x16]*:n", + callback = function() + if callbacks.run_python_selection then + callbacks.run_python_selection() + end + return true + end, + once = true, + }) + end + elseif selection.is_run_function then + if callbacks.run_python_function then + callbacks.run_python_function() + end + else + if selection.needs_input then + local placeholder = selection.text:match("%[(.-)%]") + vim.ui.input({ prompt = "Enter " .. (placeholder or "value") .. ": " }, function(input) + if input and input ~= "" then + local cmd = selection.cmd .. input + if callbacks.run_command then + callbacks.run_command(cmd) + end + else + vim.notify("Cancelled", vim.log.levels.INFO) + end + end) + else + if callbacks.run_command then + callbacks.run_command(selection.cmd) + end + end + end + end + + map("i", "", on_select) + map("n", "", on_select) + return true + end, + }) + :find() + end +end + +---Create UV venv picker for Telescope +---@param callbacks table Table with activate_venv, run_command functions +---@return function +function M.pick_uv_venv(callbacks) + return function() + local pickers = require("telescope.pickers") + local finders = require("telescope.finders") + local sorters = require("telescope.sorters") + local actions = require("telescope.actions") + local action_state = require("telescope.actions.state") + + local items = {} + if vim.fn.isdirectory(".venv") == 1 then + table.insert(items, { + text = ".venv", + path = vim.fn.getcwd() .. "/.venv", + is_current = vim.env.VIRTUAL_ENV and vim.env.VIRTUAL_ENV:match(".venv$") ~= nil, + }) + end + if #items == 0 then + table.insert(items, { text = "Create new virtual environment (uv venv)", is_create = true }) + end + + pickers + .new({}, { + prompt_title = "UV Virtual Environments", + finder = finders.new_table({ + results = items, + entry_maker = function(entry) + local display = entry.is_create and "+ " .. entry.text + or ((entry.is_current and "● " or "○ ") .. entry.text .. " (Activate)") + return { + value = entry, + display = display, + ordinal = display, + } + end, + }), + sorter = sorters.get_generic_fuzzy_sorter(), + attach_mappings = function(prompt_bufnr, map) + local function on_select() + local selection = action_state.get_selected_entry().value + actions.close(prompt_bufnr) + if selection.is_create then + if callbacks.run_command then + callbacks.run_command("uv venv") + end + else + if callbacks.activate_venv then + callbacks.activate_venv(selection.path) + end + end + end + + map("i", "", on_select) + map("n", "", on_select) + return true + end, + }) + :find() + end +end + +---Setup Telescope picker functions +---@param callbacks table Table with callback functions +---@return table Table with pick_uv_commands and pick_uv_venv functions +function M.setup(callbacks) + if not M.is_available() then + return nil + end + + return { + pick_uv_commands = M.pick_uv_commands(callbacks), + pick_uv_venv = M.pick_uv_venv(callbacks), + } +end + +return M diff --git a/tests/picker_config_spec.lua b/tests/picker_config_spec.lua new file mode 100644 index 0000000..e33de3c --- /dev/null +++ b/tests/picker_config_spec.lua @@ -0,0 +1,81 @@ +-- Tests for picker configuration module +local picker_config = require("uv.picker.config") + +describe("picker.config", function() + describe("valid picker values", function() + it("accepts 'auto' as valid picker", function() + assert.is_true(picker_config.is_valid("auto")) + end) + + it("accepts 'snacks' as valid picker", function() + assert.is_true(picker_config.is_valid("snacks")) + end) + + it("accepts 'telescope' as valid picker", function() + assert.is_true(picker_config.is_valid("telescope")) + end) + + it("accepts 'fzf-lua' as valid picker", function() + assert.is_true(picker_config.is_valid("fzf-lua")) + end) + + it("accepts false to disable picker", function() + assert.is_true(picker_config.is_valid(false)) + end) + + it("rejects invalid string values", function() + assert.is_false(picker_config.is_valid("invalid")) + assert.is_false(picker_config.is_valid("")) + end) + + it("rejects invalid types", function() + assert.is_false(picker_config.is_valid(123)) + assert.is_false(picker_config.is_valid({})) + assert.is_false(picker_config.is_valid(nil)) + end) + + -- Backwards compatibility: true should be treated as "auto" + it("accepts true for backwards compatibility (treated as auto)", function() + assert.is_true(picker_config.is_valid(true)) + end) + end) + + describe("normalize", function() + it("returns auto unchanged", function() + assert.are.equal("auto", picker_config.normalize("auto")) + end) + + it("returns snacks unchanged", function() + assert.are.equal("snacks", picker_config.normalize("snacks")) + end) + + it("returns telescope unchanged", function() + assert.are.equal("telescope", picker_config.normalize("telescope")) + end) + + it("returns fzf-lua unchanged", function() + assert.are.equal("fzf-lua", picker_config.normalize("fzf-lua")) + end) + + it("converts true to auto for backwards compatibility", function() + assert.are.equal("auto", picker_config.normalize(true)) + end) + + it("returns false unchanged (disabled)", function() + assert.is_false(picker_config.normalize(false)) + end) + + it("defaults to auto for invalid values", function() + assert.are.equal("auto", picker_config.normalize("invalid")) + assert.are.equal("auto", picker_config.normalize(nil)) + assert.are.equal("auto", picker_config.normalize(123)) + end) + end) + + describe("get_available_pickers", function() + it("returns a table", function() + local available = picker_config.get_available_pickers() + assert.is_table(available) + end) + end) +end)