aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/nvim/api/vim.c21
-rw-r--r--src/nvim/edit.c4
-rw-r--r--src/nvim/getchar.c156
-rw-r--r--src/nvim/keycodes.h4
-rw-r--r--src/nvim/normal.c7
-rw-r--r--src/nvim/terminal.c4
-rw-r--r--test/functional/api/vim_spec.lua56
-rw-r--r--test/functional/terminal/tui_spec.lua13
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()