Merge pull request #39741 from zeertzjq/vim-9.2.0470

vim-patch:9.2.{0470,0473}: TextPut{Pre,Post}
This commit is contained in:
zeertzjq
2026-05-13 07:51:11 +08:00
committed by GitHub
10 changed files with 367 additions and 19 deletions

View File

@@ -1266,6 +1266,46 @@ TextChangedT After a change was made to the text in the
current buffer in |Terminal-mode|. Otherwise
the same as TextChanged.
*TextPutPost*
TextPutPost After text has been put in the current buffer.
The following values in |v:event| are mostly
the same as |TextYankPost|:
operator The operation performed,
either 'p' or 'P'.
regcontents Text that was put. For
|quote_=|, this is the result
of the expression.
regname Name of the register or empty
string for the unnamed
register. For |nvim_put()|
this is set to '_'.
regtype Type of the register, see
|getregtype()|.
visual True if the operation is
performed in |Visual| mode.
Not triggered when |quote_| is used nor when
called recursively.
It is not allowed to change the buffer text,
see |textlock|.
Note that for the |quote_.| register, since
the last inserted text is buffered into the
input buffer (buffer isn't modified directly),
this autocommand is called directly after
|TextPutPre|.
*TextPutPre*
TextPutPre Before text has been put in the current buffer.
Same values as |TextPutPost| in |v:event|. It
is valid to call |setreg()| in this
autocommand, allowing you to process and
modify the text in "regcontents" before it is
put. However this does not apply to |quote_#|,
|quote_=|, |quote_%|, |quote_:|, |quote_/| or |quote_.|.
Not triggered when |quote_| is used nor when
called recursively.
It is not allowed to change the buffer text,
see |textlock|.
*TextYankPost*
TextYankPost Just after a |yank| or |deleting| command, but not
if the black hole register |quote_| is used nor

View File

@@ -144,6 +144,7 @@ EVENTS
• |:delmarks| now triggers the |MarkSet| autocommand with line==col==0, same
as |nvim_buf_del_mark()|
• |TextPutPre| and |TextPutPost| are triggered before/after putting text.
HIGHLIGHTS

View File

@@ -207,6 +207,8 @@ error('Cannot require a meta file')
--- |'TextChangedI'
--- |'TextChangedP'
--- |'TextChangedT'
--- |'TextPutPost'
--- |'TextPutPre'
--- |'TextYankPost'
--- |'UIEnter'
--- |'UILeave'

View File

@@ -1462,7 +1462,7 @@ void nvim_put(ArrayOf(String) lines, String type, Boolean after, Boolean follow,
TRY_WRAP(err, {
bool VIsual_was_active = VIsual_active;
msg_silent++; // Avoid "N more lines" message.
do_put(0, reg, after ? FORWARD : BACKWARD, 1, follow ? PUT_CURSEND : 0);
do_put('_', reg, after ? FORWARD : BACKWARD, 1, follow ? PUT_CURSEND : 0);
msg_silent--;
VIsual_active = VIsual_was_active;
});

View File

@@ -126,6 +126,8 @@ return {
TextChangedI = true, -- text was modified in Insert mode(no popup)
TextChangedP = true, -- text was modified in Insert mode(popup)
TextChangedT = true, -- text was modified in Terminal mode
TextPutPost = true, -- after some text was put
TextPutPre = true, -- before some text was put
TextYankPost = true, -- after a yank or delete was done (y, d, c)
UIEnter = false, -- after UI attaches
UILeave = false, -- after UI detaches

View File

@@ -63,6 +63,7 @@
#include "nvim/os/os_defs.h"
#include "nvim/plines.h"
#include "nvim/pos_defs.h"
#include "nvim/register.h"
#include "nvim/state.h"
#include "nvim/state_defs.h"
#include "nvim/strings.h"
@@ -661,6 +662,10 @@ void stuffRedoReadbuff(const char *s)
void stuffReadbuffLen(const char *s, ptrdiff_t len)
{
if (add_last_insert == 1) { // Only add if this is the first call, for
// recursive calls, ignore.
ga_concat_len(&last_insert_ga, s, (size_t)len);
}
add_buff(&readbuf1, s, len);
}

View File

@@ -1169,27 +1169,37 @@ void op_yank_reg(oparg_T *oap, bool message, yankreg_T *reg, bool append)
/// @param reg_width The width, only used if "reg_type" is kMTBlockWise.
/// @param[out] buf Buffer to store formatted string. The allocated size should
/// be at least NUMBUFLEN+2 to always fit the value.
/// @param buf_len The allocated size of the buffer.
void format_reg_type(MotionType reg_type, colnr_T reg_width, char *buf, size_t buf_len)
/// @param bufsize The allocated size of the buffer.
///
/// @return The length of the register type string.
size_t format_reg_type(MotionType reg_type, colnr_T reg_width, char *buf, size_t bufsize)
FUNC_ATTR_NONNULL_ALL
{
assert(buf_len > 1);
assert(bufsize > 1);
switch (reg_type) {
case kMTLineWise:
buf[0] = 'V';
buf[1] = NUL;
break;
return 1;
case kMTCharWise:
buf[0] = 'v';
buf[1] = NUL;
break;
return 1;
case kMTBlockWise:
snprintf(buf, buf_len, CTRL_V_STR "%" PRIdCOLNR, reg_width + 1);
break;
return vim_snprintf_safelen(buf, bufsize, CTRL_V_STR "%" PRIdCOLNR, reg_width + 1);
case kMTUnknown:
buf[0] = NUL;
break;
return 0;
}
abort();
}
static void add_regtype_to_dict(yankreg_T *reg, dict_T *dict, char *buf, size_t bufsize)
{
// "reg" is NULL when pasting a special register, which is charwise.
size_t len = format_reg_type(reg != NULL ? reg->y_type : kMTCharWise,
reg != NULL ? reg->y_width : 0, buf, bufsize);
tv_dict_add_str_len(dict, S_LEN("regtype"), buf, (int)len);
}
/// Execute autocommands for TextYankPost.
@@ -1222,8 +1232,7 @@ void do_autocmd_textyankpost(oparg_T *oap, yankreg_T *reg)
// Register type.
char buf[NUMBUFLEN + 2];
format_reg_type(reg->y_type, reg->y_width, buf, ARRAY_SIZE(buf));
tv_dict_add_str(dict, S_LEN("regtype"), buf);
add_regtype_to_dict(reg, dict, buf, ARRAY_SIZE(buf));
// Name of requested register, or empty string for unnamed operation.
buf[0] = (char)oap->regname;
@@ -1252,6 +1261,78 @@ void do_autocmd_textyankpost(oparg_T *oap, yankreg_T *reg)
recursive = false;
}
/// Trigger TextPutPre or TextPutPost autocommand.
///
/// @param reg May be NULL, if special register
/// @param insert Not NULL if special register, except '.'
/// @param post If Post or Pre
/// @param dir BACKWARD for 'P', FORWARD for 'p'
static void put_do_autocmd(int regname, yankreg_T *reg, const String *insert, bool post,
Direction dir)
{
static bool recursive = false;
if (recursive || (regname == '_' && reg == NULL)) {
return;
}
save_v_event_T save_v_event;
dict_T *v_event = get_v_event(&save_v_event);
list_T *list = tv_list_alloc(reg != NULL ? (ptrdiff_t)reg->y_size : 1);
if (regname == '.') {
if (last_insert_ga.ga_data != NULL) {
// Get the last inserted text to place in "regcontents"
tv_list_append_string(list, last_insert_ga.ga_data, last_insert_ga.ga_len);
}
} else if (insert != NULL) {
tv_list_append_string(list, insert->data, (ssize_t)insert->size);
} else {
assert(reg != NULL);
for (size_t n = 0; n < reg->y_size; n++) {
tv_list_append_string(list, reg->y_array[n].data, (ssize_t)reg->y_array[n].size);
}
}
tv_list_set_lock(list, VAR_FIXED);
tv_dict_add_list(v_event, S_LEN("regcontents"), list);
char buf[NUMBUFLEN + 2];
// register name or empty string for unnamed operation
buf[0] = (char)regname;
buf[1] = NUL;
size_t buflen = (buf[0] == NUL) ? 0 : 1;
tv_dict_add_str_len(v_event, S_LEN("regname"), buf, (int)buflen);
// kind of operation (P, p)
buf[0] = dir == BACKWARD ? 'P' : 'p';
buf[1] = NUL;
buflen = 1;
tv_dict_add_str_len(v_event, S_LEN("operator"), buf, (int)buflen);
add_regtype_to_dict(reg, v_event, buf, sizeof(buf));
tv_dict_add_bool(v_event, S_LEN("visual"), VIsual_active);
// Lock the dictionary and its keys
tv_dict_set_keys_readonly(v_event);
recursive = true;
textlock++;
if (post) {
apply_autocmds(EVENT_TEXTPUTPOST, NULL, NULL, false, curbuf);
} else {
apply_autocmds(EVENT_TEXTPUTPRE, NULL, NULL, false, curbuf);
}
textlock--;
recursive = false;
// Empty the dictionary, v:event is still valid
restore_v_event(v_event, &save_v_event);
}
/// Yanks the text between "oap->start" and "oap->end" into a yank register.
/// If we are to append (uppercase register), we first yank into a new yank
/// register and then concatenate the old and the new one.
@@ -1320,9 +1401,23 @@ void do_put(int regname, yankreg_T *reg, int dir, int count, int flags)
? 'c'
: (flags & PUT_LINE ? 'i' : (dir == FORWARD ? 'a' : 'i'));
bool has_textput_events = has_event(EVENT_TEXTPUTPRE) || has_event(EVENT_TEXTPUTPOST);
if (has_textput_events) {
add_last_insert++;
}
// To avoid 'autoindent' on linewise puts, create a new line with `:put _`.
if (flags & PUT_LINE) {
do_put('_', NULL, dir, 1, PUT_LINE);
const int save_add_last_insert = add_last_insert;
add_last_insert = 0;
stuffcharReadbuff(K_COMMAND);
if (dir == FORWARD) {
stuffReadbuffLen(S_LEN("put _"));
} else {
stuffReadbuffLen(S_LEN("put! _"));
}
stuffcharReadbuff(CAR);
add_last_insert = save_add_last_insert;
}
// If given a count when putting linewise, we stuff the readbuf with the
@@ -1337,14 +1432,27 @@ void do_put(int regname, yankreg_T *reg, int dir, int count, int flags)
// back to the previous line in the case of 'noautoindent' and
// 'backspace' includes "eol". So we insert a dummy space for Ctrl_U
// to consume.
stuffReadbuff("\n ");
stuffcharReadbuff(Ctrl_U);
static const char s[] = { '\n', ' ', Ctrl_U, NUL };
stuffReadbuffLen(S_LEN(s));
}
}
} else {
stuff_inserted(command_start_char, count, false);
}
// Since the text is not inserted into the buffer immediately, just call
// TextPutPost after TextPutPre.
if (has_event(EVENT_TEXTPUTPRE)) {
put_do_autocmd('.', NULL, NULL, false, dir);
}
if (has_event(EVENT_TEXTPUTPOST)) {
put_do_autocmd('.', NULL, NULL, true, dir);
}
if (has_textput_events && --add_last_insert == 0) {
ga_clear(&last_insert_ga);
}
// Putting the text is done later, so can't move the cursor to the next
// character. Simulate it with motion commands after the insert.
if (flags & PUT_CURSEND) {
@@ -1454,7 +1562,20 @@ void do_put(int regname, yankreg_T *reg, int dir, int count, int flags)
y_size = 1; // use fake one-line yank register
y_array = &insert_string;
}
if (has_event(EVENT_TEXTPUTPRE)) {
put_do_autocmd(regname, NULL, &insert_string, false, dir);
}
} else {
if (has_event(EVENT_TEXTPUTPRE)) {
yankreg_T *const save_reg = reg;
if (reg == NULL) {
// Make sure to call this before we set the variables, as setreg()
// may be called and invalidate them.
reg = get_yank_register(regname, YREG_PASTE);
}
put_do_autocmd(regname, reg, NULL, false, dir);
reg = save_reg;
}
// in case of replacing visually selected text
// the yankreg might already have been saved to avoid
// just restoring the deleted text.
@@ -2069,6 +2190,15 @@ end:
curbuf->b_op_start = orig_start;
curbuf->b_op_end = orig_end;
}
if (has_event(EVENT_TEXTPUTPOST)) {
if (insert_string.data == NULL) {
put_do_autocmd(regname, reg, NULL, true, dir);
} else {
put_do_autocmd(regname, NULL, &insert_string, true, dir);
}
}
if (allocated) {
xfree(insert_string.data);
}

View File

@@ -1,10 +1,16 @@
#pragma once
#include "nvim/ascii_defs.h"
#include "nvim/ex_cmds_defs.h"
#include "nvim/ex_cmds_defs.h" // IWYU pragma: keep
#include "nvim/macros_defs.h"
#include "nvim/register_defs.h"
/// Used by TextPutPost/TextPutPre autocommands for the '.' register. If
/// "add_last_insert" is == 1, then "stuff_inserted" will add the last inserted
/// text to "last_insert_ga".
EXTERN garray_T last_insert_ga INIT( = { 0, 0, 1, 64, NULL });
EXTERN int add_last_insert INIT( = 0);
#include "register.h.generated.h"
#include "register.h.inline.generated.h"

View File

@@ -1555,29 +1555,53 @@ describe('API', function()
)
end)
it('inserts text', function()
exec([[
let g:pre_event = []
let g:post_event = []
au TextPutPre * let g:pre_event = copy(v:event)
au TextPutPost * let g:post_event = copy(v:event)
]])
-- linewise
api.nvim_put({ 'line 1', 'line 2', 'line 3' }, 'l', true, true)
local lines = { 'line 1', 'line 2', 'line 3' }
local expected_event = {
regcontents = lines,
regname = '_',
operator = 'p',
regtype = 'V',
visual = false,
}
api.nvim_put(lines, 'l', true, true)
expect([[
line 1
line 2
line 3]])
eq({ 0, 4, 1, 0 }, fn.getpos('.'))
eq(expected_event, api.nvim_get_var('pre_event'))
eq(expected_event, api.nvim_get_var('post_event'))
command('%delete _')
-- charwise
expected_event.regtype = 'v'
api.nvim_put({ 'line 1', 'line 2', 'line 3' }, 'c', true, false)
expect([[
line 1
line 2
line 3]])
eq({ 0, 1, 1, 0 }, fn.getpos('.')) -- follow=false
eq(expected_event, api.nvim_get_var('pre_event'))
eq(expected_event, api.nvim_get_var('post_event'))
-- blockwise
api.nvim_put({ 'AA', 'BB' }, 'b', true, true)
lines = { 'AA', 'BB' }
expected_event.regcontents = lines
expected_event.regtype = '\0222'
api.nvim_put(lines, 'b', true, true)
expect([[
lAAine 1
lBBine 2
line 3]])
eq({ 0, 2, 4, 0 }, fn.getpos('.'))
eq(expected_event, api.nvim_get_var('pre_event'))
eq(expected_event, api.nvim_get_var('post_event'))
command('%delete _')
-- Empty lines list.
api.nvim_put({}, 'c', true, true)
@@ -1590,19 +1614,27 @@ describe('API', function()
]])
api.nvim_put({ 'AB' }, 'c', true, true)
-- after=false, follow=true
api.nvim_put({ 'line 1', 'line 2' }, 'c', false, true)
lines = { 'line 1', 'line 2' }
expected_event.regcontents = lines
expected_event.regtype = 'v'
expected_event.operator = 'P'
api.nvim_put(lines, 'c', false, true)
expect([[
Aline 1
line 2B]])
eq({ 0, 2, 7, 0 }, fn.getpos('.'))
eq(expected_event, api.nvim_get_var('pre_event'))
eq(expected_event, api.nvim_get_var('post_event'))
command('%delete _')
api.nvim_put({ 'AB' }, 'c', true, true)
-- after=false, follow=false
api.nvim_put({ 'line 1', 'line 2' }, 'c', false, false)
api.nvim_put(lines, 'c', false, false)
expect([[
Aline 1
line 2B]])
eq({ 0, 1, 2, 0 }, fn.getpos('.'))
eq(expected_event, api.nvim_get_var('pre_event'))
eq(expected_event, api.nvim_get_var('post_event'))
eq('', api.nvim_eval('v:errmsg'))
end)

View File

@@ -5330,4 +5330,134 @@ func Test_SwapExists_b_nwindows()
%bw!
endfunc
func Test_TextPutX()
enew!
let g:pre_event = []
let g:post_event = []
au TextPutPre * let g:pre_event = copy(v:event)
au TextPutPost * let g:post_event = copy(v:event)
call setreg('a', ['foo'], 'v')
norm "ap
call assert_equal(
\ #{regcontents: ['foo'], regname: 'a', operator: 'p',
\ visual: v:false, regtype: 'v'},
\ g:pre_event)
call assert_equal(g:pre_event, g:post_event)
call setreg('', ['hello'], 'V')
norm P
call assert_equal(
\ #{regcontents: ['hello'], regname: '', operator: 'P',
\ visual: v:false, regtype: 'V'},
\ g:pre_event)
call assert_equal(g:pre_event, g:post_event)
call setreg('', ['maybe', 'true'], 'V')
norm Vp
call assert_equal(
\ #{regcontents: ['maybe', 'true'], regname: '', operator: 'P',
\ regtype: 'V', visual: v:true},
\ g:pre_event)
call assert_equal(g:pre_event, g:post_event)
call assert_equal({}, v:event)
call feedkeys("iinserted text\<CR>below\<Esc>", 'x')
norm ".p
call assert_equal(
\ #{regcontents: ["inserted text\nbelow"], regname: '.',
\ operator: 'p', regtype: 'v', visual: v:false},
\ g:pre_event)
call assert_equal(g:pre_event, g:post_event)
call feedkeys("\"=201\<CR>p", 'x')
call assert_equal(
\ #{regcontents: ["201"], regname: '=',
\ operator: 'p', regtype: 'v', visual: v:false},
\ g:pre_event)
call assert_equal(g:pre_event, g:post_event)
vsplit some.txt
wincmd l
norm "#p
call assert_equal(
\ #{regcontents: ["some.txt"], regname: '#',
\ operator: 'p', regtype: 'v', visual: v:false},
\ g:pre_event)
call assert_equal(g:pre_event, g:post_event)
wincmd h
bw!
if has('clipboard_working')
let @+ = 'clipboard'
norm "+p
call assert_equal(
\ #{regcontents: ["clipboard"], regname: '+',
\ operator: 'p', regtype: 'v', visual: v:false},
\ g:pre_event)
call assert_equal(g:pre_event, g:post_event)
endif
%delete " Clear buffer
au! TextPutPre
au! TextPutPost
let g:pre_event = []
let g:post_event = []
au TextPutPre * call setreg(v:event['regname'],
\ getreg('', 0, v:true) + ['!']) " Unnamed register should be same as regname
call setreg('', ['hello', 'world'])
norm p
call assert_equal(['', 'hello', 'world', '!'], getline(1, '$'))
au! TextPutPre
" Test that special registers cannot be modified
%delete
au TextPutPre * call setreg('=', '"modified"') | let g:pre_event = copy(v:event)
" Set up the expression register to evaluate to a known value.
call feedkeys("\"=\"original\"\<CR>p", 'x')
" The original value is what got put, not the modified one.
call assert_equal(['original'], getline(1, '$'))
" v:event still reports the original value.
call assert_equal(['original'], g:pre_event['regcontents'])
au! TextPutPre
let g:pre_event = []
for round in range(2)
" Recursive ". register calls have the same contents for post and pre.
au TextPutPre * put . | let g:pre_event = copy(v:event)
au TextPutPost * let g:post_event = copy(v:event)
call feedkeys("iinserted\<Esc>", 'x')
norm! ".p
call assert_equal(
\ #{regcontents: ["inserted"], regname: '.',
\ operator: 'p', regtype: 'v', visual: v:false},
\ g:pre_event)
call assert_equal(g:pre_event, g:post_event)
au! TextPutPre
au! TextPutPost
" Pasting ". register without TextPutPre/TextPutPost autocommands should
" not interfere with these autocommands in the next round.
norm! ".p
endfor
unlet g:post_event
unlet g:pre_event
bwipe!
endfunc
" vim: shiftwidth=2 sts=2 expandtab