From 213c0c38293c2e6fc5a30229ee0e41f0769ae12f Mon Sep 17 00:00:00 2001 From: r0nsha Date: Fri, 27 Jun 2025 14:09:47 +0300 Subject: [PATCH] refactor: extract input prompt logic into dedicated modules move input prompt creation and management to lua/lib/input.lua define configuration types and defaults in lua/lib/config.lua extract common utility functions to lua/lib/utils.lua lua/wrapinput.lua now serves as the main entry point improves code organization and maintainability --- lua/lib/config.lua | 34 ++++++++ lua/lib/input.lua | 149 +++++++++++++++++++++++++++++++++++ lua/lib/utils.lua | 58 ++++++++++++++ lua/wrapinput.lua | 188 +-------------------------------------------- 4 files changed, 244 insertions(+), 185 deletions(-) create mode 100644 lua/lib/config.lua create mode 100644 lua/lib/input.lua create mode 100644 lua/lib/utils.lua diff --git a/lua/lib/config.lua b/lua/lib/config.lua new file mode 100644 index 0000000..859253d --- /dev/null +++ b/lua/lib/config.lua @@ -0,0 +1,34 @@ +---@class wrapinput.Config +---@field opts wrapinput.Opts +---@field width wrapinput.Limits +---@field height wrapinput.Limits +---@field padding integer How much padding will be added to the end of the input buffer +---@field win vim.api.keyset.win_config + +---@class wrapinput.Opts +---@field numbers "always" | "multiline" | "never" When to show line numbers +--- "always" will always show line numbers +--- "multiline" will only show line numbers if the input's height is > 1 +--- "never" will never show line numbers + +---@class wrapinput.Limits +---@field min integer +---@field max integer + +return { + ---@type wrapinput.Config + defaults = { + opts = { numbers = "multiline" }, + padding = 5, + width = { min = 20, max = 60 }, + height = { min = 1, max = 6 }, + win = { + title = "Input: ", + style = "minimal", + focusable = true, + relative = "cursor", + col = -1, + height = 1, + }, + }, +} diff --git a/lua/lib/input.lua b/lua/lib/input.lua new file mode 100644 index 0000000..fb2f7d8 --- /dev/null +++ b/lua/lib/input.lua @@ -0,0 +1,149 @@ +local utils = require("lib.utils") + +---@class wrapinput.Input +---@field config wrapinput.Config +---@field winnr integer +---@field bufnr integer +---@field on_confirm fun(input?: string) +local Input = {} + +local defaults = { + opts = { numbers = "multiline" }, + padding = 5, + width = { min = 20, max = 60 }, + height = { min = 1, max = 6 }, + win = { + title = "Input: ", + style = "minimal", + focusable = true, + relative = "cursor", + col = -1, + height = 1, + }, +} + +local group = vim.api.nvim_create_augroup("wrapinput.nvim", { clear = true }) + +---@param config wrapinput.Config +---@param opts any +---@param on_confirm fun(input?: string) +function Input:new(config, opts, on_confirm) + local i = {} + i.config = vim.tbl_deep_extend("force", defaults, config, { win = { title = opts.prompt or "Input: " } }) + i.default = opts.default or "" + i.on_confirm = on_confirm or function() end + setmetatable(i, self) + self.__index = self + return i +end + +---@param default string +function Input:open(default) + local width = + utils.clamp(vim.fn.strdisplaywidth(default) + self.config.padding, self.config.width.min, self.config.width.max) + + -- Position window relative to the cursor, such that it doesn't overlap with the cursor's line. + local curr_win = vim.api.nvim_get_current_win() + local cursor_row = vim.api.nvim_win_get_cursor(curr_win)[1] + local win_config = (cursor_row <= 3) and { anchor = "NW", row = 1, width = width } + or { anchor = "SW", row = 0, width = width } + self.config = vim.tbl_deep_extend("keep", self.config, { win = win_config }) + + -- Create buffer and floating window. + self.bufnr = vim.api.nvim_create_buf(false, true) + utils.set_options( + { buftype = "prompt", bufhidden = "wipe", textwidth = self.config.width.max }, + { buf = self.bufnr } + ) + vim.fn.prompt_setprompt(self.bufnr, "") + + self.winnr = vim.api.nvim_open_win(self.bufnr, true, self.config.win) + utils.set_options({ wrap = true, linebreak = true, winhighlight = "Search:None" }, { win = self.winnr }) + + -- Write default value and put cursor at the end + vim.api.nvim_buf_set_text(self.bufnr, 0, 0, 0, 0, { default }) + vim.cmd("startinsert") + vim.api.nvim_win_set_cursor(self.winnr, { 1, vim.str_utfindex(default, "utf-8") + 1 }) + + self:resize() + self:autocmds() + self:mappings() +end + +---@param result string? +function Input:close(result) + vim.cmd("stopinsert") + vim.api.nvim_win_close(self.winnr, true) + self.on_confirm(result) +end + +function Input:resize() + local text = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) + local line = table.concat(text, "") + + if line == "" then + vim.api.nvim_win_set_width( + self.winnr, + utils.clamp(self.config.padding, self.config.width.min, self.config.width.max) + ) + vim.api.nvim_win_set_height(self.winnr, 1) + return + end + + local lines = utils.split_wrapped_lines(line, self.config.width.max) + + local lens = vim.tbl_map(function(l) + return vim.fn.strdisplaywidth(l) + end, lines) + local width = + utils.clamp(math.max(unpack(lens)) + self.config.padding, self.config.width.min, self.config.width.max + #lines) + vim.api.nvim_win_set_width(self.winnr, width) + + local height = utils.clamp(#lines, self.config.height.min, self.config.height.max) + vim.api.nvim_win_set_height(self.winnr, height) + + if self.config.opts.numbers == "always" or (self.config.opts.numbers == "multiline" and height > 1) then + utils.set_option_if_globally_enabled("number", self.winnr) + utils.set_option_if_globally_enabled("relativenumber", self.winnr) + end + + if + vim.api.nvim_get_option_value("number", { win = self.winnr }) + or vim.api.nvim_get_option_value("relativenumber", { win = self.winnr }) + then + vim.api.nvim_win_set_width(self.winnr, width + utils.get_linenr_width()) + end +end + +function Input:autocmds() + vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { + group = group, + buffer = self.bufnr, + callback = function() + self:resize() + end, + }) +end + +function Input:mappings() + ---@param mode string|string[] + ---@param lhs string + ---@param rhs string|function + local function map(mode, lhs, rhs) + vim.keymap.set(mode, lhs, rhs, { buffer = self.bufnr }) + end + + map({ "n", "i", "v" }, "", function() + self:close(vim.api.nvim_buf_get_lines(self.bufnr, 0, 1, false)[1]) + end) + + map("n", "", function() + self:close() + end) + + map("n", "q", function() + self:close() + end) +end + +return Input diff --git a/lua/lib/utils.lua b/lua/lib/utils.lua new file mode 100644 index 0000000..039da5f --- /dev/null +++ b/lua/lib/utils.lua @@ -0,0 +1,58 @@ +local M = {} + +---@param options table +---@param opts vim.api.keyset.option +function M.set_options(options, opts) + for k, v in pairs(options) do + vim.api.nvim_set_option_value(k, v, opts) + end +end + +---@param option string +---@param winnr integer +function M.set_option_if_globally_enabled(option, winnr) + if vim.api.nvim_get_option_value(option, { scope = "global" }) then + vim.api.nvim_set_option_value(option, true, { win = winnr }) + end +end + +---@param value number +---@param min number +---@param max number +---@return number +function M.clamp(value, min, max) + return math.min(math.max(value, min), max) +end + +---@param text string +---@param width integer +---@return string[] +function M.split_wrapped_lines(text, width) + if text == "" then + return {} + end + + ---@type string[] + local lines = {} + local textlen = vim.fn.strchars(text, true) + + 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 + +function M.get_linenr_width() + local line_count = vim.api.nvim_buf_line_count(0) + local line_digits = math.floor(math.log10(math.max(1, line_count))) + 1 + local numberwidth_opt = vim.opt.numberwidth:get() + return math.max(line_digits, numberwidth_opt) +end + +return M diff --git a/lua/wrapinput.lua b/lua/wrapinput.lua index df0ffdd..af0e32c 100644 --- a/lua/wrapinput.lua +++ b/lua/wrapinput.lua @@ -1,194 +1,12 @@ ----@class wrapinput.Config ----@field width wrapinput.Limits ----@field height wrapinput.Limits ----@field padding integer ----@field win vim.api.keyset.win_config - ----@class wrapinput.Limits ----@field min integer ----@field max integer - -local group = vim.api.nvim_create_augroup("wrapinput.nvim", { clear = true }) - ----@type wrapinput.Config -local defaults = { - padding = 5, - width = { min = 20, max = 60 }, - height = { min = 1, max = 6 }, - win = { - title = "Input: ", - style = "minimal", - focusable = true, - relative = "cursor", - col = -1, - height = 1, - }, -} - ----@param value number ----@param min number ----@param max number ----@return number -local function clamp(value, min, max) - return math.min(math.max(value, min), max) -end - ----@return vim.api.keyset.win_config -local function get_relative_win_config() - local curr_win = vim.api.nvim_get_current_win() - local cursor_row = vim.api.nvim_win_get_cursor(curr_win)[1] - if cursor_row <= 3 then - return { anchor = "NW", row = 1 } - else - return { anchor = "SW", row = 0 } - end -end - ----@param text string ----@param width integer ----@return string[] -local function split_wrapped_lines(text, width) - if text == "" then - return {} - end - - ---@type string[] - local lines = {} - local textlen = vim.fn.strchars(text, true) - - 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 option string ----@param winnr integer -local function set_option_if_globally_enabled(option, winnr) - if vim.api.nvim_get_option_value(option, { scope = "global" }) then - vim.api.nvim_set_option_value(option, true, { win = winnr }) - end -end - ----@param winnr integer ----@param bufnr integer ----@param config wrapinput.Config -local function resize(winnr, bufnr, config) - local text = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - local line = table.concat(text, "") - - if line == "" then - vim.api.nvim_win_set_width(winnr, clamp(config.padding, config.width.min, config.width.max)) - vim.api.nvim_win_set_height(winnr, 1) - return - end - - local lines = split_wrapped_lines(line, config.width.max) - - local lens = vim.tbl_map(function(l) - return vim.fn.strdisplaywidth(l) - end, lines) - local width = clamp(math.max(unpack(lens)) + config.padding + #lines, config.width.min, config.width.max + #lines) - vim.api.nvim_win_set_width(winnr, width) - - local height = clamp(#lines, config.height.min, config.height.max) - vim.api.nvim_win_set_height(winnr, height) - - if height > 1 then - set_option_if_globally_enabled("number", winnr) - set_option_if_globally_enabled("relativenumber", winnr) - end -end - ----@param winnr integer ----@param bufnr integer ----@param config wrapinput.Config -local function setup_autocmds(winnr, bufnr, config) - vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { - group = group, - buffer = bufnr, - callback = function() - resize(winnr, bufnr, config) - end, - }) -end - ----@param options table ----@param opts vim.api.keyset.option -local function set_options(options, opts) - for k, v in pairs(options) do - vim.api.nvim_set_option_value(k, v, opts) - end -end - ----@param winnr integer ----@param bufnr integer ----@param on_confirm fun(input?: string) -local function setup_mappings(winnr, bufnr, on_confirm) - ---@param result string? - local function close(result) - vim.cmd("stopinsert") - vim.api.nvim_win_close(winnr, true) - on_confirm(result) - end - - ---@param mode string|string[] - ---@param lhs string - ---@param rhs string|function - local function map(mode, lhs, rhs) - vim.keymap.set(mode, lhs, rhs, { buffer = bufnr }) - end - - map({ "n", "i", "v" }, "", function() - close(vim.api.nvim_buf_get_lines(bufnr, 0, 1, false)[1]) - end) - map("n", "", close) - map("n", "q", close) -end +local Input = require("lib.input") local M = {} ----@param config wrapinput.Config ----@param opts table ----@param on_confirm fun(input: string?) -function M.input(config, opts, on_confirm) - config = vim.tbl_deep_extend("force", defaults, config, { win = { title = opts.prompt } }) - local default = opts.default or "" - on_confirm = on_confirm or function() end - - local width = clamp(vim.fn.strdisplaywidth(default) + config.padding, config.width.min, config.width.max) - vim.notify("width: " .. tostring(width)) - config = vim.tbl_deep_extend("keep", config, { win = get_relative_win_config() }, { win = { width = width } }) - - -- Create buffer and floating window. - local bufnr = vim.api.nvim_create_buf(false, true) - set_options({ buftype = "prompt", bufhidden = "wipe", textwidth = config.width.max }, { buf = bufnr }) - vim.fn.prompt_setprompt(bufnr, "") - - local winnr = vim.api.nvim_open_win(bufnr, true, config.win) - set_options({ wrap = true, linebreak = true, winhighlight = "Search:None" }, { win = winnr }) - - -- write default value and put cursor at the end - vim.api.nvim_buf_set_text(bufnr, 0, 0, 0, 0, { default }) - vim.cmd("startinsert") - vim.api.nvim_win_set_cursor(winnr, { 1, vim.str_utfindex(default, "utf-8") + 1 }) - - resize(winnr, bufnr, config) - - setup_mappings(winnr, bufnr, on_confirm) - setup_autocmds(winnr, bufnr, config) -end - ---@param config? wrapinput.Config function M.setup(config) vim.ui.input = function(opts, on_confirm) - M.input(config or {}, opts or {}, on_confirm) + local input = Input:new(config or {}, opts or {}, on_confirm) + input:open(opts.default or "") end end -- 2.54.0