From 21f0f7bca5a13fe847478ef27dc26bced9b3f3d1 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 18 Jun 2019 00:00:51 +0200 Subject: [PATCH 01/30] paste: WIP #4448 --- runtime/doc/eval.txt | 4 +- src/nvim/event/loop.c | 9 +- src/nvim/ex_getln.c | 2 +- src/nvim/getchar.c | 14 +- src/nvim/globals.h | 6 - src/nvim/keymap.c | 1 - src/nvim/keymap.h | 5 +- src/nvim/os/input.c | 4 +- src/nvim/terminal.c | 4 - src/nvim/tui/input.c | 43 +++-- src/nvim/types.h | 6 + test/functional/terminal/helpers.lua | 3 + test/functional/terminal/paste_spec.lua | 204 ++++++++++++++++++++++++ test/functional/ui/input_spec.lua | 8 +- 14 files changed, 265 insertions(+), 48 deletions(-) create mode 100644 test/functional/terminal/paste_spec.lua diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index 5d30ac15b3..2ec72f7717 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -4215,11 +4215,11 @@ getchar([expr]) *getchar()* :endfunction < You may also receive synthetic characters, such as - ||. Often you will want to ignore this and get + ||. Often you will want to ignore this and get another character: > :function GetKey() : let c = getchar() - : while c == "\" + : while c == "\" : let c = getchar() : endwhile : return c diff --git a/src/nvim/event/loop.c b/src/nvim/event/loop.c index 609c723c57..93ec28bb81 100644 --- a/src/nvim/event/loop.c +++ b/src/nvim/event/loop.c @@ -36,6 +36,10 @@ void loop_init(Loop *loop, void *data) /// Processes all `Loop.fast_events` events. /// Does NOT process `Loop.events`, that is an application-specific decision. /// +/// @param loop +/// @param ms 0: non-blocking poll. +/// >0: timeout after `ms`. +/// <0: wait forever. /// @returns true if `ms` timeout was reached bool loop_poll_events(Loop *loop, int ms) { @@ -104,10 +108,10 @@ static void loop_deferred_event(void **argv) void loop_on_put(MultiQueue *queue, void *data) { Loop *loop = data; - // Sometimes libuv will run pending callbacks(timer for example) before + // Sometimes libuv will run pending callbacks (timer for example) before // blocking for a poll. If this happens and the callback pushes a event to one // of the queues, the event would only be processed after the poll - // returns(user hits a key for example). To avoid this scenario, we call + // returns (user hits a key for example). To avoid this scenario, we call // uv_stop when a event is enqueued. uv_stop(&loop->uv); } @@ -162,6 +166,7 @@ static void async_cb(uv_async_t *handle) { Loop *l = handle->loop->data; uv_mutex_lock(&l->mutex); + // Flush thread_events to fast_events for processing on main loop. while (!multiqueue_empty(l->thread_events)) { Event ev = multiqueue_get(l->thread_events); multiqueue_put_event(l->fast_events, ev); diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index e8d650accf..2b01e2d72b 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -532,7 +532,7 @@ static int command_line_check(VimState *state) static int command_line_execute(VimState *state, int key) { - if (key == K_IGNORE || key == K_PASTE) { + if (key == K_IGNORE) { return -1; // get another key } diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c index 03f64c2019..d1b4751a00 100644 --- a/src/nvim/getchar.c +++ b/src/nvim/getchar.c @@ -151,7 +151,6 @@ static char_u typebuf_init[TYPELEN_INIT]; /* initial typebuf.tb_buf */ static char_u noremapbuf_init[TYPELEN_INIT]; /* initial typebuf.tb_noremap */ static size_t last_recorded_len = 0; // number of last recorded chars -static const uint8_t ui_toggle[] = { K_SPECIAL, KS_EXTRA, KE_PASTE, 0 }; #ifdef INCLUDE_GENERATED_DECLARATIONS # include "getchar.c.generated.h" @@ -1902,14 +1901,8 @@ static int vgetorpeek(int advance) } } - // Check for a key that can toggle the 'paste' option - if (mp == NULL && (State & (INSERT|NORMAL))) { - bool match = typebuf_match_len(ui_toggle, &mlen); - if (!match && mlen != typebuf.tb_len && *p_pt != NUL) { - // didn't match ui_toggle_key and didn't try the whole typebuf, - // check the 'pastetoggle' - match = typebuf_match_len(p_pt, &mlen); - } + if (*p_pt != NUL && mp == NULL && (State & (INSERT|NORMAL))) { + bool match = typebuf_match_len(p_pt, &mlen); if (match) { // write chars to script file(s) if (mlen > typebuf.tb_maplen) { @@ -1940,8 +1933,7 @@ static int vgetorpeek(int advance) } if ((mp == NULL || max_mlen >= mp_match_len) - && keylen != KEYLEN_PART_MAP - && !(keylen == KEYLEN_PART_KEY && c1 == ui_toggle[0])) { + && keylen != KEYLEN_PART_MAP) { // No matching mapping found or found a non-matching mapping that // matches at least what the matching mapping matched keylen = 0; diff --git a/src/nvim/globals.h b/src/nvim/globals.h index 3bdbff79b4..82fc7c1218 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -72,12 +72,6 @@ # define VIMRC_FILE ".nvimrc" #endif -typedef enum { - kNone = -1, - kFalse = 0, - kTrue = 1, -} TriState; - EXTERN struct nvim_stats_s { int64_t fsync; int64_t redraw; diff --git a/src/nvim/keymap.c b/src/nvim/keymap.c index 27052da9d8..2d117b5308 100644 --- a/src/nvim/keymap.c +++ b/src/nvim/keymap.c @@ -309,7 +309,6 @@ static const struct key_name_entry { { K_ZERO, "Nul" }, { K_SNR, "SNR" }, { K_PLUG, "Plug" }, - { K_PASTE, "Paste" }, { K_COMMAND, "Cmd" }, { 0, NULL } // NOTE: When adding a long name update MAX_KEY_NAME_LEN. diff --git a/src/nvim/keymap.h b/src/nvim/keymap.h index 7f0483826d..cc02a6fb4f 100644 --- a/src/nvim/keymap.h +++ b/src/nvim/keymap.h @@ -239,14 +239,12 @@ enum key_extra { , KE_DROP = 95 // DnD data is available // , KE_CURSORHOLD = 96 // CursorHold event - , KE_NOP = 97 // doesn't do something + , KE_NOP = 97 // no-op: does nothing , KE_FOCUSGAINED = 98 // focus gained , KE_FOCUSLOST = 99 // focus lost // , KE_MOUSEMOVE = 100 // mouse moved with no button down // , KE_CANCEL = 101 // return from vgetc , KE_EVENT = 102 // event - , KE_PASTE = 103 // special key to toggle the 'paste' option. - // sent only by UIs , KE_COMMAND = 104 // special key }; @@ -443,7 +441,6 @@ enum key_extra { #define K_DROP TERMCAP2KEY(KS_EXTRA, KE_DROP) #define K_EVENT TERMCAP2KEY(KS_EXTRA, KE_EVENT) -#define K_PASTE TERMCAP2KEY(KS_EXTRA, KE_PASTE) #define K_COMMAND TERMCAP2KEY(KS_EXTRA, KE_COMMAND) /* Bits for modifier mask */ diff --git a/src/nvim/os/input.c b/src/nvim/os/input.c index 95e9e8e414..83ac3dfa62 100644 --- a/src/nvim/os/input.c +++ b/src/nvim/os/input.c @@ -448,7 +448,7 @@ static void process_interrupts(void) size_t consume_count = 0; RBUFFER_EACH_REVERSE(input_buffer, c, i) { - if ((uint8_t)c == 3) { + if ((uint8_t)c == Ctrl_C) { got_int = true; consume_count = i; break; @@ -456,7 +456,7 @@ static void process_interrupts(void) } if (got_int && consume_count) { - // Remove everything typed before the CTRL-C + // Remove all unprocessed input (typeahead) before the CTRL-C. rbuffer_consumed(input_buffer, consume_count); } } diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index 3faf6dd5bb..ffa05e2599 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -475,10 +475,6 @@ static int terminal_execute(VimState *state, int key) TerminalState *s = (TerminalState *)state; switch (key) { - // Temporary fix until paste events gets implemented - case K_PASTE: - break; - case K_LEFTMOUSE: case K_LEFTDRAG: case K_LEFTRELEASE: diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index fe8ffee8e0..f090b1384a 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -16,7 +16,8 @@ #include "nvim/os/input.h" #include "nvim/event/rstream.h" -#define PASTETOGGLE_KEY "" +#define PASTE_KEY "" +#define PASTEPOST_KEY "" #define KEY_BUFFER_SIZE 0xfff #ifdef INCLUDE_GENERATED_DECLARATIONS @@ -292,9 +293,12 @@ static void tk_getkeys(TermInput *input, bool force) } } - if (result != TERMKEY_RES_AGAIN || input->paste_enabled) { + if (result != TERMKEY_RES_AGAIN) { return; } + // else: Partial keypress event was found in the buffer, but it does not + // yet contain all the bytes required. `key` structure indicates what + // termkey_getkey_force() would return. int ms = get_key_code_timeout(); @@ -326,8 +330,8 @@ static bool handle_focus_event(TermInput *input) if (rbuffer_size(input->read_stream.buffer) > 2 && (!rbuffer_cmp(input->read_stream.buffer, "\x1b[I", 3) || !rbuffer_cmp(input->read_stream.buffer, "\x1b[O", 3))) { - // Advance past the sequence bool focus_gained = *rbuffer_get(input->read_stream.buffer, 2) == 'I'; + // Advance past the sequence rbuffer_consumed(input->read_stream.buffer, 3); aucmd_schedule_focusgained(focus_gained); return true; @@ -343,16 +347,31 @@ static bool handle_bracketed_paste(TermInput *input) bool enable = *rbuffer_get(input->read_stream.buffer, 4) == '0'; // Advance past the sequence rbuffer_consumed(input->read_stream.buffer, 6); - if (input->paste_enabled == enable) { + if (enable && input->paste_enabled) { + // Bogus "paste start"; forward it. + tinput_enqueue(input, "200~", sizeof("200~") - 1); + return true; + } else if (!enable && !input->paste_enabled) { + // Bogus "paste stop"; ignore it. return true; } - tinput_enqueue(input, PASTETOGGLE_KEY, sizeof(PASTETOGGLE_KEY) - 1); + input->paste_enabled = enable; + if (enable) { + loop_schedule(&main_loop, event_create(apply_pastepre, 0)); + } else { + tinput_enqueue(input, PASTEPOST_KEY, sizeof(PASTEPOST_KEY) - 1); + } return true; } return false; } +static void apply_pastepre(void **argv) // MAIN thread +{ + //TODO +} + static bool handle_forced_escape(TermInput *input) { if (rbuffer_size(input->read_stream.buffer) > 1 @@ -477,9 +496,11 @@ static void tinput_read_cb(Stream *stream, RBuffer *buf, size_t count_, continue; } - // Find the next 'esc' and push everything up to it(excluding). This is done - // so the `handle_bracketed_paste`/`handle_forced_escape` calls above work - // as expected. + // + // Find the next ESC and push everything up to it (excluding), so it will + // be the first thing encountered on the next iteration. The `handle_*` + // calls (above) depend on this. + // size_t count = 0; RBUFFER_EACH(input->read_stream.buffer, c, i) { count = i + 1; @@ -488,7 +509,6 @@ static void tinput_read_cb(Stream *stream, RBuffer *buf, size_t count_, break; } } - RBUFFER_UNTIL_EMPTY(input->read_stream.buffer, ptr, len) { size_t consumed = termkey_push_bytes(input->tk, ptr, MIN(count, len)); // termkey_push_bytes can return (size_t)-1, so it is possible that @@ -505,7 +525,8 @@ static void tinput_read_cb(Stream *stream, RBuffer *buf, size_t count_, } } while (rbuffer_size(input->read_stream.buffer)); tinput_flush(input, true); - // Make sure the next input escape sequence fits into the ring buffer - // without wrap around, otherwise it could be misinterpreted. + // Make sure the next input escape sequence fits into the ring buffer without + // wraparound, else it could be misinterpreted (because rbuffer_read_ptr() + // exposes the underlying buffer to callers unaware of the wraparound). rbuffer_reset(input->read_stream.buffer); } diff --git a/src/nvim/types.h b/src/nvim/types.h index 5bcc0c3e1b..87560a43da 100644 --- a/src/nvim/types.h +++ b/src/nvim/types.h @@ -23,4 +23,10 @@ typedef int LuaRef; typedef struct expand expand_T; +typedef enum { + kNone = -1, + kFalse = 0, + kTrue = 1, +} TriState; + #endif // NVIM_TYPES_H diff --git a/test/functional/terminal/helpers.lua b/test/functional/terminal/helpers.lua index 18f0b9e4c1..2d99a08614 100644 --- a/test/functional/terminal/helpers.lua +++ b/test/functional/terminal/helpers.lua @@ -1,3 +1,6 @@ +-- To test tui/input.c, this module spawns `nvim` inside :terminal and sends +-- bytes via jobsend(). Note: the functional/helpers.lua test-session methods +-- operate on the _host_ session, _not_ the child session. local helpers = require('test.functional.helpers')(nil) local Screen = require('test.functional.ui.screen') local nvim_dir = helpers.nvim_dir diff --git a/test/functional/terminal/paste_spec.lua b/test/functional/terminal/paste_spec.lua new file mode 100644 index 0000000000..1c1f57246c --- /dev/null +++ b/test/functional/terminal/paste_spec.lua @@ -0,0 +1,204 @@ +-- TUI tests for "bracketed paste" mode. +-- http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode +local helpers = require('test.functional.helpers') +local child_tui = require('test.functional.tui.child_session') +local Screen = require('test.functional.ui.screen') +local execute = helpers.execute +local nvim_dir = helpers.nvim_dir +local eval = helpers.eval +local eq = helpers.eq +local feed_tui = child_tui.feed_data + +describe('tui paste', function() + local screen + + before_each(function() + helpers.clear() + screen = child_tui.screen_setup(0, '["'..helpers.nvim_prog.. + '", "-u", "NONE", "-i", "NONE", "--cmd", "set noswapfile"]') + + -- Pasting can be really slow in the TUI, especially in ASAN. + screen.timeout = 5000 + + screen:expect([[ + {1: } | + ~ | + ~ | + ~ | + [No Name] | + | + -- TERMINAL -- | + ]]) + end) + + after_each(function() + screen:detach() + end) + + local function setup_harness() + -- Delete the default PastePre/PastePost autocmds. + feed_tui(":autocmd! PastePre,PastePost\n") + + -- Set up test handlers. + feed_tui(":autocmd PastePre * ".. + "call feedkeys('iPastePre mode:'.mode(),'n')\n") + feed_tui(":autocmd PastePost * ".. + "call feedkeys('PastePost mode:'.mode(),'n')\n") + end + + it('handles long bursts of input', function() + execute('set ruler') + local t = {} + for i = 1, 3000 do + t[i] = 'item ' .. tostring(i) + end + feed_tui('i\027[200~') + feed_tui(table.concat(t, '\n')) + feed_tui('\027[201~') + screen:expect([[ + item 2997 | + item 2998 | + item 2999 | + item 3000{1: } | + [No Name] [+] 3000,10 Bot| + -- INSERT -- | + -- TERMINAL -- | + ]]) + end) + + it('raises PastePre, PastePost in normal-mode', function() + setup_harness() + + -- Send the "start paste" sequence. + feed_tui("\027[200~") + feed_tui("\npasted from terminal (1)\npasted from terminal (2)\n") + -- Send the "stop paste" sequence. + feed_tui("\027[201~") + + screen:expect([[ + PastePre mode:n | + pasted from terminal (1) | + pasted from terminal (2) | + PastePost mode:i{1: } | + [No Name] [+] | + -- INSERT -- | + -- TERMINAL -- | + ]]) + end) + + it('forwards spurious "start paste" sequence', function() + setup_harness() + -- If multiple "start paste" sequences are sent without a corresponding + -- "stop paste" sequence, only the first occurrence should be consumed. + + -- Send the "start paste" sequence. + feed_tui("\027[200~") + feed_tui("\npasted from terminal (1)\n") + -- Send spurious "start paste" sequence. + feed_tui("\027[200~") + feed_tui("\n") + -- Send the "stop paste" sequence. + feed_tui("\027[201~") + + screen:expect([[ + PastePre mode:n | + pasted from terminal (1) | + {1:^[}200~ | + PastePost mode:i{2: } | + [No Name] [+] | + -- INSERT -- | + -- TERMINAL -- | + ]], { + [1] = {foreground = 4}, + [2] = {reverse = true}, + }) + end) + + it('ignores spurious "stop paste" sequence', function() + setup_harness() + -- If "stop paste" sequence is received without a preceding "start paste" + -- sequence, it should be ignored. + + feed_tui("i") + -- Send "stop paste" sequence. + feed_tui("\027[201~") + + screen:expect([[ + {1: } | + ~ | + ~ | + ~ | + [No Name] | + -- INSERT -- | + -- TERMINAL -- | + ]]) + end) + + it('raises PastePre, PastePost in command-mode', function() + -- The default PastePre/PastePost handlers set the 'paste' option. To test, + -- we define a command-mode map, then assert that the mapping was ignored + -- during paste. + feed_tui(":cnoremap st XXX\n") + + feed_tui(":not pasted") + + -- Paste did not start, so the mapping _should_ apply. + screen:expect([[ + | + ~ | + ~ | + ~ | + [No Name] | + :not paXXXed{1: } | + -- TERMINAL -- | + ]]) + + feed_tui("\003") -- CTRL-C + feed_tui(":") + feed_tui("\027[200~") -- Send the "start paste" sequence. + feed_tui("pasted") + + -- Paste started, so the mapping should _not_ apply. + screen:expect([[ + | + ~ | + ~ | + ~ | + [No Name] | + :pasted{1: } | + -- TERMINAL -- | + ]]) + + feed_tui("\003") -- CTRL-C + feed_tui(":") + feed_tui("\027[201~") -- Send the "stop paste" sequence. + feed_tui("not pasted") + + -- Paste stopped, so the mapping _should_ apply. + screen:expect([[ + | + ~ | + ~ | + ~ | + [No Name] | + :not paXXXed{1: } | + -- TERMINAL -- | + ]]) + + end) + + -- TODO + it('sets undo-point after consecutive pastes', function() + end) + + -- TODO + it('handles missing "stop paste" sequence', function() + end) + + -- TODO: error when pasting into 'nomodifiable' buffer: + -- [error @ do_put:2656] 17043 - Failed to save undo information + it("handles 'nomodifiable' buffer gracefully", function() + end) + +end) + diff --git a/test/functional/ui/input_spec.lua b/test/functional/ui/input_spec.lua index 0009f2c31b..7b5c6aa29d 100644 --- a/test/functional/ui/input_spec.lua +++ b/test/functional/ui/input_spec.lua @@ -127,7 +127,7 @@ describe('feeding large chunks of input with ', function() for i = 1, 20000 do t[i] = 'item ' .. tostring(i) end - feed('i') + command('doautocmd PastePre') screen:expect([[ ^ | ~ | @@ -161,7 +161,7 @@ describe('feeding large chunks of input with ', function() item 20000^ | -- INSERT (paste) -- | ]]) - feed('') + command('doautocmd PastePost') screen:expect([[ item 19988 | item 19989 | @@ -175,8 +175,8 @@ describe('feeding large chunks of input with ', function() item 19997 | item 19998 | item 19999 | - item 20000^ | - -- INSERT -- 20000,11 Bot | + item 2000^0 | + 20000,10 Bot | ]]) end) end) From 75663aaf0fa1122a34781e7c08ebd9a5c2194f42 Mon Sep 17 00:00:00 2001 From: ZyX Date: Fri, 19 Jul 2019 15:17:54 +0200 Subject: [PATCH 02/30] TUI/paste: collect data, invoke user callback #4448 --- src/nvim/tui/input.c | 88 ++++++++++++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 24 deletions(-) diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index f090b1384a..2e5486f340 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -102,17 +102,65 @@ static void tinput_done_event(void **argv) input_done(); } +static Array string_to_array(const String input) +{ + Array ret = { .size = 0, .items = NULL }; + for (size_t i = 0; i < input.size; i++) { + const char *const start = input.data + i; + const char *const end = xmemscan(start, NL, input.size - i); + i += (size_t) (end - start); + ret.size++; + } + ret.items = xmalloc(ret.size * sizeof(*ret.items)); + size_t array_idx = 0; + for (size_t i = 0; i < input.size; i++) { + const char *const start = input.data + i; + const size_t line_len + = (size_t) ((char *) xmemscan(start, NL, input.size - i) + - start); + i += line_len; + + String item = { + .size = line_len, + .data = xmemdupz(start, line_len), + }; + memchrsub(item.data, NUL, NL, line_len); + ret.items[array_idx++] = STRING_OBJ(item); + } + ret.capacity = ret.size; + + return ret; +} + static void tinput_wait_enqueue(void **argv) { TermInput *input = argv[0]; RBUFFER_UNTIL_EMPTY(input->key_buffer, buf, len) { - size_t consumed = input_enqueue((String){.data = buf, .size = len}); - if (consumed) { - rbuffer_consumed(input->key_buffer, consumed); - } - rbuffer_reset(input->key_buffer); - if (consumed < len) { - break; + const String keys = { .data = buf, .size = len }; + if (input->paste_enabled) { + Object keys_array = ARRAY_OBJ(string_to_array(keys)); + Array args = { .capacity = 1, .size = 1, .items = &keys_array }; + Error err = ERROR_INIT; + Object fret = nvim_call_function(STATIC_CSTR_AS_STRING("PasteCallback"), + args, &err); + if ((fret.type == kObjectTypeInteger && fret.data.integer) + || (fret.type == kObjectTypeBoolean && fret.data.boolean) + || (fret.type == kObjectTypeString && fret.data.string.size)) { + input->paste_enabled = false; + } + api_free_object(fret); + api_free_object(keys_array); + rbuffer_consumed(input->key_buffer, len); + rbuffer_reset(input->key_buffer); + } else { + const size_t consumed = input_enqueue(keys); + if (consumed) { + rbuffer_consumed(input->key_buffer, consumed); + } + rbuffer_reset(input->key_buffer); + if (consumed < len) { + break; + } } } uv_mutex_lock(&input->key_buffer_mutex); @@ -293,7 +341,7 @@ static void tk_getkeys(TermInput *input, bool force) } } - if (result != TERMKEY_RES_AGAIN) { + if (result != TERMKEY_RES_AGAIN || input->paste_enabled) { return; } // else: Partial keypress event was found in the buffer, but it does not @@ -345,33 +393,25 @@ static bool handle_bracketed_paste(TermInput *input) && (!rbuffer_cmp(input->read_stream.buffer, "\x1b[200~", 6) || !rbuffer_cmp(input->read_stream.buffer, "\x1b[201~", 6))) { bool enable = *rbuffer_get(input->read_stream.buffer, 4) == '0'; + if (input->paste_enabled && enable) { + // Pasting "enable paste" code literally. + return false; + } // Advance past the sequence rbuffer_consumed(input->read_stream.buffer, 6); - if (enable && input->paste_enabled) { - // Bogus "paste start"; forward it. - tinput_enqueue(input, "200~", sizeof("200~") - 1); - return true; - } else if (!enable && !input->paste_enabled) { - // Bogus "paste stop"; ignore it. + if (input->paste_enabled == enable) { return true; } - input->paste_enabled = enable; - if (enable) { - loop_schedule(&main_loop, event_create(apply_pastepre, 0)); - } else { - tinput_enqueue(input, PASTEPOST_KEY, sizeof(PASTEPOST_KEY) - 1); + if (!enable) { + tinput_flush(input, true); } + input->paste_enabled = enable; return true; } return false; } -static void apply_pastepre(void **argv) // MAIN thread -{ - //TODO -} - static bool handle_forced_escape(TermInput *input) { if (rbuffer_size(input->read_stream.buffer) > 1 From 9a92ba88ddc986b459282f486abb5a989002645b Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sat, 17 Aug 2019 10:59:26 +0200 Subject: [PATCH 03/30] TUI/paste: push bytes directly (avoid libtermkey) --- src/nvim/tui/input.c | 48 +++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index 2e5486f340..54332128f2 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -102,32 +102,23 @@ static void tinput_done_event(void **argv) input_done(); } +// TODO: send [''] to indicate EOF. static Array string_to_array(const String input) { - Array ret = { .size = 0, .items = NULL }; - for (size_t i = 0; i < input.size; i++) { - const char *const start = input.data + i; - const char *const end = xmemscan(start, NL, input.size - i); - i += (size_t) (end - start); - ret.size++; - } - ret.items = xmalloc(ret.size * sizeof(*ret.items)); - size_t array_idx = 0; + Array ret = ARRAY_DICT_INIT; for (size_t i = 0; i < input.size; i++) { const char *const start = input.data + i; const size_t line_len - = (size_t) ((char *) xmemscan(start, NL, input.size - i) - - start); + = (size_t)((char *)xmemscan(start, NL, input.size - i) - start); i += line_len; - String item = { + String s = { .size = line_len, .data = xmemdupz(start, line_len), }; - memchrsub(item.data, NUL, NL, line_len); - ret.items[array_idx++] = STRING_OBJ(item); + memchrsub(s.data, NUL, NL, line_len); + ADD(ret, STRING_OBJ(s)); } - ret.capacity = ret.size; return ret; } @@ -152,6 +143,12 @@ static void tinput_wait_enqueue(void **argv) api_free_object(keys_array); rbuffer_consumed(input->key_buffer, len); rbuffer_reset(input->key_buffer); + if (ERROR_SET(&err)) { + msg_putchar('\n'); + // TODO(justinmk): emsgf() does not display, why? + msg_printf_attr(HL_ATTR(HLF_E)|MSG_HIST, "paste: %s", err.msg); + api_clear_error(&err); + } } else { const size_t consumed = input_enqueue(keys); if (consumed) { @@ -341,7 +338,7 @@ static void tk_getkeys(TermInput *input, bool force) } } - if (result != TERMKEY_RES_AGAIN || input->paste_enabled) { + if (result != TERMKEY_RES_AGAIN) { return; } // else: Partial keypress event was found in the buffer, but it does not @@ -412,6 +409,7 @@ static bool handle_bracketed_paste(TermInput *input) return false; } +// ESC NUL => static bool handle_forced_escape(TermInput *input) { if (rbuffer_size(input->read_stream.buffer) > 1 @@ -549,14 +547,28 @@ static void tinput_read_cb(Stream *stream, RBuffer *buf, size_t count_, break; } } + // Push bytes directly (paste). + if (input->paste_enabled) { + RBUFFER_UNTIL_EMPTY(input->read_stream.buffer, ptr, len) { + size_t consumed = MIN(count, len); + assert(consumed <= input->read_stream.buffer->size); + tinput_enqueue(input, ptr, consumed); + rbuffer_consumed(input->read_stream.buffer, consumed); + if (!(count -= consumed)) { + break; + } + } + continue; + } + // Push through libtermkey (translates to "" strings, etc.). RBUFFER_UNTIL_EMPTY(input->read_stream.buffer, ptr, len) { size_t consumed = termkey_push_bytes(input->tk, ptr, MIN(count, len)); // termkey_push_bytes can return (size_t)-1, so it is possible that // `consumed > input->read_stream.buffer->size`, but since tk_getkeys is - // called soon, it shouldn't happen + // called soon, it shouldn't happen. assert(consumed <= input->read_stream.buffer->size); rbuffer_consumed(input->read_stream.buffer, consumed); - // Need to process the keys now since there's no guarantee "count" will + // Process the keys now: there is no guarantee `count` will // fit into libtermkey's input buffer. tk_getkeys(input, false); if (!(count -= consumed)) { From 0a4ef38e430b100b7b91577911c9ebdd3ecde94f Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sun, 18 Aug 2019 22:21:11 +0200 Subject: [PATCH 04/30] log: log_key() --- src/nvim/keymap.c | 11 +++++++++++ src/nvim/state.c | 4 +--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/nvim/keymap.c b/src/nvim/keymap.c index 2d117b5308..eab65f2625 100644 --- a/src/nvim/keymap.c +++ b/src/nvim/keymap.c @@ -940,3 +940,14 @@ char_u *replace_termcodes(const char_u *from, const size_t from_len, return *bufp; } +/// Logs a single key as a human-readable keycode. +void log_key(int log_level, int key) +{ + if (log_level < MIN_LOG_LEVEL) { + return; + } + char *keyname = key == K_EVENT + ? "K_EVENT" + : (char *)get_special_key_name(key, mod_mask); + LOG(log_level, "input: %s", keyname); +} diff --git a/src/nvim/state.c b/src/nvim/state.c index 7c7d035366..dbf04eebec 100644 --- a/src/nvim/state.c +++ b/src/nvim/state.c @@ -65,9 +65,7 @@ getkey: } #if MIN_LOG_LEVEL <= DEBUG_LOG_LEVEL - char *keyname = key == K_EVENT - ? "K_EVENT" : (char *)get_special_key_name(key, mod_mask); - DLOG("input: %s", keyname); + log_key(DEBUG_LOG_LEVEL, key); #endif int execute_result = s->execute(s, key); From 7df566060c6ca4acbd7b42c1b40adf6058e49982 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sun, 18 Aug 2019 22:25:03 +0200 Subject: [PATCH 05/30] lua/stdlib: cleanup --- src/nvim/lua/vim.lua | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index 46c96b455f..9854496415 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -8,8 +8,8 @@ -- -- Guideline: "If in doubt, put it in the runtime". -- --- Most functions should live directly on `vim.`, not sub-modules. The only --- "forbidden" names are those claimed by legacy `if_lua`: +-- Most functions should live directly in `vim.`, not in submodules. +-- The only "forbidden" names are those claimed by legacy `if_lua`: -- $ vim -- :lua for k,v in pairs(vim) do print(k) end -- buffer @@ -161,6 +161,16 @@ local function inspect(object, options) -- luacheck: no unused error(object, options) -- Stub for gen_vimdoc.py end +--- Defers the wrapped callback until the Nvim API is safe to call. +--- +--@see |vim-loop-callbacks| +local function schedule_wrap(cb) + return (function (...) + local args = {...} + vim.schedule(function() cb(unpack(args)) end) + end) +end + local function __index(t, key) if key == 'inspect' then t.inspect = require('vim.inspect') @@ -172,16 +182,6 @@ local function __index(t, key) end end ---- Defers the wrapped callback until when the nvim API is safe to call. ---- ---- See |vim-loop-callbacks| -local function schedule_wrap(cb) - return (function (...) - local args = {...} - vim.schedule(function() cb(unpack(args)) end) - end) -end - local module = { _update_package_paths = _update_package_paths, _os_proc_children = _os_proc_children, From 6d277f43a287d62c10fb1ed8d93247ddf4a437d9 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sun, 18 Aug 2019 22:55:54 +0200 Subject: [PATCH 06/30] TUI/paste: define paste function as Lua builtin - Define in Lua so that it is compiled-in (available with `-u NONE`). TODO: Eventually we will want a 'pastefunc' option or some other way to override the default paste handler. --- src/nvim/lua/vim.lua | 18 ++++++++++++++++++ src/nvim/tui/input.c | 5 +++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index 9854496415..922878d6ce 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -93,6 +93,23 @@ local function _os_proc_children(ppid) return children end +-- Default paste function. +local function _paste(data) + -- local eof = (data == {''}) + local curline = vim.api.nvim_call_function('line', {'.'}) + vim.api.nvim_buf_set_lines( + 0, + curline, + curline, + false, + data) + vim.api.nvim_call_function('cursor', {curline + #data, 1}) + -- if eof then + -- vim.api.nvim_command('redraw') + -- end + return 0 +end + -- TODO(ZyX-I): Create compatibility layer. --{{{1 package.path updater function -- Last inserted paths. Used to clear out items from package.[c]path when they @@ -186,6 +203,7 @@ local module = { _update_package_paths = _update_package_paths, _os_proc_children = _os_proc_children, _os_proc_info = _os_proc_info, + _paste = _paste, _system = _system, schedule_wrap = schedule_wrap, } diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index 54332128f2..dc79a22862 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -132,8 +132,9 @@ static void tinput_wait_enqueue(void **argv) Object keys_array = ARRAY_OBJ(string_to_array(keys)); Array args = { .capacity = 1, .size = 1, .items = &keys_array }; Error err = ERROR_INIT; - Object fret = nvim_call_function(STATIC_CSTR_AS_STRING("PasteCallback"), - args, &err); + Object fret + = nvim_execute_lua(STATIC_CSTR_AS_STRING("return vim._paste(...)"), + args, &err); if ((fret.type == kObjectTypeInteger && fret.data.integer) || (fret.type == kObjectTypeBoolean && fret.data.boolean) || (fret.type == kObjectTypeString && fret.data.string.size)) { From 68ea9a7c8a7a74ec6ec9782528527cf70b92a376 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 19 Aug 2019 00:18:41 +0200 Subject: [PATCH 07/30] TUI/paste: always flush on paste mode-change Flush input before entering, not only when leaving, paste mode. Else there could be pending input which will erroneously be sent to the paste handler. --- src/nvim/lua/vim.lua | 10 +++++---- src/nvim/tui/input.c | 6 +----- test/functional/terminal/tui_spec.lua | 29 +++++++++++++-------------- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index 922878d6ce..47feba0f85 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -96,17 +96,19 @@ end -- Default paste function. local function _paste(data) -- local eof = (data == {''}) - local curline = vim.api.nvim_call_function('line', {'.'}) + local curline = vim.api.nvim_call_function('line', {'.'}) - 1 vim.api.nvim_buf_set_lines( 0, curline, curline, false, data) - vim.api.nvim_call_function('cursor', {curline + #data, 1}) + vim.api.nvim_call_function( + 'cursor', + {curline + #data, 9999999}) + -- TODO: do not redraw (slow!) until paste is finished. -- if eof then - -- vim.api.nvim_command('redraw') - -- end + vim.api.nvim_command('redraw') return 0 end diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index dc79a22862..b16f93ae66 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -16,8 +16,6 @@ #include "nvim/os/input.h" #include "nvim/event/rstream.h" -#define PASTE_KEY "" -#define PASTEPOST_KEY "" #define KEY_BUFFER_SIZE 0xfff #ifdef INCLUDE_GENERATED_DECLARATIONS @@ -401,9 +399,7 @@ static bool handle_bracketed_paste(TermInput *input) return true; } - if (!enable) { - tinput_flush(input, true); - } + tinput_flush(input, true); input->paste_enabled = enable; return true; } diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index af55ec1555..b990652fc0 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -146,10 +146,8 @@ describe('TUI', function() ]], attrs) end) - it('automatically sends for bracketed paste sequences', function() + it('bracketed Paste', function() -- Pasting can be really slow in the TUI, specially in ASAN. - -- This will be fixed later but for now we require a high timeout. - screen.timeout = 60000 feed_data('i\027[200~') screen:expect([[ {1: } | @@ -157,29 +155,30 @@ describe('TUI', function() {4:~ }| {4:~ }| {5:[No Name] }| - {3:-- INSERT (paste) --} | + {3:-- INSERT --} | {3:-- TERMINAL --} | ]]) feed_data('pasted from terminal') screen:expect([[ pasted from terminal{1: } | - {4:~ }| - {4:~ }| - {4:~ }| - {5:[No Name] [+] }| - {3:-- INSERT (paste) --} | - {3:-- TERMINAL --} | - ]]) - feed_data('\027[201~') - screen:expect([[ - pasted from terminal{1: } | - {4:~ }| + | {4:~ }| {4:~ }| {5:[No Name] [+] }| {3:-- INSERT --} | {3:-- TERMINAL --} | ]]) + feed_data('\027[201~') -- End paste. + feed_data('\027\000') -- ESC: go to Normal mode. + screen:expect([[ + pasted from termina{1:l} | + | + {4:~ }| + {4:~ }| + {5:[No Name] [+] }| + | + {3:-- TERMINAL --} | + ]]) end) it('handles pasting a specific amount of text', function() From 4389401a7c82ca43a3634f65f57815af06fe9abd Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 19 Aug 2019 00:41:58 +0200 Subject: [PATCH 08/30] paste: abort paste if handler does not return true --- src/nvim/lua/vim.lua | 2 +- src/nvim/tui/input.c | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index 47feba0f85..54fce47fd0 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -109,7 +109,7 @@ local function _paste(data) -- TODO: do not redraw (slow!) until paste is finished. -- if eof then vim.api.nvim_command('redraw') - return 0 + return true -- Paste will not continue if not returning `true`. end -- TODO(ZyX-I): Create compatibility layer. diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index b16f93ae66..163bc41dae 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -133,9 +133,8 @@ static void tinput_wait_enqueue(void **argv) Object fret = nvim_execute_lua(STATIC_CSTR_AS_STRING("return vim._paste(...)"), args, &err); - if ((fret.type == kObjectTypeInteger && fret.data.integer) - || (fret.type == kObjectTypeBoolean && fret.data.boolean) - || (fret.type == kObjectTypeString && fret.data.string.size)) { + if (fret.type != kObjectTypeBoolean || !fret.data.boolean) { + // Abort paste if handler does not return true. input->paste_enabled = false; } api_free_object(fret); From abd55be19a2f1443cfffb8d4953f86f32efe40aa Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 19 Aug 2019 01:01:40 +0200 Subject: [PATCH 09/30] paste: fixup tests --- src/nvim/lua/vim.lua | 2 +- test/functional/terminal/tui_spec.lua | 17 ++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index 54fce47fd0..e20fc1472e 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -96,7 +96,7 @@ end -- Default paste function. local function _paste(data) -- local eof = (data == {''}) - local curline = vim.api.nvim_call_function('line', {'.'}) - 1 + local curline = vim.api.nvim_call_function('line', {'.'}) vim.api.nvim_buf_set_lines( 0, curline, diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index b990652fc0..127cd69975 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -146,8 +146,7 @@ describe('TUI', function() ]], attrs) end) - it('bracketed Paste', function() - -- Pasting can be really slow in the TUI, specially in ASAN. + it('paste: Insert mode', function() feed_data('i\027[200~') screen:expect([[ {1: } | @@ -160,8 +159,8 @@ describe('TUI', function() ]]) feed_data('pasted from terminal') screen:expect([[ - pasted from terminal{1: } | | + pasted from terminal{1: } | {4:~ }| {4:~ }| {5:[No Name] [+] }| @@ -171,8 +170,8 @@ describe('TUI', function() feed_data('\027[201~') -- End paste. feed_data('\027\000') -- ESC: go to Normal mode. screen:expect([[ - pasted from termina{1:l} | | + pasted from termina{1:l} | {4:~ }| {4:~ }| {5:[No Name] [+] }| @@ -181,24 +180,20 @@ describe('TUI', function() ]]) end) - it('handles pasting a specific amount of text', function() - -- Need extra time for this test, specially in ASAN. - screen.timeout = 60000 + it('pasting a specific amount of text #10311', function() feed_data('i\027[200~'..string.rep('z', 64)..'\027[201~') screen:expect([[ + | zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz| zzzzzzzzzzzzzz{1: } | {4:~ }| - {4:~ }| {5:[No Name] [+] }| {3:-- INSERT --} | {3:-- TERMINAL --} | ]]) end) - it('can handle arbitrarily long bursts of input', function() - -- Need extra time for this test, specially in ASAN. - screen.timeout = 60000 + it('big burst of input (bracketed paste)', function() feed_command('set ruler') local t = {} for i = 1, 3000 do From f99caa755c84788f0e2e9959ccad7c4539fb4927 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 19 Aug 2019 01:14:06 +0200 Subject: [PATCH 10/30] paste: use chansend() in Terminal-mode --- src/nvim/lua/vim.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index e20fc1472e..49eb99c81a 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -95,15 +95,23 @@ end -- Default paste function. local function _paste(data) + local call = vim.api.nvim_call_function + local mode = call('mode', {}) + if mode == 't' then + call('chansend', + {vim.api.nvim_buf_get_option(0, 'channel'), data}) + return true + end + -- local eof = (data == {''}) - local curline = vim.api.nvim_call_function('line', {'.'}) + local curline = call('line', {'.'}) vim.api.nvim_buf_set_lines( 0, curline, curline, false, data) - vim.api.nvim_call_function( + call( 'cursor', {curline + #data, 9999999}) -- TODO: do not redraw (slow!) until paste is finished. From 4cc56905cb886327bcf2f0454a2164910dc5df3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Linse?= Date: Sat, 27 May 2017 10:08:42 +0200 Subject: [PATCH 11/30] API: nvim_put #6819 --- src/nvim/api/vim.c | 48 ++++++++++++++++++++++++++++++++ src/nvim/ops.c | 69 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 115 insertions(+), 2 deletions(-) diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index d027eca59a..4c1f8dcc39 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -36,6 +36,7 @@ #include "nvim/eval.h" #include "nvim/eval/typval.h" #include "nvim/fileio.h" +#include "nvim/ops.h" #include "nvim/option.h" #include "nvim/state.h" #include "nvim/syntax.h" @@ -1204,6 +1205,53 @@ Dictionary nvim_get_namespaces(void) return retval; } +/// @param lines contents. One empty line for no-op, zero lines to emulate error +/// @param type type ("c", "l", "b") or empty to guess from contents +/// @param name if emulates put from a register, otherwise empty +/// @param prev True to emulate "P" otherwise "p" +/// @param count repeat count +/// @param[out] err details of an error that have occurred, if any. +void nvim_put(ArrayOf(String) lines, String type, String regname, Boolean prev, Integer count, Error *err) + FUNC_API_SINCE(6) +{ + if (regname.size > 1) { + api_set_error(err, + kErrorTypeValidation, + "regname must be a single ASCII char or the empty string"); + return; + } + 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; + } + + 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"); + goto cleanup; + } + String line = lines.items[i].data.string; + reg->y_array[i] = (char_u *)xmemdupz(line.data, line.size); + memchrsub(reg->y_array[i], NUL, NL, line.size); + } + + finish_yankreg_from_object(reg, false); + + int name = regname.size ? regname.data[0] : NUL; + do_put(name, reg, prev ? BACKWARD : FORWARD, (long)count, 0); + +cleanup: + free_register(reg); + xfree(reg); + +} + /// Subscribes to event broadcasts. /// /// @param channel_id Channel id (passed automatically by the dispatcher) diff --git a/src/nvim/ops.c b/src/nvim/ops.c index 4f1709bb1f..ebf5c7a7bc 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -2732,7 +2732,7 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) * Using inserted text works differently, because the register includes * special characters (newlines, etc.). */ - if (regname == '.') { + if (regname == '.' && !reg) { bool non_linewise_vis = (VIsual_active && VIsual_mode != 'V'); // PUT_LINE has special handling below which means we use 'i' to start. @@ -2815,7 +2815,7 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) * For special registers '%' (file name), '#' (alternate file name) and * ':' (last command line), etc. we have to create a fake yank register. */ - if (get_spec_reg(regname, &insert_string, &allocated, true)) { + if (!reg && get_spec_reg(regname, &insert_string, &allocated, true)) { if (insert_string == NULL) { return; } @@ -5675,6 +5675,71 @@ end: return target; } +/// @param[out] reg Expected to be empty +bool prepare_yankreg_from_object(yankreg_T *reg, String regtype, size_t lines) +{ + if (regtype.size > 1) { + return false; + } + char type = regtype.data ? regtype.data[0] : NUL; + + switch (type) { + case 0: + reg->y_type = kMTUnknown; + break; + case 'v': case 'c': + reg->y_type = kMTCharWise; + break; + case 'V': case 'l': + reg->y_type = kMTLineWise; + break; + case 'b': case Ctrl_V: + reg->y_type = kMTBlockWise; + break; + default: + return false; + } + + reg->y_array = xcalloc(lines, sizeof(uint8_t *)); + reg->y_size = lines; + reg->additional_data = NULL; + reg->timestamp = 0; + return true; +} + +void finish_yankreg_from_object(yankreg_T *reg, bool clipboard_adjust) +{ + if (reg->y_size > 0 && strlen((char *)reg->y_array[reg->y_size-1]) == 0) { + // a known-to-be charwise yank might have a final linebreak + // but otherwise there is no line after the final newline + if (reg->y_type != kMTCharWise) { + if (reg->y_type == kMTUnknown || clipboard_adjust) { + xfree(reg->y_array[reg->y_size-1]); + reg->y_size--; + } + if (reg->y_type == kMTUnknown) { + reg->y_type = kMTLineWise; + } + } + } else { + if (reg->y_type == kMTUnknown) { + reg->y_type = kMTCharWise; + } + } + + if (reg->y_type == kMTBlockWise) { + size_t maxlen = 0; + for (size_t i = 0; i < reg->y_size; i++) { + size_t rowlen = STRLEN(reg->y_array[i]); + if (rowlen > maxlen) { + maxlen = rowlen; + } + } + assert(maxlen <= INT_MAX); + reg->y_width = (int)maxlen - 1; + } +} + static bool get_clipboard(int name, yankreg_T **target, bool quiet) { // show message on error From 9e25a36467228bbcb8c6db88d2acb69cf7145af3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Linse?= Date: Sun, 28 May 2017 16:11:13 +0200 Subject: [PATCH 12/30] API: nvim_put #6819: try to fix Insert, Visual --- src/nvim/api/vim.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 4c1f8dcc39..31ddfa57f1 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -29,6 +29,7 @@ #include "nvim/ex_docmd.h" #include "nvim/screen.h" #include "nvim/memline.h" +#include "nvim/mark.h" #include "nvim/memory.h" #include "nvim/message.h" #include "nvim/popupmnu.h" @@ -1244,7 +1245,16 @@ void nvim_put(ArrayOf(String) lines, String type, String regname, Boolean prev, finish_yankreg_from_object(reg, false); int name = regname.size ? regname.data[0] : NUL; - do_put(name, reg, prev ? BACKWARD : FORWARD, (long)count, 0); + bool VIsual_was_active = VIsual_active; + int flags = 0; + if (State & INSERT) { + flags |= PUT_CURSEND; + } else if (VIsual_active) { + // TODO: fix VIsual when cursor is before, or emulate the delete as well + flags |= lt(VIsual, curwin->w_cursor) ? PUT_CURSEND : 0; + } + do_put(name, reg, prev ? BACKWARD : FORWARD, (long)count, flags); + VIsual_active = VIsual_was_active; cleanup: free_register(reg); From e1177be363f84f5f4f34c21b760bc47f70d5fa48 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 19 Aug 2019 23:43:19 +0200 Subject: [PATCH 13/30] API: nvim_put #6819 --- src/nvim/api/vim.c | 28 ++++++++++----------- test/functional/api/vim_spec.lua | 43 +++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 31ddfa57f1..bafb21bd4e 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -1206,21 +1206,18 @@ Dictionary nvim_get_namespaces(void) return retval; } -/// @param lines contents. One empty line for no-op, zero lines to emulate error +/// Inserts text at cursor. +/// +/// Compare |:put| and |p| which are always linewise. +/// +/// @param lines contents /// @param type type ("c", "l", "b") or empty to guess from contents -/// @param name if emulates put from a register, otherwise empty -/// @param prev True to emulate "P" otherwise "p" -/// @param count repeat count -/// @param[out] err details of an error that have occurred, if any. -void nvim_put(ArrayOf(String) lines, String type, String regname, Boolean prev, Integer count, Error *err) +/// @param direction behave like |P| instead of |p| +/// @param[out] err Error details, if any +void nvim_put(ArrayOf(String) lines, String type, Boolean direction, + Error *err) FUNC_API_SINCE(6) { - if (regname.size > 1) { - api_set_error(err, - kErrorTypeValidation, - "regname must be a single ASCII char or the empty string"); - return; - } yankreg_T *reg = xcalloc(sizeof(yankreg_T), 1); if (!prepare_yankreg_from_object(reg, type, lines.size)) { api_set_error(err, @@ -1229,6 +1226,9 @@ void nvim_put(ArrayOf(String) lines, String type, String regname, Boolean prev, type.data); return; } + if (lines.size == 0) { + goto cleanup; // Nothing to do. + } for (size_t i = 0; i < lines.size; i++) { if (lines.items[i].type != kObjectTypeString) { @@ -1244,7 +1244,6 @@ void nvim_put(ArrayOf(String) lines, String type, String regname, Boolean prev, finish_yankreg_from_object(reg, false); - int name = regname.size ? regname.data[0] : NUL; bool VIsual_was_active = VIsual_active; int flags = 0; if (State & INSERT) { @@ -1253,13 +1252,12 @@ void nvim_put(ArrayOf(String) lines, String type, String regname, Boolean prev, // TODO: fix VIsual when cursor is before, or emulate the delete as well flags |= lt(VIsual, curwin->w_cursor) ? PUT_CURSEND : 0; } - do_put(name, reg, prev ? BACKWARD : FORWARD, (long)count, flags); + do_put(0, reg, direction ? BACKWARD : FORWARD, 1, flags); VIsual_active = VIsual_was_active; cleanup: free_register(reg); xfree(reg); - } /// Subscribes to event broadcasts. diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 0cd81619c1..09c297940c 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -5,6 +5,7 @@ local NIL = helpers.NIL local clear, nvim, eq, neq = helpers.clear, helpers.nvim, helpers.eq, helpers.neq local command = helpers.command local eval = helpers.eval +local expect = helpers.expect local funcs = helpers.funcs local iswin = helpers.iswin local meth_pcall = helpers.meth_pcall @@ -365,6 +366,40 @@ describe('API', function() end) end) + describe('nvim_put', function() + it('inserts text', function() + -- linewise + nvim('put', {'line 1','line 2','line 3'}, 'l', false) + expect([[ + + line 1 + line 2 + line 3]]) + command('%delete _') + -- charwise + nvim('put', {'line 1','line 2','line 3'}, 'c', false) + expect([[ + line 1 + line 2 + line 3]]) + -- blockwise + nvim('put', {'AA','BB'}, 'b', false) + expect([[ + lAAine 1 + lBBine 2 + line 3]]) + command('%delete _') + -- Empty lines list. + nvim('put', {}, 'c', false) + expect([[]]) + -- Single empty line. + nvim('put', {''}, 'c', false) + expect([[ + ]]) + eq('', nvim('eval', 'v:errmsg')) + end) + end) + describe('nvim_strwidth', function() it('works', function() eq(3, nvim('strwidth', 'abc')) @@ -626,12 +661,12 @@ describe('API', function() -- Make any RPC request (can be non-async: op-pending does not block). nvim('get_current_buf') -- Buffer should not change. - helpers.expect([[ + expect([[ FIRST LINE SECOND LINE]]) -- Now send input to complete the operator. nvim('input', 'j') - helpers.expect([[ + expect([[ first line second line]]) end) @@ -664,7 +699,7 @@ describe('API', function() nvim('get_api_info') -- Send input to complete the mapping. nvim('input', 'd') - helpers.expect([[ + expect([[ FIRST LINE SECOND LINE]]) eq('it worked...', helpers.eval('g:foo')) @@ -680,7 +715,7 @@ describe('API', function() nvim('get_api_info') -- Send input to complete the mapping. nvim('input', 'x') - helpers.expect([[ + expect([[ FIRST LINE SECOND LINfooE]]) end) From 5a2894d67753b408ada3b89c1b7fbd9152977203 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 20 Aug 2019 01:21:27 +0200 Subject: [PATCH 14/30] paste: use nvim_put() --- src/nvim/lua/vim.lua | 22 +-- src/nvim/tui/input.c | 1 - test/functional/terminal/paste_spec.lua | 204 ------------------------ test/functional/terminal/tui_spec.lua | 80 +++++++++- test/functional/ui/input_spec.lua | 71 --------- 5 files changed, 82 insertions(+), 296 deletions(-) delete mode 100644 test/functional/terminal/paste_spec.lua diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index 49eb99c81a..6759e4436b 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -94,26 +94,14 @@ local function _os_proc_children(ppid) end -- Default paste function. -local function _paste(data) +local function _paste(lines) + -- local eof = (lines == {''}) local call = vim.api.nvim_call_function local mode = call('mode', {}) - if mode == 't' then - call('chansend', - {vim.api.nvim_buf_get_option(0, 'channel'), data}) - return true - end - - -- local eof = (data == {''}) local curline = call('line', {'.'}) - vim.api.nvim_buf_set_lines( - 0, - curline, - curline, - false, - data) - call( - 'cursor', - {curline + #data, 9999999}) + -- vim.api.nvim_set_option('paste', true) + vim.api.nvim_put(lines, 'c', false) + -- vim.api.nvim_set_option('paste', false) -- TODO: do not redraw (slow!) until paste is finished. -- if eof then vim.api.nvim_command('redraw') diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index 163bc41dae..79615e30da 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -100,7 +100,6 @@ static void tinput_done_event(void **argv) input_done(); } -// TODO: send [''] to indicate EOF. static Array string_to_array(const String input) { Array ret = ARRAY_DICT_INIT; diff --git a/test/functional/terminal/paste_spec.lua b/test/functional/terminal/paste_spec.lua deleted file mode 100644 index 1c1f57246c..0000000000 --- a/test/functional/terminal/paste_spec.lua +++ /dev/null @@ -1,204 +0,0 @@ --- TUI tests for "bracketed paste" mode. --- http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode -local helpers = require('test.functional.helpers') -local child_tui = require('test.functional.tui.child_session') -local Screen = require('test.functional.ui.screen') -local execute = helpers.execute -local nvim_dir = helpers.nvim_dir -local eval = helpers.eval -local eq = helpers.eq -local feed_tui = child_tui.feed_data - -describe('tui paste', function() - local screen - - before_each(function() - helpers.clear() - screen = child_tui.screen_setup(0, '["'..helpers.nvim_prog.. - '", "-u", "NONE", "-i", "NONE", "--cmd", "set noswapfile"]') - - -- Pasting can be really slow in the TUI, especially in ASAN. - screen.timeout = 5000 - - screen:expect([[ - {1: } | - ~ | - ~ | - ~ | - [No Name] | - | - -- TERMINAL -- | - ]]) - end) - - after_each(function() - screen:detach() - end) - - local function setup_harness() - -- Delete the default PastePre/PastePost autocmds. - feed_tui(":autocmd! PastePre,PastePost\n") - - -- Set up test handlers. - feed_tui(":autocmd PastePre * ".. - "call feedkeys('iPastePre mode:'.mode(),'n')\n") - feed_tui(":autocmd PastePost * ".. - "call feedkeys('PastePost mode:'.mode(),'n')\n") - end - - it('handles long bursts of input', function() - execute('set ruler') - local t = {} - for i = 1, 3000 do - t[i] = 'item ' .. tostring(i) - end - feed_tui('i\027[200~') - feed_tui(table.concat(t, '\n')) - feed_tui('\027[201~') - screen:expect([[ - item 2997 | - item 2998 | - item 2999 | - item 3000{1: } | - [No Name] [+] 3000,10 Bot| - -- INSERT -- | - -- TERMINAL -- | - ]]) - end) - - it('raises PastePre, PastePost in normal-mode', function() - setup_harness() - - -- Send the "start paste" sequence. - feed_tui("\027[200~") - feed_tui("\npasted from terminal (1)\npasted from terminal (2)\n") - -- Send the "stop paste" sequence. - feed_tui("\027[201~") - - screen:expect([[ - PastePre mode:n | - pasted from terminal (1) | - pasted from terminal (2) | - PastePost mode:i{1: } | - [No Name] [+] | - -- INSERT -- | - -- TERMINAL -- | - ]]) - end) - - it('forwards spurious "start paste" sequence', function() - setup_harness() - -- If multiple "start paste" sequences are sent without a corresponding - -- "stop paste" sequence, only the first occurrence should be consumed. - - -- Send the "start paste" sequence. - feed_tui("\027[200~") - feed_tui("\npasted from terminal (1)\n") - -- Send spurious "start paste" sequence. - feed_tui("\027[200~") - feed_tui("\n") - -- Send the "stop paste" sequence. - feed_tui("\027[201~") - - screen:expect([[ - PastePre mode:n | - pasted from terminal (1) | - {1:^[}200~ | - PastePost mode:i{2: } | - [No Name] [+] | - -- INSERT -- | - -- TERMINAL -- | - ]], { - [1] = {foreground = 4}, - [2] = {reverse = true}, - }) - end) - - it('ignores spurious "stop paste" sequence', function() - setup_harness() - -- If "stop paste" sequence is received without a preceding "start paste" - -- sequence, it should be ignored. - - feed_tui("i") - -- Send "stop paste" sequence. - feed_tui("\027[201~") - - screen:expect([[ - {1: } | - ~ | - ~ | - ~ | - [No Name] | - -- INSERT -- | - -- TERMINAL -- | - ]]) - end) - - it('raises PastePre, PastePost in command-mode', function() - -- The default PastePre/PastePost handlers set the 'paste' option. To test, - -- we define a command-mode map, then assert that the mapping was ignored - -- during paste. - feed_tui(":cnoremap st XXX\n") - - feed_tui(":not pasted") - - -- Paste did not start, so the mapping _should_ apply. - screen:expect([[ - | - ~ | - ~ | - ~ | - [No Name] | - :not paXXXed{1: } | - -- TERMINAL -- | - ]]) - - feed_tui("\003") -- CTRL-C - feed_tui(":") - feed_tui("\027[200~") -- Send the "start paste" sequence. - feed_tui("pasted") - - -- Paste started, so the mapping should _not_ apply. - screen:expect([[ - | - ~ | - ~ | - ~ | - [No Name] | - :pasted{1: } | - -- TERMINAL -- | - ]]) - - feed_tui("\003") -- CTRL-C - feed_tui(":") - feed_tui("\027[201~") -- Send the "stop paste" sequence. - feed_tui("not pasted") - - -- Paste stopped, so the mapping _should_ apply. - screen:expect([[ - | - ~ | - ~ | - ~ | - [No Name] | - :not paXXXed{1: } | - -- TERMINAL -- | - ]]) - - end) - - -- TODO - it('sets undo-point after consecutive pastes', function() - end) - - -- TODO - it('handles missing "stop paste" sequence', function() - end) - - -- TODO: error when pasting into 'nomodifiable' buffer: - -- [error @ do_put:2656] 17043 - Failed to save undo information - it("handles 'nomodifiable' buffer gracefully", function() - end) - -end) - diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 127cd69975..0aa9dace0e 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -1,5 +1,9 @@ -- TUI acceptance tests. -- Uses :terminal as a way to send keys and assert screen state. +-- +-- "bracketed paste" terminal feature: +-- http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode + local helpers = require('test.functional.helpers')(after_each) local uname = helpers.uname local thelpers = require('test.functional.terminal.helpers') @@ -159,10 +163,10 @@ describe('TUI', function() ]]) feed_data('pasted from terminal') screen:expect([[ - | pasted from terminal{1: } | {4:~ }| {4:~ }| + {4:~ }| {5:[No Name] [+] }| {3:-- INSERT --} | {3:-- TERMINAL --} | @@ -170,10 +174,10 @@ describe('TUI', function() feed_data('\027[201~') -- End paste. feed_data('\027\000') -- ESC: go to Normal mode. screen:expect([[ - | pasted from termina{1:l} | {4:~ }| {4:~ }| + {4:~ }| {5:[No Name] [+] }| | {3:-- TERMINAL --} | @@ -183,10 +187,10 @@ describe('TUI', function() it('pasting a specific amount of text #10311', function() feed_data('i\027[200~'..string.rep('z', 64)..'\027[201~') screen:expect([[ - | zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz| zzzzzzzzzzzzzz{1: } | {4:~ }| + {4:~ }| {5:[No Name] [+] }| {3:-- INSERT --} | {3:-- TERMINAL --} | @@ -211,6 +215,76 @@ describe('TUI', function() ]]) end) + it('forwards spurious "start paste" sequence', function() + -- If multiple "start paste" sequences are sent without a corresponding + -- "stop paste" sequence, only the first occurrence should be consumed. + + -- Send the "start paste" sequence. + feed_data('i\027[200~') + feed_data('\npasted from terminal (1)\n') + -- Send spurious "start paste" sequence. + feed_data('\027[200~') + feed_data('\n') + -- Send the "stop paste" sequence. + feed_data('\027[201~') + + screen:expect{grid=[[ + | + pasted from terminal (1) | + {6:^[}[200~{1: } | + {4:~ }| + {5:[No Name] [+] }| + {3:-- INSERT --} | + {3:-- TERMINAL --} | + ]], + attr_ids={ + [1] = {reverse = true}, + [2] = {background = tonumber('0x00000b')}, + [3] = {bold = true}, + [4] = {foreground = tonumber('0x00000c')}, + [5] = {bold = true, reverse = true}, + [6] = {foreground = tonumber('0x000051')}, + }} + end) + + it('ignores spurious "stop paste" sequence', function() + -- If "stop paste" sequence is received without a preceding "start paste" + -- sequence, it should be ignored. + feed_data('i') + -- Send "stop paste" sequence. + feed_data('\027[201~') + screen:expect([[ + {1: } | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] }| + {3:-- INSERT --} | + {3:-- TERMINAL --} | + ]]) + end) + + -- TODO + it('in normal-mode', function() + end) + + -- TODO + it('in command-mode', function() + end) + + -- TODO + it('sets undo-point after consecutive pastes', function() + end) + + -- TODO + it('handles missing "stop paste" sequence', function() + end) + + -- TODO: error when pasting into 'nomodifiable' buffer: + -- [error @ do_put:2656] 17043 - Failed to save undo information + it("handles 'nomodifiable' buffer gracefully", function() + end) + it('allows termguicolors to be set at runtime', function() screen:set_option('rgb', true) screen:set_default_attr_ids({ diff --git a/test/functional/ui/input_spec.lua b/test/functional/ui/input_spec.lua index 7b5c6aa29d..12d0e4f40b 100644 --- a/test/functional/ui/input_spec.lua +++ b/test/functional/ui/input_spec.lua @@ -110,77 +110,6 @@ describe('mappings', function() end) end) -describe('feeding large chunks of input with ', function() - local screen - before_each(function() - clear() - screen = Screen.new() - screen:attach() - feed_command('set ruler') - end) - - it('ok', function() - if helpers.skip_fragile(pending) then - return - end - local t = {} - for i = 1, 20000 do - t[i] = 'item ' .. tostring(i) - end - command('doautocmd PastePre') - screen:expect([[ - ^ | - ~ | - ~ | - ~ | - ~ | - ~ | - ~ | - ~ | - ~ | - ~ | - ~ | - ~ | - ~ | - -- INSERT (paste) -- | - ]]) - feed(table.concat(t, '')) - screen:expect([[ - item 19988 | - item 19989 | - item 19990 | - item 19991 | - item 19992 | - item 19993 | - item 19994 | - item 19995 | - item 19996 | - item 19997 | - item 19998 | - item 19999 | - item 20000^ | - -- INSERT (paste) -- | - ]]) - command('doautocmd PastePost') - screen:expect([[ - item 19988 | - item 19989 | - item 19990 | - item 19991 | - item 19992 | - item 19993 | - item 19994 | - item 19995 | - item 19996 | - item 19997 | - item 19998 | - item 19999 | - item 2000^0 | - 20000,10 Bot | - ]]) - end) -end) - describe('input utf sequences that contain CSI/K_SPECIAL', function() before_each(clear) it('ok', function() From d303790ee751916a00a45ee91ff1cf3ab82928c8 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 20 Aug 2019 16:55:26 +0200 Subject: [PATCH 15/30] paste: test --- test/functional/terminal/tui_spec.lua | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 0aa9dace0e..4a450b2fb4 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -29,7 +29,8 @@ describe('TUI', function() before_each(function() clear() screen = thelpers.screen_setup(0, '["'..nvim_prog - ..'", "-u", "NONE", "-i", "NONE", "--cmd", "set noswapfile noshowcmd noruler undodir=. directory=. viewdir=. backupdir=."]') + ..'", "-u", "NONE", "-i", "NONE", "--cmd", "' + ..nvim_set..' laststatus=2 background=dark'..'"]') screen:expect([[ {1: } | {4:~ }| @@ -151,6 +152,7 @@ describe('TUI', function() end) it('paste: Insert mode', function() + -- "bracketed paste" feed_data('i\027[200~') screen:expect([[ {1: } | @@ -184,26 +186,30 @@ describe('TUI', function() ]]) end) - it('pasting a specific amount of text #10311', function() + it('paste: exactly 64 bytes #10311', function() + -- "bracketed paste" feed_data('i\027[200~'..string.rep('z', 64)..'\027[201~') + feed_data('\003') -- CTRL-C screen:expect([[ zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz| - zzzzzzzzzzzzzz{1: } | + zzzzzzzzzzzzz{1:z} | {4:~ }| {4:~ }| {5:[No Name] [+] }| - {3:-- INSERT --} | + | {3:-- TERMINAL --} | ]]) end) - it('big burst of input (bracketed paste)', function() + it('paste: big burst of input', function() feed_command('set ruler') local t = {} for i = 1, 3000 do t[i] = 'item ' .. tostring(i) end - feed_data('i\027[200~'..table.concat(t, '\n')..'\027[201~') + local expected = table.concat(t, '\n') + -- "bracketed paste" + feed_data('i\027[200~'..expected..'\027[201~') screen:expect([[ item 2997 | item 2998 | @@ -231,8 +237,8 @@ describe('TUI', function() screen:expect{grid=[[ | pasted from terminal (1) | - {6:^[}[200~{1: } | - {4:~ }| + {6:^[}[200~ | + {1: } | {5:[No Name] [+] }| {3:-- INSERT --} | {3:-- TERMINAL --} | From 0221a9220a2ec0691a7139c8362aba80d1f3b8ee Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 20 Aug 2019 19:41:45 +0200 Subject: [PATCH 16/30] paste: edge-case: handle EOL at end-of-buffer This is "readfile()-style", see also ":help channel-lines". --- runtime/doc/eval.txt | 11 ----------- src/nvim/api/vim.c | 10 +++++++--- src/nvim/tui/input.c | 11 ++++++++--- test/functional/terminal/tui_spec.lua | 13 +++++++------ 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index 2ec72f7717..897b5df072 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -4214,17 +4214,6 @@ getchar([expr]) *getchar()* : endwhile :endfunction < - You may also receive synthetic characters, such as - ||. Often you will want to ignore this and get - another character: > - :function GetKey() - : let c = getchar() - : while c == "\" - : let c = getchar() - : endwhile - : return c - :endfunction - getcharmod() *getcharmod()* The result is a Number which is the state of the modifiers for the last obtained character with getchar() or in another way. diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index bafb21bd4e..9ba855b61f 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -1210,9 +1210,13 @@ Dictionary nvim_get_namespaces(void) /// /// Compare |:put| and |p| which are always linewise. /// -/// @param lines contents -/// @param type type ("c", "l", "b") or empty to guess from contents -/// @param direction behave like |P| instead of |p| +/// @param lines |readfile()|-style list of lines. |channel-lines| +/// @param type Edit behavior: +/// - "b" |blockwise-visual| mode +/// - "c" |characterwise| mode +/// - "l" |linewise| mode +/// - "" guess by contents +/// @param direction Behave like |P| instead of |p| /// @param[out] err Error details, if any void nvim_put(ArrayOf(String) lines, String type, Boolean direction, Error *err) diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index 79615e30da..8ee9640f9f 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -104,9 +104,9 @@ static Array string_to_array(const String input) { Array ret = ARRAY_DICT_INIT; for (size_t i = 0; i < input.size; i++) { - const char *const start = input.data + i; - const size_t line_len - = (size_t)((char *)xmemscan(start, NL, input.size - i) - start); + 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 = { @@ -115,6 +115,11 @@ static Array string_to_array(const String input) }; 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; diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 4a450b2fb4..6b12c1a889 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -189,20 +189,20 @@ describe('TUI', function() it('paste: exactly 64 bytes #10311', function() -- "bracketed paste" feed_data('i\027[200~'..string.rep('z', 64)..'\027[201~') - feed_data('\003') -- CTRL-C + feed_data(' end') screen:expect([[ zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz| - zzzzzzzzzzzzz{1:z} | + zzzzzzzzzzzzzz end{1: } | {4:~ }| {4:~ }| {5:[No Name] [+] }| - | + {3:-- INSERT --} | {3:-- TERMINAL --} | ]]) end) it('paste: big burst of input', function() - feed_command('set ruler') + feed_data(':set ruler\013') local t = {} for i = 1, 3000 do t[i] = 'item ' .. tostring(i) @@ -210,12 +210,13 @@ describe('TUI', function() local expected = table.concat(t, '\n') -- "bracketed paste" feed_data('i\027[200~'..expected..'\027[201~') + feed_data(' end') screen:expect([[ item 2997 | item 2998 | item 2999 | - item 3000{1: } | - {5:[No Name] [+] 3000,10 Bot}| + item 3000 end{1: } | + {5:[No Name] [+] 3000,14 Bot}| {3:-- INSERT --} | {3:-- TERMINAL --} | ]]) From 517bf99ddb79ca27b13491572a9439e982409abc Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 20 Aug 2019 20:03:21 +0200 Subject: [PATCH 17/30] API: nvim_put: Avoid "N more lines" message --- src/nvim/api/vim.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 9ba855b61f..900c3bab58 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -1256,7 +1256,9 @@ void nvim_put(ArrayOf(String) lines, String type, Boolean direction, // TODO: fix VIsual when cursor is before, or emulate the delete as well flags |= lt(VIsual, curwin->w_cursor) ? PUT_CURSEND : 0; } + msg_silent++; // Avoid "N more lines" message. do_put(0, reg, direction ? BACKWARD : FORWARD, 1, flags); + msg_silent--; VIsual_active = VIsual_was_active; cleanup: From 1fdae25b2b932439fdef9e70b42e82e3153b937a Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 20 Aug 2019 22:04:46 +0200 Subject: [PATCH 18/30] test/tui_spec: connect to child session --- test/functional/terminal/tui_spec.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 6b12c1a889..e63d47bbc2 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -25,12 +25,14 @@ if helpers.pending_win32(pending) then return end describe('TUI', function() local screen + local child_session before_each(function() clear() - screen = thelpers.screen_setup(0, '["'..nvim_prog - ..'", "-u", "NONE", "-i", "NONE", "--cmd", "' - ..nvim_set..' laststatus=2 background=dark'..'"]') + local child_server = helpers.new_pipename() + screen = thelpers.screen_setup(0, + string.format([=[["%s", "--listen", "%s", "-u", "NONE", "-i", "NONE", "--cmd", "%s laststatus=2 background=dark"]]=], + nvim_prog, child_server, nvim_set)) screen:expect([[ {1: } | {4:~ }| @@ -40,6 +42,7 @@ describe('TUI', function() | {3:-- TERMINAL --} | ]]) + child_session = helpers.connect(child_server) end) after_each(function() From 613296936ba30ae73f3391c2e3c36096f3703c06 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 20 Aug 2019 22:41:21 +0200 Subject: [PATCH 19/30] API: nvim_put: always PUT_CURSEND Fixes strange behavior where sometimes the buffer contents of a series of paste chunks (vim._paste) would be out-of-order. Now the tui_spec.lua screen-tests are much more reliable. But they still sometimes fail because of off-by-one cursor (caused by "typeahead race" resulting in wrong mode; fixed later in this patch-series). --- src/nvim/api/vim.c | 8 +------- test/functional/api/vim_spec.lua | 1 + test/functional/terminal/tui_spec.lua | 17 ++++++++++++++--- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 900c3bab58..d3e368d01b 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -1249,13 +1249,7 @@ void nvim_put(ArrayOf(String) lines, String type, Boolean direction, finish_yankreg_from_object(reg, false); bool VIsual_was_active = VIsual_active; - int flags = 0; - if (State & INSERT) { - flags |= PUT_CURSEND; - } else if (VIsual_active) { - // TODO: fix VIsual when cursor is before, or emulate the delete as well - flags |= lt(VIsual, curwin->w_cursor) ? PUT_CURSEND : 0; - } + int flags = PUT_CURSEND; msg_silent++; // Avoid "N more lines" message. do_put(0, reg, direction ? BACKWARD : FORWARD, 1, flags); msg_silent--; diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 09c297940c..20046147b8 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -382,6 +382,7 @@ describe('API', function() line 1 line 2 line 3]]) + command('1') -- blockwise nvim('put', {'AA','BB'}, 'b', false) expect([[ diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index e63d47bbc2..3719af005c 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -190,9 +190,16 @@ describe('TUI', function() end) it('paste: exactly 64 bytes #10311', function() + local expected = string.rep('z', 64) -- "bracketed paste" - feed_data('i\027[200~'..string.rep('z', 64)..'\027[201~') + feed_data('i\027[200~'..expected..'\027[201~') feed_data(' end') + expected = expected..' end' + retry(nil, nil, function() + local _, buflines = child_session:request( + 'nvim_buf_get_lines', 0, 0, -1, false) + eq({expected}, buflines) + end) screen:expect([[ zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz| zzzzzzzzzzzzzz end{1: } | @@ -210,9 +217,13 @@ describe('TUI', function() for i = 1, 3000 do t[i] = 'item ' .. tostring(i) end - local expected = table.concat(t, '\n') -- "bracketed paste" - feed_data('i\027[200~'..expected..'\027[201~') + feed_data('i\027[200~'..table.concat(t, '\n')..'\027[201~') + retry(nil, nil, function() + local _, buflines = child_session:request( + 'nvim_buf_get_lines', 0, 0, -1, false) + eq(t, buflines) + end) feed_data(' end') screen:expect([[ item 2997 | From 93e5f0235b8e85423d0284231661ba4b0d7caa07 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 20 Aug 2019 23:53:13 +0200 Subject: [PATCH 20/30] API: nvim_put: "follow" parameter --- src/nvim/api/vim.c | 14 ++++++++------ src/nvim/lua/vim.lua | 2 +- test/functional/api/vim_spec.lua | 31 +++++++++++++++++++++++++------ 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index d3e368d01b..02000907f9 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -1206,7 +1206,7 @@ Dictionary nvim_get_namespaces(void) return retval; } -/// Inserts text at cursor. +/// Puts text at cursor. /// /// Compare |:put| and |p| which are always linewise. /// @@ -1216,10 +1216,11 @@ Dictionary nvim_get_namespaces(void) /// - "c" |characterwise| mode /// - "l" |linewise| mode /// - "" guess by contents -/// @param direction Behave like |P| instead of |p| +/// @param after Insert after cursor (like |p|), or before (like |P|). +/// @param follow Place cursor at end of inserted text. /// @param[out] err Error details, if any -void nvim_put(ArrayOf(String) lines, String type, Boolean direction, - Error *err) +void nvim_put(ArrayOf(String) lines, String type, Boolean after, + Boolean follow, Error *err) FUNC_API_SINCE(6) { yankreg_T *reg = xcalloc(sizeof(yankreg_T), 1); @@ -1249,9 +1250,10 @@ void nvim_put(ArrayOf(String) lines, String type, Boolean direction, finish_yankreg_from_object(reg, false); bool VIsual_was_active = VIsual_active; - int flags = PUT_CURSEND; msg_silent++; // Avoid "N more lines" message. - do_put(0, reg, direction ? BACKWARD : FORWARD, 1, flags); + do_put(0, reg, + after ? FORWARD : BACKWARD, 1, + follow ? PUT_CURSEND : 0); msg_silent--; VIsual_active = VIsual_was_active; diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index 6759e4436b..0b94417b3e 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -100,7 +100,7 @@ local function _paste(lines) local mode = call('mode', {}) local curline = call('line', {'.'}) -- vim.api.nvim_set_option('paste', true) - vim.api.nvim_put(lines, 'c', false) + vim.api.nvim_put(lines, 'c', true, true) -- vim.api.nvim_set_option('paste', false) -- TODO: do not redraw (slow!) until paste is finished. -- if eof then diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 20046147b8..3f3d9b74bb 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -369,34 +369,53 @@ describe('API', function() describe('nvim_put', function() it('inserts text', function() -- linewise - nvim('put', {'line 1','line 2','line 3'}, 'l', false) + nvim('put', {'line 1','line 2','line 3'}, 'l', true, true) expect([[ line 1 line 2 line 3]]) + eq({0,4,1,0}, funcs.getpos('.')) command('%delete _') -- charwise - nvim('put', {'line 1','line 2','line 3'}, 'c', false) + nvim('put', {'line 1','line 2','line 3'}, 'c', true, false) expect([[ line 1 line 2 line 3]]) - command('1') + eq({0,1,1,0}, funcs.getpos('.')) -- follow=false -- blockwise - nvim('put', {'AA','BB'}, 'b', false) + nvim('put', {'AA','BB'}, 'b', true, true) expect([[ lAAine 1 lBBine 2 line 3]]) + eq({0,2,4,0}, funcs.getpos('.')) command('%delete _') -- Empty lines list. - nvim('put', {}, 'c', false) + nvim('put', {}, 'c', true, true) + eq({0,1,1,0}, funcs.getpos('.')) expect([[]]) -- Single empty line. - nvim('put', {''}, 'c', false) + nvim('put', {''}, 'c', true, true) + eq({0,1,1,0}, funcs.getpos('.')) expect([[ ]]) + nvim('put', {'AB'}, 'c', true, true) + -- after=false, follow=true + nvim('put', {'line 1','line 2'}, 'c', false, true) + expect([[ + Aline 1 + line 2B]]) + eq({0,2,7,0}, funcs.getpos('.')) + command('%delete _') + nvim('put', {'AB'}, 'c', true, true) + -- after=false, follow=false + nvim('put', {'line 1','line 2'}, 'c', false, false) + expect([[ + Aline 1 + line 2B]]) + eq({0,1,2,0}, funcs.getpos('.')) eq('', nvim('eval', 'v:errmsg')) end) end) From 5ae6849517d2a025c3359e771ac1e01a68ec24c8 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Wed, 21 Aug 2019 01:55:12 +0200 Subject: [PATCH 21/30] paste: phases, dots - Send `phase` parameter to the paste handler. - Redraw at intervals and when paste terminates. - Show "..." throbber during paste to indicate activity. --- src/nvim/lua/vim.lua | 46 ++++++++++++++++++++++++++++++------------ src/nvim/tui/input.c | 48 ++++++++++++++++++++++++++------------------ src/nvim/tui/input.h | 2 +- 3 files changed, 63 insertions(+), 33 deletions(-) diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index 0b94417b3e..dca61d814a 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -94,19 +94,39 @@ local function _os_proc_children(ppid) end -- Default paste function. -local function _paste(lines) - -- local eof = (lines == {''}) - local call = vim.api.nvim_call_function - local mode = call('mode', {}) - local curline = call('line', {'.'}) - -- vim.api.nvim_set_option('paste', true) - vim.api.nvim_put(lines, 'c', true, true) - -- vim.api.nvim_set_option('paste', false) - -- TODO: do not redraw (slow!) until paste is finished. - -- if eof then - vim.api.nvim_command('redraw') - return true -- Paste will not continue if not returning `true`. -end +local _paste = (function() + local tdots = 0 + local tredraw = 0 + local tick = 0 + return function(lines, phase) + local call = vim.api.nvim_call_function + local now = vim.loop.now() + if phase == 1 then + tdots = now + tredraw = now + tick = 0 + if (call('mode', {})):find('[vV]') then + vim.api.nvim_feedkeys('', 'n', false) + end + end + vim.api.nvim_put(lines, 'c', true, true) + if (now - tredraw >= 1000) or phase == 1 or phase == 3 then + tredraw = now + vim.api.nvim_command('redraw') + vim.api.nvim_command('redrawstatus') + end + if (now - tdots >= 100) then + local dots = ('.'):rep(tick % 4) + tdots = now + tick = tick + 1 + vim.api.nvim_command(('echo "%s"'):format(dots)) + end + if phase == 3 then + vim.api.nvim_command('echo ""') + end + return true -- Paste will not continue if not returning `true`. + end +end)() -- TODO(ZyX-I): Create compatibility layer. --{{{1 package.path updater function diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index 8ee9640f9f..fc06f21339 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -25,7 +25,7 @@ void tinput_init(TermInput *input, Loop *loop) { input->loop = loop; - input->paste_enabled = false; + input->paste = 0; input->in_fd = 0; input->key_buffer = rbuffer_new(KEY_BUFFER_SIZE); uv_mutex_init(&input->key_buffer_mutex); @@ -130,19 +130,20 @@ static void tinput_wait_enqueue(void **argv) TermInput *input = argv[0]; RBUFFER_UNTIL_EMPTY(input->key_buffer, buf, len) { const String keys = { .data = buf, .size = len }; - if (input->paste_enabled) { - Object keys_array = ARRAY_OBJ(string_to_array(keys)); - Array args = { .capacity = 1, .size = 1, .items = &keys_array }; + if (input->paste) { Error err = ERROR_INIT; - Object fret + 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); - if (fret.type != kObjectTypeBoolean || !fret.data.boolean) { - // Abort paste if handler does not return true. - input->paste_enabled = false; - } - api_free_object(fret); - api_free_object(keys_array); + 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); rbuffer_consumed(input->key_buffer, len); rbuffer_reset(input->key_buffer); if (ERROR_SET(&err)) { @@ -392,18 +393,27 @@ static bool handle_bracketed_paste(TermInput *input) && (!rbuffer_cmp(input->read_stream.buffer, "\x1b[200~", 6) || !rbuffer_cmp(input->read_stream.buffer, "\x1b[201~", 6))) { bool enable = *rbuffer_get(input->read_stream.buffer, 4) == '0'; - if (input->paste_enabled && enable) { - // Pasting "enable paste" code literally. - return false; + if (input->paste && enable) { + return false; // Pasting "start paste" code literally. } // Advance past the sequence rbuffer_consumed(input->read_stream.buffer, 6); - if (input->paste_enabled == enable) { - return true; + if (!!input->paste == enable) { + return true; // Spurious "disable paste" code. } - tinput_flush(input, true); - input->paste_enabled = enable; + if (enable) { + // Flush before starting paste. + tinput_flush(input, true); + // Paste phase: "first-chunk". + input->paste = 1; + } else { + // Paste phase: "last-chunk". + input->paste = 3; + tinput_flush(input, true); + // Paste phase: "disabled". + input->paste = 0; + } return true; } return false; @@ -548,7 +558,7 @@ static void tinput_read_cb(Stream *stream, RBuffer *buf, size_t count_, } } // Push bytes directly (paste). - if (input->paste_enabled) { + if (input->paste) { RBUFFER_UNTIL_EMPTY(input->read_stream.buffer, ptr, len) { size_t consumed = MIN(count, len); assert(consumed <= input->read_stream.buffer->size); diff --git a/src/nvim/tui/input.h b/src/nvim/tui/input.h index 7d59cf5c6a..26a8447eb2 100644 --- a/src/nvim/tui/input.h +++ b/src/nvim/tui/input.h @@ -9,7 +9,7 @@ typedef struct term_input { int in_fd; - bool paste_enabled; + uint8_t paste; // Phases: 0=disabled 1=first-chunk 2=continue 3=last-chunk bool waiting; TermKey *tk; #if TERMKEY_VERSION_MAJOR > 0 || TERMKEY_VERSION_MINOR > 18 From c95f5d166fad75ad8383f76675d06907687066a7 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Thu, 22 Aug 2019 00:19:46 +0200 Subject: [PATCH 22/30] paste: workaround typeahead race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workaround this failure: [ ERROR ] test/functional/terminal/tui_spec.lua @ 192: TUI paste: exactly 64 bytes test/functional/helpers.lua:403: retry() attempts: 478 test/functional/terminal/tui_spec.lua:201: Expected objects to be the same. Passed in: (table: 0x47cd77e8) { *[1] = 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz endz' } Expected: (table: 0x47cd7830) { *[1] = 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz end' } This happens because `curwin->w_cursor.col` is sometimes decremented at the end of `do_put`... because the editor is in Normal-mode instead of the expected Insert-mode. Caused by "typeahead race" (#10826): there may be queued input in the main thread not yet processed, thus the editor mode (`State` global) will be "wrong" during paste. Example: input "i" followed immediately by a paste sequence: i... ^ "i" does not get processed in time, so the editor is in Normal-mode instead of Insert-mode while handling the paste. Attempted workarounds: - vim.api.nvim_feedkeys('','x',false) in vim._paste() - exec_normal() in tinput_wait_enqueue() - LOOP_PROCESS_EVENTS(&main_loop,…,0) in tinput_wait_enqueue() ref #10826 --- test/functional/terminal/tui_spec.lua | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 3719af005c..adf968712d 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -191,8 +191,14 @@ describe('TUI', function() it('paste: exactly 64 bytes #10311', function() local expected = string.rep('z', 64) + feed_data('i') + -- Wait for Insert-mode (avoid "typeahead race" #10826). + retry(nil, nil, function() + local _, m = child_session:request('nvim_get_mode') + eq('i', m.mode) + end) -- "bracketed paste" - feed_data('i\027[200~'..expected..'\027[201~') + feed_data('\027[200~'..expected..'\027[201~') feed_data(' end') expected = expected..' end' retry(nil, nil, function() @@ -217,8 +223,14 @@ describe('TUI', function() for i = 1, 3000 do t[i] = 'item ' .. tostring(i) end + feed_data('i') + -- Wait for Insert-mode (avoid "typeahead race" #10826). + retry(nil, nil, function() + local _, m = child_session:request('nvim_get_mode') + eq('i', m.mode) + end) -- "bracketed paste" - feed_data('i\027[200~'..table.concat(t, '\n')..'\027[201~') + feed_data('\027[200~'..table.concat(t, '\n')..'\027[201~') retry(nil, nil, function() local _, buflines = child_session:request( 'nvim_buf_get_lines', 0, 0, -1, false) From eacc70fb3ebae6d76112ab10647a42339f5f223f Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sat, 24 Aug 2019 13:54:27 +0200 Subject: [PATCH 23/30] API: nvim_paste --- runtime/doc/api.txt | 43 ++++++++++++++++++++++++ runtime/doc/provider.txt | 33 ++++++++++++++++++ runtime/doc/term.txt | 6 ---- src/nvim/api/private/helpers.c | 29 ++++++++++++++++ src/nvim/api/vim.c | 48 ++++++++++++++++++++++----- src/nvim/getchar.c | 15 ++++----- src/nvim/lua/vim.lua | 9 +++-- src/nvim/tui/input.c | 40 ++-------------------- test/functional/api/vim_spec.lua | 37 +++++++++++++++++++++ test/functional/terminal/tui_spec.lua | 12 ++++--- 10 files changed, 205 insertions(+), 67 deletions(-) diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 2c6b053994..32d7f5eb1e 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -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. diff --git a/runtime/doc/provider.txt b/runtime/doc/provider.txt index dc045c360a..eb6d562e18 100644 --- a/runtime/doc/provider.txt +++ b/runtime/doc/provider.txt @@ -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* diff --git a/runtime/doc/term.txt b/runtime/doc/term.txt index 978f50dd55..4f4d379f01 100644 --- a/runtime/doc/term.txt +++ b/runtime/doc/term.txt @@ -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": diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index 6b05d1ac0a..3443f85e20 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -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. /// diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 02000907f9..b355491dcc 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -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; diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c index d1b4751a00..0ef0c852a4 100644 --- a/src/nvim/getchar.c +++ b/src/nvim/getchar.c @@ -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; diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index dca61d814a..637a4baf33 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -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 diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index fc06f21339..33062e88d3 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -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)) { diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 3f3d9b74bb..212c4f4300 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -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) diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index adf968712d..414838444f 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -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: From bfc5a18f4b6cb4bc2335440254c346d731063b46 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sat, 24 Aug 2019 14:01:09 +0200 Subject: [PATCH 24/30] paste: insert text "before" cursor in Insert-mode --- src/nvim/lua/vim.lua | 7 ++++++- test/functional/terminal/tui_spec.lua | 10 +++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index 637a4baf33..59438c8667 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -101,6 +101,7 @@ local _paste = (function() return function(lines, phase) local call = vim.api.nvim_call_function local now = vim.loop.now() + local mode = call('mode', {}):sub(1,1) if phase == 1 then tdots = now tredraw = now @@ -110,7 +111,11 @@ local _paste = (function() -- nvim_cancel() -- end end - vim.api.nvim_put(lines, 'c', true, true) + if mode == 'i' or mode == 'R' then + vim.api.nvim_put(lines, 'c', false, true) + else + vim.api.nvim_put(lines, 'c', true, true) + end if (now - tredraw >= 1000) or phase == 1 or phase == 3 then tredraw = now vim.api.nvim_command('redraw') diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 414838444f..e6d9dcddb9 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -156,19 +156,19 @@ describe('TUI', function() it('paste: Insert mode', function() -- "bracketed paste" - feed_data('i\027[200~') + feed_data('i""\027i\027[200~') screen:expect([[ - {1: } | + "{1:"} | {4:~ }| {4:~ }| {4:~ }| - {5:[No Name] }| + {5:[No Name] [+] }| {3:-- INSERT --} | {3:-- TERMINAL --} | ]]) feed_data('pasted from terminal') screen:expect([[ - pasted from terminal{1: } | + "pasted from terminal{1:"} | {4:~ }| {4:~ }| {4:~ }| @@ -179,7 +179,7 @@ describe('TUI', function() feed_data('\027[201~') -- End paste. feed_data('\027\000') -- ESC: go to Normal mode. screen:expect([[ - pasted from termina{1:l} | + "pasted from termina{1:l}" | {4:~ }| {4:~ }| {4:~ }| From 5b41070c639f979023178042bea8e5fcc8a898fe Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sun, 25 Aug 2019 10:20:38 +0200 Subject: [PATCH 25/30] paste: implement redo (AKA dot-repeat) - Normal-mode redo idiom(?): prepend "i" and append ESC. - Insert-mode only needs AppendToRedobuffLit(). - Cmdline-mode: only paste the first line. --- src/nvim/api/vim.c | 23 ++- src/nvim/lua/vim.lua | 30 ++-- src/nvim/tui/input.c | 4 +- src/nvim/tui/input.h | 3 +- test/functional/terminal/tui_spec.lua | 211 ++++++++++++++++++++------ 5 files changed, 204 insertions(+), 67 deletions(-) diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index b355491dcc..6d6fd85266 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -1212,7 +1212,7 @@ Dictionary nvim_get_namespaces(void) /// 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). +/// @param 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) @@ -1227,8 +1227,13 @@ Boolean nvim_paste(String data, Integer phase, Error *err) api_set_error(err, kErrorTypeValidation, "Invalid phase: %"PRId64, phase); return false; } + if (!(State & CMDLINE) && !(State & INSERT) && (phase == -1 || phase == 1)) { + ResetRedobuff(); + AppendCharToRedobuff('a'); // Dot-repeat. + } + Array lines = string_to_array(data); Array args = ARRAY_DICT_INIT; - ADD(args, ARRAY_OBJ(string_to_array(data))); + ADD(args, ARRAY_OBJ(lines)); ADD(args, INTEGER_OBJ(phase)); Object rv = nvim_execute_lua(STATIC_CSTR_AS_STRING("return vim._paste(...)"), @@ -1236,6 +1241,20 @@ Boolean nvim_paste(String data, Integer phase, Error *err) // Abort paste if handler does not return true. bool ok = !ERROR_SET(err) && (rv.type == kObjectTypeBoolean && rv.data.boolean); + if (ok && !(State & CMDLINE)) { // Dot-repeat. + for (size_t i = 0; i < lines.size; i++) { + String s = lines.items[i].data.string; + assert(data.size <= INT_MAX); + AppendToRedobuffLit((char_u *)s.data, (int)s.size); + // readfile()-style: "\n" is indicated by presence of N+1 item. + if (i + 1 < lines.size) { + AppendCharToRedobuff(NL); + } + } + } + if (!(State & CMDLINE) && !(State & INSERT) && (phase == -1 || phase == 3)) { + AppendCharToRedobuff(ESC); // Dot-repeat. + } api_free_object(rv); api_free_array(args); diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index 59438c8667..05951fbd0f 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -93,35 +93,32 @@ local function _os_proc_children(ppid) return children end --- Default paste function. +-- Default nvim_paste() handler. local _paste = (function() - local tdots = 0 - local tredraw = 0 - local tick = 0 + local tdots, tredraw, tick, got_line1 = 0, 0, 0, false return function(lines, phase) local call = vim.api.nvim_call_function local now = vim.loop.now() local mode = call('mode', {}):sub(1,1) - if phase == 1 then - tdots = now - tredraw = now - tick = 0 - -- TODO - -- if mode == 'i' or mode == 'R' then - -- nvim_cancel() - -- end + if phase < 2 then -- Reset flags. + tdots, tredraw, tick, got_line1 = now, now, 0, false end - if mode == 'i' or mode == 'R' then + if mode == 'c' and not got_line1 then -- cmdline-mode: paste only 1 line. + got_line1 = (#lines > 1) + vim.api.nvim_set_option('paste', true) -- For nvim_input(). + local line1, _ = string.gsub(lines[1], '[\r\n\012\027]', ' ') + vim.api.nvim_input(line1) -- Scrub "\r". + elseif mode == 'i' or mode == 'R' then vim.api.nvim_put(lines, 'c', false, true) else vim.api.nvim_put(lines, 'c', true, true) end - if (now - tredraw >= 1000) or phase == 1 or phase == 3 then + if (now - tredraw >= 1000) or phase == -1 or phase > 2 then tredraw = now vim.api.nvim_command('redraw') vim.api.nvim_command('redrawstatus') end - if (now - tdots >= 100) then + if phase ~= -1 and (now - tdots >= 100) then local dots = ('.'):rep(tick % 4) tdots = now tick = tick + 1 @@ -129,8 +126,9 @@ local _paste = (function() -- message when there are zero dots. vim.api.nvim_command(('echo "%s"'):format(dots)) end - if phase == 3 then + if phase == -1 or phase == 3 then vim.api.nvim_command('echo ""') + vim.api.nvim_set_option('paste', false) end return true -- Paste will not continue if not returning `true`. end diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index 33062e88d3..f9f39c36ff 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -373,9 +373,9 @@ static bool handle_bracketed_paste(TermInput *input) tinput_flush(input, true); // Paste phase: "first-chunk". input->paste = 1; - } else { + } else if (input->paste != 0) { // Paste phase: "last-chunk". - input->paste = 3; + input->paste = input->paste == 2 ? 3 : -1; tinput_flush(input, true); // Paste phase: "disabled". input->paste = 0; diff --git a/src/nvim/tui/input.h b/src/nvim/tui/input.h index 26a8447eb2..a4071fab40 100644 --- a/src/nvim/tui/input.h +++ b/src/nvim/tui/input.h @@ -9,7 +9,8 @@ typedef struct term_input { int in_fd; - uint8_t paste; // Phases: 0=disabled 1=first-chunk 2=continue 3=last-chunk + // Phases: -1=all 0=disabled 1=first-chunk 2=continue 3=last-chunk + int8_t paste; bool waiting; TermKey *tk; #if TERMKEY_VERSION_MAJOR > 0 || TERMKEY_VERSION_MINOR > 18 diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index e6d9dcddb9..08ff06e0a5 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -49,6 +49,24 @@ describe('TUI', function() screen:detach() end) + -- Wait for mode in the child Nvim (avoid "typeahead race" #10826). + local function wait_for_mode(mode) + retry(nil, nil, function() + local _, m = child_session:request('nvim_get_mode') + eq(mode, m.mode) + end) + end + + -- Assert buffer contents in the child Nvim. + local function expect_child_buf_lines(expected) + assert(type({}) == type(expected)) + retry(nil, nil, function() + local _, buflines = child_session:request( + 'nvim_buf_get_lines', 0, 0, -1, false) + eq(expected, buflines) + end) + end + it('rapid resize #7572 #7628', function() -- Need buffer rows to provoke the behavior. feed_data(":edit test/functional/fixtures/bigfile.txt:") @@ -136,7 +154,7 @@ describe('TUI', function() ]]) end) - it('accepts ascii control sequences', function() + it('accepts ASCII control sequences', function() feed_data('i') feed_data('\022\007') -- ctrl+g feed_data('\022\022') -- ctrl+v @@ -187,25 +205,142 @@ describe('TUI', function() | {3:-- TERMINAL --} | ]]) + -- Dot-repeat/redo. + feed_data('2.') + screen:expect([[ + "pasted from terminapasted from terminalpasted fro| + m termina{1:l}l" | + {4:~ }| + {4:~ }| + {5:[No Name] [+] }| + | + {3:-- TERMINAL --} | + ]]) + -- Undo. + feed_data('u') + expect_child_buf_lines({'"pasted from terminal"'}) + feed_data('u') + expect_child_buf_lines({''}) + end) + + it('paste: normal-mode', function() + feed_data(':set ruler') + wait_for_mode('c') + feed_data('\n') + wait_for_mode('n') + local expected = {'line 1', ' line 2', 'ESC:\027 / CR: \013'} + local expected_attr = { + [3] = {bold = true}, + [4] = {foreground = tonumber('0x00000c')}, + [5] = {bold = true, reverse = true}, + [11] = {foreground = tonumber('0x000051')}, + [12] = {reverse = true, foreground = tonumber('0x000051')}, + } + -- "bracketed paste" + feed_data('\027[200~'..table.concat(expected,'\n')..'\027[201~') + screen:expect{ + grid=[[ + line 1 | + line 2 | + ESC:{11:^[} / CR: {12:^}{11:M} | + {4:~ }| + {5:[No Name] [+] 3,13-14 All}| + | + {3:-- TERMINAL --} | + ]], + attr_ids=expected_attr} + -- Dot-repeat/redo. + feed_data('.') + screen:expect{ + grid=[[ + line 2 | + ESC:{11:^[} / CR: {11:^M}line 1 | + line 2 | + ESC:{11:^[} / CR: {12:^}{11:M} | + {5:[No Name] [+] 5,13-14 Bot}| + | + {3:-- TERMINAL --} | + ]], + attr_ids=expected_attr} + -- Undo. + feed_data('u') + expect_child_buf_lines(expected) + feed_data('u') + expect_child_buf_lines({''}) + end) + + it('paste: cmdline-mode inserts 1 line', function() + feed_data('ifoo\n') -- Insert some text (for dot-repeat later). + feed_data('\027:""') -- Enter Cmdline-mode. + feed_data('\027[D') -- to place cursor between quotes. + wait_for_mode('c') + -- "bracketed paste" + feed_data('\027[200~line 1\nline 2\n\027[201~') + screen:expect{grid=[[ + foo | + | + {4:~ }| + {4:~ }| + {5:[No Name] [+] }| + :"line 1{1:"} | + {3:-- TERMINAL --} | + ]]} + -- Dot-repeat/redo. + feed_data('\027\000') + wait_for_mode('n') + feed_data('.') + screen:expect{grid=[[ + foo | + foo | + {1: } | + {4:~ }| + {5:[No Name] [+] }| + | + {3:-- TERMINAL --} | + ]]} + end) + + it('paste: cmdline-mode collects chunks of unfinished line', function() + local function expect_cmdline(expected) + retry(nil, nil, function() + local _, cmdline = child_session:request( + 'nvim_call_function', 'getcmdline', {}) + eq(expected, cmdline) + end) + end + feed_data('\027:""') -- Enter Cmdline-mode. + feed_data('\027[D') -- to place cursor between quotes. + wait_for_mode('c') + feed_data('\027[200~stuff 1 ') + expect_cmdline('"stuff 1 "') + -- Discards everything after the first line. + feed_data('more\nstuff 2\nstuff 3\n') + expect_cmdline('"stuff 1 more"') + feed_data('stuff 3') + expect_cmdline('"stuff 1 more"') + -- End the paste sequence. + feed_data('\027[201~') + feed_data(' typed') + expect_cmdline('"stuff 1 more typed"') + end) + + -- TODO + it('paste: other modes', function() + -- Other modes act like CTRL-C + paste. + end) + + it("paste: in 'nomodifiable' buffer", function() end) it('paste: exactly 64 bytes #10311', function() local expected = string.rep('z', 64) feed_data('i') - -- Wait for Insert-mode (avoid "typeahead race" #10826). - retry(nil, nil, function() - local _, m = child_session:request('nvim_get_mode') - eq('i', m.mode) - end) + wait_for_mode('i') -- "bracketed paste" feed_data('\027[200~'..expected..'\027[201~') feed_data(' end') expected = expected..' end' - retry(nil, nil, function() - local _, buflines = child_session:request( - 'nvim_buf_get_lines', 0, 0, -1, false) - eq({expected}, buflines) - end) + expect_child_buf_lines({expected}) screen:expect([[ zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz| zzzzzzzzzzzzzz end{1: } | @@ -218,24 +353,16 @@ describe('TUI', function() end) it('paste: big burst of input', function() - feed_data(':set ruler\013') + feed_data(':set ruler\n') local t = {} for i = 1, 3000 do t[i] = 'item ' .. tostring(i) end feed_data('i') - -- Wait for Insert-mode (avoid "typeahead race" #10826). - retry(nil, nil, function() - local _, m = child_session:request('nvim_get_mode') - eq('i', m.mode) - end) + wait_for_mode('i') -- "bracketed paste" feed_data('\027[200~'..table.concat(t, '\n')..'\027[201~') - retry(nil, nil, function() - local _, buflines = child_session:request( - 'nvim_buf_get_lines', 0, 0, -1, false) - eq(t, buflines) - end) + expect_child_buf_lines(t) feed_data(' end') screen:expect([[ item 2997 | @@ -246,9 +373,22 @@ describe('TUI', function() {3:-- INSERT --} | {3:-- TERMINAL --} | ]]) + feed_data('\027\000') -- ESC: go to Normal mode. + wait_for_mode('n') + -- Dot-repeat/redo. + feed_data('.') + screen:expect([[ + item 2997 | + item 2998 | + item 2999 | + item 3000 en{1:d}d | + {5:[No Name] [+] 5999,13 Bot}| + | + {3:-- TERMINAL --} | + ]]) end) - it('forwards spurious "start paste" sequence', function() + it('paste: forwards spurious "start paste" code', function() -- If multiple "start paste" sequences are sent without a corresponding -- "stop paste" sequence, only the first occurrence should be consumed. @@ -280,7 +420,7 @@ describe('TUI', function() }} end) - it('ignores spurious "stop paste" sequence', function() + it('paste: ignores spurious "stop paste" code', function() -- If "stop paste" sequence is received without a preceding "start paste" -- sequence, it should be ignored. feed_data('i') @@ -298,28 +438,7 @@ describe('TUI', function() end) -- TODO - it('paste: normal-mode', function() - end) - - -- TODO - it('paste: command-mode inserts 1 line', function() - end) - - -- TODO - 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('paste: handles missing "stop paste" sequence', function() - end) - - -- TODO: error when pasting into 'nomodifiable' buffer: - -- [error @ do_put:2656] 17043 - Failed to save undo information - it("handles 'nomodifiable' buffer gracefully", function() + it('paste: handles missing "stop paste" code', function() end) it('allows termguicolors to be set at runtime', function() From 4344ac11119abd20ba911d72cf540321277dd150 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 26 Aug 2019 03:15:09 +0200 Subject: [PATCH 26/30] paste: tickle cursor HACK: The cursor does not get repositioned after the paste completes. Scheduling a dummy event seems to fix it. Test case: 0. Revert this commit. 1. Paste some text in Normal-mode. 2. Notice the cursor is still in the cmdline area. --- src/nvim/api/vim.c | 8 ++++++-- src/nvim/event/loop.c | 4 ++++ src/nvim/tui/tui.c | 6 +----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 6d6fd85266..3631fbff66 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -1252,11 +1252,15 @@ Boolean nvim_paste(String data, Integer phase, Error *err) } } } + api_free_object(rv); + api_free_array(args); if (!(State & CMDLINE) && !(State & INSERT) && (phase == -1 || phase == 3)) { AppendCharToRedobuff(ESC); // Dot-repeat. } - api_free_object(rv); - api_free_array(args); + if (phase == -1 || phase == 3) { + // XXX: Tickle main loop to ensure cursor is updated. + loop_schedule_deferred(&main_loop, event_create(loop_dummy_event, 0)); + } return ok; } diff --git a/src/nvim/event/loop.c b/src/nvim/event/loop.c index 93ec28bb81..529ddd8eba 100644 --- a/src/nvim/event/loop.c +++ b/src/nvim/event/loop.c @@ -162,6 +162,10 @@ size_t loop_size(Loop *loop) return rv; } +void loop_dummy_event(void **argv) +{ +} + static void async_cb(uv_async_t *handle) { Loop *l = handle->loop->data; diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 9fdc6eceba..ea8f9d9f71 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -430,7 +430,7 @@ static void tui_main(UIBridgeData *bridge, UI *ui) tui_terminal_after_startup(ui); // Tickle `main_loop` with a dummy event, else the initial "focus-gained" // terminal response may not get processed until user hits a key. - loop_schedule_deferred(&main_loop, event_create(tui_dummy_event, 0)); + loop_schedule_deferred(&main_loop, event_create(loop_dummy_event, 0)); } // "Passive" (I/O-driven) loop: TUI thread "main loop". while (!tui_is_stopped(ui)) { @@ -449,10 +449,6 @@ static void tui_main(UIBridgeData *bridge, UI *ui) xfree(data); } -static void tui_dummy_event(void **argv) -{ -} - /// Handoff point between the main (ui_bridge) thread and the TUI thread. static void tui_scheduler(Event event, void *d) { From ed60015266356b3c0c42aa34698d9287f22fcba1 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 26 Aug 2019 20:57:57 +0200 Subject: [PATCH 27/30] paste: handle vim.paste() failure - Show error only once per "paste stream". - Drain remaining chunks until phase=3. - Lay groundwork for "cancel". - Constrain semantics of "cancel" to mean "client must stop"; it is unrelated to presence of error(s). --- src/nvim/api/vim.c | 52 +++++++++++++++++---------- src/nvim/tui/input.c | 9 +++-- test/functional/api/vim_spec.lua | 24 +++++++++++++ test/functional/terminal/tui_spec.lua | 51 ++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 23 deletions(-) diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 3631fbff66..910b76d02d 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -1206,7 +1206,7 @@ Dictionary nvim_get_namespaces(void) return retval; } -/// Paste +/// Pastes at cursor, in any mode. /// /// Invokes the `vim.paste` handler, which handles each mode appropriately. /// Sets redo/undo. Faster than |nvim_input()|. @@ -1219,29 +1219,44 @@ Dictionary nvim_get_namespaces(void) /// - 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 +/// @return +/// - true: Client may continue pasting. +/// - false: Client must cancel the paste. Boolean nvim_paste(String data, Integer phase, Error *err) FUNC_API_SINCE(6) { + static bool draining = false; + bool cancel = false; + if (phase < -1 || phase > 3) { api_set_error(err, kErrorTypeValidation, "Invalid phase: %"PRId64, phase); return false; } + Array args = ARRAY_DICT_INIT; + Object rv = OBJECT_INIT; + if (phase == -1 || phase == 1) { // Start of paste-stream. + draining = false; + } else if (draining) { + // Skip remaining chunks. Report error only once per "stream". + goto theend; + } + Array lines = string_to_array(data); + ADD(args, ARRAY_OBJ(lines)); + ADD(args, INTEGER_OBJ(phase)); + rv = nvim_execute_lua(STATIC_CSTR_AS_STRING("return vim._paste(...)"), args, + err); + if (ERROR_SET(err)) { + draining = true; + goto theend; + } if (!(State & CMDLINE) && !(State & INSERT) && (phase == -1 || phase == 1)) { ResetRedobuff(); AppendCharToRedobuff('a'); // Dot-repeat. } - Array lines = string_to_array(data); - Array args = ARRAY_DICT_INIT; - ADD(args, ARRAY_OBJ(lines)); - 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); - if (ok && !(State & CMDLINE)) { // Dot-repeat. + // vim.paste() decides if client should cancel. Errors do NOT cancel: we + // want to drain remaining chunks (rather than divert them to main input). + cancel = (rv.type != kObjectTypeBoolean || !rv.data.boolean); + if (!cancel && !(State & CMDLINE)) { // Dot-repeat. for (size_t i = 0; i < lines.size; i++) { String s = lines.items[i].data.string; assert(data.size <= INT_MAX); @@ -1252,20 +1267,21 @@ Boolean nvim_paste(String data, Integer phase, Error *err) } } } - api_free_object(rv); - api_free_array(args); if (!(State & CMDLINE) && !(State & INSERT) && (phase == -1 || phase == 3)) { AppendCharToRedobuff(ESC); // Dot-repeat. } - if (phase == -1 || phase == 3) { +theend: + api_free_object(rv); + api_free_array(args); + if (cancel || phase == -1 || phase == 3) { // End of paste-stream. // XXX: Tickle main loop to ensure cursor is updated. loop_schedule_deferred(&main_loop, event_create(loop_dummy_event, 0)); } - return ok; + return !cancel; } -/// Puts text at cursor. +/// Puts text at cursor, in any mode. /// /// Compare |:put| and |p| which are always linewise. /// diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index f9f39c36ff..c74ef58ba1 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -107,13 +107,12 @@ static void tinput_wait_enqueue(void **argv) const String keys = { .data = buf, .size = len }; if (input->paste) { Error err = ERROR_INIT; - Boolean rv = nvim_paste(keys, input->paste, &err); - // Paste phase: "continue" (unless handler failed). - input->paste = rv && !ERROR_SET(&err) ? 2 : 0; + // Paste phase: "continue" (unless handler canceled). + input->paste = !nvim_paste(keys, input->paste, &err) + ? 0 : (1 == input->paste ? 2 : input->paste); rbuffer_consumed(input->key_buffer, len); rbuffer_reset(input->key_buffer); if (ERROR_SET(&err)) { - msg_putchar('\n'); // TODO(justinmk): emsgf() does not display, why? msg_printf_attr(HL_ATTR(HLF_E)|MSG_HIST, "paste: %s", err.msg); api_clear_error(&err); @@ -373,7 +372,7 @@ static bool handle_bracketed_paste(TermInput *input) tinput_flush(input, true); // Paste phase: "first-chunk". input->paste = 1; - } else if (input->paste != 0) { + } else if (input->paste) { // Paste phase: "last-chunk". input->paste = input->paste == 2 ? 3 : -1; tinput_flush(input, true); diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 212c4f4300..01e4a3a1a0 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -395,6 +395,11 @@ describe('API', function() eq({0,3,6,0}, funcs.getpos('.')) eq(false, nvim('get_option', 'paste')) end) + it('vim.paste() failure', function() + nvim('execute_lua', 'vim._paste = (function(lines, phase) error("fake fail") end)', {}) + expect_err([[Error executing lua: %[string "%"]:1: fake fail]], + request, 'nvim_paste', 'line 1\nline 2\nline 3', 1) + end) end) describe('nvim_put', function() @@ -455,6 +460,25 @@ describe('API', function() eq({0,1,2,0}, funcs.getpos('.')) eq('', nvim('eval', 'v:errmsg')) end) + + it('detects charwise/linewise text (empty {type})', function() + -- linewise (final item is empty string) + nvim('put', {'line 1','line 2','line 3',''}, '', true, true) + expect([[ + + line 1 + line 2 + line 3]]) + eq({0,4,1,0}, funcs.getpos('.')) + command('%delete _') + -- charwise (final item is non-empty) + nvim('put', {'line 1','line 2','line 3'}, '', true, true) + expect([[ + line 1 + line 2 + line 3]]) + eq({0,3,6,0}, funcs.getpos('.')) + end) end) describe('nvim_strwidth', function() diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 08ff06e0a5..e7db39b5d0 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -324,6 +324,57 @@ describe('TUI', function() expect_cmdline('"stuff 1 more typed"') end) + it('paste: recovers from vim.paste() failure', function() + child_session:request('nvim_execute_lua', [[ + _G.save_paste_fn = vim._paste + vim._paste = function(lines, phase) error("fake fail") end + ]], {}) + -- Start pasting... + feed_data('\027[200~line 1\nline 2\n') + screen:expect{grid=[[ + | + {4:~ }| + {4:~ }| + {5: }| + {8:paste: Error executing lua: [string ""]:2: f}| + {10:Press ENTER or type command to continue}{1: } | + {3:-- TERMINAL --} | + ]]} + -- Remaining chunks are discarded after vim.paste() failure. + feed_data('line 3\nline 4\n') + feed_data('line 5\nline 6\n') + feed_data('line 7\nline 8\n') + -- Stop paste. + feed_data('\027[201~') + feed_data('\n') -- + -- Editor should still work after failed/drained paste. + feed_data('ityped input...\027\000') + screen:expect{grid=[[ + typed input..{1:.} | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] [+] }| + | + {3:-- TERMINAL --} | + ]]} + -- Paste works if vim.paste() succeeds. + child_session:request('nvim_execute_lua', [[ + vim._paste = _G.save_paste_fn + ]], {}) + feed_data('\027[200~line A\nline B\n\027[201~') + feed_data('\n') -- + screen:expect{grid=[[ + typed input...line A | + line B | + {1: } | + {4:~ }| + {5:[No Name] [+] }| + | + {3:-- TERMINAL --} | + ]]} + end) + -- TODO it('paste: other modes', function() -- Other modes act like CTRL-C + paste. From 87389c6a57cf9fa91746503c479cdbea348030b9 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 27 Aug 2019 05:19:25 +0200 Subject: [PATCH 28/30] paste: make vim.paste() "public" --- runtime/doc/if_lua.txt | 19 ++++++ runtime/doc/provider.txt | 20 +++--- src/nvim/api/vim.c | 2 +- src/nvim/lua/vim.lua | 96 +++++++++++++++------------ test/functional/api/vim_spec.lua | 2 +- test/functional/terminal/tui_spec.lua | 6 +- 6 files changed, 89 insertions(+), 56 deletions(-) diff --git a/runtime/doc/if_lua.txt b/runtime/doc/if_lua.txt index a9b8c5fae8..1837e14623 100644 --- a/runtime/doc/if_lua.txt +++ b/runtime/doc/if_lua.txt @@ -533,6 +533,25 @@ inspect({object}, {options}) *vim.inspect()* See also: ~ https://github.com/kikito/inspect.lua +paste({lines}, {phase}) *vim.paste()* + Paste handler, invoked by |nvim_paste()| when a conforming UI + (such as the |TUI|) pastes text into the editor. + + Parameters: ~ + {lines} |readfile()|-style list of lines to paste. + |channel-lines| + {phase} -1: "non-streaming" paste: the call contains all + lines. If paste is "streamed", `phase` indicates the stream state: + • 1: starts the paste (exactly once) + • 2: continues the paste (zero or more times) + • 3: ends the paste (exactly once) + + Return: ~ + false if client should cancel the paste. + + See also: ~ + |paste| + diff --git a/runtime/doc/provider.txt b/runtime/doc/provider.txt index eb6d562e18..833be8a103 100644 --- a/runtime/doc/provider.txt +++ b/runtime/doc/provider.txt @@ -222,11 +222,11 @@ 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. +text to the editor", whereas clipboard provides features like |quote-+| to get +and set the OS clipboard directly. For example, middle-click or CTRL-SHIFT-v +(macOS: CMD-v) in your terminal 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, @@ -235,19 +235,21 @@ 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()|. +GUIs can paste 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. +line is pasted, to avoid accidentally executing many commands. Use the +|cmdline-window| if you really want to paste multiple lines to the cmdline. 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: > +You can implement a custom paste handler by redefining |vim.paste()|. +Example: > - vim._paste = (function(lines, phase) + vim.paste = (function(lines, phase) vim.api.nvim_put(lines, 'c', true, true) end) diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 910b76d02d..4f132ddbae 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -1243,7 +1243,7 @@ Boolean nvim_paste(String data, Integer phase, Error *err) Array lines = string_to_array(data); ADD(args, ARRAY_OBJ(lines)); ADD(args, INTEGER_OBJ(phase)); - rv = nvim_execute_lua(STATIC_CSTR_AS_STRING("return vim._paste(...)"), args, + rv = nvim_execute_lua(STATIC_CSTR_AS_STRING("return vim.paste(...)"), args, err); if (ERROR_SET(err)) { draining = true; diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index 05951fbd0f..fd34b8545d 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -93,47 +93,6 @@ local function _os_proc_children(ppid) return children end --- Default nvim_paste() handler. -local _paste = (function() - local tdots, tredraw, tick, got_line1 = 0, 0, 0, false - return function(lines, phase) - local call = vim.api.nvim_call_function - local now = vim.loop.now() - local mode = call('mode', {}):sub(1,1) - if phase < 2 then -- Reset flags. - tdots, tredraw, tick, got_line1 = now, now, 0, false - end - if mode == 'c' and not got_line1 then -- cmdline-mode: paste only 1 line. - got_line1 = (#lines > 1) - vim.api.nvim_set_option('paste', true) -- For nvim_input(). - local line1, _ = string.gsub(lines[1], '[\r\n\012\027]', ' ') - vim.api.nvim_input(line1) -- Scrub "\r". - elseif mode == 'i' or mode == 'R' then - vim.api.nvim_put(lines, 'c', false, true) - else - vim.api.nvim_put(lines, 'c', true, true) - end - if (now - tredraw >= 1000) or phase == -1 or phase > 2 then - tredraw = now - vim.api.nvim_command('redraw') - vim.api.nvim_command('redrawstatus') - end - if phase ~= -1 and (now - tdots >= 100) then - 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 == -1 or phase == 3 then - vim.api.nvim_command('echo ""') - vim.api.nvim_set_option('paste', false) - end - return true -- Paste will not continue if not returning `true`. - end -end)() - -- TODO(ZyX-I): Create compatibility layer. --{{{1 package.path updater function -- Last inserted paths. Used to clear out items from package.[c]path when they @@ -202,6 +161,59 @@ local function inspect(object, options) -- luacheck: no unused error(object, options) -- Stub for gen_vimdoc.py end +--- Paste handler, invoked by |nvim_paste()| when a conforming UI +--- (such as the |TUI|) pastes text into the editor. +--- +--@see |paste| +--- +--@param lines |readfile()|-style list of lines to paste. |channel-lines| +--@param phase -1: "non-streaming" paste: the call contains all lines. +--- If paste is "streamed", `phase` indicates the stream state: +--- - 1: starts the paste (exactly once) +--- - 2: continues the paste (zero or more times) +--- - 3: ends the paste (exactly once) +--@returns false if client should cancel the paste. +local function paste(lines, phase) end -- luacheck: no unused +paste = (function() + local tdots, tredraw, tick, got_line1 = 0, 0, 0, false + return function(lines, phase) + local call = vim.api.nvim_call_function + local now = vim.loop.now() + local mode = call('mode', {}):sub(1,1) + if phase < 2 then -- Reset flags. + tdots, tredraw, tick, got_line1 = now, now, 0, false + end + if mode == 'c' and not got_line1 then -- cmdline-mode: paste only 1 line. + got_line1 = (#lines > 1) + vim.api.nvim_set_option('paste', true) -- For nvim_input(). + local line1, _ = string.gsub(lines[1], '[\r\n\012\027]', ' ') + vim.api.nvim_input(line1) -- Scrub "\r". + elseif mode == 'i' or mode == 'R' then + vim.api.nvim_put(lines, 'c', false, true) + else + vim.api.nvim_put(lines, 'c', true, true) + end + if (now - tredraw >= 1000) or phase == -1 or phase > 2 then + tredraw = now + vim.api.nvim_command('redraw') + vim.api.nvim_command('redrawstatus') + end + if phase ~= -1 and (now - tdots >= 100) then + 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 == -1 or phase == 3 then + vim.api.nvim_command('echo ""') + vim.api.nvim_set_option('paste', false) + end + return true -- Paste will not continue if not returning `true`. + end +end)() + --- Defers the wrapped callback until the Nvim API is safe to call. --- --@see |vim-loop-callbacks| @@ -227,8 +239,8 @@ local module = { _update_package_paths = _update_package_paths, _os_proc_children = _os_proc_children, _os_proc_info = _os_proc_info, - _paste = _paste, _system = _system, + paste = paste, schedule_wrap = schedule_wrap, } diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 01e4a3a1a0..47a04795f8 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -396,7 +396,7 @@ describe('API', function() eq(false, nvim('get_option', 'paste')) end) it('vim.paste() failure', function() - nvim('execute_lua', 'vim._paste = (function(lines, phase) error("fake fail") end)', {}) + nvim('execute_lua', 'vim.paste = (function(lines, phase) error("fake fail") end)', {}) expect_err([[Error executing lua: %[string "%"]:1: fake fail]], request, 'nvim_paste', 'line 1\nline 2\nline 3', 1) end) diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index e7db39b5d0..aafbbc04b1 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -326,8 +326,8 @@ describe('TUI', function() it('paste: recovers from vim.paste() failure', function() child_session:request('nvim_execute_lua', [[ - _G.save_paste_fn = vim._paste - vim._paste = function(lines, phase) error("fake fail") end + _G.save_paste_fn = vim.paste + vim.paste = function(lines, phase) error("fake fail") end ]], {}) -- Start pasting... feed_data('\027[200~line 1\nline 2\n') @@ -360,7 +360,7 @@ describe('TUI', function() ]]} -- Paste works if vim.paste() succeeds. child_session:request('nvim_execute_lua', [[ - vim._paste = _G.save_paste_fn + vim.paste = _G.save_paste_fn ]], {}) feed_data('\027[200~line A\nline B\n\027[201~') feed_data('\n') -- From 46aa254bf30d567bd2da4fbfab33bbdcbb111a37 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 27 Aug 2019 05:19:32 +0200 Subject: [PATCH 29/30] paste: handle 'nomodifiable' - nvim_paste(): Marshal through luaeval() instead of nvim_execute_lua() because the latter seems to hide some errors. - Handle 'nomodifiable' in `nvim_put()` explicitly. - Require explicit `false` from `vim.paste()` in order to "cancel", otherwise assume true ("continue"). --- src/nvim/api/vim.c | 21 ++++++--- test/functional/api/vim_spec.lua | 5 +++ test/functional/terminal/tui_spec.lua | 61 ++++++++++++++++++++++++--- 3 files changed, 75 insertions(+), 12 deletions(-) diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 4f132ddbae..bf73a721fb 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -1211,6 +1211,11 @@ Dictionary nvim_get_namespaces(void) /// 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). +/// /// @param data Multiline input. May be binary (containing NUL bytes). /// @param phase -1: paste in a single call (i.e. without streaming). /// To "stream" a paste, call `nvim_paste` sequentially with @@ -1233,6 +1238,7 @@ Boolean nvim_paste(String data, Integer phase, Error *err) return false; } Array args = ARRAY_DICT_INIT; + Array args2 = ARRAY_DICT_INIT; Object rv = OBJECT_INIT; if (phase == -1 || phase == 1) { // Start of paste-stream. draining = false; @@ -1243,8 +1249,9 @@ Boolean nvim_paste(String data, Integer phase, Error *err) Array lines = string_to_array(data); ADD(args, ARRAY_OBJ(lines)); ADD(args, INTEGER_OBJ(phase)); - rv = nvim_execute_lua(STATIC_CSTR_AS_STRING("return vim.paste(...)"), args, - err); + ADD(args2, STRING_OBJ(cstr_to_string("vim.paste(_A[1], _A[2])"))); + ADD(args2, ARRAY_OBJ(args2)); + rv = nvim_call_function(STATIC_CSTR_AS_STRING("luaeval"), args2, err); if (ERROR_SET(err)) { draining = true; goto theend; @@ -1255,7 +1262,7 @@ Boolean nvim_paste(String data, Integer phase, Error *err) } // vim.paste() decides if client should cancel. Errors do NOT cancel: we // want to drain remaining chunks (rather than divert them to main input). - cancel = (rv.type != kObjectTypeBoolean || !rv.data.boolean); + cancel = (rv.type == kObjectTypeBoolean && !rv.data.boolean); if (!cancel && !(State & CMDLINE)) { // Dot-repeat. for (size_t i = 0; i < lines.size; i++) { String s = lines.items[i].data.string; @@ -1271,9 +1278,9 @@ Boolean nvim_paste(String data, Integer phase, Error *err) AppendCharToRedobuff(ESC); // Dot-repeat. } theend: - api_free_object(rv); - api_free_array(args); + api_free_array(args2); if (cancel || phase == -1 || phase == 3) { // End of paste-stream. + draining = false; // XXX: Tickle main loop to ensure cursor is updated. loop_schedule_deferred(&main_loop, event_create(loop_dummy_event, 0)); } @@ -1303,6 +1310,10 @@ void nvim_put(ArrayOf(String) lines, String type, Boolean after, api_set_error(err, kErrorTypeValidation, "Invalid type: '%s'", type.data); goto cleanup; } + if (!MODIFIABLE(curbuf)) { + api_set_error(err, kErrorTypeException, "Buffer is not 'modifiable'"); + goto cleanup; + } if (lines.size == 0) { goto cleanup; // Nothing to do. } diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 47a04795f8..884e07e2c5 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -409,6 +409,11 @@ describe('API', function() expect_err("Invalid type: 'x'", request, 'nvim_put', {'foo'}, 'x', false, false) end) + it("fails if 'nomodifiable'", function() + command('set nomodifiable') + expect_err([[Buffer is not 'modifiable']], request, + 'nvim_put', {'a','b'}, 'l', true, true) + end) it('inserts text', function() -- linewise nvim('put', {'line 1','line 2','line 3'}, 'l', true, true) diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index aafbbc04b1..7435b293fd 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -329,14 +329,27 @@ describe('TUI', function() _G.save_paste_fn = vim.paste vim.paste = function(lines, phase) error("fake fail") end ]], {}) + -- Prepare something for dot-repeat/redo. + feed_data('ifoo\n\027\000') + wait_for_mode('n') + screen:expect{grid=[[ + foo | + {1: } | + {4:~ }| + {4:~ }| + {5:[No Name] [+] }| + | + {3:-- TERMINAL --} | + ]]} + wait_for_mode('n') -- Start pasting... feed_data('\027[200~line 1\nline 2\n') screen:expect{grid=[[ + foo | | {4:~ }| - {4:~ }| {5: }| - {8:paste: Error executing lua: [string ""]:2: f}| + {8:paste: Vim:E5108: Error while calling lua chunk fo}| {10:Press ENTER or type command to continue}{1: } | {3:-- TERMINAL --} | ]]} @@ -347,13 +360,24 @@ describe('TUI', function() -- Stop paste. feed_data('\027[201~') feed_data('\n') -- + --Dot-repeat/redo is not modified by failed paste. + feed_data('.') + screen:expect{grid=[[ + foo | + foo | + {1: } | + {4:~ }| + {5:[No Name] [+] }| + | + {3:-- TERMINAL --} | + ]]} -- Editor should still work after failed/drained paste. feed_data('ityped input...\027\000') screen:expect{grid=[[ + foo | + foo | typed input..{1:.} | {4:~ }| - {4:~ }| - {4:~ }| {5:[No Name] [+] }| | {3:-- TERMINAL --} | @@ -365,9 +389,35 @@ describe('TUI', function() feed_data('\027[200~line A\nline B\n\027[201~') feed_data('\n') -- screen:expect{grid=[[ + foo | typed input...line A | line B | {1: } | + {5:[No Name] [+] }| + | + {3:-- TERMINAL --} | + ]]} + end) + + it("paste: 'nomodifiable' buffer", function() + child_session:request('nvim_command', 'set nomodifiable') + feed_data('\027[200~fail 1\nfail 2\n\027[201~') + screen:expect{grid=[[ + | + {5: }| + {8:paste: Vim:E5108: Error while calling lua chunk fo}| + {8:r luaeval(): [string "-- Nvim-Lua stdlib: the `vim}| + {8:` module (:help l..."]:193: Buffer is not 'modifia}| + {10:Press ENTER or type command to continue}{1: } | + {3:-- TERMINAL --} | + ]]} + feed_data('\n') -- + child_session:request('nvim_command', 'set modifiable') + feed_data('\027[200~success 1\nsuccess 2\n\027[201~') + screen:expect{grid=[[ + success 1 | + success 2 | + {1: } | {4:~ }| {5:[No Name] [+] }| | @@ -380,9 +430,6 @@ describe('TUI', function() -- Other modes act like CTRL-C + paste. end) - it("paste: in 'nomodifiable' buffer", function() - end) - it('paste: exactly 64 bytes #10311', function() local expected = string.rep('z', 64) feed_data('i') From 3157baed83b7e94f2ff92e6fd97e85dab41a1c94 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 27 Aug 2019 05:19:36 +0200 Subject: [PATCH 30/30] API: TRY_WRAP() for "abort-causing non-exception errors" - Introduce TRY_WRAP() until we have an *architectural* solution. - TODO: bfredl idea: prepare error-handling at "top level" (nv_event). - nvim_paste(): Revert luaeval() hack (see parent commit). - With TRY_WRAP() in nvim_put(), 'nomodifiable' error now correctly "bubbles up". --- src/nvim/api/vim.c | 63 +++++++++++++-------------- test/functional/api/vim_spec.lua | 2 +- test/functional/terminal/tui_spec.lua | 26 +++-------- 3 files changed, 39 insertions(+), 52 deletions(-) diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index bf73a721fb..1ca0d8789d 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -54,6 +54,20 @@ # include "api/vim.c.generated.h" #endif +// `msg_list` controls the collection of abort-causing non-exception errors, +// which would otherwise be ignored. This pattern is from do_cmdline(). +// +// TODO(bfredl): prepare error-handling at "top level" (nv_event). +#define TRY_WRAP(code) \ + do { \ + struct msglist **saved_msg_list = msg_list; \ + struct msglist *private_msg_list; \ + msg_list = &private_msg_list; \ + private_msg_list = NULL; \ + code \ + msg_list = saved_msg_list; /* Restore the exception context. */ \ + } while (0) + void api_vim_init(void) FUNC_API_NOEXPORT { @@ -392,13 +406,7 @@ Object nvim_eval(String expr, Error *err) static int recursive = 0; // recursion depth Object rv = OBJECT_INIT; - // `msg_list` controls the collection of abort-causing non-exception errors, - // which would otherwise be ignored. This pattern is from do_cmdline(). - struct msglist **saved_msg_list = msg_list; - struct msglist *private_msg_list; - msg_list = &private_msg_list; - private_msg_list = NULL; - + TRY_WRAP({ // Initialize `force_abort` and `suppress_errthrow` at the top level. if (!recursive) { force_abort = false; @@ -423,8 +431,8 @@ Object nvim_eval(String expr, Error *err) } tv_clear(&rettv); - msg_list = saved_msg_list; // Restore the exception context. recursive--; + }); return rv; } @@ -474,13 +482,7 @@ static Object _call_function(String fn, Array args, dict_T *self, Error *err) } } - // `msg_list` controls the collection of abort-causing non-exception errors, - // which would otherwise be ignored. This pattern is from do_cmdline(). - struct msglist **saved_msg_list = msg_list; - struct msglist *private_msg_list; - msg_list = &private_msg_list; - private_msg_list = NULL; - + TRY_WRAP({ // Initialize `force_abort` and `suppress_errthrow` at the top level. if (!recursive) { force_abort = false; @@ -502,8 +504,8 @@ static Object _call_function(String fn, Array args, dict_T *self, Error *err) rv = vim_to_object(&rettv); } tv_clear(&rettv); - msg_list = saved_msg_list; // Restore the exception context. recursive--; + }); free_vim_args: while (i > 0) { @@ -1238,7 +1240,6 @@ Boolean nvim_paste(String data, Integer phase, Error *err) return false; } Array args = ARRAY_DICT_INIT; - Array args2 = ARRAY_DICT_INIT; Object rv = OBJECT_INIT; if (phase == -1 || phase == 1) { // Start of paste-stream. draining = false; @@ -1249,9 +1250,8 @@ Boolean nvim_paste(String data, Integer phase, Error *err) Array lines = string_to_array(data); ADD(args, ARRAY_OBJ(lines)); ADD(args, INTEGER_OBJ(phase)); - ADD(args2, STRING_OBJ(cstr_to_string("vim.paste(_A[1], _A[2])"))); - ADD(args2, ARRAY_OBJ(args2)); - rv = nvim_call_function(STATIC_CSTR_AS_STRING("luaeval"), args2, err); + rv = nvim_execute_lua(STATIC_CSTR_AS_STRING("return vim.paste(...)"), args, + err); if (ERROR_SET(err)) { draining = true; goto theend; @@ -1278,7 +1278,8 @@ Boolean nvim_paste(String data, Integer phase, Error *err) AppendCharToRedobuff(ESC); // Dot-repeat. } theend: - api_free_array(args2); + api_free_object(rv); + api_free_array(args); if (cancel || phase == -1 || phase == 3) { // End of paste-stream. draining = false; // XXX: Tickle main loop to ensure cursor is updated. @@ -1310,10 +1311,6 @@ void nvim_put(ArrayOf(String) lines, String type, Boolean after, api_set_error(err, kErrorTypeValidation, "Invalid type: '%s'", type.data); goto cleanup; } - if (!MODIFIABLE(curbuf)) { - api_set_error(err, kErrorTypeException, "Buffer is not 'modifiable'"); - goto cleanup; - } if (lines.size == 0) { goto cleanup; // Nothing to do. } @@ -1331,13 +1328,15 @@ void nvim_put(ArrayOf(String) lines, String type, Boolean after, finish_yankreg_from_object(reg, false); - bool VIsual_was_active = VIsual_active; - msg_silent++; // Avoid "N more lines" message. - do_put(0, reg, - after ? FORWARD : BACKWARD, 1, - follow ? PUT_CURSEND : 0); - msg_silent--; - VIsual_active = VIsual_was_active; + TRY_WRAP({ + try_start(); + bool VIsual_was_active = VIsual_active; + msg_silent++; // Avoid "N more lines" message. + do_put(0, reg, after ? FORWARD : BACKWARD, 1, follow ? PUT_CURSEND : 0); + msg_silent--; + VIsual_active = VIsual_was_active; + try_end(err); + }); cleanup: free_register(reg); diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 884e07e2c5..647fab5c43 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -411,7 +411,7 @@ describe('API', function() end) it("fails if 'nomodifiable'", function() command('set nomodifiable') - expect_err([[Buffer is not 'modifiable']], request, + expect_err([[Vim:E21: Cannot make changes, 'modifiable' is off]], request, 'nvim_put', {'a','b'}, 'l', true, true) end) it('inserts text', function() diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 7435b293fd..5445ff0127 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -185,6 +185,7 @@ describe('TUI', function() {3:-- TERMINAL --} | ]]) feed_data('pasted from terminal') + expect_child_buf_lines({'"pasted from terminal"'}) screen:expect([[ "pasted from terminal{1:"} | {4:~ }| @@ -196,6 +197,7 @@ describe('TUI', function() ]]) feed_data('\027[201~') -- End paste. feed_data('\027\000') -- ESC: go to Normal mode. + wait_for_mode('n') screen:expect([[ "pasted from termina{1:l}" | {4:~ }| @@ -207,6 +209,8 @@ describe('TUI', function() ]]) -- Dot-repeat/redo. feed_data('2.') + expect_child_buf_lines( + {'"pasted from terminapasted from terminalpasted from terminall"'}) screen:expect([[ "pasted from terminapasted from terminalpasted fro| m termina{1:l}l" | @@ -341,18 +345,10 @@ describe('TUI', function() | {3:-- TERMINAL --} | ]]} - wait_for_mode('n') -- Start pasting... feed_data('\027[200~line 1\nline 2\n') - screen:expect{grid=[[ - foo | - | - {4:~ }| - {5: }| - {8:paste: Vim:E5108: Error while calling lua chunk fo}| - {10:Press ENTER or type command to continue}{1: } | - {3:-- TERMINAL --} | - ]]} + wait_for_mode('n') + screen:expect{any='paste: Error executing lua'} -- Remaining chunks are discarded after vim.paste() failure. feed_data('line 3\nline 4\n') feed_data('line 5\nline 6\n') @@ -402,15 +398,7 @@ describe('TUI', function() it("paste: 'nomodifiable' buffer", function() child_session:request('nvim_command', 'set nomodifiable') feed_data('\027[200~fail 1\nfail 2\n\027[201~') - screen:expect{grid=[[ - | - {5: }| - {8:paste: Vim:E5108: Error while calling lua chunk fo}| - {8:r luaeval(): [string "-- Nvim-Lua stdlib: the `vim}| - {8:` module (:help l..."]:193: Buffer is not 'modifia}| - {10:Press ENTER or type command to continue}{1: } | - {3:-- TERMINAL --} | - ]]} + screen:expect{any='Vim:E21'} feed_data('\n') -- child_session:request('nvim_command', 'set modifiable') feed_data('\027[200~success 1\nsuccess 2\n\027[201~')