fix(input): adjust implementation to avoid bugs in prompt buffer (#2)
This commit is contained in:
parent
362cc2c54b
commit
189bbc6562
|
@ -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
|
||||
|
|
1
doc/tags
1
doc/tags
|
@ -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()*
|
||||
|
|
|
@ -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%)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue