diff --git a/lua/dressing/input.lua b/lua/dressing/input.lua index 66c2314..9dfb58b 100644 --- a/lua/dressing/input.lua +++ b/lua/dressing/input.lua @@ -241,9 +241,22 @@ M.trigger_completion = function() end end +---@param lines string[] +---@return integer +local function 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 + +---@param config table +---@param prompt_lines string[] +---@param default? string ---@return integer ---@return boolean -local function create_or_update_win(config, prompt, opts) +local function create_or_update_win(config, prompt_lines, default) local parent_win = 0 local winopt local win_conf @@ -265,17 +278,22 @@ local function create_or_update_win(config, prompt, opts) noautocmd = true, } end + -- First calculate the desired base width of the modal local prefer_width = util.calculate_width(config.relative, config.prefer_width, config, parent_win) -- Then expand the width to fit the prompt and default value - prefer_width = math.max(prefer_width, 4 + vim.api.nvim_strwidth(prompt)) - if opts.default then - prefer_width = math.max(prefer_width, 2 + vim.api.nvim_strwidth(opts.default)) + prefer_width = math.max(prefer_width, 4 + get_max_strwidth(prompt_lines)) + if default then + prefer_width = math.max(prefer_width, 2 + vim.api.nvim_strwidth(default)) end -- Then recalculate to clamp final value to min/max local width = util.calculate_width(config.relative, prefer_width, config, parent_win) winopt.row = util.calculate_row(config.relative, 1, parent_win) + if #prompt_lines > 1 then + -- If we're going to add a multiline prompt window, adjust the positioning down to make room + winopt.row = winopt.row + #prompt_lines + end winopt.col = util.calculate_col(config.relative, width, parent_win) winopt.width = width @@ -289,136 +307,194 @@ local function create_or_update_win(config, prompt, opts) winopt.win = nil end end - if vim.fn.has("nvim-0.9") == 1 then - winopt.title = prompt:gsub("^%s*(.-)%s*$", " %1 ") + if vim.fn.has("nvim-0.9") == 1 and #prompt_lines == 1 then + winopt.title = prompt_lines[1]:gsub("^%s*(.-)%s*$", " %1 ") -- We used to use "prompt_align" here winopt.title_pos = config.prompt_align or config.title_pos end winopt = config.override(winopt) or winopt + local winid, start_in_insert -- If the floating win was already open if win_conf then -- Make sure the previous on_confirm callback is called with nil vim.schedule(context.on_confirm) vim.api.nvim_win_set_config(context.winid, winopt) - return context.winid, context.start_in_insert + winid = context.winid + start_in_insert = context.start_in_insert else - local start_in_insert = string.sub(vim.api.nvim_get_mode().mode, 1, 1) == "i" + start_in_insert = string.sub(vim.api.nvim_get_mode().mode, 1, 1) == "i" local bufnr = vim.api.nvim_create_buf(false, true) - local winid = vim.api.nvim_open_win(bufnr, true, winopt) - return winid, start_in_insert + winid = vim.api.nvim_open_win(bufnr, true, winopt) end -end -setmetatable(M, { - -- use schedule_wrap to avoid a bug when vim opens - -- (see https://github.com/stevearc/dressing.nvim/issues/15) - __call = util.schedule_wrap_before_vimenter(function(_, opts, on_confirm) - vim.validate({ - on_confirm = { on_confirm, "function", false }, - }) - opts = opts or {} - if type(opts) ~= "table" then - opts = { prompt = tostring(opts) } - end - local config = global_config.get_mod_config("input", opts) - if not config.enabled then - return patch.original_mods.input(opts, on_confirm) + -- If the prompt is multiple lines, create another window for it + local prev_prompt_win = vim.w[winid].prompt_win + if prev_prompt_win and vim.api.nvim_win_is_valid(prev_prompt_win) then + vim.api.nvim_win_close(prev_prompt_win, true) + end + if #prompt_lines > 1 then + -- HACK to force the parent window to position itself + -- See https://github.com/neovim/neovim/issues/13403 + vim.cmd("redraw") + local prompt_buf = vim.api.nvim_create_buf(false, true) + vim.bo[prompt_buf].swapfile = false + vim.bo[prompt_buf].bufhidden = "wipe" + local row = -1 * #prompt_lines + local col = 0 + if winopt.border then + row = row - 2 + col = col - 1 end - if vim.fn.hlID("DressingInputText") ~= 0 then - vim.notify( - 'DressingInputText highlight group is deprecated. Set winhighlight="NormalFloat:MyHighlightGroup" instead', - vim.log.levels.WARN - ) + local prompt_win = vim.api.nvim_open_win(prompt_buf, false, { + relative = "win", + win = winid, + width = winopt.width, + height = #prompt_lines, + row = row, + col = col, + focusable = false, + zindex = (winopt.zindex or 50) - 1, + style = "minimal", + border = winopt.border, + noautocmd = true, + }) + for option, value in pairs(config.win_options) do + vim.api.nvim_set_option_value(option, value, { scope = "local", win = prompt_win }) end + vim.api.nvim_buf_set_lines(prompt_buf, 0, -1, true, prompt_lines) + vim.api.nvim_create_autocmd("WinClosed", { + pattern = tostring(winid), + once = true, + nested = true, + callback = function() + vim.api.nvim_win_close(prompt_win, true) + end, + }) + vim.w[winid].prompt_win = prompt_win + end - -- Create or update the window - local prompt = string.gsub(opts.prompt or config.default_prompt, "\n", " ") + ---@cast winid integer + ---@cast start_in_insert boolean + return winid, start_in_insert +end - local winid, start_in_insert = create_or_update_win(config, prompt, opts) - context = { - winid = winid, - on_confirm = on_confirm, - opts = opts, - history_idx = nil, - start_in_insert = start_in_insert, - } - for option, value in pairs(config.win_options) do - vim.api.nvim_set_option_value(option, value, { scope = "local", win = winid }) - end - local bufnr = vim.api.nvim_win_get_buf(winid) +---@param opts string|dressing.InputOptions +---@param on_confirm fun(text?: string) +local function show_input(opts, on_confirm) + vim.validate({ + on_confirm = { on_confirm, "function", false }, + }) + opts = opts or {} + if type(opts) ~= "table" then + opts = { prompt = tostring(opts) } + end + local config = global_config.get_mod_config("input", opts) + if not config.enabled then + return patch.original_mods.input(opts, on_confirm) + end + if vim.fn.hlID("DressingInputText") ~= 0 then + vim.notify( + 'DressingInputText highlight group is deprecated. Set winhighlight="NormalFloat:MyHighlightGroup" instead', + vim.log.levels.WARN + ) + end - -- Finish setting up the buffer - vim.bo[bufnr].swapfile = false - vim.bo[bufnr].bufhidden = "wipe" - for k, v in pairs(config.buf_options) do - vim.bo[bufnr][k] = v - end + local prompt = opts.prompt or config.default_prompt + local prompt_lines = vim.split(prompt, "\n", { plain = true, trimempty = true }) - map_util.create_plug_maps(bufnr, keymaps) - for mode, user_maps in pairs(config.mappings) do - map_util.create_maps_to_plug(bufnr, mode, user_maps, "DressingInput:") - end + -- Create or update the window + local winid, start_in_insert = create_or_update_win(config, prompt_lines, opts.default) + context = { + winid = winid, + on_confirm = on_confirm, + opts = opts, + history_idx = nil, + start_in_insert = start_in_insert, + } + for option, value in pairs(config.win_options) do + vim.api.nvim_set_option_value(option, value, { scope = "local", win = winid }) + end + local bufnr = vim.api.nvim_win_get_buf(winid) - if config.insert_only then - vim.keymap.set("i", "", M.close, { buffer = bufnr }) - end + -- Finish setting up the buffer + vim.bo[bufnr].swapfile = false + vim.bo[bufnr].bufhidden = "wipe" + for k, v in pairs(config.buf_options) do + vim.bo[bufnr][k] = v + end - vim.bo[bufnr].filetype = "DressingInput" - local default = string.gsub(opts.default or "", "\n", " ") - vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { default }) - if vim.fn.has("nvim-0.9") == 0 then - util.add_title_to_win( - winid, - string.gsub(prompt, "^%s*(.-)%s*$", "%1"), - { align = config.prompt_align } - ) - end + map_util.create_plug_maps(bufnr, keymaps) + for mode, user_maps in pairs(config.mappings) do + map_util.create_maps_to_plug(bufnr, mode, user_maps, "DressingInput:") + end - vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { - desc = "Update highlights", - buffer = bufnr, - callback = function() - remove_extra_lines() - apply_highlight() - end, - }) + if config.insert_only then + vim.keymap.set("i", "", M.close, { buffer = bufnr }) + end - -- Configure nvim-cmp if installed - local has_cmp, cmp = pcall(require, "cmp") - if has_cmp then - cmp.setup.buffer({ - enabled = opts.completion ~= nil, - sources = { - { name = "omni" }, - }, - }) - end - -- Disable mini.nvim completion if installed - vim.api.nvim_buf_set_var(bufnr, "minicompletion_disable", true) - if opts.completion then - vim.bo[bufnr].completefunc = "v:lua.require'dressing.input'.completefunc" - vim.bo[bufnr].omnifunc = "v:lua.require'dressing.input'.completefunc" - -- Only set up user completion if cmp is not active - if not has_cmp or not pcall(require, "cmp_omni") then - vim.keymap.set("i", "", M.trigger_completion, { buffer = bufnr, expr = true }) - end - end + vim.bo[bufnr].filetype = "DressingInput" + local default = string.gsub(opts.default or "", "\n", " ") + vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { default }) + if vim.fn.has("nvim-0.9") == 0 and #prompt_lines == 1 then + util.add_title_to_win( + winid, + string.gsub(prompt_lines[1], "^%s*(.-)%s*$", "%1"), + { align = config.prompt_align } + ) + end - vim.api.nvim_create_autocmd("BufLeave", { - desc = "Cancel vim.ui.input", - buffer = bufnr, - nested = true, - once = true, - callback = M.close, - }) + vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { + desc = "Update highlights", + buffer = bufnr, + callback = function() + remove_extra_lines() + apply_highlight() + end, + }) - if config.start_in_insert then - vim.cmd("startinsert!") + -- Configure nvim-cmp if installed + local has_cmp, cmp = pcall(require, "cmp") + if has_cmp then + cmp.setup.buffer({ + enabled = opts.completion ~= nil, + sources = { + { name = "omni" }, + }, + }) + end + -- Disable mini.nvim completion if installed + vim.api.nvim_buf_set_var(bufnr, "minicompletion_disable", true) + if opts.completion then + vim.bo[bufnr].completefunc = "v:lua.require'dressing.input'.completefunc" + vim.bo[bufnr].omnifunc = "v:lua.require'dressing.input'.completefunc" + -- Only set up user completion if cmp is not active + if not has_cmp or not pcall(require, "cmp_omni") then + vim.keymap.set("i", "", M.trigger_completion, { buffer = bufnr, expr = true }) end - close_completion_window() - apply_highlight() + end + + vim.api.nvim_create_autocmd("BufLeave", { + desc = "Cancel vim.ui.input", + buffer = bufnr, + nested = true, + once = true, + callback = M.close, + }) + + if config.start_in_insert then + vim.cmd("startinsert!") + end + close_completion_window() + apply_highlight() +end + +setmetatable(M, { + -- use schedule_wrap to avoid a bug when vim opens + -- (see https://github.com/stevearc/dressing.nvim/issues/15) + __call = util.schedule_wrap_before_vimenter(function(_, opts, on_confirm) + show_input(opts, on_confirm) end), })