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.

initial commit

r0nsha (Jun 26, 2025, 9:31 PM +0300) dcdf788a

+206
+203
lua/wrapinput/init.lua
··· 1 + ---@class wrapinput.Config 2 + ---@field prompt string 3 + ---@field default string 4 + ---@field padding integer 5 + ---@field max_width integer 6 + ---@field max_height integer 7 + ---@field win vim.api.keyset.win_config 8 + 9 + local group = vim.api.nvim_create_augroup("wrapinput.nvim", { clear = true }) 10 + local default_prompt = "Input: " 11 + 12 + local M = {} 13 + 14 + ---@type wrapinput.Config 15 + local defaults = { 16 + prompt = default_prompt, 17 + default = "", 18 + padding = 20, 19 + max_width = 50, 20 + max_height = 8, 21 + win = { 22 + title = default_prompt, 23 + style = "minimal", 24 + focusable = true, 25 + relative = "cursor", 26 + col = -1, 27 + height = 1, 28 + }, 29 + } 30 + 31 + ---@return vim.api.keyset.win_config 32 + local function get_relative_win_config() 33 + local curr_win = vim.api.nvim_get_current_win() 34 + local cursor_row = vim.api.nvim_win_get_cursor(curr_win)[1] 35 + if cursor_row == 1 then 36 + return { anchor = "NW", row = 1 } 37 + else 38 + return { anchor = "SW", row = 0 } 39 + end 40 + end 41 + 42 + ---@param text string 43 + ---@param width integer 44 + ---@return string[] 45 + local function guesstimate_wrapped_lines(text, width) 46 + local breakat = vim.o.breakat 47 + local linebreak = vim.wo.linebreak 48 + local showbreak = vim.o.showbreak 49 + local showbreak_width = vim.fn.strdisplaywidth(showbreak) 50 + 51 + local result = {} 52 + local current_line = "" 53 + local current_width = 0 54 + local last_break_idx = nil 55 + 56 + local i = 1 57 + while i <= #text do 58 + local char = text:sub(i, i) 59 + 60 + local char_width = vim.fn.strdisplaywidth(char) 61 + 62 + -- Track legal break point 63 + if breakat:find(vim.pesc(char), 1, true) then 64 + last_break_idx = #current_line + 1 65 + end 66 + 67 + -- If this char would overflow screen width 68 + if current_width + char_width > width then 69 + if linebreak and last_break_idx then 70 + table.insert(result, current_line:sub(1, last_break_idx)) 71 + i = i - (#current_line - last_break_idx) 72 + elseif last_break_idx then 73 + table.insert(result, current_line:sub(1, last_break_idx)) 74 + i = i - (#current_line - last_break_idx) 75 + else 76 + table.insert(result, current_line) 77 + end 78 + 79 + -- Reset for wrapped line 80 + current_line = showbreak 81 + current_width = showbreak_width 82 + last_break_idx = nil 83 + else 84 + current_line = current_line .. char 85 + current_width = current_width + char_width 86 + i = i + 1 87 + end 88 + end 89 + 90 + if #current_line > 0 then 91 + table.insert(result, current_line) 92 + end 93 + 94 + return result 95 + end 96 + 97 + ---@param winnr integer 98 + ---@param bufnr integer 99 + ---@param config wrapinput.Config 100 + local function resize(winnr, bufnr, config) 101 + local line = vim.api.nvim_buf_get_lines(bufnr, 0, 1, false)[1] 102 + 103 + if line == "" then 104 + vim.api.nvim_win_set_width(winnr, config.padding) 105 + vim.api.nvim_win_set_height(winnr, 1) 106 + return 107 + end 108 + 109 + local lines = guesstimate_wrapped_lines(line, config.max_width) 110 + 111 + -- set width 112 + local lens = vim.tbl_map(function(l) 113 + return vim.fn.strdisplaywidth(l) 114 + end, lines) 115 + local width = math.max(unpack(lens)) + config.padding 116 + width = width > config.max_width and config.max_width or width 117 + vim.api.nvim_win_set_width(winnr, width + 1) 118 + 119 + -- set height 120 + local height = #lines 121 + height = height > config.max_height and config.max_height or height 122 + vim.api.nvim_win_set_height(winnr, height) 123 + end 124 + 125 + ---@param winnr integer 126 + ---@param bufnr integer 127 + ---@param config wrapinput.Config 128 + local function setup_autocmds(winnr, bufnr, config) 129 + vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { 130 + group = group, 131 + buffer = bufnr, 132 + callback = function() 133 + resize(winnr, bufnr, config) 134 + end, 135 + }) 136 + end 137 + 138 + ---@param options table<string, any> 139 + ---@param opts vim.api.keyset.option 140 + local function set_options(options, opts) 141 + for k, v in pairs(options) do 142 + vim.api.nvim_set_option_value(k, v, opts) 143 + end 144 + end 145 + 146 + ---@param winnr integer 147 + ---@param bufnr integer 148 + ---@param on_confirm fun(input?: string) 149 + local function setup_mappings(winnr, bufnr, on_confirm) 150 + ---@param result string? 151 + local function close(result) 152 + vim.cmd("stopinsert") 153 + vim.api.nvim_win_close(winnr, true) 154 + on_confirm(result) 155 + end 156 + 157 + ---@param mode string|string[] 158 + ---@param lhs string 159 + ---@param rhs string|function 160 + local function map(mode, lhs, rhs) 161 + vim.keymap.set(mode, lhs, rhs, { buffer = bufnr }) 162 + end 163 + 164 + map({ "n", "i", "v" }, "<cr>", function() 165 + close(vim.api.nvim_buf_get_lines(bufnr, 0, 1, false)[1]) 166 + end) 167 + map("n", "<esc>", close) 168 + map("n", "q", close) 169 + end 170 + 171 + ---@param config wrapinput.Config 172 + ---@param on_confirm fun(input: string?) 173 + function M.input(config, on_confirm) 174 + config = vim.tbl_deep_extend("force", defaults, config, { win = { title = config.prompt } }) 175 + on_confirm = on_confirm or function() end 176 + 177 + config = vim.tbl_deep_extend( 178 + "keep", 179 + config, 180 + { win = get_relative_win_config() }, 181 + { win = { width = vim.fn.strdisplaywidth(config.default) + config.padding } } 182 + ) 183 + 184 + -- Create buffer and floating window. 185 + local bufnr = vim.api.nvim_create_buf(false, true) 186 + set_options({ buftype = "prompt", bufhidden = "wipe", textwidth = config.max_width }, { buf = bufnr }) 187 + vim.fn.prompt_setprompt(bufnr, "") 188 + 189 + local winnr = vim.api.nvim_open_win(bufnr, true, config.win) 190 + set_options({ wrap = true, linebreak = true, winhighlight = "Search:None" }, { win = winnr }) 191 + 192 + -- write default value and put cursor at the end 193 + vim.api.nvim_buf_set_text(bufnr, 0, 0, 0, 0, { config.default }) 194 + vim.cmd("startinsert") 195 + vim.api.nvim_win_set_cursor(winnr, { 1, vim.str_utfindex(config.default, "utf-8") + 1 }) 196 + 197 + resize(winnr, bufnr, config) 198 + 199 + setup_mappings(winnr, bufnr, on_confirm) 200 + setup_autocmds(winnr, bufnr, config) 201 + end 202 + 203 + return M
+3
plugin/wrapinput.lua
··· 1 + vim.ui.input = function(opts, on_confirm) 2 + require("wrapinput").input(opts or {}, on_confirm) 3 + end