diff options
author | zeertzjq <zeertzjq@outlook.com> | 2024-09-22 06:02:48 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-22 06:02:48 +0800 |
commit | e697c1b43dfbeab132fee4149157f7abd08c51a0 (patch) | |
tree | 4fb363d573f9606f7314799e03c6ba9e0f5fd0e1 | |
parent | 1d815acd78e5b961302985b80d2b625947902386 (diff) | |
download | rneovim-e697c1b43dfbeab132fee4149157f7abd08c51a0.tar.gz rneovim-e697c1b43dfbeab132fee4149157f7abd08c51a0.tar.bz2 rneovim-e697c1b43dfbeab132fee4149157f7abd08c51a0.zip |
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
-rw-r--r-- | src/nvim/api/vim.c | 21 | ||||
-rw-r--r-- | src/nvim/edit.c | 4 | ||||
-rw-r--r-- | src/nvim/getchar.c | 156 | ||||
-rw-r--r-- | src/nvim/keycodes.h | 4 | ||||
-rw-r--r-- | src/nvim/normal.c | 7 | ||||
-rw-r--r-- | src/nvim/terminal.c | 4 | ||||
-rw-r--r-- | test/functional/api/vim_spec.lua | 56 | ||||
-rw-r--r-- | test/functional/terminal/tui_spec.lua | 13 |
8 files changed, 233 insertions, 32 deletions
diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index d10ee91042..4b80369654 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -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; diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 13623eaa91..f06dc124f0 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -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. diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c index ba0b629896..31f31904e0 100644 --- a/src/nvim/getchar.c +++ b/src/nvim/getchar.c @@ -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); +} diff --git a/src/nvim/keycodes.h b/src/nvim/keycodes.h index 18af3f87d6..5a7ddd4847 100644 --- a/src/nvim/keycodes.h +++ b/src/nvim/keycodes.h @@ -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 diff --git a/src/nvim/normal.c b/src/nvim/normal.c index b9ce891b49..be9987cc7f 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -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) { diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index ea3617098b..b916660024 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -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++; diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 71703c9b05..ae61e50624 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -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)', {}) diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 3e837e796d..a8a664a568 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -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() |