aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/api.txt43
-rw-r--r--runtime/doc/eval.txt11
-rw-r--r--runtime/doc/if_lua.txt19
-rw-r--r--runtime/doc/provider.txt35
-rw-r--r--runtime/doc/term.txt6
-rw-r--r--src/nvim/api/private/helpers.c29
-rw-r--r--src/nvim/api/vim.c171
-rw-r--r--src/nvim/event/loop.c13
-rw-r--r--src/nvim/ex_getln.c2
-rw-r--r--src/nvim/getchar.c29
-rw-r--r--src/nvim/globals.h6
-rw-r--r--src/nvim/keymap.c12
-rw-r--r--src/nvim/keymap.h5
-rw-r--r--src/nvim/lua/vim.lua78
-rw-r--r--src/nvim/ops.c69
-rw-r--r--src/nvim/os/input.c4
-rw-r--r--src/nvim/state.c4
-rw-r--r--src/nvim/terminal.c4
-rw-r--r--src/nvim/tui/input.c94
-rw-r--r--src/nvim/tui/input.h3
-rw-r--r--src/nvim/tui/tui.c6
-rw-r--r--src/nvim/types.h6
-rw-r--r--test/functional/api/vim_spec.lua129
-rw-r--r--test/functional/terminal/helpers.lua3
-rw-r--r--test/functional/terminal/tui_spec.lua366
-rw-r--r--test/functional/ui/input_spec.lua71
26 files changed, 996 insertions, 222 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/eval.txt b/runtime/doc/eval.txt
index 5d30ac15b3..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
- |<CursorHold>|. Often you will want to ignore this and get
- another character: >
- :function GetKey()
- : let c = getchar()
- : while c == "\<CursorHold>"
- : 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/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 dc045c360a..833be8a103 100644
--- a/runtime/doc/provider.txt
+++ b/runtime/doc/provider.txt
@@ -219,6 +219,41 @@ 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 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,
+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 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. 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 by redefining |vim.paste()|.
+Example: >
+
+ vim.paste = (function(lines, phase)
+ vim.api.nvim_put(lines, 'c', true, true)
+ end)
+
+==============================================================================
X11 selection mechanism *clipboard-x11* *x11-selection*
X11 clipboard providers store text in "selections". Selections are owned by an
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 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
diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua
index 0cd81619c1..647fab5c43 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,126 @@ 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)
+ it('vim.paste() failure', function()
+ nvim('execute_lua', 'vim.paste = (function(lines, phase) error("fake fail") end)', {})
+ expect_err([[Error executing lua: %[string "%<nvim>"]:1: fake fail]],
+ request, 'nvim_paste', 'line 1\nline 2\nline 3', 1)
+ 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("fails if 'nomodifiable'", function()
+ command('set nomodifiable')
+ expect_err([[Vim:E21: Cannot make changes, 'modifiable' is off]], 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)
+ 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', true, false)
+ expect([[
+ line 1
+ line 2
+ line 3]])
+ eq({0,1,1,0}, funcs.getpos('.')) -- follow=false
+ -- blockwise
+ 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', true, true)
+ eq({0,1,1,0}, funcs.getpos('.'))
+ expect([[]])
+ -- Single empty line.
+ 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)
+
+ 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()
it('works', function()
eq(3, nvim('strwidth', 'abc'))
@@ -626,12 +747,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 +785,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 +801,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)
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/tui_spec.lua b/test/functional/terminal/tui_spec.lua
index af55ec1555..5445ff0127 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')
@@ -21,11 +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", "set noswapfile noshowcmd noruler undodir=. directory=. viewdir=. backupdir=."]')
+ 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:~ }|
@@ -35,12 +42,31 @@ describe('TUI', function()
|
{3:-- TERMINAL --} |
]])
+ child_session = helpers.connect(child_server)
end)
after_each(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:")
@@ -128,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
@@ -146,49 +172,264 @@ describe('TUI', function()
]], attrs)
end)
- it('automatically sends <Paste> for bracketed paste sequences', 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~')
+ it('paste: Insert mode', function()
+ -- "bracketed paste"
+ feed_data('i""\027i\027[200~')
screen:expect([[
- {1: } |
+ "{1:"} |
{4:~ }|
{4:~ }|
{4:~ }|
- {5:[No Name] }|
- {3:-- INSERT (paste) --} |
+ {5:[No Name] [+] }|
+ {3:-- INSERT --} |
{3:-- TERMINAL --} |
]])
feed_data('pasted from terminal')
+ expect_child_buf_lines({'"pasted from terminal"'})
screen:expect([[
- pasted from terminal{1: } |
+ "pasted from terminal{1:"} |
{4:~ }|
{4:~ }|
{4:~ }|
{5:[No Name] [+] }|
- {3:-- INSERT (paste) --} |
+ {3:-- INSERT --} |
{3:-- TERMINAL --} |
]])
- feed_data('\027[201~')
+ feed_data('\027[201~') -- End paste.
+ feed_data('\027\000') -- ESC: go to Normal mode.
+ wait_for_mode('n')
screen:expect([[
- pasted from terminal{1: } |
+ "pasted from termina{1:l}" |
{4:~ }|
{4:~ }|
{4:~ }|
{5:[No Name] [+] }|
- {3:-- INSERT --} |
+ |
+ {3:-- TERMINAL --} |
+ ]])
+ -- 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" |
+ {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') -- <Left> 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') -- <Left> 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)
+
+ 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
+ ]], {})
+ -- 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 --} |
+ ]]}
+ -- Start pasting...
+ feed_data('\027[200~line 1\nline 2\n')
+ 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')
+ feed_data('line 7\nline 8\n')
+ -- Stop paste.
+ feed_data('\027[201~')
+ feed_data('\n') -- <Enter>
+ --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:~ }|
+ {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') -- <Enter>
+ screen:expect{grid=[[
+ foo |
+ typed input...line A |
+ line B |
+ {1: } |
+ {5:[No Name] [+] }|
+ |
+ {3:-- TERMINAL --} |
+ ]]}
end)
- it('handles pasting a specific amount of text', function()
- -- Need extra time for this test, specially in ASAN.
- screen.timeout = 60000
- feed_data('i\027[200~'..string.rep('z', 64)..'\027[201~')
+ 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{any='Vim:E21'}
+ feed_data('\n') -- <Enter>
+ 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] [+] }|
+ |
+ {3:-- TERMINAL --} |
+ ]]}
+ end)
+
+ -- TODO
+ it('paste: other modes', function()
+ -- Other modes act like CTRL-C + paste.
+ end)
+
+ it('paste: exactly 64 bytes #10311', function()
+ local expected = string.rep('z', 64)
+ feed_data('i')
+ wait_for_mode('i')
+ -- "bracketed paste"
+ feed_data('\027[200~'..expected..'\027[201~')
+ feed_data(' end')
+ expected = expected..' end'
+ expect_child_buf_lines({expected})
screen:expect([[
zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz|
- zzzzzzzzzzzzzz{1: } |
+ zzzzzzzzzzzzzz end{1: } |
{4:~ }|
{4:~ }|
{5:[No Name] [+] }|
@@ -197,26 +438,95 @@ describe('TUI', function()
]])
end)
- it('can handle arbitrarily long bursts of input', function()
- -- Need extra time for this test, specially in ASAN.
- screen.timeout = 60000
- feed_command('set ruler')
+ it('paste: big burst of input', function()
+ feed_data(':set ruler\n')
local t = {}
for i = 1, 3000 do
t[i] = 'item ' .. tostring(i)
end
- feed_data('i\027[200~'..table.concat(t, '\n')..'\027[201~')
+ feed_data('i')
+ wait_for_mode('i')
+ -- "bracketed paste"
+ feed_data('\027[200~'..table.concat(t, '\n')..'\027[201~')
+ expect_child_buf_lines(t)
+ feed_data(' end')
+ screen:expect([[
+ item 2997 |
+ item 2998 |
+ item 2999 |
+ item 3000 end{1: } |
+ {5:[No Name] [+] 3000,14 Bot}|
+ {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{1: } |
- {5:[No Name] [+] 3000,10 Bot}|
+ item 3000 en{1:d}d |
+ {5:[No Name] [+] 5999,13 Bot}|
+ |
+ {3:-- TERMINAL --} |
+ ]])
+ end)
+
+ 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.
+
+ -- 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: } |
+ {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('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')
+ -- Send "stop paste" sequence.
+ feed_data('\027[201~')
+ screen:expect([[
+ {1: } |
+ {4:~ }|
+ {4:~ }|
+ {4:~ }|
+ {5:[No Name] }|
{3:-- INSERT --} |
{3:-- TERMINAL --} |
]])
end)
+ -- TODO
+ it('paste: handles missing "stop paste" code', 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 0009f2c31b..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 <Paste>', 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
- feed('i<Paste>')
- screen:expect([[
- ^ |
- ~ |
- ~ |
- ~ |
- ~ |
- ~ |
- ~ |
- ~ |
- ~ |
- ~ |
- ~ |
- ~ |
- ~ |
- -- INSERT (paste) -- |
- ]])
- feed(table.concat(t, '<Enter>'))
- 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) -- |
- ]])
- feed('<Paste>')
- 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 -- 20000,11 Bot |
- ]])
- end)
-end)
-
describe('input utf sequences that contain CSI/K_SPECIAL', function()
before_each(clear)
it('ok', function()