diff --git a/README.md b/README.md index c6dda0b..8547a98 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,25 @@ return { -- Dependencies are lazy-loaded by default unless specified otherwise. dependencies = { - { 'nvim-telescope/telescope.nvim' }, - { 'nvim-telescope/telescope-ui-select.nvim' }, + { 'akinsho/toggleterm.nvim' }, { 'nvim-lua/plenary.nvim' }, { 'folke/which-key.nvim' }, - { 'nvim-treesitter/nvim-treesitter' } + { 'nvim-treesitter/nvim-treesitter' }, - -- install picker based on your likeing - { 'akinsho/toggleterm.nvim' }, + + -- install picker based on your likeing, remove or ignore rest + + -- telescope + { 'nvim-telescope/telescope.nvim' }, + { 'nvim-telescope/telescope-ui-select.nvim' }, + + -- Snacks picker + {'folke/snacks.nvim', opts = { + picker = {enabled = true, ui_select = true } + }}, + + -- Mini.Pick + {'nvim-mini/mini.pick'}, }, } ``` @@ -55,7 +66,8 @@ return { vim.g.pioConfig ={ lsp = 'clangd', -- value: clangd | ccls clangd_source = 'ccls', -- value: ccls | compiledb, For detailed explation check :help platformio-clangd_source - picker_backend = 'auto', -- value: auto | telescope | ui_select, default: auto, check :help platformio-picker + picker_backend = 'auto', -- value: auto | telescope | snacks | mini_pick | ui_select + -- default: auto, check :help platformio-picker menu_key = '\\', -- replace this menu key to your convenience debug = false -- enable debug messages } @@ -111,11 +123,23 @@ return { -- Dependencies are lazy-loaded by default unless specified otherwise. dependencies = { { 'akinsho/toggleterm.nvim' }, - { 'nvim-telescope/telescope.nvim' }, - { 'nvim-telescope/telescope-ui-select.nvim' }, { 'nvim-lua/plenary.nvim' }, { 'folke/which-key.nvim' }, - { 'nvim-treesitter/nvim-treesitter' } + { 'nvim-treesitter/nvim-treesitter' }, + + -- install picker based on your likeing, remove or ignore rest + + -- telescope + { 'nvim-telescope/telescope.nvim' }, + { 'nvim-telescope/telescope-ui-select.nvim' }, + + -- Snacks picker + {'folke/snacks.nvim', opts = { + picker = {enabled = true, ui_select = true } + }}, + + -- Mini.Pick + {'nvim-mini/mini.pick'}, }, } ``` diff --git a/doc/platformio.txt b/doc/platformio.txt index 82a74de..8253d3e 100644 --- a/doc/platformio.txt +++ b/doc/platformio.txt @@ -141,17 +141,31 @@ picker_backend *platformio-picker* Allowed values: - `auto` - Default. Tries Telescope backend first. If Telescope is unavailable, - falls back to `vim.ui.select` backend. + Default. Tries backends in this order, using the first one that is + installed: `telescope`, then `snacks`, then `mini_pick`, then + `ui_select`. `ui_select` is always available since it only needs + Neovim's built-in `vim.ui.select`, so `auto` never fails to find + a backend. - `telescope` - Force Telescope picker backend. If Telescope backend cannot be loaded, - plugin falls back to `vim.ui.select` as a safety fallback. + Force the Telescope picker backend (requires telescope.nvim). + + - `snacks` + Force the Snacks picker backend (requires folke/snacks.nvim with + its `picker` module enabled). + + - `mini_pick` + Force the mini.pick backend (requires nvim-mini/mini.pick). - `ui_select` Force `vim.ui.select` backend. This works with Neovim built-in `vim.ui.select` or any plugin that overrides it (for example, - telescope-ui-select, dressing.nvim, snacks picker adapters, etc.). + telescope-ui-select, dressing.nvim, snacks' `ui_select` option, + mini.pick's `MiniPick.ui_select`, etc.). + + Unlike `auto`, explicitly requesting `telescope`, `snacks`, `mini_pick`, + or `ui_select` does NOT fall back to another backend if the requested + one isn't installed—it reports an error and the picker does not open. menu_key *platformio-menu_key* Key prefix that opens the PlatformIO which-key menu (requires diff --git a/lua/platformio/init.lua b/lua/platformio/init.lua index 8f0e5bd..6ed6840 100644 --- a/lua/platformio/init.lua +++ b/lua/platformio/init.lua @@ -189,17 +189,22 @@ function M.setup(user_config) user_config.clangd_source = M.config.clangd_source end end - if - user_config.picker_backend - and user_config.picker_backend ~= 'auto' - and user_config.picker_backend ~= 'telescope' - and user_config.picker_backend ~= 'ui_select' - then - vim.api.nvim_echo( - { { 'Invalid picker backend {allowed "auto", "telescope" or "ui_select"} (default "' .. M.config.picker_backend .. '" will be used)', 'ErrorMsg' } }, - true, - {} - ) + local valid_picker_backends = { + auto = true, + telescope = true, + snacks = true, + mini_pick = true, + ui_select = true, + } + if user_config.picker_backend and not valid_picker_backends[user_config.picker_backend] then + vim.api.nvim_echo({ + { + 'Invalid picker backend {allowed "auto", "telescope", "snacks", "mini_pick" or "ui_select"} (default "' + .. M.config.picker_backend + .. '" will be used)', + 'ErrorMsg', + }, + }, true, {}) user_config.picker_backend = M.config.picker_backend end diff --git a/lua/platformio/pickers/init.lua b/lua/platformio/pickers/init.lua index b9e6163..508dd0a 100644 --- a/lua/platformio/pickers/init.lua +++ b/lua/platformio/pickers/init.lua @@ -2,6 +2,8 @@ local M = {} local config = require('platformio').config +local AUTO_ORDER = { 'telescope', 'snacks', 'mini_pick', 'ui_select' } + local function load_backend(name) local ok, backend = pcall(require, 'platformio.pickers.' .. name) if ok then @@ -10,54 +12,55 @@ local function load_backend(name) return nil end +-- 'auto' tries backends in order and silently falls back; an explicit backend +-- name that isn't installed reports why and does not fall back to another one. local function get_backend() local backend_name = config.picker_backend or 'auto' - if backend_name == 'telescope' then - return load_backend('telescope') or load_backend('ui_select') + if backend_name == 'auto' then + for _, name in ipairs(AUTO_ORDER) do + local backend = load_backend(name) + if backend then + return backend + end + end + vim.notify('No picker backend available for PlatformIO.', vim.log.levels.ERROR) + return nil end - if backend_name == 'ui_select' then - return load_backend('ui_select') + local backend = load_backend(backend_name) + if not backend then + vim.notify(string.format('PlatformIO: picker backend "%s" is not installed.', backend_name), vim.log.levels.ERROR) end - - return load_backend('telescope') or load_backend('ui_select') + return backend end function M.pick_board(boards, on_select) local backend = get_backend() - if not backend then - vim.notify('No picker backend available for PlatformIO.', vim.log.levels.ERROR) - return + if backend then + backend.pick_board(boards, on_select) end - backend.pick_board(boards, on_select) end function M.pick_framework(frameworks, on_select) local backend = get_backend() - if not backend then - vim.notify('No picker backend available for PlatformIO.', vim.log.levels.ERROR) - return + if backend then + backend.pick_framework(frameworks, on_select) end - backend.pick_framework(frameworks, on_select) end function M.pick_library(libraries, on_select) local backend = get_backend() - if not backend then - vim.notify('No picker backend available for PlatformIO.', vim.log.levels.ERROR) - return + if backend then + backend.pick_library(libraries, on_select) end - backend.pick_library(libraries, on_select) end function M.pick_terminal(terminals, on_select) local backend = get_backend() - if not backend then - vim.notify('No picker backend available for PlatformIO.', vim.log.levels.ERROR) - return + if backend then + backend.pick_terminal(terminals, on_select) end - backend.pick_terminal(terminals, on_select) end return M diff --git a/lua/platformio/pickers/mini_pick.lua b/lua/platformio/pickers/mini_pick.lua new file mode 100644 index 0000000..5ecdc93 --- /dev/null +++ b/lua/platformio/pickers/mini_pick.lua @@ -0,0 +1,116 @@ +local MiniPick = require('mini.pick') + +local M = {} + +local SEP = ' | ' +local COLUMN_NS = vim.api.nvim_create_namespace('platformio_pick_columns') + +local function to_items(list, label_fn) + local items = {} + for i, entry in ipairs(list) do + items[i] = { text = label_fn(entry), data = entry } + end + return items +end + +local function pad(text, width) + text = text or '' + if #text >= width then + return text:sub(1, width) + end + return text .. string.rep(' ', width - #text) +end + +local function framework_label(framework) + return framework +end + +local BOARD_NAME_W, BOARD_VENDOR_W = 35, 20 + +local function board_label(board) + return pad(board.name or '', BOARD_NAME_W) .. SEP .. pad(board.vendor or '', BOARD_VENDOR_W) .. SEP .. (board.platform or '') +end + +local LIB_NAME_W, LIB_OWNER_W = 20, 20 + +local function library_label(lib) + local owner = (lib.owner and lib.owner.username) or '' + return pad(lib.name or '', LIB_NAME_W) .. SEP .. pad(owner, LIB_OWNER_W) .. SEP .. (lib.description or '') +end + +local function terminal_label(entry) + local is_hidden = vim.api.nvim_buf_is_loaded(entry.term.bufnr) and (vim.fn.bufwinid(entry.term.bufnr) == -1) + return string.format('%d:%s (hidden: %s)', entry.term.id, entry.termtype, tostring(is_hidden)) +end + +local function data_preview(buf_id, item) + vim.bo[buf_id].filetype = 'lua' + vim.bo[buf_id].modifiable = true + vim.api.nvim_buf_set_lines(buf_id, 0, -1, false, vim.split(vim.inspect(item.data), '\n')) + vim.bo[buf_id].modifiable = false +end + +-- Colors the two padded columns and the separators on top of mini.pick's default +-- rendering (which still handles line text + query match highlighting). +local function columns_show(col1_w, col2_w, hl1, hl2) + return function(buf_id, items, query) + MiniPick.default_show(buf_id, items, query) + vim.api.nvim_buf_clear_namespace(buf_id, COLUMN_NS, 0, -1) + local sep_len = #SEP + local col2_start = col1_w + sep_len + local col2_end = col2_start + col2_w + local opts = { hl_mode = 'combine', priority = 100 } + for i = 1, #items do + local row = i - 1 + pcall(vim.api.nvim_buf_set_extmark, buf_id, COLUMN_NS, row, 0, vim.tbl_extend('force', opts, { end_col = col1_w, hl_group = hl1 })) + pcall(vim.api.nvim_buf_set_extmark, buf_id, COLUMN_NS, row, col1_w, vim.tbl_extend('force', opts, { end_col = col2_start, hl_group = 'Delimiter' })) + pcall(vim.api.nvim_buf_set_extmark, buf_id, COLUMN_NS, row, col2_start, vim.tbl_extend('force', opts, { end_col = col2_end, hl_group = hl2 })) + pcall( + vim.api.nvim_buf_set_extmark, + buf_id, + COLUMN_NS, + row, + col2_end, + vim.tbl_extend('force', opts, { end_col = col2_end + sep_len, hl_group = 'Delimiter' }) + ) + end + end +end + +local board_show = columns_show(BOARD_NAME_W, BOARD_VENDOR_W, 'Title', 'Comment') +local library_show = columns_show(LIB_NAME_W, LIB_OWNER_W, 'Title', 'Comment') + +local function open_picker(items, name, on_select, preview_fn, show_fn) + MiniPick.start({ + source = { + items = items, + name = name, + preview = preview_fn, + show = show_fn, + choose = function(item) + if item then + on_select(item.data) + end + return false + end, + }, + }) +end + +function M.pick_framework(frameworks, on_select) + open_picker(to_items(frameworks, framework_label), 'Frameworks', on_select) +end + +function M.pick_board(boards, on_select) + open_picker(to_items(boards, board_label), 'Boards', on_select, data_preview, board_show) +end + +function M.pick_library(libraries, on_select) + open_picker(to_items(libraries, library_label), 'Libraries', on_select, data_preview, library_show) +end + +function M.pick_terminal(terminals, on_select) + open_picker(to_items(terminals, terminal_label), 'PIO terminals', on_select) +end + +return M diff --git a/lua/platformio/pickers/snacks.lua b/lua/platformio/pickers/snacks.lua new file mode 100644 index 0000000..8bd4c8b --- /dev/null +++ b/lua/platformio/pickers/snacks.lua @@ -0,0 +1,114 @@ +local Snacks = require('snacks') +local align = Snacks.picker.util.align + +local M = {} + +local SEP = ' ▏' + +local function to_items(list, text_fn) + local items = {} + for i, entry in ipairs(list) do + items[i] = { idx = i, text = text_fn(entry), data = entry } + end + return items +end + +local function plain_format(item) + return { { item.text } } +end + +local function framework_text(framework) + return framework +end + +local function board_text(board) + return string.format('%s %s %s', board.name or '', board.vendor or '', board.platform or '') +end + +local function board_format(item) + local board = item.data + return { + { align(board.name or '', 35, { truncate = true }), 'SnacksPickerLabel' }, + { SEP, 'SnacksPickerDelim' }, + { align(board.vendor or '', 20, { truncate = true }), 'SnacksPickerComment' }, + { SEP, 'SnacksPickerDelim' }, + { board.platform or '' }, + } +end + +local function library_text(lib) + local owner = (lib.owner and lib.owner.username) or '' + return string.format('%s %s %s', lib.name or '', owner, lib.description or '') +end + +local function library_format(item) + local lib = item.data + local owner = (lib.owner and lib.owner.username) or '' + return { + { align(lib.name or '', 20, { truncate = true }), 'SnacksPickerLabel' }, + { SEP, 'SnacksPickerDelim' }, + { align(owner, 20, { truncate = true }), 'SnacksPickerComment' }, + { SEP, 'SnacksPickerDelim' }, + { lib.description or '' }, + } +end + +local function terminal_text(entry) + local is_hidden = vim.api.nvim_buf_is_loaded(entry.term.bufnr) and (vim.fn.bufwinid(entry.term.bufnr) == -1) + return string.format('%d:%s (hidden: %s)', entry.term.id, entry.termtype, tostring(is_hidden)) +end + +local function terminal_format(item) + local entry = item.data + local is_hidden = vim.api.nvim_buf_is_loaded(entry.term.bufnr) and (vim.fn.bufwinid(entry.term.bufnr) == -1) + return { + { align(tostring(entry.term.id), 3, { align = 'right' }), 'SnacksPickerLabel' }, + { SEP, 'SnacksPickerDelim' }, + { align(entry.termtype, 8), 'SnacksPickerComment' }, + { SEP, 'SnacksPickerDelim' }, + { 'hidden: ' .. tostring(is_hidden) }, + } +end + +local function data_preview(ctx) + vim.bo[ctx.buf].filetype = 'lua' + vim.bo[ctx.buf].modifiable = true + vim.api.nvim_buf_set_lines(ctx.buf, 0, -1, false, vim.split(vim.inspect(ctx.item.data), '\n')) + vim.bo[ctx.buf].modifiable = false + return true +end + +local function open_picker(items, title, on_select, format_fn, preview_fn, layout) + Snacks.picker.pick({ + title = title, + items = items, + format = format_fn or plain_format, + preview = preview_fn, + layout = layout, + confirm = function(picker, item) + picker:close() + if item then + on_select(item.data) + end + end, + }) +end + +function M.pick_framework(frameworks, on_select) + -- a bare framework name has nothing worth previewing, so skip the preview pane entirely + open_picker(to_items(frameworks, framework_text), 'Frameworks', on_select, nil, nil, { preview = false }) +end + +function M.pick_board(boards, on_select) + open_picker(to_items(boards, board_text), 'Boards', on_select, board_format, data_preview) +end + +function M.pick_library(libraries, on_select) + open_picker(to_items(libraries, library_text), 'Libraries', on_select, library_format, data_preview) +end + +function M.pick_terminal(terminals, on_select) + open_picker(to_items(terminals, terminal_text), 'PIO terminals', on_select, terminal_format, data_preview) +end + +return M