mirror of
				https://github.com/neovim/neovim.git
				synced 2025-11-04 01:34:25 +00: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:
		@@ -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()
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user