diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/nvim/api/private/helpers.c | 29 | ||||
-rw-r--r-- | src/nvim/api/vim.c | 171 | ||||
-rw-r--r-- | src/nvim/event/loop.c | 13 | ||||
-rw-r--r-- | src/nvim/ex_getln.c | 2 | ||||
-rw-r--r-- | src/nvim/getchar.c | 29 | ||||
-rw-r--r-- | src/nvim/globals.h | 6 | ||||
-rw-r--r-- | src/nvim/keymap.c | 12 | ||||
-rw-r--r-- | src/nvim/keymap.h | 5 | ||||
-rw-r--r-- | src/nvim/lua/vim.lua | 78 | ||||
-rw-r--r-- | src/nvim/ops.c | 69 | ||||
-rw-r--r-- | src/nvim/os/input.c | 4 | ||||
-rw-r--r-- | src/nvim/state.c | 4 | ||||
-rw-r--r-- | src/nvim/terminal.c | 4 | ||||
-rw-r--r-- | src/nvim/tui/input.c | 94 | ||||
-rw-r--r-- | src/nvim/tui/input.h | 3 | ||||
-rw-r--r-- | src/nvim/tui/tui.c | 6 | ||||
-rw-r--r-- | src/nvim/types.h | 6 |
17 files changed, 433 insertions, 102 deletions
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 d027eca59a..1ca0d8789d 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" @@ -36,6 +37,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" @@ -52,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 { @@ -390,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; @@ -421,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; } @@ -472,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; @@ -500,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) { @@ -1204,6 +1208,141 @@ Dictionary nvim_get_namespaces(void) return retval; } +/// 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). +/// +/// @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 +/// 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: 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. + } + // 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); + 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. + } +theend: + 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. + loop_schedule_deferred(&main_loop, event_create(loop_dummy_event, 0)); + } + + return !cancel; +} + +/// Puts text at cursor, in any mode. +/// +/// Compare |:put| and |p| which are always linewise. +/// +/// @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 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 after, + Boolean follow, Error *err) + FUNC_API_SINCE(6) +{ + yankreg_T *reg = xcalloc(sizeof(yankreg_T), 1); + if (!prepare_yankreg_from_object(reg, type, lines.size)) { + api_set_error(err, kErrorTypeValidation, "Invalid type: '%s'", type.data); + goto cleanup; + } + if (lines.size == 0) { + goto cleanup; // Nothing to do. + } + + for (size_t i = 0; i < lines.size; i++) { + if (lines.items[i].type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, + "Invalid lines (expected array of 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); + + 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); + xfree(reg); +} + /// Subscribes to event broadcasts. /// /// @param channel_id Channel id (passed automatically by the dispatcher) diff --git a/src/nvim/event/loop.c b/src/nvim/event/loop.c index 609c723c57..529ddd8eba 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); } @@ -158,10 +162,15 @@ 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; 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..0ef0c852a4 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" @@ -524,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; @@ -1902,14 +1898,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 +1930,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..eab65f2625 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. @@ -941,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/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/lua/vim.lua b/src/nvim/lua/vim.lua index 46c96b455f..fd34b8545d 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,69 @@ 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| +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,21 +235,12 @@ 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, _os_proc_info = _os_proc_info, _system = _system, + paste = paste, schedule_wrap = schedule_wrap, } 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 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/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); 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..c74ef58ba1 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -16,7 +16,6 @@ #include "nvim/os/input.h" #include "nvim/event/rstream.h" -#define PASTETOGGLE_KEY "<Paste>" #define KEY_BUFFER_SIZE 0xfff #ifdef INCLUDE_GENERATED_DECLARATIONS @@ -26,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); @@ -105,13 +104,28 @@ 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) { + Error err = ERROR_INIT; + // 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)) { + // 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) { + rbuffer_consumed(input->key_buffer, consumed); + } + rbuffer_reset(input->key_buffer); + if (consumed < len) { + break; + } } } uv_mutex_lock(&input->key_buffer_mutex); @@ -292,9 +306,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 +343,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; @@ -341,18 +358,33 @@ 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 && 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. + } + + if (enable) { + // Flush before starting paste. + tinput_flush(input, true); + // Paste phase: "first-chunk". + input->paste = 1; + } else if (input->paste) { + // Paste phase: "last-chunk". + input->paste = input->paste == 2 ? 3 : -1; + tinput_flush(input, true); + // Paste phase: "disabled". + input->paste = 0; } - tinput_enqueue(input, PASTETOGGLE_KEY, sizeof(PASTETOGGLE_KEY) - 1); - input->paste_enabled = enable; return true; } return false; } +// ESC NUL => <Esc> static bool handle_forced_escape(TermInput *input) { if (rbuffer_size(input->read_stream.buffer) > 1 @@ -477,9 +509,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,15 +522,28 @@ static void tinput_read_cb(Stream *stream, RBuffer *buf, size_t count_, break; } } - + // Push bytes directly (paste). + 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); + tinput_enqueue(input, ptr, consumed); + rbuffer_consumed(input->read_stream.buffer, consumed); + if (!(count -= consumed)) { + break; + } + } + continue; + } + // Push through libtermkey (translates to "<keycode>" 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)) { @@ -505,7 +552,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/tui/input.h b/src/nvim/tui/input.h index 7d59cf5c6a..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; - bool paste_enabled; + // 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/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) { 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 |