diff options
-rw-r--r-- | runtime/doc/eval.txt | 4 | ||||
-rw-r--r-- | src/nvim/event/loop.c | 9 | ||||
-rw-r--r-- | src/nvim/ex_getln.c | 2 | ||||
-rw-r--r-- | src/nvim/getchar.c | 14 | ||||
-rw-r--r-- | src/nvim/globals.h | 6 | ||||
-rw-r--r-- | src/nvim/keymap.c | 1 | ||||
-rw-r--r-- | src/nvim/keymap.h | 5 | ||||
-rw-r--r-- | src/nvim/os/input.c | 4 | ||||
-rw-r--r-- | src/nvim/terminal.c | 4 | ||||
-rw-r--r-- | src/nvim/tui/input.c | 43 | ||||
-rw-r--r-- | src/nvim/types.h | 6 | ||||
-rw-r--r-- | test/functional/terminal/helpers.lua | 3 | ||||
-rw-r--r-- | test/functional/terminal/paste_spec.lua | 204 | ||||
-rw-r--r-- | test/functional/ui/input_spec.lua | 8 |
14 files changed, 265 insertions, 48 deletions
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 - |<CursorHold>|. Often you will want to ignore this and get + |<LeftMouse>|. Often you will want to ignore this and get another character: > :function GetKey() : let c = getchar() - : while c == "\<CursorHold>" + : while c == "\<LeftMouse>" : 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 // <Cmd> 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 "<Paste>" +#define PASTE_KEY "<Paste>" +#define PASTEPOST_KEY "<PastePost>" #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, "<C-v><Esc>200~", sizeof("<C-v><Esc>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 <Paste>', function() for i = 1, 20000 do t[i] = 'item ' .. tostring(i) end - feed('i<Paste>') + command('doautocmd PastePre') screen:expect([[ ^ | ~ | @@ -161,7 +161,7 @@ describe('feeding large chunks of input with <Paste>', function() item 20000^ | -- INSERT (paste) -- | ]]) - feed('<Paste>') + command('doautocmd PastePost') screen:expect([[ item 19988 | item 19989 | @@ -175,8 +175,8 @@ describe('feeding large chunks of input with <Paste>', function() item 19997 | item 19998 | item 19999 | - item 20000^ | - -- INSERT -- 20000,11 Bot | + item 2000^0 | + 20000,10 Bot | ]]) end) end) |