mirror of
https://github.com/neovim/neovim.git
synced 2025-02-25 18:55:25 -06:00
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:
parent
1d815acd78
commit
e697c1b43d
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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++;
|
||||
|
@ -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)', {})
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user