diff --git a/lua/input-form/inputs/select.lua b/lua/input-form/inputs/select.lua index 76ef213..52b0463 100644 --- a/lua/input-form/inputs/select.lua +++ b/lua/input-form/inputs/select.lua @@ -173,17 +173,40 @@ function M:open_dropdown() local max_h = config.options.select.max_height local height = math.min(#lines, max_h) - -- Position the dropdown's top border immediately beneath the input's bottom - -- border. Content origin row = (input content row) + (input bottom border = 1) - -- + (dropdown top border = 1) + 1 = self._layout.row + 3. + -- Prefer stitching the dropdown's top border into the select's bottom + -- border for a compact, merged look: + -- + -- ╭─ Label ─────╮ + -- │ Option 1 ⌃ │ + -- ├─────────────┤ <- shared row (dropdown's top border with T-junction + -- │ Option 1 │ connectors, overlaid on the select's bottom) + -- │ Option 2 │ + -- ╰─────────────╯ + -- + -- The dropdown is positioned so its top border row coincides with the + -- select's bottom border row. With a higher zindex, the dropdown's top + -- border (├─┤ / ╠═╣) wins, producing the visible T-junctions. + local cfg_border = config.options.window.border + local merged_border = utils.merged_top_border(cfg_border) + local dropdown_row, dropdown_border + if merged_border then + dropdown_row = self._layout.row + 2 + dropdown_border = merged_border + else + -- Fallback for unmergeable borders (`"none"`, `"shadow"`, ...): keep the + -- dropdown on its own, one row below the select. + dropdown_row = self._layout.row + 3 + dropdown_border = cfg_border + end + self.dropdown_win = vim.api.nvim_open_win(self.dropdown_buf, true, { relative = "editor", - row = self._layout.row + 3, + row = dropdown_row, col = self._layout.col, width = self._layout.width, height = height, style = "minimal", - border = "rounded", + border = dropdown_border, focusable = true, zindex = 100, }) diff --git a/lua/input-form/utils.lua b/lua/input-form/utils.lua index 2ecbae4..67fbe94 100644 --- a/lua/input-form/utils.lua +++ b/lua/input-form/utils.lua @@ -43,6 +43,57 @@ end --- their UI plugins' exclusion lists as a fallback. M.FORM_FILETYPE = "input-form" +--- Character sets for the built-in border styles accepted by `nvim_open_win`. +--- Order is clockwise from top-left: TL, T, TR, R, BR, B, BL, L. +local BORDER_CHARS = { + rounded = { "╭", "─", "╮", "│", "╯", "─", "╰", "│" }, + single = { "┌", "─", "┐", "│", "┘", "─", "└", "│" }, + double = { "╔", "═", "╗", "║", "╝", "═", "╚", "║" }, + solid = { " ", " ", " ", " ", " ", " ", " ", " " }, +} + +-- T-junction connectors used to replace the top corners when stitching two +-- boxes together (the bottom of box A and the top of box B share a row). +local MERGE_CONNECTORS = { + rounded = { left = "├", right = "┤" }, + single = { left = "├", right = "┤" }, + double = { left = "╠", right = "╣" }, + solid = { left = " ", right = " " }, +} + +--- Build an 8-element border array whose top row is a T-junction stitching +--- into the bottom of a parent box above it. Accepts either one of the +--- built-in border style names or an existing 8-element border array. +--- +--- Returns `nil` for unrecognised / non-mergeable borders (e.g. `"none"`, +--- `"shadow"`) so the caller can fall back to an unmerged layout. +---@param border string|table +---@return table|nil +function M.merged_top_border(border) + local chars, connectors + if type(border) == "string" then + chars = BORDER_CHARS[border] + connectors = MERGE_CONNECTORS[border] + elseif type(border) == "table" and #border == 8 then + chars = vim.deepcopy(border) + -- Best-effort fallback for custom arrays: use the straight T's. + connectors = { left = "├", right = "┤" } + end + if not chars or not connectors then + return nil + end + return { + connectors.left, + chars[2], + connectors.right, + chars[4], + chars[5], + chars[6], + chars[7], + chars[8], + } +end + local _excluded_registered = false -- Append `ft` to a list-shaped config field if missing. diff --git a/tests/test_inputs_select.lua b/tests/test_inputs_select.lua index 615b460..6cf909b 100644 --- a/tests/test_inputs_select.lua +++ b/tests/test_inputs_select.lua @@ -113,6 +113,27 @@ T["select input"]["uses custom chevrons from config"] = function() helpers.expect.no_match(open_line, "⌃") end +T["select input"]["dropdown border merges into select's bottom border"] = function() + -- `rounded` default → T-junctions are ├ and ┤. + child.lua([[ + _G.t = _G.mk('a') + _G.t:mount({ row = 5, col = 5, width = 30 }) + _G.t._layout = { row = 10, col = 5, width = 30 } + _G.t:open_dropdown() + ]]) + local cfg = child.lua_get([[vim.api.nvim_win_get_config(_G.t.dropdown_win)]]) + -- Dropdown row must overlap the select's bottom border row (= layout.row + 2 + -- for the content origin, putting the top border at layout.row + 1). + eq(cfg.row, 12) + -- Border is an 8-element array with T-junctions in the top corners. + eq(type(cfg.border), "table") + -- nvim_win_get_config returns borders as { { char, hl_group }, ... }. + local tl = type(cfg.border[1]) == "table" and cfg.border[1][1] or cfg.border[1] + local tr = type(cfg.border[3]) == "table" and cfg.border[3][1] or cfg.border[3] + eq(tl, "├") + eq(tr, "┤") +end + T["select input"]["rejects empty options list"] = function() local ok = child.lua_get([[ (function()