Merge #4448 'paste: redesign'

fix #3447
fix #3566
fix #7066
fix #7212
fix #7273
fix #7455
fix #10415

NA vim-patches:
vim-patch:8.1.1198
vim-patch:8.1.0224
vim-patch:8.0.1299
vim-patch:8.0.0569
vim-patch:8.0.0303
vim-patch:8.0.0296
vim-patch:8.0.0244
vim-patch:8.0.0238
vim-patch:8.0.0232
vim-patch:8.0.0231
vim-patch:8.0.0230
vim-patch:8.0.0210
This commit is contained in:
Justin M. Keyes 2019-08-28 01:56:02 +02:00 committed by GitHub
commit 82d52b229d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1028 additions and 254 deletions

View File

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

View File

@ -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.

View File

@ -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|

View File

@ -218,6 +218,41 @@ The "copy" function stores a list of lines and the register type. The "paste"
function returns the clipboard as a `[lines, regtype]` list, where `lines` is
a list of lines and `regtype` is a register type conforming to |setreg()|.
==============================================================================
Paste *provider-paste* *paste*
"Paste" is a separate concept from |clipboard|: paste means "dump a bunch of
text to the editor", whereas clipboard 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*

View File

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

View File

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

View File

@ -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)

View File

@ -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);

View File

@ -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
}

View File

@ -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;

View File

@ -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;

View File

@ -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);
}

View File

@ -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 */

View File

@ -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,
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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:

View File

@ -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);
}

View File

@ -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

View File

@ -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)
{

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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,75 +172,359 @@ 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
it('paste: Insert mode', function()
-- "bracketed paste"
feed_data('i""\027i\027[200~')
screen:expect([[
"{1:"} |
{4:~ }|
{4:~ }|
{4:~ }|
{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:"} |
{4:~ }|
{4:~ }|
{4:~ }|
{5:[No Name] [+] }|
{3:-- INSERT --} |
{3:-- TERMINAL --} |
]])
feed_data('\027[201~') -- End paste.
feed_data('\027\000') -- ESC: go to Normal mode.
wait_for_mode('n')
screen:expect([[
"pasted from termina{1:l}" |
{4:~ }|
{4:~ }|
{4:~ }|
{5:[No Name] [+] }|
|
{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("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 end{1: } |
{4:~ }|
{4:~ }|
{5:[No Name] [+] }|
{3:-- INSERT --} |
{3:-- TERMINAL --} |
]])
end)
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')
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 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 (paste) --} |
{3:-- TERMINAL --} |
]])
feed_data('pasted from terminal')
screen:expect([[
pasted from terminal{1: } |
{4:~ }|
{4:~ }|
{4:~ }|
{5:[No Name] [+] }|
{3:-- INSERT (paste) --} |
{3:-- TERMINAL --} |
]])
feed_data('\027[201~')
screen:expect([[
pasted from terminal{1: } |
{4:~ }|
{4:~ }|
{4:~ }|
{5:[No Name] [+] }|
{3:-- INSERT --} |
{3:-- TERMINAL --} |
]])
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~')
screen:expect([[
zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz|
zzzzzzzzzzzzzz{1: } |
{4:~ }|
{4:~ }|
{5:[No Name] [+] }|
{3:-- INSERT --} |
{3:-- TERMINAL --} |
]])
end)
it('can handle arbitrarily long bursts of input', function()
-- Need extra time for this test, specially in ASAN.
screen.timeout = 60000
feed_command('set ruler')
local t = {}
for i = 1, 3000 do
t[i] = 'item ' .. tostring(i)
end
feed_data('i\027[200~'..table.concat(t, '\n')..'\027[201~')
screen:expect([[
item 2997 |
item 2998 |
item 2999 |
item 3000{1: } |
{5:[No Name] [+] 3000,10 Bot}|
{3:-- INSERT --} |
{3:-- TERMINAL --} |
]])
-- TODO
it('paste: handles missing "stop paste" code', function()
end)
it('allows termguicolors to be set at runtime', function()

View File

@ -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()