API: nvim_paste

This commit is contained in:
Justin M. Keyes 2019-08-24 13:54:27 +02:00
parent c95f5d166f
commit eacc70fb3e
10 changed files with 205 additions and 67 deletions

View File

@ -793,6 +793,49 @@ nvim_get_namespaces() *nvim_get_namespaces()*
Return: ~
dict that maps from names to namespace ids.
nvim_paste({data}, {phase}) *nvim_paste()*
Pastes at cursor, in any mode.
Invokes the `vim.paste` handler, which handles each mode
appropriately. Sets redo/undo. Faster than |nvim_input()|.
Errors ('nomodifiable', `vim.paste()` failure, …) are
reflected in `err` but do not affect the return value (which
is strictly decided by `vim.paste()` ). On error, subsequent
calls are ignored ("drained") until the next paste is
initiated (phase 1 or -1).
Parameters: ~
{data} Multiline input. May be binary (containing NUL
bytes).
{phase} -1: paste in a single call (i.e. without
streaming). To "stream" a paste, call `nvim_paste` sequentially with these `phase` values:
• 1: starts the paste (exactly once)
• 2: continues the paste (zero or more times)
• 3: ends the paste (exactly once)
Return: ~
• true: Client may continue pasting.
• false: Client must cancel the paste.
nvim_put({lines}, {type}, {after}, {follow}) *nvim_put()*
Puts text at cursor, in any mode.
Compare |:put| and |p| which are always linewise.
Parameters: ~
{lines} |readfile()|-style list of lines.
|channel-lines|
{type} Edit behavior:
• "b" |blockwise-visual| mode
• "c" |characterwise| mode
• "l" |linewise| mode
• "" guess by contents
{after} Insert after cursor (like |p|), or before (like
|P|).
{follow} Place cursor at end of inserted text.
nvim_subscribe({event}) *nvim_subscribe()*
Subscribes to event broadcasts.

View File

@ -218,6 +218,39 @@ The "copy" function stores a list of lines and the register type. The "paste"
function returns the clipboard as a `[lines, regtype]` list, where `lines` is
a list of lines and `regtype` is a register type conforming to |setreg()|.
==============================================================================
Paste *provider-paste* *paste*
"Paste" is a separate concept from |clipboard|: paste means "dump a bunch of
text to the editor", whereas clipboard adds features like |quote-+| to get and
set the OS clipboard buffer directly. When you middle-click or CTRL-SHIFT-v
(macOS: CMD-v) to paste text into your terminal, this is "paste", not
"clipboard": the terminal application (Nvim) just gets a stream of text, it
does not interact with the clipboard directly.
*bracketed-paste-mode*
Pasting in the |TUI| depends on the "bracketed paste" terminal capability,
which allows terminal applications to distinguish between user input and
pasted text. https://cirw.in/blog/bracketed-paste
This works automatically if your terminal supports it.
*ui-paste*
GUIs can opt-into Nvim's amazing paste-handling by calling |nvim_paste()|.
PASTE BEHAVIOR ~
Paste always inserts text after the cursor. In cmdline-mode only the first
line is pasted, to avoid accidentally executing many commands.
When pasting a huge amount of text, screen updates are throttled and the
message area shows a "..." pulse.
You can implement a custom paste handler. Example: >
vim._paste = (function(lines, phase)
vim.api.nvim_put(lines, 'c', true, true)
end)
==============================================================================
X11 selection mechanism *clipboard-x11* *x11-selection*

View File

@ -219,12 +219,6 @@ effect on some UIs.
==============================================================================
Using the mouse *mouse-using*
*bracketed-paste-mode*
Nvim enables bracketed paste by default. Bracketed paste mode allows terminal
applications to distinguish between typed text and pasted text. Thus you can
paste text without Nvim trying to format or indent the text.
See also https://cirw.in/blog/bracketed-paste
*mouse-mode-table* *mouse-overview*
Overview of what the mouse buttons do, when 'mousemodel' is "extend":

View File

@ -745,6 +745,35 @@ String ga_take_string(garray_T *ga)
return str;
}
/// Creates "readfile()-style" ArrayOf(String).
///
/// - NUL bytes are replaced with NL (form-feed).
/// - If last line ends with NL an extra empty list item is added.
Array string_to_array(const String input)
{
Array ret = ARRAY_DICT_INIT;
for (size_t i = 0; i < input.size; i++) {
const char *start = input.data + i;
const char *end = xmemscan(start, NL, input.size - i);
const size_t line_len = (size_t)(end - start);
i += line_len;
String s = {
.size = line_len,
.data = xmemdupz(start, line_len),
};
memchrsub(s.data, NUL, NL, line_len);
ADD(ret, STRING_OBJ(s));
// If line ends at end-of-buffer, add empty final item.
// This is "readfile()-style", see also ":help channel-lines".
if (i + 1 == input.size && end[0] == NL) {
ADD(ret, STRING_OBJ(cchar_to_string(NUL)));
}
}
return ret;
}
/// Set, tweak, or remove a mapping in a mode. Acts as the implementation for
/// functions like @ref nvim_buf_set_keymap.
///

View File

@ -1206,6 +1206,42 @@ Dictionary nvim_get_namespaces(void)
return retval;
}
/// Paste
///
/// Invokes the `vim.paste` handler, which handles each mode appropriately.
/// Sets redo/undo. Faster than |nvim_input()|.
///
/// @param data Multiline input. May be binary (containing NUL bytes).
/// @param phase Pass -1 to paste as one big buffer (i.e. without streaming).
/// To "stream" a paste, call `nvim_paste` sequentially with
/// these `phase` values:
/// - 1: starts the paste (exactly once)
/// - 2: continues the paste (zero or more times)
/// - 3: ends the paste (exactly once)
/// @param[out] err Error details, if any
/// @return true if paste should continue, false if paste was canceled
Boolean nvim_paste(String data, Integer phase, Error *err)
FUNC_API_SINCE(6)
{
if (phase < -1 || phase > 3) {
api_set_error(err, kErrorTypeValidation, "Invalid phase: %"PRId64, phase);
return false;
}
Array args = ARRAY_DICT_INIT;
ADD(args, ARRAY_OBJ(string_to_array(data)));
ADD(args, INTEGER_OBJ(phase));
Object rv
= nvim_execute_lua(STATIC_CSTR_AS_STRING("return vim._paste(...)"),
args, err);
// Abort paste if handler does not return true.
bool ok = !ERROR_SET(err)
&& (rv.type == kObjectTypeBoolean && rv.data.boolean);
api_free_object(rv);
api_free_array(args);
return ok;
}
/// Puts text at cursor.
///
/// Compare |:put| and |p| which are always linewise.
@ -1225,11 +1261,8 @@ void nvim_put(ArrayOf(String) lines, String type, Boolean after,
{
yankreg_T *reg = xcalloc(sizeof(yankreg_T), 1);
if (!prepare_yankreg_from_object(reg, type, lines.size)) {
api_set_error(err,
kErrorTypeValidation,
"Invalid regtype %s",
type.data);
return;
api_set_error(err, kErrorTypeValidation, "Invalid type: '%s'", type.data);
goto cleanup;
}
if (lines.size == 0) {
goto cleanup; // Nothing to do.
@ -1237,9 +1270,8 @@ void nvim_put(ArrayOf(String) lines, String type, Boolean after,
for (size_t i = 0; i < lines.size; i++) {
if (lines.items[i].type != kObjectTypeString) {
api_set_error(err,
kErrorTypeValidation,
"All items in the lines array must be strings");
api_set_error(err, kErrorTypeValidation,
"Invalid lines (expected array of strings)");
goto cleanup;
}
String line = lines.items[i].data.string;

View File

@ -523,15 +523,12 @@ void AppendToRedobuff(const char *s)
}
}
/*
* Append to Redo buffer literally, escaping special characters with CTRL-V.
* K_SPECIAL and CSI are escaped as well.
*/
void
AppendToRedobuffLit (
char_u *str,
int len /* length of "str" or -1 for up to the NUL */
)
/// Append to Redo buffer literally, escaping special characters with CTRL-V.
/// K_SPECIAL and CSI are escaped as well.
///
/// @param str String to append
/// @param len Length of `str` or -1 for up to the NUL.
void AppendToRedobuffLit(const char_u *str, int len)
{
if (block_redo) {
return;

View File

@ -105,9 +105,10 @@ local _paste = (function()
tdots = now
tredraw = now
tick = 0
if (call('mode', {})):find('[vV]') then
vim.api.nvim_feedkeys('', 'n', false)
end
-- TODO
-- if mode == 'i' or mode == 'R' then
-- nvim_cancel()
-- end
end
vim.api.nvim_put(lines, 'c', true, true)
if (now - tredraw >= 1000) or phase == 1 or phase == 3 then
@ -119,6 +120,8 @@ local _paste = (function()
local dots = ('.'):rep(tick % 4)
tdots = now
tick = tick + 1
-- Use :echo because Lua print('') is a no-op, and we want to clear the
-- message when there are zero dots.
vim.api.nvim_command(('echo "%s"'):format(dots))
end
if phase == 3 then

View File

@ -100,31 +100,6 @@ static void tinput_done_event(void **argv)
input_done();
}
static Array string_to_array(const String input)
{
Array ret = ARRAY_DICT_INIT;
for (size_t i = 0; i < input.size; i++) {
const char *start = input.data + i;
const char *end = xmemscan(start, NL, input.size - i);
const size_t line_len = (size_t)(end - start);
i += line_len;
String s = {
.size = line_len,
.data = xmemdupz(start, line_len),
};
memchrsub(s.data, NUL, NL, line_len);
ADD(ret, STRING_OBJ(s));
// If line ends at end-of-buffer, add empty final item.
// This is "readfile()-style", see also ":help channel-lines".
if (i + 1 == input.size && end[0] == NL) {
ADD(ret, STRING_OBJ(cchar_to_string(NUL)));
}
}
return ret;
}
static void tinput_wait_enqueue(void **argv)
{
TermInput *input = argv[0];
@ -132,18 +107,9 @@ static void tinput_wait_enqueue(void **argv)
const String keys = { .data = buf, .size = len };
if (input->paste) {
Error err = ERROR_INIT;
Array args = ARRAY_DICT_INIT;
ADD(args, ARRAY_OBJ(string_to_array(keys)));
ADD(args, INTEGER_OBJ(input->paste));
Object rv
= nvim_execute_lua(STATIC_CSTR_AS_STRING("return vim._paste(...)"),
args, &err);
input->paste = (rv.type == kObjectTypeBoolean && rv.data.boolean)
? 2 // Paste phase: "continue".
: 0; // Abort paste if handler does not return true.
api_free_object(rv);
api_free_array(args);
Boolean rv = nvim_paste(keys, input->paste, &err);
// Paste phase: "continue" (unless handler failed).
input->paste = rv && !ERROR_SET(&err) ? 2 : 0;
rbuffer_consumed(input->key_buffer, len);
rbuffer_reset(input->key_buffer);
if (ERROR_SET(&err)) {

View File

@ -366,7 +366,44 @@ describe('API', function()
end)
end)
describe('nvim_paste', function()
it('validates args', function()
expect_err('Invalid phase: %-2', request,
'nvim_paste', 'foo', -2)
expect_err('Invalid phase: 4', request,
'nvim_paste', 'foo', 4)
end)
it('non-streaming', function()
-- With final "\n".
nvim('paste', 'line 1\nline 2\nline 3\n', -1)
expect([[
line 1
line 2
line 3
]])
-- Cursor follows the paste.
eq({0,4,1,0}, funcs.getpos('.'))
eq(false, nvim('get_option', 'paste'))
command('%delete _')
-- Without final "\n".
nvim('paste', 'line 1\nline 2\nline 3', -1)
expect([[
line 1
line 2
line 3]])
-- Cursor follows the paste.
eq({0,3,6,0}, funcs.getpos('.'))
eq(false, nvim('get_option', 'paste'))
end)
end)
describe('nvim_put', function()
it('validates args', function()
expect_err('Invalid lines %(expected array of strings%)', request,
'nvim_put', {42}, 'l', false, false)
expect_err("Invalid type: 'x'", request,
'nvim_put', {'foo'}, 'x', false, false)
end)
it('inserts text', function()
-- linewise
nvim('put', {'line 1','line 2','line 3'}, 'l', true, true)

View File

@ -298,19 +298,23 @@ describe('TUI', function()
end)
-- TODO
it('in normal-mode', function()
it('paste: normal-mode', function()
end)
-- TODO
it('in command-mode', function()
it('paste: command-mode inserts 1 line', function()
end)
-- TODO
it('sets undo-point after consecutive pastes', function()
it('paste: sets undo-point after consecutive pastes', function()
end)
it('paste: other modes', function()
-- Other modes act like CTRL-C + paste.
end)
-- TODO
it('handles missing "stop paste" sequence', function()
it('paste: handles missing "stop paste" sequence', function()
end)
-- TODO: error when pasting into 'nomodifiable' buffer: