diff --git a/README.md b/README.md index 1f87e91..4065421 100644 --- a/README.md +++ b/README.md @@ -30,21 +30,22 @@ Configuration is optional, only needed if you want to override defaults. ### Available Options -| Option | Description | Type | Notes | -| ---------------- | ---------------------------------------- | -------------------- | ----------------------------------- | -| `icon` | Icon displayed next to the prompt | `string` | N/A | -| `default_prompt` | Default text for the prompt | `string` | N/A | -| `win_options` | Window-level Vim options | `table` | See `:h nvim_win_set_option` | -| `win_config` | Window configuration for `nvim_open_win` | `table` | See `:h nvim_open_win` | -| `width_options` | Dynamic width settings | `table` | See [Width Options](#width-options) | - -#### Width Options - -| Sub-Option | Description | Type | Notes | -| ----------- | --------------------- | ------------------ | ------------------------------------ | -| `prefer` | Preferred input width | `number` | Default target width | -| `min_value` | Minimum allowed width | `{number, number}` | Fixed width or ratio of window width | -| `max_value` | Maximum allowed width | `{number, number}` | Fixed width or ratio of window width | +| Option | Description | Type | Notes | +| ---------------- | ---------------------------------------- | -------------------- | --------------------------------- | +| `icon` | Icon displayed next to the prompt | `string` | N/A | +| `default_prompt` | Default text for the prompt | `string` | N/A | +| `win_options` | Window-level Vim options | `table` | See `:h nvim_win_set_option` | +| `win_config` | Window configuration for `nvim_open_win` | `table` | See `:h nvim_open_win` | +| `size_options` | Dynamic sizing configuration | `table` | See [Size Options](#size-options) | + +#### Size Options + +| Option | Description | Type | Notes | +| ------------ | ---------------------------------- | --------- | ----- | +| `width.min` | Minimum width of the input window | `integer` | N/A | +| `width.max` | Maximum width of the input window | `integer` | N/A | +| `height.min` | Minimum height of the input window | `integer` | N/A | +| `height.max` | Maximum height of the input window | `integer` | N/A | ### Default Configuration @@ -53,27 +54,29 @@ require("input").setup({ icon = "", default_prompt = "Input", win_options = { - wrap = false, - list = true, - listchars = "precedes:…,extends:…", - sidescrolloff = 0, + wrap = true, + linebreak = true, + winhighlight = "Search:None", }, win_config = { relative = "cursor", anchor = "NW", border = vim.o.winborder, row = 1, - col = 1, - width = 40, + col = -1, + width = 1, height = 1, - focusable = false, - noautocmd = true, style = "minimal", }, - width_options = { - prefer = 40, - min_value = { 20, 0.2 }, - max_value = { 140, 0.9 }, + size_options = { + width = { + min = 40, + max = 60, + }, + height = { + min = 1, + max = 6, + }, }, }) ``` @@ -85,6 +88,7 @@ These are the default key mappings: | Keybinding | Mode(s) | Action | | ---------- | ------- | --------------------------------------- | | `` | i, n | Cancel content changes and close input | +| `` | n | Cancel content changes and close input | | `q` | n | Cancel content changes and close input | | `` | i, n | Confirm content changes and close input | @@ -109,6 +113,7 @@ For more, see `:h winhighlight`. ## 👀 Inspiration - [dressing.nvim](https://github.com/stevearc/dressing.nvim) +- [multinput.nvim](https://github.com/r0nsha/multinput.nvim) ## :scroll: Contribution diff --git a/lua/input/config.lua b/lua/input/config.lua index d4319ca..d19719d 100644 --- a/lua/input/config.lua +++ b/lua/input/config.lua @@ -1,14 +1,17 @@ ----@class input.width_options ----@field prefer number ----@field min_value number[] ----@field max_value number[] +---@class input.type.range +---@field min integer +---@field max integer + +---@class input.config.size_options +---@field width input.type.range +---@field height input.type.range ---@class input.config ---@field icon string ---@field default_prompt string ---@field win_options vim.wo ---@field win_config vim.api.keyset.win_config ----@field width_options input.width_options +---@field size_options input.config.size_options local config = {} ---@type input.config @@ -16,25 +19,29 @@ local defaults = { icon = " ", default_prompt = "Input", win_options = { - wrap = false, - list = true, - listchars = "precedes:…,extends:…", - sidescrolloff = 0, + wrap = true, + linebreak = true, + winhighlight = "Search:None", }, win_config = { relative = "cursor", anchor = "NW", border = vim.o.winborder, row = 1, - col = 1, - width = 40, + col = -1, + width = 1, height = 1, style = "minimal", }, - width_options = { - prefer = 40, - min_value = { 20, 0.2 }, - max_value = { 140, 0.9 }, + size_options = { + width = { + min = 40, + max = 60, + }, + height = { + min = 1, + max = 6, + }, }, } diff --git a/lua/input/init.lua b/lua/input/init.lua index 8309c53..aae3ae3 100644 --- a/lua/input/init.lua +++ b/lua/input/init.lua @@ -7,24 +7,62 @@ local buf_options = { filetype = "input", } +---Trim and pad title. +---@param title string +---@return string +local function trim_and_pad_title(title) + title = vim.trim(title):gsub(":$", "") + + return (" %s "):format(title) +end + +---Clamp value to between min and max. +---@param value number +---@param min number +---@param max number +local function clamp(value, min, max) + return math.max(math.min(value, max), min) +end + +---Split wrapped lines. +---@param text string +---@param width number +local function split_wrapped_lines(text, width) + if text == "" then + return {} + end + + local lines = {} + local textlen = vim.fn.strcharlen(text) + + local i = 0 + + while i < textlen do + local len = i + width <= textlen and width or textlen - i + local new_line = vim.fn.strcharpart(text, i, len) + + table.insert(lines, new_line) + + i = i + len + end + + return lines +end + ---@param opts? vim.ui.input.Opts ---@param on_confirm fun(input?: string) local function input(opts, on_confirm) opts = opts or {} local config = require "input.config" - local utils = require "input.utils" + + local size_options = config.size_options local win_config = config.win_config local prompt = opts.prompt or config.default_prompt local default = opts.default or "" - local prompt_lines = vim.split(prompt, "\n", { plain = true, trimempty = true }) - local width = utils.calculate_width(win_config.relative, win_config.width, config.width_options) - width = math.max(width, utils.get_max_strwidth(prompt_lines) + 4, vim.api.nvim_strwidth(default) + 2) - - win_config.title = utils.trim_and_pad_title(prompt) - win_config.width = utils.calculate_width(win_config.relative, width, config.width_options) + win_config.title = trim_and_pad_title(prompt) -- Create buffer. local bufnr = vim.api.nvim_create_buf(false, true) @@ -56,8 +94,29 @@ local function input(opts, on_confirm) confirm(nil) end + local function resize() + local content = table.concat(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), "") + local width, height = size_options.width.min, size_options.height.min + + if content ~= "" then + local lines = split_wrapped_lines(content, size_options.width.max) + local max_len_width = vim.iter(lines):fold(size_options.width.min, function(acc, line) + return math.max(acc, vim.api.nvim_strwidth(line)) + end) + + width = clamp(max_len_width, size_options.width.min, size_options.width.max) + 1 + height = clamp( + width == size_options.width.max + 1 and #lines + 1 or #lines, + size_options.height.min, + size_options.height.max + ) + end + + vim.api.nvim_win_set_config(winid, { width = width, height = height }) + end + local prompt_icon = (" %s "):format(config.icon) - local icon_end_col = #prompt_icon + local icon_end_col = vim.fn.strlen(prompt_icon) vim.fn.prompt_setprompt(bufnr, prompt_icon) vim.fn.prompt_setcallback(bufnr, confirm) @@ -65,16 +124,14 @@ local function input(opts, on_confirm) vim.api.nvim_win_call(winid, function() vim.api.nvim_buf_set_text(bufnr, 0, icon_end_col, 0, icon_end_col, { default }) + resize() vim.cmd.startinsert() end) - vim.api.nvim_win_set_cursor(winid, { 1, #default + icon_end_col }) + vim.api.nvim_win_set_cursor(winid, { 1, vim.api.nvim_strwidth(default) + icon_end_col }) local ns = vim.api.nvim_create_namespace "input" - vim.api.nvim_buf_set_extmark(bufnr, ns, 0, 1, { - hl_group = "InputIcon", - end_col = icon_end_col - 1, - }) + vim.hl.range(bufnr, ns, "InputIcon", { 0, 1 }, { 0, icon_end_col }) vim.keymap.set("n", "", cancel, { buffer = bufnr }) vim.keymap.set("n", "q", cancel, { buffer = bufnr }) @@ -107,6 +164,16 @@ local function input(opts, on_confirm) end end, }) + + vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { + group = augroup, + desc = "Resize vim.ui.input", + buffer = bufnr, + nested = true, + callback = function() + resize() + end, + }) end function M.setup(opts) diff --git a/lua/input/utils.lua b/lua/input/utils.lua deleted file mode 100644 index c717629..0000000 --- a/lua/input/utils.lua +++ /dev/null @@ -1,92 +0,0 @@ ----Thanks: https://github.com/stevearc/dressing.nvim/blob/master/lua/dressing/util.lua -local M = {} - ----@param value number ----@param max_value number ----@return number -local function calculate_float(value, max_value) - if value then - local _, p = math.modf(value) - - if p ~= 0 then - return math.min(max_value, value * max_value) - end - end - - return value -end - ----@param value number[] ----@param max_value number ----@param aggregator fun(x: number, ...: number) ----@param limit number ----@return number -local function calculate_list(value, max_value, aggregator, limit) - local result = limit - - for _, v in ipairs(value) do - result = aggregator(result, calculate_float(v, max_value)) - end - - return result -end - ----@param relative "cursor"|"editor"|"laststatus"|"mouse"|"tabline"|"win" ----@param winid? integer ----@return integer -local function get_max_width(relative, winid) - return relative == "editor" and vim.o.columns or vim.api.nvim_win_get_width(winid or 0) -end - ----Calculate size. ----@param current_size number ----@param size_options input.width_options ----@param total_size number ----@return integer -local function calculate_size(current_size, size_options, total_size) - local result = calculate_float(current_size, total_size) - local min_val = calculate_list(size_options.min_value, total_size, math.max, 1) - local max_val = calculate_list(size_options.max_value, total_size, math.min, total_size) - - if not result then - result = calculate_float(size_options.prefer, total_size) - end - - result = math.min(result, max_val) - result = math.max(result, min_val) - - return math.floor(result) -end - ----Calculate width. ----@param relative "cursor"|"editor"|"laststatus"|"mouse"|"tabline"|"win" ----@param current_size number ----@param width_options input.width_options ----@return integer -function M.calculate_width(relative, current_size, width_options) - return calculate_size(current_size, width_options, get_max_width(relative)) -end - ----Get max string width. ----@param lines string[] ----@return integer -function M.get_max_strwidth(lines) - local max = 0 - - for _, line in ipairs(lines) do - max = math.max(max, vim.api.nvim_strwidth(line)) - end - - return max -end - ----Trim and pad title. ----@param title string ----@return string -function M.trim_and_pad_title(title) - title = vim.trim(title):gsub(":$", "") - - return (" %s "):format(title) -end - -return M