feat: checkbox field

This commit is contained in:
2026-04-05 23:28:56 +03:00
parent 52d5fd2a38
commit 94a784f64c
14 changed files with 597 additions and 19 deletions

View File

@@ -11,7 +11,7 @@ floating window. Create a single window containing multiple typed inputs
- Bordered floating window with optional title
- Keyboard-navigable: `<Tab>` / `<S-Tab>` to move between inputs
- Input types: `text`, `multiline`, `select`
- Input types: `text`, `multiline`, `select`, `checkbox`
- Select dropdowns open with `<CR>`; arrows navigate; `<CR>` confirms
- Submit with `<C-s>` — results delivered as a `{ [name] = value }` table
- Cancel with `<Esc>`
@@ -141,6 +141,30 @@ All inputs share `name` (string, required — the key in the result table) and
`value()` returns the selected `id` (not the label).
#### `checkbox`
```lua
{ name = "agree", label = "I agree", type = "checkbox", default = false }
```
Unlike text/multiline/select, checkboxes render **inline** — no border, no
separate label row. The glyph sits immediately next to the label, and any
validation error is appended on the same line:
```
☐ I agree (must be checked)
```
- `default` (optional) — boolean (defaults to `false`).
- `value()` returns a boolean.
- Toggled with the configured `keymaps.toggle` key (default `<Space>`) or
the `keymaps.open_select` key (default `<CR>`) — both work so users get
a consistent "interact with this field" key.
- Glyphs come from `style.checkbox.{checked, unchecked}` (defaults
`"☑"` / `"☐"`).
- Pair with `validators.checked()` to require the box to be ticked (see
[Validation](#validation)).
## Validation
Each input spec accepts an optional `validator` function:
@@ -170,6 +194,10 @@ V.min_length(n, [msg]) -- at least `n` characters
V.max_length(n, [msg]) -- at most `n` characters
V.matches(lua_pattern, [msg]) -- match a Lua pattern
V.is_number([msg]) -- tonumber() must succeed
V.checked([required], [msg]) -- checkbox must equal `required`
-- (default true; default messages
-- "(must be checked)" /
-- "(must be unchecked)")
V.one_of({ "a", "b", ... }, [msg]) -- value must be in the list
V.custom(predicate, msg) -- wrap a `fun(v): boolean` predicate
V.chain(v1, v2, ...) -- run validators in order, first error wins
@@ -217,6 +245,24 @@ validator = function(value)
end
```
### Checkbox glyphs
Override the characters rendered by `checkbox` inputs via `style.checkbox`:
```lua
require("input-form").setup({
style = {
checkbox = {
checked = "☑", -- default
unchecked = "☐", -- default
},
},
})
```
Alternatives that render well in most fonts: `[x]` / `[ ]`, `✔` / `·`,
`●` / `○`.
### Select chevrons
The glyphs shown on the right side of `select` inputs to indicate the
@@ -311,6 +357,7 @@ require("input-form").setup({
submit = "<C-s>",
cancel = "<Esc>",
open_select = "<CR>",
toggle = "<Space>",
},
select = {
max_height = 10,

View File

@@ -117,6 +117,8 @@ Default configuration for |input-form|.
cancel = "<Esc>",
--- Open the dropdown when focused on a `select` input.
open_select = "<CR>",
--- Toggle the value of a `checkbox` input.
toggle = "<Space>",
},
--- Options for `select` inputs.
select = {
@@ -138,6 +140,16 @@ Default configuration for |input-form|.
--- Glyph shown when the dropdown is open.
open = "⌃",
},
--- Glyphs shown in `checkbox` inputs.
checkbox = {
--- Shown when the box is checked.
checked = "☑",
--- Shown when the box is unchecked.
unchecked = "☐",
--- Blank rows rendered above and below a checkbox to visually separate
--- it from adjacent bordered inputs. Set to `0` to pack tightly.
padding = 1,
},
--- Highlight groups applied on every `form:show()`. Each entry is passed
--- directly to `vim.api.nvim_set_hl(0, name, spec)`, so any option that
--- `nvim_set_hl` accepts (`fg`, `bg`, `link`, `bold`, `italic`,
@@ -238,6 +250,19 @@ Parameters ~
Return ~
`(function)`
------------------------------------------------------------------------------
*M.checked()*
`M.checked`({required}, {msg})
Require a checkbox value to equal the given boolean. Intended for
`checkbox` inputs ("must agree to terms", "must enable feature", ...).
Parameters ~
{required} `(boolean|nil)` The required value. Defaults to `true`.
{msg} `(string|nil)` Override error message. Defaults to
`"(must be checked)"` when `required` is `true`, otherwise
`"(must be unchecked)"`.
Return ~
`(function)`
------------------------------------------------------------------------------
*M.one_of()*
`M.one_of`({choices}, {msg})

View File

@@ -1,6 +1,7 @@
Default input-form.txt /*Default*
M.Form input-form.txt /*M.Form*
M.chain() input-form.txt /*M.chain()*
M.checked() input-form.txt /*M.checked()*
M.config input-form.txt /*M.config*
M.custom() input-form.txt /*M.custom()*
M.is_number() input-form.txt /*M.is_number()*

View File

@@ -41,6 +41,8 @@ M.defaults = {
cancel = "<Esc>",
--- Open the dropdown when focused on a `select` input.
open_select = "<CR>",
--- Toggle the value of a `checkbox` input.
toggle = "<Space>",
},
--- Options for `select` inputs.
select = {
@@ -62,6 +64,16 @@ M.defaults = {
--- Glyph shown when the dropdown is open.
open = "",
},
--- Glyphs shown in `checkbox` inputs.
checkbox = {
--- Shown when the box is checked.
checked = "",
--- Shown when the box is unchecked.
unchecked = "",
--- Blank rows rendered above and below a checkbox to visually separate
--- it from adjacent bordered inputs. Set to `0` to pack tightly.
padding = 1,
},
--- Highlight groups applied on every `form:show()`. Each entry is passed
--- directly to `vim.api.nvim_set_hl(0, name, spec)`, so any option that
--- `nvim_set_hl` accepts (`fg`, `bg`, `link`, `bold`, `italic`,

View File

@@ -72,17 +72,39 @@ function M:_compute_layout()
local parent_inner_w = outer_width - 2 -- minus parent border
local child_outer_w = parent_inner_w - pad_h * 2
local child_inner_w = child_outer_w - 2 -- minus child border
local child_inner_w = child_outer_w - 2 -- minus child border (for bordered inputs)
-- Extra blank rows rendered above/below a checkbox (borderless input) so
-- its glyph doesn't butt directly against an adjacent bordered input's
-- border. Configurable via `style.checkbox.padding`.
local cb_pad = (opts.style and opts.style.checkbox and opts.style.checkbox.padding) or 0
local rows = {}
local inner_h = pad_top
for i, input in ipairs(self._inputs) do
local h = input:height()
-- NB: avoid the `a and b or c` idiom — `is_bordered()` legitimately
-- returns `false` and that must not get coerced back to the default.
local bordered = true
if type(input.is_bordered) == "function" then
bordered = input:is_bordered()
end
local top_pad = (not bordered) and cb_pad or 0
local bot_pad = (not bordered) and cb_pad or 0
local outer_h = bordered and (h + 2) or (h + top_pad + bot_pad)
-- Editor-row offset from `parent_row` to pass as `nvim_open_win`'s `row`
-- parameter for this child. `row` refers to the window's OUTER top-left
-- (i.e. the border origin for bordered windows, the content row for
-- borderless windows). Parent_row is itself a border origin, so every
-- child needs a `+1` to clear the parent's top border — matching the
-- `+1` already applied on the column axis in `show()`.
local content_offset = inner_h + 1 + top_pad
table.insert(rows, {
top_border_offset = inner_h, -- row inside parent content where child's top border sits
bordered = bordered,
content_row_offset = content_offset,
value_height = h,
})
inner_h = inner_h + h + 2 -- child's full outer height (content + 2 border rows)
inner_h = inner_h + outer_h
if i < #self._inputs then
inner_h = inner_h + sep
end
@@ -104,6 +126,7 @@ function M:_compute_layout()
parent_col = parent_col,
parent_inner_w = parent_inner_w,
parent_inner_h = inner_h,
child_outer_w = child_outer_w,
child_inner_w = child_inner_w,
pad_h = pad_h,
rows = rows,
@@ -174,20 +197,27 @@ function M:show()
"FloatFooter:InputFormHelp",
}, ",")
-- Mount each input as its own bordered child floating window.
-- Mount each input as its own floating window. Bordered inputs get their
-- own border + label; borderless inputs (e.g. checkbox) render inline but
-- still align their CONTENT column with the bordered siblings' content
-- (not their border column) so everything lines up visually.
local border = config.options.window.border
for i, input in ipairs(self._inputs) do
local r = layout.rows[i]
-- Child's content origin: inside the parent content area, offset by the
-- row's top_border_offset plus one row for the child's own top border;
-- and one col inside the parent plus horizontal padding plus one for the
-- child's own left border.
input:mount({
row = layout.parent_row + r.top_border_offset + 1,
col = layout.parent_col + layout.pad_h + 1,
-- Bordered children get `+1` to clear the parent's left border; their
-- content then sits at `+2`. Borderless children shift an extra column
-- so their content column lines up with the bordered siblings' content
-- column (not their border column).
local col_offset = r.bordered and 1 or 2
local mount_opts = {
row = layout.parent_row + r.content_row_offset,
col = layout.parent_col + layout.pad_h + col_offset,
width = layout.child_inner_w,
border = border,
})
}
if r.bordered then
mount_opts.border = border
end
input:mount(mount_opts)
self:_install_keymaps(input)
self:_install_validation(input)
end
@@ -267,7 +297,8 @@ function M:_validate_all()
if input.validator then
input._touched = true
local err = input.validator(input:value())
if err == "" then
-- Only strings count as errors; nil / false / other types = no error.
if type(err) ~= "string" or err == "" then
err = nil
end
input._error = err
@@ -348,7 +379,8 @@ function M:_validate_input(input)
return
end
local err = input.validator(input:value())
if err == "" then
-- Only strings count as errors; nil / false / other types = no error.
if type(err) ~= "string" or err == "" then
err = nil
end
input._error = err
@@ -362,6 +394,20 @@ function M:_render_validation(input)
if not (win and vim.api.nvim_win_is_valid(win)) then
return
end
-- Borderless inputs (checkbox) render the error inline — they already read
-- `self._error` from their own `_render_display()`.
local bordered = true
if type(input.is_bordered) == "function" then
bordered = input:is_bordered()
end
if not bordered then
if type(input._render_display) == "function" then
input:_render_display()
end
return
end
local has_error = input._error ~= nil
if has_error then
@@ -418,17 +464,21 @@ function M:_help_line()
if nav then
table.insert(parts, nav .. " navigate")
end
-- Only advertise open_select if the form actually has a select input.
local has_select = false
-- Only advertise type-specific keys if the form actually has such an input.
local has_select, has_checkbox = false, false
for _, input in ipairs(self._inputs) do
if input.type == "select" then
has_select = true
break
elseif input.type == "checkbox" then
has_checkbox = true
end
end
if has_select then
add(km.open_select, "open")
end
if has_checkbox then
add(km.toggle, "toggle")
end
add(km.submit, "submit")
add(km.cancel, "cancel")
return table.concat(parts, " ")
@@ -487,6 +537,20 @@ function M:_install_keymaps(input)
-- Block insert mode on the select display buffer.
vim.keymap.set("n", "i", "<Nop>", { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "a", "<Nop>", { buffer = buf, nowait = true, silent = true })
elseif input.type == "checkbox" then
-- Toggle on the configured toggle key AND on open_select so users who
-- prefer <CR> for all interactions get a single key for every field.
map("n", km.toggle, function()
input:toggle()
end)
if km.open_select and km.open_select ~= km.toggle then
map("n", km.open_select, function()
input:toggle()
end)
end
-- Block insert mode on the checkbox display buffer.
vim.keymap.set("n", "i", "<Nop>", { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "a", "<Nop>", { buffer = buf, nowait = true, silent = true })
elseif input.type == "text" then
-- Single-line text inputs must never contain newlines. <CR> in insert
-- mode just exits insert mode (accepting the value) rather than inserting

View File

@@ -0,0 +1,150 @@
--- Boolean checkbox input component.
---
--- Unlike text/multiline/select, the checkbox is borderless and renders
--- inline: the label sits next to the glyph, and any validation error is
--- appended to the same line (`☑ Label (must be checked)`). The error
--- portion is highlighted with `InputFormFieldError`.
---
--- Toggles via the configured `keymaps.toggle` key (default `<Space>`) and
--- also the `keymaps.open_select` key, so users get a consistent
--- "interact with this field" key.
--- `value()` returns a boolean.
local config = require("input-form.config")
local utils = require("input-form.utils")
local M = {}
M.__index = M
--- Create a new checkbox from its spec.
---@param spec table { name, label, default? }
---@return table
function M.new(spec)
return setmetatable({
type = "checkbox",
name = spec.name,
label = spec.label or spec.name,
_value = spec.default == true,
buf = nil,
win = nil,
}, M)
end
function M:height()
return 1
end
--- Checkboxes are rendered without a surrounding border/title so the form's
--- layout packs them more tightly than the other input types.
function M:is_bordered()
return false
end
local NS = vim.api.nvim_create_namespace("input-form-checkbox")
local function glyph_for(checked)
local style = config.options.style or {}
local cb = style.checkbox or {}
if checked then
return cb.checked or ""
end
return cb.unchecked or ""
end
function M:_render_display()
if not (self.buf and vim.api.nvim_buf_is_valid(self.buf)) then
return
end
local glyph = glyph_for(self._value)
local base = glyph
if self.label and self.label ~= "" then
base = glyph .. " " .. self.label
end
local err = (self._error and self._error ~= "") and self._error or nil
local line = err and (base .. " " .. err) or base
vim.bo[self.buf].modifiable = true
vim.api.nvim_buf_set_lines(self.buf, 0, -1, false, { line })
vim.bo[self.buf].modifiable = false
-- Highlight the error suffix (if any).
vim.api.nvim_buf_clear_namespace(self.buf, NS, 0, -1)
if err then
local err_start = #base + 1 -- byte offset of the space before the error
pcall(vim.api.nvim_buf_set_extmark, self.buf, NS, 0, err_start, {
end_col = #line,
hl_group = "InputFormFieldError",
})
end
end
function M:mount(layout)
self._width = layout.width
self.buf = vim.api.nvim_create_buf(false, true)
vim.bo[self.buf].buftype = "nofile"
vim.bo[self.buf].bufhidden = "wipe"
vim.bo[self.buf].swapfile = false
utils.mark_form_buffer(self.buf)
self.win = vim.api.nvim_open_win(self.buf, false, {
relative = "editor",
row = layout.row,
col = layout.col,
width = layout.width,
height = 1,
style = "minimal",
focusable = true,
zindex = 50,
})
vim.wo[self.win].winhl = "NormalFloat:InputFormField"
self:_render_display()
end
function M:value()
return self._value
end
function M:unmount()
if self.win and vim.api.nvim_win_is_valid(self.win) then
vim.api.nvim_win_close(self.win, true)
end
if self.buf and vim.api.nvim_buf_is_valid(self.buf) then
vim.api.nvim_buf_delete(self.buf, { force = true })
end
self.win = nil
self.buf = nil
end
function M:focus()
if self.win and vim.api.nvim_win_is_valid(self.win) then
vim.api.nvim_set_current_win(self.win)
pcall(vim.api.nvim_win_set_cursor, self.win, { 1, 0 })
end
end
--- Toggle the checkbox state. Notifies the form (for validation) if an
--- `_on_change` hook has been installed.
function M:toggle()
self._value = not self._value
self:_render_display()
if self._on_change then
self._on_change()
end
end
--- Programmatically set the checkbox value.
---@param v any Truthy for checked, falsey for unchecked.
function M:set(v)
local new = v and true or false
if new == self._value then
return
end
self._value = new
self:_render_display()
if self._on_change then
self._on_change()
end
end
return M

View File

@@ -6,6 +6,7 @@ M.types = {
text = require("input-form.inputs.text"),
multiline = require("input-form.inputs.multiline"),
select = require("input-form.inputs.select"),
checkbox = require("input-form.inputs.checkbox"),
}
--- Build an input component instance from a user-provided spec.

View File

@@ -27,6 +27,10 @@ function M:height()
return self._height
end
function M:is_bordered()
return true
end
function M:mount(layout)
self.buf = vim.api.nvim_create_buf(false, true)
vim.bo[self.buf].buftype = "nofile"

View File

@@ -39,6 +39,10 @@ function M:height()
return 1
end
function M:is_bordered()
return true
end
local function label_for(options, id)
for _, opt in ipairs(options) do
if opt.id == id then

View File

@@ -24,6 +24,10 @@ function M:height()
return 1
end
function M:is_bordered()
return true
end
--- Create the backing buffer and floating window.
---@param layout table { row, col, width }
function M:mount(layout)

View File

@@ -82,6 +82,28 @@ function M.is_number(msg)
end
end
--- Require a checkbox value to equal the given boolean. Intended for
--- `checkbox` inputs ("must agree to terms", "must enable feature", ...).
---@param required boolean|nil The required value. Defaults to `true`.
---@param msg string|nil Override error message. Defaults to
--- `"(must be checked)"` when `required` is `true`, otherwise
--- `"(must be unchecked)"`.
---@return function
function M.checked(required, msg)
if required == nil then
required = true
end
if type(msg) ~= "string" then
msg = required and "(must be checked)" or "(must be unchecked)"
end
return function(value)
if value == required then
return nil
end
return msg
end
end
--- Require the value to be one of the given choices (useful for text inputs
--- that must match a fixed allowlist; select inputs should use their
--- `options` list instead).

View File

@@ -262,6 +262,116 @@ T["form"]["validation"]["inputs without a validator are never marked touched"] =
eq(child.lua_get([[_G.vf._inputs[2]._error]]), vim.NIL)
end
T["form"]["checkbox"] = MiniTest.new_set()
T["form"]["checkbox"]["lays out without overlapping adjacent bordered inputs"] = function()
child.lua([[
_G.cf = require('input-form').create_form({
inputs = {
{ name = 'a', label = 'A', type = 'text' },
{ name = 'b', label = 'B', type = 'checkbox' },
{ name = 'c', label = 'C', type = 'text' },
},
on_submit = function() end,
})
_G.cf:show()
]])
-- `content_row_offset` is the editor-row offset from `parent_row` passed
-- as `nvim_open_win`'s `row` parameter. `row` is the window's OUTER
-- top-left (border origin for bordered wins, content row for borderless),
-- and parent_row is itself a border origin, so every child gets a `+1` to
-- clear the parent's top border. Bordered text consumes 3 inner rows,
-- checkbox consumes 1 + 2*style.checkbox.padding (default 1). Expected
-- offsets: 1 (text border at inner row 0), 5 (checkbox content at inner
-- row 4 — one blank pad row after the text's bottom border), 7 (next
-- text border at inner row 6 — one blank pad row after the checkbox).
eq(child.lua_get([[_G.cf._layout.rows[1].content_row_offset]]), 1)
eq(child.lua_get([[_G.cf._layout.rows[2].content_row_offset]]), 5)
eq(child.lua_get([[_G.cf._layout.rows[3].content_row_offset]]), 7)
eq(child.lua_get([[_G.cf._layout.rows[2].bordered]]), false)
eq(child.lua_get([[_G.cf._layout.parent_inner_h]]), 9)
end
T["form"]["checkbox"]["is included in submit results as a boolean"] = function()
child.lua([[
_G.submit_result = nil
_G.cf = require('input-form').create_form({
inputs = {
{ name = 'agree', label = 'Agree', type = 'checkbox', default = false },
{ name = 'subscribe', label = 'Subscribe', type = 'checkbox', default = true },
},
on_submit = function(r) _G.submit_result = r end,
})
_G.cf:show()
_G.cf:submit()
]])
eq(child.lua_get([[_G.submit_result]]), { agree = false, subscribe = true })
end
T["form"]["checkbox"]["toggle key flips value and updates result"] = function()
child.lua([[
_G.submit_result = nil
_G.cf = require('input-form').create_form({
inputs = {
{ name = 'agree', label = 'Agree', type = 'checkbox', default = false },
},
on_submit = function(r) _G.submit_result = r end,
})
_G.cf:show()
vim.api.nvim_set_current_win(_G.cf._inputs[1].win)
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes('<Space>', true, false, true), 'x', false
)
_G.cf:submit()
]])
eq(child.lua_get([[_G.submit_result]]), { agree = true })
end
T["form"]["checkbox"]["is toggled by open_select key too"] = function()
child.lua([[
_G.cf = require('input-form').create_form({
inputs = {
{ name = 'agree', label = 'Agree', type = 'checkbox' },
},
on_submit = function() end,
})
_G.cf:show()
vim.api.nvim_set_current_win(_G.cf._inputs[1].win)
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes('<CR>', true, false, true), 'x', false
)
]])
eq(child.lua_get([[_G.cf._inputs[1]:value()]]), true)
end
T["form"]["checkbox"]["validator can require checked"] = function()
child.lua([[
local V = require('input-form.validators')
_G.submit_result = nil
_G.cf = require('input-form').create_form({
inputs = {
{
name = 'agree',
label = 'Agree',
type = 'checkbox',
default = false,
validator = V.custom(function(v) return v == true end, 'You must agree'),
},
},
on_submit = function(r) _G.submit_result = r end,
})
_G.cf:show()
_G.cf:submit() -- should be blocked
]])
eq(child.lua_get([[_G.submit_result]]), vim.NIL)
eq(child.lua_get([[_G.cf._inputs[1]._error]]), "You must agree")
-- Toggle on, validator re-runs (via _on_change), error clears.
child.lua([[_G.cf._inputs[1]:toggle()]])
eq(child.lua_get([[_G.cf._inputs[1]._error]]), vim.NIL)
child.lua([[_G.cf:submit()]])
eq(child.lua_get([[_G.submit_result]]), { agree = true })
end
T["form"]["keymaps are installed on each input buffer"] = function()
child.lua([[_G.f = _G.make_form(); _G.f:show()]])
-- <Tab> should be mapped in normal mode on the first input's buffer.

View File

@@ -0,0 +1,118 @@
local helpers = dofile("tests/helpers.lua")
local MiniTest = require("mini.test")
local child = helpers.new_child_neovim()
local eq = helpers.expect.equality
local T = MiniTest.new_set({
hooks = {
pre_case = function()
child.restart({ "-u", "scripts/minimal_init.lua" })
child.lua([[require('input-form').setup()]])
end,
post_once = child.stop,
},
})
T["checkbox input"] = MiniTest.new_set()
T["checkbox input"]["defaults to false"] = function()
child.lua([[_G.t = require('input-form.inputs.checkbox').new({ name = 'c', label = 'C' })]])
eq(child.lua_get([[_G.t:value()]]), false)
end
T["checkbox input"]["honors explicit default = true"] = function()
child.lua(
[[_G.t = require('input-form.inputs.checkbox').new({ name = 'c', label = 'C', default = true })]]
)
eq(child.lua_get([[_G.t:value()]]), true)
end
T["checkbox input"]["height is 1"] = function()
child.lua([[_G.t = require('input-form.inputs.checkbox').new({ name = 'c', label = 'C' })]])
eq(child.lua_get([[_G.t:height()]]), 1)
end
T["checkbox input"]["mount renders inline glyph + label"] = function()
child.lua([[
_G.t = require('input-form.inputs.checkbox').new({ name = 'c', label = 'C' })
_G.t:mount({ row = 5, col = 5, width = 20 })
]])
eq(child.lua_get([[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)]]), { "☐ C" })
child.lua([[
_G.t2 = require('input-form.inputs.checkbox').new({ name = 'c', label = 'C', default = true })
_G.t2:mount({ row = 8, col = 5, width = 20 })
]])
eq(child.lua_get([[vim.api.nvim_buf_get_lines(_G.t2.buf, 0, -1, false)]]), { "☑ C" })
end
T["checkbox input"]["is borderless"] = function()
child.lua([[_G.t = require('input-form.inputs.checkbox').new({ name = 'c', label = 'C' })]])
eq(child.lua_get([[_G.t:is_bordered()]]), false)
end
T["checkbox input"]["toggle flips the value and re-renders"] = function()
child.lua([[
_G.t = require('input-form.inputs.checkbox').new({ name = 'c', label = 'Agree' })
_G.t:mount({ row = 5, col = 5, width = 20 })
_G.t:toggle()
]])
eq(child.lua_get([[_G.t:value()]]), true)
eq(child.lua_get([[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)]]), { "☑ Agree" })
child.lua([[_G.t:toggle()]])
eq(child.lua_get([[_G.t:value()]]), false)
eq(child.lua_get([[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)]]), { "☐ Agree" })
end
T["checkbox input"]["renders error inline after label"] = function()
child.lua([[
_G.t = require('input-form.inputs.checkbox').new({ name = 'c', label = 'Agree' })
_G.t:mount({ row = 5, col = 5, width = 30 })
_G.t._error = "(must be checked)"
_G.t:_render_display()
]])
eq(
child.lua_get([[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)]]),
{ "☐ Agree (must be checked)" }
)
-- Clearing the error removes the suffix.
child.lua([[_G.t._error = nil; _G.t:_render_display()]])
eq(child.lua_get([[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)]]), { "☐ Agree" })
end
T["checkbox input"]["set updates the value idempotently"] = function()
child.lua([[
_G.t = require('input-form.inputs.checkbox').new({ name = 'c', label = 'C' })
_G.t:mount({ row = 5, col = 5, width = 20 })
_G.changes = 0
_G.t._on_change = function() _G.changes = _G.changes + 1 end
_G.t:set(true)
_G.t:set(true) -- no-op
_G.t:set(false)
]])
eq(child.lua_get([[_G.t:value()]]), false)
eq(child.lua_get([[_G.changes]]), 2)
end
T["checkbox input"]["display buffer is read-only"] = function()
child.lua([[
_G.t = require('input-form.inputs.checkbox').new({ name = 'c', label = 'C' })
_G.t:mount({ row = 5, col = 5, width = 20 })
]])
eq(child.lua_get([[vim.bo[_G.t.buf].modifiable]]), false)
end
T["checkbox input"]["custom glyphs from style config"] = function()
child.lua([[
require('input-form').setup({
style = { checkbox = { checked = '✔', unchecked = '·' } }
})
_G.t = require('input-form.inputs.checkbox').new({ name = 'c', label = 'C' })
_G.t:mount({ row = 5, col = 5, width = 20 })
]])
eq(child.lua_get([[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)]]), { "· C" })
child.lua([[_G.t:toggle()]])
eq(child.lua_get([[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)]]), { "✔ C" })
end
return T

View File

@@ -49,6 +49,22 @@ T["validators"]["is_number"] = function()
eq(child.lua_get([[V.is_number()("3.14")]]), vim.NIL)
end
T["validators"]["checked"] = function()
-- Defaults to requiring `true`.
eq(child.lua_get([[V.checked()(true)]]), vim.NIL)
eq(child.lua_get([[V.checked()(false)]]), "(must be checked)")
eq(child.lua_get([[V.checked()(nil)]]), "(must be checked)")
-- Explicit required value.
eq(child.lua_get([[V.checked(true)(true)]]), vim.NIL)
eq(child.lua_get([[V.checked(true)(false)]]), "(must be checked)")
-- Require UNchecked.
eq(child.lua_get([[V.checked(false)(false)]]), vim.NIL)
eq(child.lua_get([[V.checked(false)(true)]]), "(must be unchecked)")
-- Custom message (second arg).
eq(child.lua_get([[V.checked(true, "please tick the box")(false)]]), "please tick the box")
eq(child.lua_get([[V.checked(false, "leave it off")(true)]]), "leave it off")
end
T["validators"]["one_of"] = function()
eq(child.lua_get([[V.one_of({ 'a', 'b' })('c')]]), "Value is not allowed")
eq(child.lua_get([[V.one_of({ 'a', 'b' })('a')]]), vim.NIL)