Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 18 additions & 4 deletions lua/android/ui/input.lua
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -118,7 +132,7 @@ function M.prompt(opts)
})
end

vim.cmd("startinsert")
vim.cmd("startinsert!")
end

return M
34 changes: 15 additions & 19 deletions lua/android/ui/picker.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 18 additions & 0 deletions lua/tests/android/logcat/controls/filter_input_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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" } },
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lua/tests/android/logcat/controls/package_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
46 changes: 44 additions & 2 deletions lua/tests/android/ui/input_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ local function with_input(lines, run)
local state = {
autocmd = nil,
callbacks = {},
commands = {},
open_opts = nil,
}

save(vim, "o")
Expand All @@ -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 {}
Expand All @@ -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
Expand Down Expand Up @@ -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
63 changes: 45 additions & 18 deletions lua/tests/android/ui/picker_filter_input/fallback_input.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -51,25 +77,26 @@ 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()
local result = run_filter_input_without_telescope()
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