fix(paste): improve repeating of pasted text (#30438)

- Fixes 'autoindent' being applied during redo.
- Makes redoing a large paste significantly faster.
- Stores pasted text in the register being recorded.

Fix #28561
This commit is contained in:
zeertzjq 2024-09-22 06:02:48 +08:00 committed by GitHub
parent 1d815acd78
commit e697c1b43d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 233 additions and 32 deletions

View File

@ -1259,30 +1259,19 @@ Boolean nvim_paste(String data, Boolean crlf, Integer phase, Arena *arena, Error
draining = true;
goto theend;
}
if (!(State & (MODE_CMDLINE | MODE_INSERT)) && (phase == -1 || phase == 1)) {
ResetRedobuff();
AppendCharToRedobuff('a'); // Dot-repeat.
if (phase == -1 || phase == 1) {
paste_store(kFalse, NULL_STRING, crlf);
}
// 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 & MODE_CMDLINE)) { // Dot-repeat.
for (size_t i = 0; i < lines.size; i++) {
String s = lines.items[i].data.string;
assert(s.size <= INT_MAX);
AppendToRedobuffLit(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 & (MODE_CMDLINE | MODE_INSERT)) && (phase == -1 || phase == 3)) {
AppendCharToRedobuff(ESC); // Dot-repeat.
if (!cancel) {
paste_store(kNone, data, crlf);
}
theend:
if (cancel || phase == -1 || phase == 3) { // End of paste-stream.
draining = false;
paste_store(kTrue, NULL_STRING, crlf);
}
return !cancel;

View File

@ -907,6 +907,10 @@ static int insert_handle_key(InsertState *s)
case K_IGNORE: // Something mapped to nothing
break;
case K_PASTE_START:
paste_repeat(1);
goto check_pum;
case K_EVENT: // some event
state_handle_k_event();
// If CTRL-G U was used apply it to the next typed key.

View File

@ -13,6 +13,7 @@
#include "nvim/api/private/defs.h"
#include "nvim/api/private/helpers.h"
#include "nvim/api/vim.h"
#include "nvim/ascii_defs.h"
#include "nvim/buffer_defs.h"
#include "nvim/charset.h"
@ -308,6 +309,24 @@ static void add_num_buff(buffheader_T *buf, int n)
add_buff(buf, number, -1);
}
/// Add byte or special key 'c' to buffer "buf".
/// Translates special keys, NUL and K_SPECIAL.
static void add_byte_buff(buffheader_T *buf, int c)
{
char temp[4];
if (IS_SPECIAL(c) || c == K_SPECIAL || c == NUL) {
// Translate special key code into three byte sequence.
temp[0] = (char)K_SPECIAL;
temp[1] = (char)K_SECOND(c);
temp[2] = (char)K_THIRD(c);
temp[3] = NUL;
} else {
temp[0] = (char)c;
temp[1] = NUL;
}
add_buff(buf, temp, -1);
}
/// Add character 'c' to buffer "buf".
/// Translates special keys, NUL, K_SPECIAL and multibyte characters.
static void add_char_buff(buffheader_T *buf, int c)
@ -325,19 +344,7 @@ static void add_char_buff(buffheader_T *buf, int c)
if (!IS_SPECIAL(c)) {
c = bytes[i];
}
char temp[4];
if (IS_SPECIAL(c) || c == K_SPECIAL || c == NUL) {
// Translate special key code into three byte sequence.
temp[0] = (char)K_SPECIAL;
temp[1] = (char)K_SECOND(c);
temp[2] = (char)K_THIRD(c);
temp[3] = NUL;
} else {
temp[0] = (char)c;
temp[1] = NUL;
}
add_buff(buf, temp, -1);
add_byte_buff(buf, c);
}
}
@ -3182,3 +3189,126 @@ bool map_execute_lua(bool may_repeat)
ga_clear(&line_ga);
return true;
}
static bool paste_repeat_active = false; ///< true when paste_repeat() is pasting
/// Wraps pasted text stream with K_PASTE_START and K_PASTE_END, and
/// appends to redo buffer and/or record buffer if needed.
/// Escapes all K_SPECIAL and NUL bytes in the content.
///
/// @param state kFalse for the start of a paste
/// kTrue for the end of a paste
/// kNone for the content of a paste
/// @param str the content of the paste (only used when state is kNone)
void paste_store(const TriState state, const String str, const bool crlf)
{
if (State & MODE_CMDLINE) {
return;
}
const bool need_redo = !block_redo;
const bool need_record = reg_recording != 0 && !paste_repeat_active;
if (!need_redo && !need_record) {
return;
}
if (state != kNone) {
const int c = state == kFalse ? K_PASTE_START : K_PASTE_END;
if (need_redo) {
if (state == kFalse && !(State & MODE_INSERT)) {
ResetRedobuff();
}
add_char_buff(&redobuff, c);
}
if (need_record) {
add_char_buff(&recordbuff, c);
}
return;
}
const char *s = str.data;
const char *const str_end = str.data + str.size;
while (s < str_end) {
const char *start = s;
while (s < str_end && (uint8_t)(*s) != K_SPECIAL && *s != NUL
&& *s != NL && !(crlf && *s == CAR)) {
s++;
}
if (s > start) {
if (need_redo) {
add_buff(&redobuff, start, s - start);
}
if (need_record) {
add_buff(&recordbuff, start, s - start);
}
}
if (s < str_end) {
int c = (uint8_t)(*s++);
if (crlf && c == CAR) {
if (s < str_end && *s == NL) {
s++;
}
c = NL;
}
if (need_redo) {
add_byte_buff(&redobuff, c);
}
if (need_record) {
add_byte_buff(&recordbuff, c);
}
}
}
}
/// Gets a paste stored by paste_store() from typeahead and repeats it.
void paste_repeat(int count)
{
garray_T ga = GA_INIT(1, 32);
bool aborted = false;
no_mapping++;
got_int = false;
while (!aborted) {
ga_grow(&ga, 32);
uint8_t c1 = (uint8_t)vgetorpeek(true);
if (c1 == K_SPECIAL) {
c1 = (uint8_t)vgetorpeek(true);
uint8_t c2 = (uint8_t)vgetorpeek(true);
int c = TO_SPECIAL(c1, c2);
if (c == K_PASTE_END) {
break;
} else if (c == K_ZERO) {
ga_append(&ga, NUL);
} else if (c == K_SPECIAL) {
ga_append(&ga, K_SPECIAL);
} else {
ga_append(&ga, K_SPECIAL);
ga_append(&ga, c1);
ga_append(&ga, c2);
}
} else {
ga_append(&ga, c1);
}
aborted = got_int;
}
no_mapping--;
String str = cbuf_as_string(ga.ga_data, (size_t)ga.ga_len);
Arena arena = ARENA_EMPTY;
Error err = ERROR_INIT;
paste_repeat_active = true;
for (int i = 0; !aborted && i < count; i++) {
nvim_paste(str, false, -1, &arena, &err);
aborted = ERROR_SET(&err);
}
paste_repeat_active = false;
api_clear_error(&err);
arena_mem_free(arena_finish(&arena));
ga_clear(&ga);
}

View File

@ -380,6 +380,10 @@ enum key_extra {
#define K_KENTER TERMCAP2KEY('K', 'A') // keypad Enter
#define K_KPOINT TERMCAP2KEY('K', 'B') // keypad . or ,
// Delimits pasted text (to repeat nvim_paste). Internal-only, not sent by UIs.
#define K_PASTE_START TERMCAP2KEY('P', 'S') // paste start
#define K_PASTE_END TERMCAP2KEY('P', 'E') // paste end
#define K_K0 TERMCAP2KEY('K', 'C') // keypad 0
#define K_K1 TERMCAP2KEY('K', 'D') // keypad 1
#define K_K2 TERMCAP2KEY('K', 'E') // keypad 2

View File

@ -351,6 +351,7 @@ static const struct nv_cmd {
{ K_F1, nv_help, NV_NCW, 0 },
{ K_XF1, nv_help, NV_NCW, 0 },
{ K_SELECT, nv_select, 0, 0 },
{ K_PASTE_START, nv_paste, NV_KEEPREG, 0 },
{ K_EVENT, nv_event, NV_KEEPREG, 0 },
{ K_COMMAND, nv_colon, 0, 0 },
{ K_LUA, nv_colon, 0, 0 },
@ -6593,6 +6594,12 @@ static void nv_open(cmdarg_T *cap)
}
}
/// Handles K_PASTE_START, repeats pasted text.
static void nv_paste(cmdarg_T *cap)
{
paste_repeat(cap->count1);
}
/// Handle an arbitrary event in normal mode
static void nv_event(cmdarg_T *cap)
{

View File

@ -748,6 +748,10 @@ static int terminal_execute(VimState *state, int key)
}
break;
case K_PASTE_START:
paste_repeat(1);
break;
case K_EVENT:
// We cannot let an event free the terminal yet. It is still needed.
s->term->refcount++;

View File

@ -1301,8 +1301,62 @@ describe('API', function()
end)
it('crlf=false does not break lines at CR, CRLF', function()
api.nvim_paste('line 1\r\n\r\rline 2\nline 3\rline 4\r', false, -1)
expect('line 1\r\n\r\rline 2\nline 3\rline 4\r')
local expected = 'line 1\r\n\r\rline 2\nline 3\rline 4\r'
expect(expected)
eq({ 0, 3, 14, 0 }, fn.getpos('.'))
feed('u') -- Undo.
expect('')
feed('.') -- Dot-repeat.
expect(expected)
end)
describe('repeating a paste via redo/recording', function()
-- Test with indent and control chars and multibyte chars containing 0x80 bytes
local text = dedent(([[
foo
bar
baz
!!!%s!!!%s!!!%s!!!
倀
]]):format('\0', '\2\3\6\21\22\23\24\27', '\127'))
before_each(function()
api.nvim_set_option_value('autoindent', true, {})
end)
local function test_paste_repeat_normal_insert(is_insert)
feed('qr' .. (is_insert and 'i' or ''))
eq('r', fn.reg_recording())
api.nvim_paste(text, true, -1)
feed(is_insert and '<Esc>' or '')
expect(text)
feed('.')
expect(text:rep(2))
feed('q')
eq('', fn.reg_recording())
feed('3.')
expect(text:rep(5))
feed('2@r')
expect(text:rep(9))
end
it('works in Normal mode', function()
test_paste_repeat_normal_insert(false)
end)
it('works in Insert mode', function()
test_paste_repeat_normal_insert(true)
end)
local function test_paste_repeat_visual_select(is_select)
insert(('xxx\n'):rep(5))
feed('ggqr' .. (is_select and 'gH' or 'V'))
api.nvim_paste(text, true, -1)
feed('q')
expect(text .. ('xxx\n'):rep(4))
feed('2@r')
expect(text:rep(3) .. ('xxx\n'):rep(2))
end
it('works in Visual mode (recording only)', function()
test_paste_repeat_visual_select(false)
end)
it('works in Select mode (recording only)', function()
test_paste_repeat_visual_select(true)
end)
end)
it('vim.paste() failure', function()
api.nvim_exec_lua('vim.paste = (function(lines, phase) error("fake fail") end)', {})

View File

@ -1106,7 +1106,7 @@ describe('TUI', function()
screen:expect(expected_grid1)
-- Dot-repeat/redo.
feed_data('.')
screen:expect([[
local expected_grid2 = [[
ESC:{6:^[} / CR: |
xline 1 |
ESC:{6:^[} / CR: |
@ -1114,7 +1114,8 @@ describe('TUI', function()
{5:[No Name] [+] 5,1 Bot}|
|
{3:-- TERMINAL --} |
]])
]]
screen:expect(expected_grid2)
-- Undo.
feed_data('u')
expect_child_buf_lines(expected_crlf)
@ -1128,6 +1129,14 @@ describe('TUI', function()
feed_data('\027[200~' .. table.concat(expected_lf, '\r\n') .. '\027[201~')
screen:expect(expected_grid1)
expect_child_buf_lines(expected_crlf)
-- Dot-repeat/redo.
feed_data('.')
screen:expect(expected_grid2)
-- Undo.
feed_data('u')
expect_child_buf_lines(expected_crlf)
feed_data('u')
expect_child_buf_lines({ '' })
end)
it('paste: cmdline-mode inserts 1 line', function()