fix(input): adjust implementation to avoid bugs in prompt buffer (#2)

This commit is contained in:
Steven Arcangeli 2021-12-07 22:44:48 -08:00
parent 362cc2c54b
commit 189bbc6562
5 changed files with 148 additions and 28 deletions

View File

@ -109,5 +109,20 @@ dressing.get_config() *dressing_get_config()
}
})
===============================================================================
*dressing-prompt*
Vim has a mechanism that is built for getting input from the user: the
|prompt-buffer|. This is a specific |buftype| and comes with a lot of special
handling within vim. Neovim 0.6 and earlier has some bugs with the prompt
buffer (see https://github.com/stevearc/dressing.nvim/issues/2 and
https://github.com/neovim/neovim/issues/13715). For this reason, the default
implementation of |vim.ui.input| does NOT use the prompt buffer, and instead
mimics its behavior through other means. If you don't mind the bugs, or if
you're on a version of Neovim after 0.6 (nightly has the fixes now), you can
pass `prompt_buffer = true` to use that implementation.
There are slight visual differences in where the "prompt" text in placed, but
otherwise they should be functionally identical.
===============================================================================
vim:ft=help:et:ts=2:sw=2:sts=2:norl

View File

@ -1,6 +1,7 @@
Dressing dressing.txt /*Dressing*
dressing dressing.txt /*dressing*
dressing-configuration dressing.txt /*dressing-configuration*
dressing-prompt dressing.txt /*dressing-prompt*
dressing.nvim dressing.txt /*dressing.nvim*
dressing.txt dressing.txt /*dressing.txt*
dressing_get_config() dressing.txt /*dressing_get_config()*

View File

@ -18,6 +18,9 @@ local default_config = {
max_width = nil,
min_width = 20,
-- see :help dressing-prompt
prompt_buffer = false,
-- see :help dressing_get_config
get_config = nil,
},
@ -60,7 +63,7 @@ local default_config = {
col = 0,
border = "rounded",
-- Window options
-- Window transparency (0-100)
winblend = 10,
-- These can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)

View File

@ -6,6 +6,7 @@ local context = {
opts = nil,
on_confirm = nil,
winid = nil,
title_winid = nil,
}
local function close_completion_window()
@ -22,17 +23,40 @@ M.confirm = function(text)
close_completion_window()
local ctx = context
context = {}
vim.api.nvim_win_close(ctx.winid, true)
vim.cmd("stopinsert")
-- stopinsert will move the cursor back 1. We need to move it forward 1 to
-- put it in the place you were when you opened the modal.
local cursor = vim.api.nvim_win_get_cursor(0)
cursor[2] = cursor[2] + 1
vim.api.nvim_win_set_cursor(0, cursor)
if text == "" then
text = nil
end
vim.schedule_wrap(ctx.on_confirm)(text)
-- We have to wait briefly for the popup window to close (if present),
-- otherwise vim gets into a very weird and bad state. I was seeing text get
-- deleted from the buffer after the input window closes.
vim.defer_fn(function()
if ctx.title_winid then
pcall(vim.api.nvim_win_close, ctx.title_winid, true)
end
pcall(vim.api.nvim_win_close, ctx.winid, true)
vim.cmd("stopinsert")
-- stopinsert will move the cursor back 1. We need to move it forward 1 to
-- put it in the place you were when you opened the modal.
local cursor = vim.api.nvim_win_get_cursor(0)
cursor[2] = cursor[2] + 1
vim.api.nvim_win_set_cursor(0, cursor)
if text == "" then
text = nil
end
-- Defer the callback because we just closed windows and left insert mode.
-- In practice from my testing, if the user does something right now (like,
-- say, opening another input modal) it could happen improperly. I was
-- seeing my successive modals fail to enter insert mode.
vim.defer_fn(function()
ctx.on_confirm(text)
end, 5)
end, 5)
end
M.confirm_non_prompt = function()
local text = vim.api.nvim_buf_get_lines(0, 0, 1, true)[1]
M.confirm(text)
end
M.close = function()
M.confirm()
end
M.highlight = function()
@ -71,7 +95,11 @@ M.completefunc = function(findstart, base)
return findstart == 1 and 0 or {}
end
if findstart == 1 then
return vim.api.nvim_strwidth(context.opts.prompt)
if global_config.input.prompt_buffer then
return vim.api.nvim_strwidth(context.opts.prompt)
else
return 0
end
else
local completion = context.opts.completion
local pieces = split(completion, ",")
@ -117,9 +145,14 @@ setmetatable(M, {
end
local config = global_config.get_mod_config("input", opts)
local bufnr = vim.api.nvim_create_buf(false, true)
-- Create or update the window
local prompt = opts.prompt or config.default_prompt
local width = util.calculate_width(config.prefer_width + vim.api.nvim_strwidth(prompt), config)
local width
if config.prompt_buffer then
width = util.calculate_width(config.prefer_width + vim.api.nvim_strwidth(prompt), config)
else
width = util.calculate_width(config.prefer_width, config)
end
local winopt = {
relative = config.relative,
anchor = config.anchor,
@ -130,40 +163,96 @@ setmetatable(M, {
height = 1,
style = "minimal",
}
local winnr
local winid, bufnr, title_winid
-- If the input window is already open, hijack it
if context.winid and vim.api.nvim_win_is_valid(context.winid) then
winnr = context.winid
winid = context.winid
-- Make sure the previous on_confirm callback is called with nil
vim.schedule(context.on_confirm)
vim.api.nvim_win_set_width(winnr, width)
bufnr = vim.api.nvim_win_get_buf(winnr)
vim.api.nvim_win_set_width(winid, width)
bufnr = vim.api.nvim_win_get_buf(winid)
title_winid = context.title_winid
else
winnr = vim.api.nvim_open_win(bufnr, true, winopt)
bufnr = vim.api.nvim_create_buf(false, true)
winid = vim.api.nvim_open_win(bufnr, true, winopt)
end
context = {
winid = winnr,
winid = winid,
title_winid = title_winid,
on_confirm = on_confirm,
opts = opts,
}
vim.api.nvim_buf_set_option(bufnr, "buftype", "prompt")
-- Finish setting up the buffer
vim.api.nvim_buf_set_option(bufnr, "swapfile", false)
vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe")
vim.api.nvim_buf_set_option(bufnr, "filetype", "DressingInput")
local keyopts = { silent = true, noremap = true }
local close_rhs = "<cmd>lua require('dressing.input').confirm()<CR>"
local close_rhs = "<cmd>lua require('dressing.input').close()<CR>"
vim.api.nvim_buf_set_keymap(bufnr, "n", "<Esc>", close_rhs, keyopts)
if config.insert_only then
vim.api.nvim_buf_set_keymap(bufnr, "i", "<Esc>", close_rhs, keyopts)
end
vim.fn.prompt_setprompt(bufnr, prompt)
-- Would prefer to use v:lua directly here, but it doesn't work :(
vim.fn.prompt_setcallback(bufnr, "dressing#prompt_confirm")
vim.fn.prompt_setinterrupt(bufnr, "dressing#prompt_cancel")
if config.prompt_buffer then
vim.api.nvim_buf_set_option(bufnr, "buftype", "prompt")
vim.fn.prompt_setprompt(bufnr, prompt)
-- Would prefer to use v:lua directly here, but it doesn't work :(
vim.fn.prompt_setcallback(bufnr, "dressing#prompt_confirm")
vim.fn.prompt_setinterrupt(bufnr, "dressing#prompt_cancel")
else
local confirm_rhs = "<cmd>lua require('dressing.input').confirm_non_prompt()<CR>"
-- If we're not using the prompt buffer, we need to put the prompt into a
-- separate title window that will appear in the input window border
vim.api.nvim_buf_set_keymap(bufnr, "i", "<C-c>", close_rhs, keyopts)
vim.api.nvim_buf_set_keymap(bufnr, "i", "<CR>", confirm_rhs, keyopts)
vim.api.nvim_buf_set_keymap(bufnr, "n", "<CR>", confirm_rhs, keyopts)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { "" })
-- Disable nvim-cmp if installed
local ok, cmp = pcall(require, "cmp")
if ok then
cmp.setup.buffer({ enabled = false })
end
-- Create the title window once the main window is placed.
-- Have to defer here or the title will be in the wrong location
vim.defer_fn(function()
local titlebuf
local trimmed_prompt = string.gsub(prompt, "^%s*(.-)%s*$", "%1")
local prompt_width = math.min(width, 2 + vim.api.nvim_strwidth(trimmed_prompt))
if context.title_winid and vim.api.nvim_win_is_valid(context.title_winid) then
title_winid = context.title_winid
titlebuf = vim.api.nvim_win_get_buf(title_winid)
vim.api.nvim_win_set_width(title_winid, prompt_width)
else
titlebuf = vim.api.nvim_create_buf(false, true)
title_winid = vim.api.nvim_open_win(titlebuf, false, {
relative = "win",
win = winid,
width = prompt_width,
height = 1,
row = -1,
col = 1,
focusable = false,
zindex = 151,
style = "minimal",
noautocmd = true,
})
end
if winid == context.winid then
context.title_winid = title_winid
end
vim.api.nvim_buf_set_lines(titlebuf, 0, -1, true, { " " .. trimmed_prompt })
vim.api.nvim_buf_set_option(titlebuf, "bufhidden", "wipe")
end, 5)
end
if opts.highlight then
vim.cmd([[
autocmd TextChanged <buffer> lua require('dressing.input').highlight()
autocmd TextChangedI <buffer> lua require('dressing.input').highlight()
]])
end
if opts.completion then
vim.api.nvim_buf_set_option(bufnr, "completefunc", "v:lua.dressing_input_complete")
vim.api.nvim_buf_set_option(bufnr, "omnifunc", "")
@ -175,9 +264,11 @@ setmetatable(M, {
{ expr = true }
)
end
vim.cmd([[
autocmd BufLeave <buffer> ++nested ++once lua require('dressing.input').confirm()
autocmd BufLeave <buffer> ++nested ++once lua require('dressing.input').close()
]])
vim.cmd("startinsert!")
if opts.default then
vim.api.nvim_feedkeys(opts.default, "n", false)

View File

@ -39,3 +39,13 @@ local function next()
end
next()
-- Uncomment this to test opening a modal while the previous one is open
-- vim.ui.input(cases[1], function(text)
-- print(text)
-- end)
-- vim.defer_fn(function()
-- vim.ui.input(cases[2], function(text)
-- print(text)
-- end)
-- end, 2000)