patch 9.2.0470: No way to hook into put commands

Problem:  No way to hook into put commands
          (yochem)
Solution: Introduce TextPutPre and TextPutPost autocommands
          (Foxe Chen).

fixes:  #18701
closes: #20144

Signed-off-by: Foxe Chen <chen.foxe@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Foxe Chen
2026-05-10 19:12:22 +00:00
committed by Christian Brabandt
parent e3d9929109
commit e0781bd5bf
12 changed files with 368 additions and 28 deletions
+21
View File
@@ -195,6 +195,8 @@ static keyvalue_T event_tab[NUM_EVENTS] = {
KEYVALUE_ENTRY(-EVENT_TEXTCHANGEDI, "TextChangedI"),
KEYVALUE_ENTRY(-EVENT_TEXTCHANGEDP, "TextChangedP"),
KEYVALUE_ENTRY(-EVENT_TEXTCHANGEDT, "TextChangedT"),
KEYVALUE_ENTRY(-EVENT_TEXTPUTPOST, "TextPutPost"),
KEYVALUE_ENTRY(-EVENT_TEXTPUTPRE, "TextPutPre"),
KEYVALUE_ENTRY(-EVENT_TEXTYANKPOST, "TextYankPost"),
KEYVALUE_ENTRY(EVENT_USER, "User"),
KEYVALUE_ENTRY(EVENT_VIMENTER, "VimEnter"),
@@ -2023,6 +2025,25 @@ has_cmdundefined(void)
}
#if defined(FEAT_EVAL)
/*
* Return TRUE when there is a TextPutPost autocommand defined.
*/
int
has_textputpost(void)
{
return (first_autopat[(int)EVENT_TEXTPUTPOST] != NULL);
}
/*
* Return TRUE when there is a TextPutPre autocommand defined.
*/
int
has_textputpre(void)
{
return (first_autopat[(int)EVENT_TEXTPUTPRE] != NULL);
}
/*
* Return TRUE when there is a TextYankPost autocommand defined.
*/
+5
View File
@@ -714,6 +714,11 @@ stuffRedoReadbuff(char_u *s)
void
stuffReadbuffLen(char_u *s, long len)
{
#ifdef FEAT_EVAL
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);
#endif
add_buff(&readbuf1, s, len);
}
+8
View File
@@ -2165,3 +2165,11 @@ EXTERN bool inside_redraw_on_start_cb INIT(= false);
// If greater than zero, then silence the W23/W24 warning.
EXTERN int silence_w23_w24_msg INIT( = 0);
#ifdef FEAT_EVAL
// 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 INIT5(0, 0, 1, 64, NULL);
EXTERN int add_last_insert INIT(= 0);
#endif
+2
View File
@@ -29,6 +29,8 @@ int has_textchangedP(void);
int has_insertcharpre(void);
int has_keyinputpre(void);
int has_cmdundefined(void);
int has_textputpost(void);
int has_textputpre(void);
int has_textyankpost(void);
int has_completechanged(void);
int has_modechanged(void);
+154 -24
View File
@@ -1081,6 +1081,36 @@ shift_delete_registers(void)
}
#if defined(FEAT_EVAL)
static void
add_regtype_to_dict(int regname, dict_T *dict, char_u *buf, int bufsize)
{
size_t buflen;
long reglen;
// Register type
switch (get_reg_type(regname, &reglen))
{
case MLINE:
buf[0] = 'V';
buf[1] = NUL;
buflen = 1;
break;
case MCHAR:
buf[0] = 'v';
buf[1] = NUL;
buflen = 1;
break;
case MBLOCK:
buflen = vim_snprintf_safelen((char *)buf, bufsize,
"%c%ld", Ctrl_V, reglen + 1);
break;
default:
buf[0] = NUL;
buflen = 0;
break;
}
(void)dict_add_string_len(dict, "regtype", buf, (int)buflen);
}
void
yank_do_autocmd(oparg_T *oap, yankreg_T *reg)
{
@@ -1090,7 +1120,6 @@ yank_do_autocmd(oparg_T *oap, yankreg_T *reg)
int n;
char_u buf[NUMBUFLEN + 2];
size_t buflen;
long reglen = 0;
save_v_event_T save_v_event;
if (recursive)
@@ -1124,29 +1153,7 @@ yank_do_autocmd(oparg_T *oap, yankreg_T *reg)
buflen = (buf[0] == NUL) ? 0 : (buf[1] == NUL) ? 1 : 2;
(void)dict_add_string_len(v_event, "operator", buf, (int)buflen);
// register type
switch (get_reg_type(oap->regname, &reglen))
{
case MLINE:
buf[0] = 'V';
buf[1] = NUL;
buflen = 1;
break;
case MCHAR:
buf[0] = 'v';
buf[1] = NUL;
buflen = 1;
break;
case MBLOCK:
buflen = vim_snprintf_safelen((char *)buf, sizeof(buf),
"%c%ld", Ctrl_V, reglen + 1);
break;
default:
buf[0] = NUL;
buflen = 0;
break;
}
(void)dict_add_string_len(v_event, "regtype", buf, (int)buflen);
add_regtype_to_dict(oap->regname, v_event, buf, sizeof(buf));
// selection type - visual or not
(void)dict_add_bool(v_event, "visual", oap->is_VIsual);
@@ -1163,6 +1170,85 @@ yank_do_autocmd(oparg_T *oap, yankreg_T *reg)
// Empty the dictionary, v:event is still valid
restore_v_event(v_event, &save_v_event);
}
static void
put_do_autocmd(
int regname,
yankreg_T *reg, // May be NULL, if special register
string_T *insert, // Not NULL if special register, except '.'
bool post, // If Post or Pre
int dir) // BACKWARD for 'P', FORWARD for 'p'
{
static bool recursive = false;
dict_T *v_event;
list_T *list;
int n;
char_u buf[NUMBUFLEN + 2];
size_t buflen;
save_v_event_T save_v_event;
if (recursive || regname == '_')
return;
v_event = get_v_event(&save_v_event);
list = list_alloc();
if (list == NULL)
return;
if (regname == '.')
{
if (last_insert_ga.ga_data != NULL)
// Get the last inserted text to place in "regcontents"
list_append_string(list, last_insert_ga.ga_data,
(int)last_insert_ga.ga_len);
}
else if (insert != NULL)
{
list_append_string(list, insert->string, (int)insert->length);
}
else
{
assert(reg != NULL);
for (n = 0; n < reg->y_size; n++)
list_append_string(list, reg->y_array[n].string,
(int)reg->y_array[n].length);
}
list->lv_lock = VAR_FIXED;
(void)dict_add_list(v_event, "regcontents", list);
// register name or empty string for unnamed operation
buf[0] = (char_u)regname;
buf[1] = NUL;
buflen = (buf[0] == NUL) ? 0 : 1;
(void)dict_add_string_len(v_event, "regname", buf, (int)buflen);
// kind of operation (P, p)
buf[0] = dir == BACKWARD ? 'P' : 'p';
buf[1] = NUL;
buflen = 1;
(void)dict_add_string_len(v_event, "operator", buf, (int)buflen);
add_regtype_to_dict(regname, v_event, buf, sizeof(buf));
(void)dict_add_bool(v_event, "visual", VIsual_active);
// Lock the dictionary and its keys
dict_set_items_ro(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);
}
#endif
/*
@@ -1648,8 +1734,28 @@ do_put(
{
if (VIsual_active)
stuffcharReadbuff(VIsual_mode);
#ifdef FEAT_EVAL
if (has_textputpre() || has_textputpost())
add_last_insert++;
#endif
(void)stuff_inserted((dir == FORWARD ? (count == -1 ? 'o' : 'a') :
(count == -1 ? 'O' : 'i')), count, FALSE);
#ifdef FEAT_EVAL
// Since the text is not inserted into the buffer immediately, just call
// TextPutPost after TextPutPre.
if (has_textputpre())
put_do_autocmd('.', NULL, NULL, false, dir);
if (has_textputpost())
put_do_autocmd('.', NULL, NULL, true, dir);
if (--add_last_insert == 0)
ga_clear(&last_insert_ga);
#endif
// Putting the text is done later, so can't really move the cursor to
// the next character. Use "l" to simulate it.
if ((flags & PUT_CURSEND) && gchar_cursor() != NUL)
@@ -1733,9 +1839,22 @@ do_put(
y_size = 1; // use fake one-line yank register
y_array = &insert_string;
}
#ifdef FEAT_EVAL
if (has_textputpre())
put_do_autocmd(regname, NULL, &insert_string, false, dir);
#endif
}
else
{
#ifdef FEAT_EVAL
if (has_textputpre())
{
// Make sure to call this before we set the variables, as setreg()
// may be called and invalidate them.
get_yank_register(regname, FALSE);
put_do_autocmd(regname, y_current, NULL, false, dir);
}
#endif
get_yank_register(regname, FALSE);
y_type = y_current->y_type;
@@ -2401,6 +2520,17 @@ end:
curbuf->b_op_start = orig_start;
curbuf->b_op_end = orig_end;
}
#ifdef FEAT_EVAL
if (has_textputpost())
{
if (insert_string.string == NULL)
put_do_autocmd(regname, y_current, NULL, true, dir);
else
put_do_autocmd(regname, NULL, &insert_string, true, dir);
}
#endif
if (allocated)
vim_free(insert_string.string);
if (regname == '=')
+125
View File
@@ -5967,4 +5967,129 @@ func Test_autocmd_add_secure()
call assert_fails('sandbox call autocmd_delete([{"event": "BufRead"}])', 'E48:')
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 = []
" Test that 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
unlet g:post_event
unlet g:pre_event
bwipe!
endfunc
" vim: shiftwidth=2 sts=2 expandtab
+2
View File
@@ -729,6 +729,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
470,
/**/
469,
/**/
+2
View File
@@ -1484,6 +1484,8 @@ enum auto_event
EVENT_TEXTCHANGEDI, // text was modified in Insert mode
EVENT_TEXTCHANGEDP, // TextChangedI with popup menu visible
EVENT_TEXTCHANGEDT, // text was modified in Terminal mode
EVENT_TEXTPUTPOST, // after some text was put
EVENT_TEXTPUTPRE, // before some text was put
EVENT_TEXTYANKPOST, // after some text was yanked
EVENT_USER, // user defined autocommand
EVENT_VIMENTER, // after starting Vim