mirror of
https://github.com/neovim/neovim.git
synced 2025-02-25 18:55:25 -06:00
feat(paste): unify cancel and error behavior (#30476)
Before this PR, the behavior of nvim_paste is: - When vim.paste() returns false, return false to the client, but treat following chunks normally (i.e. rely on the client cancelling the paste as expected). - When vim.paste() throws an error, still return true to the client, but drain the following chunks in the stream without calling vim.paste(). There are two problems with such behavior: - When vim.paste() errors, the client is still supposed to send the remaining chunks of the stream, even though they do nothing. - Having different code paths for two uncommon but similar situations complicates maintenance. This PR makes both the cancel case and the error case return false to the client and drain the remaining chunks of the stream, which, apart from sharing the same code path, is beneficial whether the client checks the return value of nvim_paste or not: - If the client checks the return value, it can avoid sending the following chunks needlessly after an error. - If the client doesn't check the return value, chunks following a cancelled chunk won't be pasted on the server regardless, which leads to less confusing behavior.
This commit is contained in:
parent
032e024f8a
commit
d831392b15
@ -1155,8 +1155,8 @@ nvim_paste({data}, {crlf}, {phase}) *nvim_paste()*
|
|||||||
|
|
||||||
Errors ('nomodifiable', `vim.paste()` failure, …) are reflected in `err`
|
Errors ('nomodifiable', `vim.paste()` failure, …) are reflected in `err`
|
||||||
but do not affect the return value (which is strictly decided by
|
but do not affect the return value (which is strictly decided by
|
||||||
`vim.paste()`). On error, subsequent calls are ignored ("drained") until
|
`vim.paste()`). On error or cancel, subsequent calls are ignored
|
||||||
the next paste is initiated (phase 1 or -1).
|
("drained") until the next paste is initiated (phase 1 or -1).
|
||||||
|
|
||||||
Attributes: ~
|
Attributes: ~
|
||||||
not allowed when |textlock| is active
|
not allowed when |textlock| is active
|
||||||
@ -1173,7 +1173,7 @@ nvim_paste({data}, {crlf}, {phase}) *nvim_paste()*
|
|||||||
|
|
||||||
Return: ~
|
Return: ~
|
||||||
• true: Client may continue pasting.
|
• true: Client may continue pasting.
|
||||||
• false: Client must cancel the paste.
|
• false: Client should cancel the paste.
|
||||||
|
|
||||||
nvim_put({lines}, {type}, {after}, {follow}) *nvim_put()*
|
nvim_put({lines}, {type}, {after}, {follow}) *nvim_put()*
|
||||||
Puts text at cursor, in any mode.
|
Puts text at cursor, in any mode.
|
||||||
|
4
runtime/lua/vim/_meta/api.lua
generated
4
runtime/lua/vim/_meta/api.lua
generated
@ -1816,8 +1816,8 @@ function vim.api.nvim_parse_expression(expr, flags, highlight) end
|
|||||||
---
|
---
|
||||||
--- Errors ('nomodifiable', `vim.paste()` failure, …) are reflected in `err`
|
--- Errors ('nomodifiable', `vim.paste()` failure, …) are reflected in `err`
|
||||||
--- but do not affect the return value (which is strictly decided by
|
--- but do not affect the return value (which is strictly decided by
|
||||||
--- `vim.paste()`). On error, subsequent calls are ignored ("drained") until
|
--- `vim.paste()`). On error or cancel, subsequent calls are ignored
|
||||||
--- the next paste is initiated (phase 1 or -1).
|
--- ("drained") until the next paste is initiated (phase 1 or -1).
|
||||||
---
|
---
|
||||||
--- @param data string Multiline input. May be binary (containing NUL bytes).
|
--- @param data string Multiline input. May be binary (containing NUL bytes).
|
||||||
--- @param crlf boolean Also break lines at CR and CRLF.
|
--- @param crlf boolean Also break lines at CR and CRLF.
|
||||||
|
@ -1219,8 +1219,8 @@ void nvim_set_current_tabpage(Tabpage tabpage, Error *err)
|
|||||||
///
|
///
|
||||||
/// Errors ('nomodifiable', `vim.paste()` failure, …) are reflected in `err`
|
/// Errors ('nomodifiable', `vim.paste()` failure, …) are reflected in `err`
|
||||||
/// but do not affect the return value (which is strictly decided by
|
/// but do not affect the return value (which is strictly decided by
|
||||||
/// `vim.paste()`). On error, subsequent calls are ignored ("drained") until
|
/// `vim.paste()`). On error or cancel, subsequent calls are ignored
|
||||||
/// the next paste is initiated (phase 1 or -1).
|
/// ("drained") until the next paste is initiated (phase 1 or -1).
|
||||||
///
|
///
|
||||||
/// @param data Multiline input. May be binary (containing NUL bytes).
|
/// @param data Multiline input. May be binary (containing NUL bytes).
|
||||||
/// @param crlf Also break lines at CR and CRLF.
|
/// @param crlf Also break lines at CR and CRLF.
|
||||||
@ -1233,20 +1233,19 @@ void nvim_set_current_tabpage(Tabpage tabpage, Error *err)
|
|||||||
/// @param[out] err Error details, if any
|
/// @param[out] err Error details, if any
|
||||||
/// @return
|
/// @return
|
||||||
/// - true: Client may continue pasting.
|
/// - true: Client may continue pasting.
|
||||||
/// - false: Client must cancel the paste.
|
/// - false: Client should cancel the paste.
|
||||||
Boolean nvim_paste(String data, Boolean crlf, Integer phase, Arena *arena, Error *err)
|
Boolean nvim_paste(String data, Boolean crlf, Integer phase, Arena *arena, Error *err)
|
||||||
FUNC_API_SINCE(6)
|
FUNC_API_SINCE(6)
|
||||||
FUNC_API_TEXTLOCK_ALLOW_CMDWIN
|
FUNC_API_TEXTLOCK_ALLOW_CMDWIN
|
||||||
{
|
{
|
||||||
static bool draining = false;
|
static bool cancelled = false;
|
||||||
bool cancel = false;
|
|
||||||
|
|
||||||
VALIDATE_INT((phase >= -1 && phase <= 3), "phase", phase, {
|
VALIDATE_INT((phase >= -1 && phase <= 3), "phase", phase, {
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
if (phase == -1 || phase == 1) { // Start of paste-stream.
|
if (phase == -1 || phase == 1) { // Start of paste-stream.
|
||||||
draining = false;
|
cancelled = false;
|
||||||
} else if (draining) {
|
} else if (cancelled) {
|
||||||
// Skip remaining chunks. Report error only once per "stream".
|
// Skip remaining chunks. Report error only once per "stream".
|
||||||
goto theend;
|
goto theend;
|
||||||
}
|
}
|
||||||
@ -1255,26 +1254,26 @@ Boolean nvim_paste(String data, Boolean crlf, Integer phase, Arena *arena, Error
|
|||||||
ADD_C(args, ARRAY_OBJ(lines));
|
ADD_C(args, ARRAY_OBJ(lines));
|
||||||
ADD_C(args, INTEGER_OBJ(phase));
|
ADD_C(args, INTEGER_OBJ(phase));
|
||||||
Object rv = NLUA_EXEC_STATIC("return vim.paste(...)", args, kRetNilBool, arena, err);
|
Object rv = NLUA_EXEC_STATIC("return vim.paste(...)", args, kRetNilBool, arena, err);
|
||||||
if (ERROR_SET(err)) {
|
// vim.paste() decides if client should cancel.
|
||||||
draining = true;
|
if (ERROR_SET(err) || (rv.type == kObjectTypeBoolean && !rv.data.boolean)) {
|
||||||
goto theend;
|
cancelled = true;
|
||||||
}
|
}
|
||||||
if (phase == -1 || phase == 1) {
|
if (!cancelled && (phase == -1 || phase == 1)) {
|
||||||
paste_store(kFalse, NULL_STRING, crlf);
|
paste_store(kFalse, NULL_STRING, crlf);
|
||||||
}
|
}
|
||||||
// vim.paste() decides if client should cancel. Errors do NOT cancel: we
|
if (!cancelled) {
|
||||||
// want to drain remaining chunks (rather than divert them to main input).
|
|
||||||
cancel = (rv.type == kObjectTypeBoolean && !rv.data.boolean);
|
|
||||||
if (!cancel) {
|
|
||||||
paste_store(kNone, data, crlf);
|
paste_store(kNone, data, crlf);
|
||||||
}
|
}
|
||||||
theend:
|
if (phase == 3 || phase == (cancelled ? 2 : -1)) {
|
||||||
if (cancel || phase == -1 || phase == 3) { // End of paste-stream.
|
|
||||||
draining = false;
|
|
||||||
paste_store(kTrue, NULL_STRING, crlf);
|
paste_store(kTrue, NULL_STRING, crlf);
|
||||||
}
|
}
|
||||||
|
theend:
|
||||||
return !cancel;
|
;
|
||||||
|
bool retval = !cancelled;
|
||||||
|
if (phase == -1 || phase == 3) { // End of paste-stream.
|
||||||
|
cancelled = false;
|
||||||
|
}
|
||||||
|
return retval;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Puts text at cursor, in any mode.
|
/// Puts text at cursor, in any mode.
|
||||||
|
@ -1358,9 +1358,72 @@ describe('API', function()
|
|||||||
test_paste_repeat_visual_select(true)
|
test_paste_repeat_visual_select(true)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
it('vim.paste() failure', function()
|
local function test_paste_cancel_error(is_error)
|
||||||
api.nvim_exec_lua('vim.paste = (function(lines, phase) error("fake fail") end)', {})
|
before_each(function()
|
||||||
eq('fake fail', pcall_err(request, 'nvim_paste', 'line 1\nline 2\nline 3', false, 1))
|
exec_lua(([[
|
||||||
|
vim.paste = (function(overridden)
|
||||||
|
return function(lines, phase)
|
||||||
|
for i, line in ipairs(lines) do
|
||||||
|
if line == 'CANCEL' then
|
||||||
|
%s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return overridden(lines, phase)
|
||||||
|
end
|
||||||
|
end)(vim.paste)
|
||||||
|
]]):format(is_error and 'error("fake fail")' or 'return false'))
|
||||||
|
end)
|
||||||
|
local function check_paste_cancel_error(data, crlf, phase)
|
||||||
|
if is_error then
|
||||||
|
eq('fake fail', pcall_err(api.nvim_paste, data, crlf, phase))
|
||||||
|
else
|
||||||
|
eq(false, api.nvim_paste(data, crlf, phase))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
it('in phase -1', function()
|
||||||
|
feed('A')
|
||||||
|
check_paste_cancel_error('CANCEL', true, -1)
|
||||||
|
feed('<Esc>')
|
||||||
|
expect('')
|
||||||
|
feed('.')
|
||||||
|
expect('')
|
||||||
|
end)
|
||||||
|
it('in phase 1', function()
|
||||||
|
feed('A')
|
||||||
|
check_paste_cancel_error('CANCEL', true, 1)
|
||||||
|
feed('<Esc>')
|
||||||
|
expect('')
|
||||||
|
feed('.')
|
||||||
|
expect('')
|
||||||
|
end)
|
||||||
|
it('in phase 2', function()
|
||||||
|
feed('A')
|
||||||
|
eq(true, api.nvim_paste('aaa', true, 1))
|
||||||
|
expect('aaa')
|
||||||
|
check_paste_cancel_error('CANCEL', true, 2)
|
||||||
|
feed('<Esc>')
|
||||||
|
expect('aaa')
|
||||||
|
feed('.')
|
||||||
|
expect('aaaaaa')
|
||||||
|
end)
|
||||||
|
it('in phase 3', function()
|
||||||
|
feed('A')
|
||||||
|
eq(true, api.nvim_paste('aaa', true, 1))
|
||||||
|
expect('aaa')
|
||||||
|
eq(true, api.nvim_paste('bbb', true, 2))
|
||||||
|
expect('aaabbb')
|
||||||
|
check_paste_cancel_error('CANCEL', true, 3)
|
||||||
|
feed('<Esc>')
|
||||||
|
expect('aaabbb')
|
||||||
|
feed('.')
|
||||||
|
expect('aaabbbaaabbb')
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
describe('vim.paste() cancel', function()
|
||||||
|
test_paste_cancel_error(false)
|
||||||
|
end)
|
||||||
|
describe('vim.paste() error', function()
|
||||||
|
test_paste_cancel_error(true)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
@ -1298,6 +1298,41 @@ describe('TUI', function()
|
|||||||
expect_child_buf_lines({ 'foo', '' })
|
expect_child_buf_lines({ 'foo', '' })
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('paste: vim.paste() cancel (retval=false) with streaming #30462', function()
|
||||||
|
child_session:request(
|
||||||
|
'nvim_exec_lua',
|
||||||
|
[[
|
||||||
|
vim.paste = (function(overridden)
|
||||||
|
return function(lines, phase)
|
||||||
|
for i, line in ipairs(lines) do
|
||||||
|
if line:find('!') then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return overridden(lines, phase)
|
||||||
|
end
|
||||||
|
end)(vim.paste)
|
||||||
|
]],
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
feed_data('A')
|
||||||
|
wait_for_mode('i')
|
||||||
|
feed_data('\027[200~aaa')
|
||||||
|
expect_child_buf_lines({ 'aaa' })
|
||||||
|
feed_data('bbb')
|
||||||
|
expect_child_buf_lines({ 'aaabbb' })
|
||||||
|
feed_data('ccc!') -- This chunk is cancelled.
|
||||||
|
expect_child_buf_lines({ 'aaabbb' })
|
||||||
|
feed_data('ddd\027[201~') -- This chunk is ignored.
|
||||||
|
expect_child_buf_lines({ 'aaabbb' })
|
||||||
|
feed_data('\027[27u')
|
||||||
|
wait_for_mode('n')
|
||||||
|
feed_data('.') -- Dot-repeat only includes chunks actually pasted.
|
||||||
|
expect_child_buf_lines({ 'aaabbbaaabbb' })
|
||||||
|
feed_data('$\027[200~eee\027[201~') -- A following paste works normally.
|
||||||
|
expect_child_buf_lines({ 'aaabbbaaabbbeee' })
|
||||||
|
end)
|
||||||
|
|
||||||
it("paste: 'nomodifiable' buffer", function()
|
it("paste: 'nomodifiable' buffer", function()
|
||||||
child_session:request('nvim_command', 'set nomodifiable')
|
child_session:request('nvim_command', 'set nomodifiable')
|
||||||
child_session:request(
|
child_session:request(
|
||||||
|
Loading…
Reference in New Issue
Block a user