Skip to content

Commit

Permalink
feat: support multiline prompt in vim.ui.input (#89)
Browse files Browse the repository at this point in the history
  • Loading branch information
stevearc committed Sep 30, 2023
1 parent 73a7d54 commit 8f4d62b
Showing 1 changed file with 182 additions and 106 deletions.
288 changes: 182 additions & 106 deletions lua/dressing/input.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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", "<Esc>", 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", "<Esc>", 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 <Tab> user completion if cmp is not active
if not has_cmp or not pcall(require, "cmp_omni") then
vim.keymap.set("i", "<Tab>", 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 <Tab> user completion if cmp is not active
if not has_cmp or not pcall(require, "cmp_omni") then
vim.keymap.set("i", "<Tab>", 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),
})

Expand Down

0 comments on commit 8f4d62b

Please sign in to comment.