From 9f926ebe6d9fca8c2a737accb797b5452ce7fb0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tautvydas=20S=CC=8Cidlauskas?= Date: Sat, 26 Sep 2026 20:28:45 +0300 Subject: [PATCH] feat(menu): add FlutterCommands and FlutterFvm pickers using vim.ui.select - Both use whatever vim.ui.select UI is configured (snacks, fzf-lua, mini.pick, ...), with hints aligned in one column - Remove the telescope fvm picker (`Telescope flutter fvm`), replaced by FlutterFvm - Drop dressing.nvim (archived): its telescope option in ui.select and the README recommendation --- README.md | 19 ++-- lua/flutter-tools.lua | 3 + lua/flutter-tools/menu.lua | 82 +++++++++-------- lua/flutter-tools/ui.lua | 39 --------- lua/telescope/_extensions/flutter.lua | 1 - tests/menu_spec.lua | 121 ++++++++++++++++++++++++++ 6 files changed, 175 insertions(+), 90 deletions(-) create mode 100644 tests/menu_spec.lua diff --git a/README.md b/README.md index 3604c087..4060507a 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,6 @@ using `lazy.nvim` lazy = false, dependencies = { 'nvim-lua/plenary.nvim', - 'stevearc/dressing.nvim', -- optional for vim.ui.select }, config = true, } @@ -67,7 +66,6 @@ using `vim.pack` (built into Neovim 0.12+, see `:h vim.pack`) ```lua vim.pack.add({ 'https://github.com/nvim-lua/plenary.nvim', - 'https://github.com/stevearc/dressing.nvim', -- optional for vim.ui.select 'https://github.com/nvim-flutter/flutter-tools.nvim', }) @@ -77,8 +75,10 @@ require('flutter-tools').setup {} This plugin depends on [plenary.nvim](https://github.com/nvim-lua/plenary.nvim), please make sure it is installed. This plugin depends on `vim.ui.select` which allows users to control what UI is used for selecting -from a list of options. If you don't have a UI configured for `vim.ui.select` then I highly recommend -the excellent [dressing.nvim](https://github.com/stevearc/dressing.nvim). +from a list of options. If you don't have a UI configured for `vim.ui.select`, pickers such as +[snacks.nvim](https://github.com/folke/snacks.nvim), [fzf-lua](https://github.com/ibhagwan/fzf-lua), +[mini.pick](https://github.com/echasnovski/mini.pick) or +[telescope-ui-select](https://github.com/nvim-telescope/telescope-ui-select.nvim) can provide one. ## Warning @@ -143,6 +143,8 @@ require("flutter-tools").setup {} -- use defaults # Usage +- `FlutterCommands` - Pick a flutter-tools command from a list, shown with your `vim.ui.select` UI (e.g. snacks.nvim, fzf-lua or mini.pick). +- `FlutterFvm` - Pick a Flutter SDK installed with [fvm](https://fvm.app/) and switch the project to it. - `FlutterRun` - Run the current project. Respects `config.debugger.enabled` setting. - `FlutterDebug` - Force run current project in debug mode. - `FlutterDevices` - Brings up a list of connected devices to select from. @@ -474,6 +476,7 @@ see `:h statusline` for more information on how to create a statusline. ![telescope picker](https://user-images.githubusercontent.com/22454918/113897929-495a3e80-97c3-11eb-959f-9574319cd93c.png) You can list available commands in this plugin using [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim). +`FlutterCommands` shows the same list with whatever `vim.ui.select` UI you use. In order to set this up, you can explicitly load the extension. @@ -485,14 +488,6 @@ Or alternatively telescope can lazy load extension but the `Telescope` command w This can be accessed using `Telescope flutter commands` or `require('telescope').extensions.flutter.commands()` -#### FVM - -![telescope fvm](https://user-images.githubusercontent.com/35163478/137667084-98c00c4b-ff8c-4d1e-869e-d2d51cf86f7e.png) - -If you have [fvm](https://fvm.app/) installed and enabled in your config, you can change your Flutter SDK via a Telescope picker. - -This can be accessed using `Telescope flutter fvm` or `require('telescope').extensions.flutter.fvm()` - ## Debugging _Requires nvim-dap_ diff --git a/lua/flutter-tools.lua b/lua/flutter-tools.lua index e71ac66b..a24530f7 100644 --- a/lua/flutter-tools.lua +++ b/lua/flutter-tools.lua @@ -9,6 +9,7 @@ local dap = lazy.require("flutter-tools.dap") ---@module "flutter-tools.dap" local decorations = lazy.require("flutter-tools.decorations") ---@module "flutter-tools.decorations" local guides = lazy.require("flutter-tools.guides") ---@module "flutter-tools.guides" local log = lazy.require("flutter-tools.log") ---@module "flutter-tools.log" +local menu = lazy.require("flutter-tools.menu") ---@module "flutter-tools.menu" local lsp = lazy.require("flutter-tools.lsp") ---@module "flutter-tools.lsp" local outline = lazy.require("flutter-tools.outline") ---@module "flutter-tools.outline" local devices = lazy.require("flutter-tools.devices") ---@module "flutter-tools.devices" @@ -34,6 +35,8 @@ local function setup_commands() command("FlutterVisualDebug", commands.visual_debug) command("FlutterChangeTargetPlatform", commands.change_target_platform) command("FlutterToggleBrightness", commands.brightness) + command("FlutterCommands", menu.select_command) + command("FlutterFvm", menu.select_fvm) -- Lists command("FlutterDevices", devices.list_devices) command("FlutterEmulators", devices.list_emulators) diff --git a/lua/flutter-tools/menu.lua b/lua/flutter-tools/menu.lua index 5e5adac7..17f97c7e 100644 --- a/lua/flutter-tools/menu.lua +++ b/lua/flutter-tools/menu.lua @@ -10,21 +10,23 @@ local commands = lazy.require("flutter-tools.commands") ---@module "flutter-tool local ui = lazy.require("flutter-tools.ui") ---@module "flutter-tools.ui" ---@alias TelescopeEntry {hint: string, label: string, command: fun(), id: integer} ----@alias CustomOptions {title: string, callback: fun(bufnr: integer)} +---@alias CustomOptions {title: string} local M = {} -- Accounts for the vertical padding implicit in the dropdown. local MENU_PADDING = 4 +local function run(cmd, ...) + if not cmd then return end + local success, msg = pcall(cmd, ...) + if not success then ui.notify(msg, ui.ERROR) end +end + local function execute_command(bufnr) local selection = action_state.get_selected_entry() actions.close(bufnr) - local cmd = selection.command - if cmd then - local success, msg = pcall(cmd) - if not success then ui.notify(msg, ui.ERROR) end - end + run(selection.command) end local function command_entry_maker(max_width) @@ -65,7 +67,6 @@ end ---@param opts CustomOptions ---@return table local function picker_opts(items, opts) - local callback = opts.callback or execute_command return { prompt_title = opts.title, finder = finders.new_table({ @@ -74,8 +75,8 @@ local function picker_opts(items, opts) }), sorter = sorters.get_generic_fuzzy_sorter(), attach_mappings = function(_, map) - map("i", "", callback) - map("n", "", callback) + map("i", "", execute_command) + map("n", "", execute_command) -- If the return value of `attach_mappings` is true, then the other -- default mappings are still applies. -- Return false if you don't want any other mappings applied. @@ -97,7 +98,8 @@ function M.get_config(items, user_opts, opts) })) end -function M.commands(opts) +---@return TelescopeEntry[] +local function command_items() local cmds = {} if commands.is_running() then @@ -207,7 +209,7 @@ function M.commands(opts) }, { id = "flutter-tools-generate", - label = "Generate ", + label = "Generate", hint = "Generate code", command = commands.generate, }, @@ -265,45 +267,49 @@ function M.commands(opts) }) end - pickers.new(M.get_config(cmds, opts, { title = "Flutter tools commands" })):find() + return cmds end -local function execute_fvm_use(bufnr) - local selection = action_state.get_selected_entry() - actions.close(bufnr) - local cmd = selection.command - if cmd then - local success, msg = pcall(cmd, selection.ordinal) - if not success then ui.notify(msg, ui.ERROR) end +function M.commands(opts) + pickers.new(M.get_config(command_items(), opts, { title = "Flutter tools commands" })):find() +end + +---@param items TelescopeEntry[] +---@param prompt string +---@param on_choice fun(item: TelescopeEntry) +local function select(items, prompt, on_choice) + local width = 0 + for _, item in ipairs(items) do + width = math.max(width, vim.api.nvim_strwidth(item.label)) end + vim.ui.select(items, { + prompt = prompt, + kind = "flutter-tools", + format_item = function(item) + if not item.hint or item.hint == "" then return item.label end + local padding = string.rep(" ", width - vim.api.nvim_strwidth(item.label)) + return item.label .. padding .. " • " .. item.hint + end, + }, function(item) + if item then on_choice(item) end + end) end -function M.fvm(opts) - commands.fvm_list(function(sdks) - opts = opts and not vim.tbl_isempty(opts) and opts - or themes.get_dropdown({ - previewer = false, - layout_config = { - height = #sdks + MENU_PADDING, - }, - }) +function M.select_command() + select(command_items(), "Flutter tools commands", function(item) run(item.command) end) +end - local sdk_entries = {} +function M.select_fvm() + commands.fvm_list(function(sdks) + local items = {} for _, sdk in pairs(sdks) do - table.insert(sdk_entries, { + table.insert(items, { id = sdk.name, label = sdk.name, hint = sdk.dart_sdk_version and "(Dart SDK " .. sdk.dart_sdk_version .. ")" or "", - command = commands.fvm_use, }) end - - pickers - .new(M.get_config(sdk_entries, opts, { - title = "Change Flutter SDK", - callback = execute_fvm_use, - })) - :find() + select(items, "Change Flutter SDK", function(item) run(commands.fvm_use, item.id) end) end) end diff --git a/lua/flutter-tools/ui.lua b/lua/flutter-tools/ui.lua index 99ee37a2..4be3638f 100644 --- a/lua/flutter-tools/ui.lua +++ b/lua/flutter-tools/ui.lua @@ -95,43 +95,6 @@ end ---@param on_confirm function M.input = function(opts, on_confirm) vim.ui.input(opts, on_confirm) end ---- @param items SelectionEntry[] ---- @param title string ---- @param on_select fun(item: SelectionEntry) -local function get_telescope_picker_config(items, title, on_select) - local ok = pcall(require, "telescope") - if not ok then return end - - local filtered = vim.tbl_filter(function(value) return value.data ~= nil end, items) --[[@as SelectionEntry[]] - - return require("flutter-tools.menu").get_config( - vim.tbl_map(function(item) - local data = item.data - if item.type == entry_type.CODE_ACTION then - return { - id = data.title, - label = data.title, - command = function() on_select(data) end, - } - elseif item.type == entry_type.DEVICE then - return { - id = data.id, - label = data.name, - hint = data.platform, - command = function() on_select(data) end, - } - elseif item.type == entry_type.INFO then - return { - id = item.text, - label = item.text, - command = function() end, - } - end - end, filtered), - { title = title } - ) -end - ---@alias PopupOpts {title:string, lines: SelectionEntry[], on_select: fun(item: SelectionEntry)} ---@param opts PopupOpts function M.select(opts) @@ -143,8 +106,6 @@ function M.select(opts) prompt = title, kind = "flutter-tools", format_item = function(item) return item.text end, - -- custom key for dressing.nvim - telescope = get_telescope_picker_config(lines, title, on_select), }, function(item) if not item or item.data == nil then return end on_select(item.data) diff --git a/lua/telescope/_extensions/flutter.lua b/lua/telescope/_extensions/flutter.lua index fc798a8b..44d4400a 100644 --- a/lua/telescope/_extensions/flutter.lua +++ b/lua/telescope/_extensions/flutter.lua @@ -8,6 +8,5 @@ end return telescope.register_extension({ exports = { commands = menu.commands, - fvm = menu.fvm, }, }) diff --git a/tests/menu_spec.lua b/tests/menu_spec.lua new file mode 100644 index 00000000..04b1a3e2 --- /dev/null +++ b/tests/menu_spec.lua @@ -0,0 +1,121 @@ +describe("menu.select_command", function() + local menu + local original_select + local notifications + local shown + + before_each(function() + original_select = vim.ui.select + notifications = {} + shown = nil + package.loaded["flutter-tools.menu"] = nil + package.loaded["flutter-tools.ui"] = { + ERROR = vim.log.levels.ERROR, + notify = function(msg, level) table.insert(notifications, { msg = msg, level = level }) end, + } + vim.ui.select = function(items, opts, on_choice) + shown = { items = items, opts = opts, on_choice = on_choice } + end + menu = require("flutter-tools.menu") + end) + + after_each(function() + vim.ui.select = original_select + package.loaded["flutter-tools.menu"] = nil + package.loaded["flutter-tools.ui"] = nil + end) + + local function find_item(label) + for _, item in ipairs(shown.items) do + if item.label == label then return item end + end + end + + it("lists the commands through vim.ui.select with their hints", function() + menu.select_command() + + local run = find_item("Run") + assert.is_not_nil(run) + assert.equal("flutter-tools", shown.opts.kind) + assert.truthy(shown.opts.format_item(run):match("^Run +• Start a flutter project$")) + end) + + it("aligns the hints in one column", function() + menu.select_command() + + local columns = {} + for _, item in ipairs(shown.items) do + local column = shown.opts.format_item(item):find(" • ", 1, true) + if column then columns[column] = true end + end + assert.equal(1, vim.tbl_count(columns)) + end) + + it("runs the chosen command", function() + menu.select_command() + local ran = false + shown.on_choice({ label = "Test", command = function() ran = true end }) + assert.is_true(ran) + end) + + it("reports a failing command instead of raising", function() + menu.select_command() + shown.on_choice({ label = "Test", command = function() error("boom") end }) + assert.equal(1, #notifications) + assert.equal(vim.log.levels.ERROR, notifications[1].level) + assert.truthy(notifications[1].msg:find("boom")) + end) + + it("does nothing when the selection is cancelled", function() + menu.select_command() + shown.on_choice(nil) + assert.equal(0, #notifications) + end) +end) + +describe("menu.select_fvm", function() + local menu + local original_select + local used + local shown + + before_each(function() + original_select = vim.ui.select + used = nil + shown = nil + package.loaded["flutter-tools.menu"] = nil + package.loaded["flutter-tools.commands"] = { + fvm_list = function(callback) + callback({ + { name = "3.24.0", dart_sdk_version = "3.5.0" }, + { name = "stable" }, + }) + end, + fvm_use = function(name) used = name end, + } + vim.ui.select = function(items, opts, on_choice) + shown = { items = items, opts = opts, on_choice = on_choice } + end + menu = require("flutter-tools.menu") + end) + + after_each(function() + vim.ui.select = original_select + package.loaded["flutter-tools.menu"] = nil + package.loaded["flutter-tools.commands"] = nil + end) + + it("lists the fvm SDKs with their Dart version", function() + menu.select_fvm() + + assert.equal(2, #shown.items) + assert.equal("3.24.0 • (Dart SDK 3.5.0)", shown.opts.format_item(shown.items[1])) + assert.equal("stable", shown.opts.format_item(shown.items[2])) + end) + + it("switches to the chosen SDK", function() + menu.select_fvm() + shown.on_choice(shown.items[1]) + assert.equal("3.24.0", used) + end) +end)