A floating vim.ui.input buffer that auto-resizes to fit its contents.
0

Configure Feed

Select the types of activity you want to include in your feed.

change stylua config

r0nsha (Jun 27, 2025, 3:45 PM +0300) 0a81f99d fb19d13b

+180 -146
+6
.stylua.toml
··· 1 + column_width = 88 2 + line_endings = "Unix" 3 + indent_type = "Spaces" 4 + indent_width = 4 5 + quote_style = "AutoPreferDouble" 6 + no_call_parentheses = true
+15 -15
lua/lib/config.lua
··· 16 16 ---@field max integer 17 17 18 18 return { 19 - ---@type multinput.Config 20 - defaults = { 21 - opts = { numbers = "multiline" }, 22 - padding = 5, 23 - width = { min = 20, max = 60 }, 24 - height = { min = 1, max = 6 }, 25 - win = { 26 - title = "Input: ", 27 - style = "minimal", 28 - focusable = true, 29 - relative = "cursor", 30 - col = -1, 31 - height = 1, 32 - }, 33 - }, 19 + ---@type multinput.Config 20 + defaults = { 21 + opts = { numbers = "multiline" }, 22 + padding = 5, 23 + width = { min = 20, max = 60 }, 24 + height = { min = 1, max = 6 }, 25 + win = { 26 + title = "Input: ", 27 + style = "minimal", 28 + focusable = true, 29 + relative = "cursor", 30 + col = -1, 31 + height = 1, 32 + }, 33 + }, 34 34 }
+129 -101
lua/lib/input.lua
··· 1 - local utils = require("lib.utils") 1 + local utils = require "lib.utils" 2 2 3 3 ---@class multinput.Input 4 4 ---@field config multinput.Config ··· 8 8 local Input = {} 9 9 10 10 local defaults = { 11 - opts = { numbers = "multiline" }, 12 - padding = 5, 13 - width = { min = 10, max = 50 }, 14 - height = { min = 1, max = 8 }, 15 - win = { 16 - title = "Input: ", 17 - style = "minimal", 18 - focusable = true, 19 - relative = "cursor", 20 - col = -1, 21 - height = 1, 22 - }, 11 + opts = { numbers = "multiline" }, 12 + padding = 5, 13 + width = { min = 10, max = 50 }, 14 + height = { min = 1, max = 8 }, 15 + win = { 16 + title = "Input: ", 17 + style = "minimal", 18 + focusable = true, 19 + relative = "cursor", 20 + col = -1, 21 + height = 1, 22 + }, 23 23 } 24 24 25 25 local group = vim.api.nvim_create_augroup("multinput.nvim", { clear = true }) ··· 28 28 ---@param opts any 29 29 ---@param on_confirm fun(input?: string) 30 30 function Input:new(config, opts, on_confirm) 31 - local i = {} 32 - i.config = vim.tbl_deep_extend("force", defaults, config, { win = { title = opts.prompt or "Input: " } }) 33 - i.default = opts.default or "" 34 - i.on_confirm = on_confirm or function() end 35 - setmetatable(i, self) 36 - self.__index = self 37 - return i 31 + local i = {} 32 + i.config = vim.tbl_deep_extend( 33 + "force", 34 + defaults, 35 + config, 36 + { win = { title = opts.prompt or "Input: " } } 37 + ) 38 + i.default = opts.default or "" 39 + i.on_confirm = on_confirm or function() end 40 + setmetatable(i, self) 41 + self.__index = self 42 + return i 38 43 end 39 44 40 45 ---@param default string 41 46 function Input:open(default) 42 - local width = 43 - utils.clamp(vim.fn.strdisplaywidth(default) + self.config.padding, self.config.width.min, self.config.width.max) 47 + local width = utils.clamp( 48 + vim.fn.strdisplaywidth(default) + self.config.padding, 49 + self.config.width.min, 50 + self.config.width.max 51 + ) 44 52 45 - -- Position window relative to the cursor, such that it doesn't overlap with the cursor's line. 46 - local curr_win = vim.api.nvim_get_current_win() 47 - local cursor_row = vim.api.nvim_win_get_cursor(curr_win)[1] 48 - local win_config = (cursor_row <= 3) and { anchor = "NW", row = 1, width = width } 49 - or { anchor = "SW", row = 0, width = width } 50 - self.config = vim.tbl_deep_extend("keep", self.config, { win = win_config }) 53 + -- Position window relative to the cursor, such that it doesn't overlap with the cursor's line. 54 + local curr_win = vim.api.nvim_get_current_win() 55 + local cursor_row = vim.api.nvim_win_get_cursor(curr_win)[1] 56 + local win_config = (cursor_row <= 3) and { anchor = "NW", row = 1, width = width } 57 + or { anchor = "SW", row = 0, width = width } 58 + self.config = vim.tbl_deep_extend("keep", self.config, { win = win_config }) 51 59 52 - -- Create buffer and floating window. 53 - self.bufnr = vim.api.nvim_create_buf(false, true) 54 - utils.set_options( 55 - { buftype = "prompt", bufhidden = "wipe", textwidth = self.config.width.max }, 56 - { buf = self.bufnr } 57 - ) 58 - vim.fn.prompt_setprompt(self.bufnr, "") 60 + -- Create buffer and floating window. 61 + self.bufnr = vim.api.nvim_create_buf(false, true) 62 + utils.set_options( 63 + { buftype = "prompt", bufhidden = "wipe", textwidth = self.config.width.max }, 64 + { buf = self.bufnr } 65 + ) 66 + vim.fn.prompt_setprompt(self.bufnr, "") 59 67 60 - self.winnr = vim.api.nvim_open_win(self.bufnr, true, self.config.win) 61 - utils.set_options({ wrap = true, linebreak = true, winhighlight = "Search:None" }, { win = self.winnr }) 68 + self.winnr = vim.api.nvim_open_win(self.bufnr, true, self.config.win) 69 + utils.set_options( 70 + { wrap = true, linebreak = true, winhighlight = "Search:None" }, 71 + { win = self.winnr } 72 + ) 62 73 63 - -- Write default value and put cursor at the end 64 - vim.api.nvim_buf_set_text(self.bufnr, 0, 0, 0, 0, { default }) 65 - vim.cmd("startinsert") 66 - vim.api.nvim_win_set_cursor(self.winnr, { 1, vim.str_utfindex(default, "utf-8") + 1 }) 74 + -- Write default value and put cursor at the end 75 + vim.api.nvim_buf_set_text(self.bufnr, 0, 0, 0, 0, { default }) 76 + vim.cmd "startinsert" 77 + vim.api.nvim_win_set_cursor( 78 + self.winnr, 79 + { 1, vim.str_utfindex(default, "utf-8") + 1 } 80 + ) 67 81 68 - self:resize() 69 - self:autocmds() 70 - self:mappings() 82 + self:resize() 83 + self:autocmds() 84 + self:mappings() 71 85 end 72 86 73 87 ---@param result string? 74 88 function Input:close(result) 75 - vim.cmd("stopinsert") 76 - vim.api.nvim_win_close(self.winnr, true) 77 - self.on_confirm(result) 89 + vim.cmd "stopinsert" 90 + vim.api.nvim_win_close(self.winnr, true) 91 + self.on_confirm(result) 78 92 end 79 93 80 94 function Input:resize() 81 - local text = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) 82 - local line = table.concat(text, "") 95 + local text = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) 96 + local line = table.concat(text, "") 83 97 84 - if line == "" then 85 - vim.api.nvim_win_set_width( 86 - self.winnr, 87 - utils.clamp(self.config.padding, self.config.width.min, self.config.width.max) 88 - ) 89 - vim.api.nvim_win_set_height(self.winnr, 1) 90 - return 91 - end 98 + if line == "" then 99 + vim.api.nvim_win_set_width( 100 + self.winnr, 101 + utils.clamp( 102 + self.config.padding, 103 + self.config.width.min, 104 + self.config.width.max 105 + ) 106 + ) 107 + vim.api.nvim_win_set_height(self.winnr, 1) 108 + return 109 + end 92 110 93 - local lines = utils.split_wrapped_lines(line, self.config.width.max) 111 + local lines = utils.split_wrapped_lines(line, self.config.width.max) 94 112 95 - local lens = vim.tbl_map(function(l) 96 - return vim.fn.strdisplaywidth(l) 97 - end, lines) 98 - local width = 99 - utils.clamp(math.max(unpack(lens)) + self.config.padding, self.config.width.min, self.config.width.max + #lines) 100 - vim.api.nvim_win_set_width(self.winnr, width) 113 + local lens = vim.tbl_map(function(l) 114 + return vim.fn.strdisplaywidth(l) 115 + end, lines) 116 + local width = utils.clamp( 117 + math.max(unpack(lens)) + self.config.padding, 118 + self.config.width.min, 119 + self.config.width.max + #lines 120 + ) 121 + vim.api.nvim_win_set_width(self.winnr, width) 101 122 102 - local height = utils.clamp(#lines, self.config.height.min, self.config.height.max) 103 - vim.api.nvim_win_set_height(self.winnr, height) 123 + local height = utils.clamp(#lines, self.config.height.min, self.config.height.max) 124 + vim.api.nvim_win_set_height(self.winnr, height) 104 125 105 - if self.config.opts.numbers == "always" or (self.config.opts.numbers == "multiline" and height > 1) then 106 - utils.set_option_if_globally_enabled("number", self.winnr) 107 - utils.set_option_if_globally_enabled("relativenumber", self.winnr) 108 - end 126 + if 127 + self.config.opts.numbers == "always" 128 + or (self.config.opts.numbers == "multiline" and height > 1) 129 + then 130 + utils.set_option_if_globally_enabled("number", self.winnr) 131 + utils.set_option_if_globally_enabled("relativenumber", self.winnr) 132 + end 109 133 110 - if 111 - vim.api.nvim_get_option_value("number", { win = self.winnr }) 112 - or vim.api.nvim_get_option_value("relativenumber", { win = self.winnr }) 113 - then 114 - vim.api.nvim_win_set_width(self.winnr, width + utils.get_linenr_width()) 115 - end 134 + if 135 + vim.api.nvim_get_option_value("number", { win = self.winnr }) 136 + or vim.api.nvim_get_option_value("relativenumber", { win = self.winnr }) 137 + then 138 + vim.api.nvim_win_set_width(self.winnr, width + utils.get_linenr_width()) 139 + end 116 140 end 117 141 118 142 function Input:autocmds() 119 - vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { 120 - group = group, 121 - buffer = self.bufnr, 122 - callback = function() 123 - self:resize() 124 - end, 125 - }) 143 + vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { 144 + group = group, 145 + buffer = self.bufnr, 146 + callback = function() 147 + self:resize() 148 + end, 149 + }) 126 150 end 127 151 128 152 function Input:mappings() 129 - ---@param mode string|string[] 130 - ---@param lhs string 131 - ---@param rhs string|function 132 - local function map(mode, lhs, rhs) 133 - vim.keymap.set(mode, lhs, rhs, { buffer = self.bufnr }) 134 - end 153 + ---@param mode string|string[] 154 + ---@param lhs string 155 + ---@param rhs string|function 156 + local function map(mode, lhs, rhs) 157 + vim.keymap.set(mode, lhs, rhs, { buffer = self.bufnr }) 158 + end 135 159 136 - map({ "n", "i", "v" }, "<cr>", function() 137 - self:close(vim.api.nvim_buf_get_lines(self.bufnr, 0, 1, false)[1]) 138 - end) 139 - map({ "i" }, "<a-cr>", function() 140 - vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<cr>", true, false, true), "n", true) 141 - end) 160 + map({ "n", "i", "v" }, "<cr>", function() 161 + self:close(vim.api.nvim_buf_get_lines(self.bufnr, 0, 1, false)[1]) 162 + end) 163 + map({ "i" }, "<a-cr>", function() 164 + vim.api.nvim_feedkeys( 165 + vim.api.nvim_replace_termcodes("<cr>", true, false, true), 166 + "n", 167 + true 168 + ) 169 + end) 142 170 143 - map("n", "<esc>", function() 144 - self:close() 145 - end) 171 + map("n", "<esc>", function() 172 + self:close() 173 + end) 146 174 147 - map("n", "q", function() 148 - self:close() 149 - end) 175 + map("n", "q", function() 176 + self:close() 177 + end) 150 178 end 151 179 152 180 return Input
+25 -25
lua/lib/utils.lua
··· 3 3 ---@param options table<string, any> 4 4 ---@param opts vim.api.keyset.option 5 5 function M.set_options(options, opts) 6 - for k, v in pairs(options) do 7 - vim.api.nvim_set_option_value(k, v, opts) 8 - end 6 + for k, v in pairs(options) do 7 + vim.api.nvim_set_option_value(k, v, opts) 8 + end 9 9 end 10 10 11 11 ---@param option string 12 12 ---@param winnr integer 13 13 function M.set_option_if_globally_enabled(option, winnr) 14 - if vim.api.nvim_get_option_value(option, { scope = "global" }) then 15 - vim.api.nvim_set_option_value(option, true, { win = winnr }) 16 - end 14 + if vim.api.nvim_get_option_value(option, { scope = "global" }) then 15 + vim.api.nvim_set_option_value(option, true, { win = winnr }) 16 + end 17 17 end 18 18 19 19 ---@param value number ··· 21 21 ---@param max number 22 22 ---@return number 23 23 function M.clamp(value, min, max) 24 - return math.min(math.max(value, min), max) 24 + return math.min(math.max(value, min), max) 25 25 end 26 26 27 27 ---@param text string 28 28 ---@param width integer 29 29 ---@return string[] 30 30 function M.split_wrapped_lines(text, width) 31 - if text == "" then 32 - return {} 33 - end 31 + if text == "" then 32 + return {} 33 + end 34 34 35 - ---@type string[] 36 - local lines = {} 37 - local textlen = vim.fn.strchars(text, true) 35 + ---@type string[] 36 + local lines = {} 37 + local textlen = vim.fn.strchars(text, true) 38 38 39 - local i = 0 40 - while i < textlen do 41 - local len = i + width <= textlen and width or textlen - i 42 - local new_line = vim.fn.strcharpart(text, i, len) 39 + local i = 0 40 + while i < textlen do 41 + local len = i + width <= textlen and width or textlen - i 42 + local new_line = vim.fn.strcharpart(text, i, len) 43 43 44 - table.insert(lines, new_line) 45 - i = i + len 46 - end 44 + table.insert(lines, new_line) 45 + i = i + len 46 + end 47 47 48 - return lines 48 + return lines 49 49 end 50 50 51 51 function M.get_linenr_width() 52 - local line_count = vim.api.nvim_buf_line_count(0) 53 - local line_digits = math.floor(math.log10(math.max(1, line_count))) + 1 54 - local numberwidth_opt = vim.opt.numberwidth:get() 55 - return math.max(line_digits, numberwidth_opt) 52 + local line_count = vim.api.nvim_buf_line_count(0) 53 + local line_digits = math.floor(math.log10(math.max(1, line_count))) + 1 54 + local numberwidth_opt = vim.opt.numberwidth:get() 55 + return math.max(line_digits, numberwidth_opt) 56 56 end 57 57 58 58 return M
+5 -5
lua/multinput.lua
··· 1 - local Input = require("lib.input") 1 + local Input = require "lib.input" 2 2 3 3 local M = {} 4 4 5 5 ---@param config? multinput.Config 6 6 function M.setup(config) 7 - vim.ui.input = function(opts, on_confirm) 8 - local input = Input:new(config or {}, opts or {}, on_confirm) 9 - input:open(opts.default or "") 10 - end 7 + vim.ui.input = function(opts, on_confirm) 8 + local input = Input:new(config or {}, opts or {}, on_confirm) 9 + input:open(opts.default or "") 10 + end 11 11 end 12 12 13 13 return M