diff --git a/CHANGELOG.md b/CHANGELOG.md index 66ef381..8bec1f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on Keep a Changelog and this project adheres to SemVer. ## [Unreleased] +### Changed + +- Filter inputs now use the plugin's live-updating floating prompt when + Telescope is unavailable, with the floating prompt label derived from each + filter's existing prompt title. + ## [0.6.0] - 2026-03-25 ### Added diff --git a/lua/android/ui/input.lua b/lua/android/ui/input.lua index cb445a7..a03a99d 100644 --- a/lua/android/ui/input.lua +++ b/lua/android/ui/input.lua @@ -1,7 +1,21 @@ local M = {} -local function calc_width(prompt, default) - local base = math.max(#prompt + #default + 4, 24) +-- Wide enough for common tag, package, and short message filters without a +-- config knob for one prompt helper. +local MIN_WIDTH = 56 + +local function display_width(value) + if not value or value == "" then + return 0 + end + + return vim.fn.strdisplaywidth(value) +end + +local function calc_width(prompt, default, title) + local content_width = display_width(prompt) + display_width(default) + local title_width = display_width(title) + local base = math.max(content_width + 4, title_width + 4, MIN_WIDTH) local max = math.max(20, vim.o.columns - 4) return math.min(base, max) end @@ -43,7 +57,7 @@ function M.prompt(opts) vim.api.nvim_buf_set_lines(buf, 0, -1, false, { default }) end - local width = calc_width(prompt, default) + local width = calc_width(prompt, default, title) local height = 1 local row, col = center_position(width, height) local win = vim.api.nvim_open_win(buf, true, { @@ -118,7 +132,7 @@ function M.prompt(opts) }) end - vim.cmd("startinsert") + vim.cmd("startinsert!") end return M diff --git a/lua/android/ui/picker.lua b/lua/android/ui/picker.lua index 69e75d5..316edc0 100644 --- a/lua/android/ui/picker.lua +++ b/lua/android/ui/picker.lua @@ -70,28 +70,24 @@ local function filter_results_by_query(results, format, query) end local function fallback_filter_input(options) - if not vim.ui or not vim.ui.input then - vim.notify("vim.ui.input not available", vim.log.levels.WARN) - return + local title = options.prompt_title or "Filter" + local label = title + if label:sub(-1) ~= ":" then + label = label .. ":" end - - vim.ui.input({ - prompt = options.prompt_title or "Filter", + local input = require("android.ui.input") + input.prompt({ + title = label, + prompt = "", default = options.default or "", - }, function(value) - if value == nil then - if options.on_cancel then - options.on_cancel() + on_change = options.on_change, + on_submit = function(value) + if options.on_accept then + options.on_accept(value) end - return - end - if options.on_change then - options.on_change(value) - end - if options.on_accept then - options.on_accept(value) - end - end) + end, + on_cancel = options.on_cancel, + }) end local function set_buffer_name(buf, name) diff --git a/lua/tests/android/logcat/controls/filter_input_test.lua b/lua/tests/android/logcat/controls/filter_input_test.lua index ff681a0..e9a7e5d 100644 --- a/lua/tests/android/logcat/controls/filter_input_test.lua +++ b/lua/tests/android/logcat/controls/filter_input_test.lua @@ -157,6 +157,23 @@ local function filter_picker_receives_history() end) end +local function filter_picker_receives_prompt_text() + local state = state_with("com.saved", "Old") + local captured = {} + local stubs = { + ["android.ui.picker"] = { + filter_input = function(opts) + captured.prompt_title = opts.prompt_title + end, + }, + } + + logcat_helpers.with_logcat_context({ state = state, stubs = stubs }, function(ctx) + logcat_helpers.start_filter_edit(ctx) + assert.eq(captured.prompt_title, "Logcat filter", "prompt title") + end) +end + local function filter_history_persists() local state = logcat_helpers.build_state({ logcat = { package = "com.saved", filter = "Old", filter_history = { "old" } }, @@ -209,6 +226,7 @@ function M.run() header_rerenders_after_filter_input() filter_input_cancel_keeps_latest() filter_picker_receives_history() + filter_picker_receives_prompt_text() filter_history_persists() filter_history_persists_on_cancel() end diff --git a/lua/tests/android/logcat/controls/package_test.lua b/lua/tests/android/logcat/controls/package_test.lua index ebdd537..1ba9964 100644 --- a/lua/tests/android/logcat/controls/package_test.lua +++ b/lua/tests/android/logcat/controls/package_test.lua @@ -189,13 +189,13 @@ local function header_enter_prompts_for_package_and_filter_modal() logcat_helpers.press_enter(ctx, 2) local call = ctx.vim_state.input_calls[1] or {} - local prompt = input_calls[1] and input_calls[1].prompt_title or "" + local filter_call = input_calls[1] or {} local summary = string.format( "%d|%s|%d|%s", #ctx.vim_state.input_calls, call.prompt or "", #input_calls, - prompt + filter_call.prompt_title or "" ) assert.eq(summary, "1|Logcat package: |1|Logcat filter", "header enter") end) diff --git a/lua/tests/android/ui/input_test.lua b/lua/tests/android/ui/input_test.lua index 09bd862..9380797 100644 --- a/lua/tests/android/ui/input_test.lua +++ b/lua/tests/android/ui/input_test.lua @@ -27,6 +27,8 @@ local function with_input(lines, run) local state = { autocmd = nil, callbacks = {}, + commands = {}, + open_opts = nil, } save(vim, "o") @@ -44,7 +46,9 @@ local function with_input(lines, run) }) save(vim, "cmd") - vim.cmd = function() end + vim.cmd = function(cmd) + state.commands[#state.commands + 1] = cmd + end save(vim, "keymap") vim.keymap = vim.keymap or {} @@ -65,7 +69,10 @@ local function with_input(lines, run) vim.api.nvim_create_buf = function() return 1 end save(vim.api, "nvim_open_win") - vim.api.nvim_open_win = function() return 2 end + vim.api.nvim_open_win = function(_, _, opts) + state.open_opts = opts + return 2 + end save(vim.api, "nvim_win_is_valid") vim.api.nvim_win_is_valid = function() return true end @@ -129,9 +136,44 @@ local function on_change_skips_empty_prompt() end) end +local function startinsert_uses_append_mode() + with_input({ "old" }, function(input, state) + input.prompt({ + default = "old", + }) + + assert.eq(state.commands[#state.commands], "startinsert!", "appends at end") + end) +end + +local function uses_wider_default_width() + with_input({ "" }, function(input, state) + input.prompt({ + title = "Logcat filter:", + }) + + assert.eq(state.open_opts.width, 56, "default width") + end) +end + +local function clamps_width_to_editor() + with_input({ "" }, function(input, state) + vim.o.columns = 40 + + input.prompt({ + title = "Logcat filter:", + }) + + assert.eq(state.open_opts.width, 36, "clamped width") + end) +end + function M.run() on_change_strips_prompt_prefix() on_change_skips_empty_prompt() + startinsert_uses_append_mode() + uses_wider_default_width() + clamps_width_to_editor() end return M diff --git a/lua/tests/android/ui/picker_filter_input/fallback_input.lua b/lua/tests/android/ui/picker_filter_input/fallback_input.lua index 4b67f09..9721cb2 100644 --- a/lua/tests/android/ui/picker_filter_input/fallback_input.lua +++ b/lua/tests/android/ui/picker_filter_input/fallback_input.lua @@ -2,46 +2,72 @@ local M = {} local assert = require("tests.helpers.assert") -local function run_filter_input_without_telescope() - local flags = { on_change = false, on_accept = false, on_cancel = false } - local captured = { prompt = nil, default = nil } +local function run_filter_input_without_telescope(opts) + local options = opts or {} + local flags = { on_change = {}, on_accept = false, on_cancel = false } + local captured = { title = nil, prompt = nil, default = nil } local original_input = vim.ui and vim.ui.input local original_preload = package.preload["telescope.pickers"] local original_loaded = package.loaded["telescope.pickers"] + local original_android_preload = package.preload["android.ui.input"] + local original_android_input = package.loaded["android.ui.input"] package.preload["telescope.pickers"] = function() error("missing telescope.pickers") end package.loaded["telescope.pickers"] = nil + package.loaded["android.ui.input"] = { + prompt = function(input_opts) + captured.title = input_opts.title + captured.prompt = input_opts.prompt + captured.default = input_opts.default + if options.cancel then + input_opts.on_cancel() + return + end + if input_opts.on_change then + input_opts.on_change("live") + end + input_opts.on_submit("typed") + end, + } + vim.ui = vim.ui or {} - vim.ui.input = function(opts, cb) - captured.prompt = opts.prompt - captured.default = opts.default - cb("typed") + vim.ui.input = function() + error("vim.ui.input should not be used when floating input is available") end package.loaded["android.ui.picker"] = nil local picker = require("android.ui.picker") picker.filter_input({ items = { "one" }, - prompt_title = "Filter", + prompt_title = options.prompt_title or "Filter", default = "old", - on_change = function(value) flags.on_change = value end, + on_change = function(value) + flags.on_change[#flags.on_change + 1] = value + end, on_accept = function(value) flags.on_accept = value end, on_cancel = function() flags.on_cancel = true end, }) - vim.ui.input = original_input package.preload["telescope.pickers"] = original_preload package.loaded["telescope.pickers"] = original_loaded + package.preload["android.ui.input"] = original_android_preload + package.loaded["android.ui.input"] = original_android_input + vim.ui.input = original_input return { captured = captured, flags = flags } end -local function filter_input_fallback_sets_prompt() +local function filter_input_fallback_sets_title() local result = run_filter_input_without_telescope() - assert.eq(result.captured.prompt, "Filter", "input prompt") + assert.eq(result.captured.title, "Filter:", "input title") +end + +local function filter_input_fallback_keeps_prompt_empty() + local result = run_filter_input_without_telescope() + assert.eq(result.captured.prompt, "", "input prompt") end local function filter_input_fallback_sets_default() @@ -51,7 +77,7 @@ end local function filter_input_fallback_calls_on_change() local result = run_filter_input_without_telescope() - assert.eq(result.flags.on_change, "typed", "on_change called") + assert.table_eq(result.flags.on_change, { "live" }, "on_change called once") end local function filter_input_fallback_calls_on_accept() @@ -59,17 +85,18 @@ local function filter_input_fallback_calls_on_accept() assert.eq(result.flags.on_accept, "typed", "on_accept called") end -local function filter_input_fallback_does_not_call_on_cancel() - local result = run_filter_input_without_telescope() - assert.eq(result.flags.on_cancel, false, "on_cancel not called") +local function filter_input_fallback_calls_on_cancel() + local result = run_filter_input_without_telescope({ cancel = true }) + assert.eq(result.flags.on_cancel, true, "on_cancel called") end function M.run() - filter_input_fallback_sets_prompt() + filter_input_fallback_sets_title() + filter_input_fallback_keeps_prompt_empty() filter_input_fallback_sets_default() filter_input_fallback_calls_on_change() filter_input_fallback_calls_on_accept() - filter_input_fallback_does_not_call_on_cancel() + filter_input_fallback_calls_on_cancel() end return M