mirror of
https://github.com/chenasraf/input-form.nvim.git
synced 2026-05-18 01:38:59 +00:00
feat: dropdown chevron
This commit is contained in:
@@ -118,6 +118,10 @@ function M:show()
|
||||
end
|
||||
self._visible = true
|
||||
|
||||
-- Lazy: teach known UI plugins (nvim-scrollbar, satellite, ...) to skip
|
||||
-- form buffers. Runs once per nvim session.
|
||||
utils.register_ui_exclusions()
|
||||
|
||||
local layout = self:_compute_layout()
|
||||
self._layout = layout
|
||||
|
||||
@@ -126,6 +130,7 @@ function M:show()
|
||||
vim.bo[self._parent_buf].buftype = "nofile"
|
||||
vim.bo[self._parent_buf].bufhidden = "wipe"
|
||||
vim.bo[self._parent_buf].swapfile = false
|
||||
utils.mark_form_buffer(self._parent_buf)
|
||||
|
||||
local parent_lines = {}
|
||||
for _ = 1, layout.parent_inner_h do
|
||||
@@ -183,8 +188,15 @@ function M:show()
|
||||
border = border,
|
||||
})
|
||||
self:_install_keymaps(input)
|
||||
self:_install_validation(input)
|
||||
end
|
||||
|
||||
-- Default highlight groups for validation error state. `default = true`
|
||||
-- means user overrides take precedence.
|
||||
pcall(vim.api.nvim_set_hl, 0, "InputFormFieldError", { fg = "Red", default = true })
|
||||
pcall(vim.api.nvim_set_hl, 0, "InputFormFieldErrorBorder", { fg = "Red", default = true })
|
||||
pcall(vim.api.nvim_set_hl, 0, "InputFormFieldErrorTitle", { fg = "Red", default = true })
|
||||
|
||||
self:_focus(1)
|
||||
return self
|
||||
end
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
--- Multi-line text input component.
|
||||
|
||||
local config = require("input-form.config")
|
||||
local utils = require("input-form.utils")
|
||||
|
||||
local M = {}
|
||||
M.__index = M
|
||||
@@ -31,6 +32,7 @@ function M:mount(layout)
|
||||
vim.bo[self.buf].buftype = "nofile"
|
||||
vim.bo[self.buf].bufhidden = "wipe"
|
||||
vim.bo[self.buf].swapfile = false
|
||||
utils.mark_form_buffer(self.buf)
|
||||
local lines = vim.split(self._value, "\n", { plain = true })
|
||||
vim.api.nvim_buf_set_lines(self.buf, 0, -1, false, lines)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
--- window; j/k/arrows navigate, <CR> confirms, <Esc> cancels.
|
||||
|
||||
local config = require("input-form.config")
|
||||
local utils = require("input-form.utils")
|
||||
|
||||
local M = {}
|
||||
M.__index = M
|
||||
@@ -47,35 +48,46 @@ local function label_for(options, id)
|
||||
return ""
|
||||
end
|
||||
|
||||
local function format_display(options, id)
|
||||
return label_for(options, id)
|
||||
local CHEVRON_CLOSED = " ▼"
|
||||
local CHEVRON_OPEN = " ▲"
|
||||
|
||||
local function format_display(options, id, width, open)
|
||||
local label = label_for(options, id)
|
||||
local chevron = open and CHEVRON_OPEN or CHEVRON_CLOSED
|
||||
if not width or width <= 0 then
|
||||
return label .. chevron
|
||||
end
|
||||
local label_w = vim.fn.strdisplaywidth(label)
|
||||
local chev_w = vim.fn.strdisplaywidth(chevron)
|
||||
local pad = width - label_w - chev_w
|
||||
if pad < 1 then
|
||||
pad = 1
|
||||
end
|
||||
return label .. string.rep(" ", pad) .. chevron
|
||||
end
|
||||
|
||||
function M:_render_display()
|
||||
if self.buf and vim.api.nvim_buf_is_valid(self.buf) then
|
||||
local line = format_display(self.options, self._selected_id, self._width, self._open)
|
||||
vim.bo[self.buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(
|
||||
self.buf,
|
||||
0,
|
||||
-1,
|
||||
false,
|
||||
{ format_display(self.options, self._selected_id) }
|
||||
)
|
||||
vim.api.nvim_buf_set_lines(self.buf, 0, -1, false, { line })
|
||||
vim.bo[self.buf].modifiable = false
|
||||
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)
|
||||
vim.api.nvim_buf_set_lines(
|
||||
self.buf,
|
||||
0,
|
||||
-1,
|
||||
false,
|
||||
{ format_display(self.options, self._selected_id) }
|
||||
{ format_display(self.options, self._selected_id, self._width, self._open) }
|
||||
)
|
||||
vim.bo[self.buf].modifiable = false
|
||||
|
||||
@@ -119,6 +131,10 @@ 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)
|
||||
-- Park the cursor at col 0 so the terminal cursor block sits on the label
|
||||
-- (clean state) or on the dirty-shifted chevron's left neighbour (dirty
|
||||
-- state), never on top of the chevron itself.
|
||||
pcall(vim.api.nvim_win_set_cursor, self.win, { 1, 0 })
|
||||
end
|
||||
end
|
||||
|
||||
@@ -140,6 +156,7 @@ function M:open_dropdown()
|
||||
self.dropdown_buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[self.dropdown_buf].buftype = "nofile"
|
||||
vim.bo[self.dropdown_buf].bufhidden = "wipe"
|
||||
utils.mark_form_buffer(self.dropdown_buf)
|
||||
vim.api.nvim_buf_set_lines(self.dropdown_buf, 0, -1, false, lines)
|
||||
vim.bo[self.dropdown_buf].modifiable = false
|
||||
|
||||
@@ -160,6 +177,8 @@ function M:open_dropdown()
|
||||
focusable = true,
|
||||
zindex = 100,
|
||||
})
|
||||
self._open = true
|
||||
self:_render_display()
|
||||
vim.wo[self.dropdown_win].cursorline = true
|
||||
vim.wo[self.dropdown_win].winhl =
|
||||
"NormalFloat:InputFormDropdown,CursorLine:InputFormDropdownActive"
|
||||
@@ -192,6 +211,8 @@ function M:close_dropdown()
|
||||
end
|
||||
self.dropdown_win = nil
|
||||
self.dropdown_buf = nil
|
||||
self._open = false
|
||||
self:_render_display()
|
||||
if self.win and vim.api.nvim_win_is_valid(self.win) then
|
||||
vim.api.nvim_set_current_win(self.win)
|
||||
end
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
--- Single-line text input component.
|
||||
|
||||
local utils = require("input-form.utils")
|
||||
|
||||
local M = {}
|
||||
M.__index = M
|
||||
|
||||
@@ -29,6 +31,7 @@ function M:mount(layout)
|
||||
vim.bo[self.buf].buftype = "nofile"
|
||||
vim.bo[self.buf].bufhidden = "wipe"
|
||||
vim.bo[self.buf].swapfile = false
|
||||
utils.mark_form_buffer(self.buf)
|
||||
vim.api.nvim_buf_set_lines(self.buf, 0, -1, false, { self._value })
|
||||
|
||||
local win_cfg = {
|
||||
|
||||
@@ -39,4 +39,85 @@ function M.clamp(v, lo, hi)
|
||||
return v
|
||||
end
|
||||
|
||||
--- The filetype set on every buffer the plugin owns. Users can add this to
|
||||
--- their UI plugins' exclusion lists as a fallback.
|
||||
M.FORM_FILETYPE = "input-form"
|
||||
|
||||
local _excluded_registered = false
|
||||
|
||||
-- Append `ft` to a list-shaped config field if missing.
|
||||
local function ensure_excluded(cfg, key, ft)
|
||||
if type(cfg) ~= "table" then
|
||||
return
|
||||
end
|
||||
cfg[key] = cfg[key] or {}
|
||||
if not vim.tbl_contains(cfg[key], ft) then
|
||||
table.insert(cfg[key], ft)
|
||||
end
|
||||
end
|
||||
|
||||
--- Tell known UI plugins (scrollbars, indent guides, etc.) to skip buffers
|
||||
--- with filetype `input-form`. Idempotent. Called lazily on the first
|
||||
--- `form:show()` so it works whether or not the user called `setup()`.
|
||||
function M.register_ui_exclusions()
|
||||
if _excluded_registered then
|
||||
return
|
||||
end
|
||||
_excluded_registered = true
|
||||
|
||||
-- nvim-scrollbar (petertriho/nvim-scrollbar).
|
||||
-- Its real config lives at `require("scrollbar.config")` — the module stores
|
||||
-- the active table under `.config` and exposes it via `.get()`. We patch
|
||||
-- both paths defensively in case the module layout differs across versions.
|
||||
local ok, sbar_cfg = pcall(require, "scrollbar.config")
|
||||
if ok and sbar_cfg then
|
||||
if type(sbar_cfg.get) == "function" then
|
||||
local cfg = sbar_cfg.get()
|
||||
ensure_excluded(cfg, "excluded_filetypes", M.FORM_FILETYPE)
|
||||
ensure_excluded(cfg, "excluded_buftypes", "nofile")
|
||||
end
|
||||
if type(sbar_cfg.config) == "table" then
|
||||
ensure_excluded(sbar_cfg.config, "excluded_filetypes", M.FORM_FILETYPE)
|
||||
end
|
||||
end
|
||||
-- Some older layouts exposed the config directly on the main module.
|
||||
local ok_top, sbar = pcall(require, "scrollbar")
|
||||
if ok_top and type(sbar) == "table" and type(sbar.config) == "table" then
|
||||
ensure_excluded(sbar.config, "excluded_filetypes", M.FORM_FILETYPE)
|
||||
end
|
||||
|
||||
-- satellite.nvim (lewis6991/satellite.nvim).
|
||||
local ok2, sat_cfg = pcall(require, "satellite.config")
|
||||
if ok2 and sat_cfg then
|
||||
if type(sat_cfg.user_config) == "table" then
|
||||
ensure_excluded(sat_cfg.user_config, "excluded_filetypes", M.FORM_FILETYPE)
|
||||
end
|
||||
if type(sat_cfg.config) == "table" then
|
||||
ensure_excluded(sat_cfg.config, "excluded_filetypes", M.FORM_FILETYPE)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Mark a buffer as an internal form buffer so third-party UI plugins
|
||||
--- (scrollbars, indent guides, git signs, etc.) skip it.
|
||||
---
|
||||
--- Sets `filetype = "input-form"` plus the opt-out buffer variables
|
||||
--- recognized by common plugins. Users whose plugins don't honour these can
|
||||
--- add `input-form` to their plugin's exclusion list.
|
||||
function M.mark_form_buffer(buf)
|
||||
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
|
||||
return
|
||||
end
|
||||
vim.bo[buf].filetype = M.FORM_FILETYPE
|
||||
-- nvim-scrollbar (petertriho/nvim-scrollbar)
|
||||
vim.b[buf].scrollbar_disabled = true
|
||||
-- satellite.nvim (lewis6991/satellite.nvim)
|
||||
vim.b[buf].satellite_disable = true
|
||||
-- mini.indentscope / mini.map
|
||||
vim.b[buf].miniindentscope_disable = true
|
||||
vim.b[buf].minimap_disable = true
|
||||
-- gitsigns (defensive; unlikely on a nofile buf but cheap)
|
||||
vim.b[buf].gitsigns_disable = true
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -32,13 +32,17 @@ T["select input"] = MiniTest.new_set()
|
||||
T["select input"]["defaults to first option when none given"] = function()
|
||||
child.lua([[_G.t = _G.mk(nil); _G.t:mount({ row = 5, col = 5, width = 30 })]])
|
||||
eq(child.lua_get([[_G.t:value()]]), "a")
|
||||
eq(child.lua_get([[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)]]), { "Alpha" })
|
||||
local line = child.lua_get([==[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)[1]]==])
|
||||
helpers.expect.match(line, "^Alpha")
|
||||
helpers.expect.match(line, "▼")
|
||||
end
|
||||
|
||||
T["select input"]["honors explicit default"] = function()
|
||||
child.lua([[_G.t = _G.mk('b'); _G.t:mount({ row = 5, col = 5, width = 30 })]])
|
||||
eq(child.lua_get([[_G.t:value()]]), "b")
|
||||
eq(child.lua_get([[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)]]), { "Beta" })
|
||||
local line = child.lua_get([==[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)[1]]==])
|
||||
helpers.expect.match(line, "^Beta")
|
||||
helpers.expect.match(line, "▼")
|
||||
end
|
||||
|
||||
T["select input"]["display buffer is read-only"] = function()
|
||||
@@ -54,7 +58,9 @@ T["select input"]["select_id updates value and display"] = function()
|
||||
]])
|
||||
eq(child.lua_get([[_G.ok]]), true)
|
||||
eq(child.lua_get([[_G.t:value()]]), "c")
|
||||
eq(child.lua_get([[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)]]), { "Gamma" })
|
||||
local line = child.lua_get([==[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)[1]]==])
|
||||
helpers.expect.match(line, "^Gamma")
|
||||
helpers.expect.match(line, "▼")
|
||||
end
|
||||
|
||||
T["select input"]["open_dropdown shows all options and <CR> confirms"] = function()
|
||||
|
||||
Reference in New Issue
Block a user