# HG changeset patch # User Bram Moolenaar # Date 1631447105 -7200 # Node ID 16a7d1154be8be1f6b8321192a01bf654e89bfee # Parent 3afd5ab3d69aba75a37243b10494b64bc6cac96e patch 8.2.3430: no generic way to trigger an autocommand on mode change Commit: https://github.com/vim/vim/commit/f1e8876fa2359b572d262772747405d3616db670 Author: =?UTF-8?q?Magnus=20Gro=C3=9F?= Date: Sun Sep 12 13:39:55 2021 +0200 patch 8.2.3430: no generic way to trigger an autocommand on mode change Problem: No generic way to trigger an autocommand on mode change. Solution: Add the ModeChanged autocommand event. (Magnus Gross, closes https://github.com/vim/vim/issues/8856) diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt --- a/runtime/doc/autocmd.txt +++ b/runtime/doc/autocmd.txt @@ -366,6 +366,8 @@ Name triggered by ~ |InsertCharPre| when a character was typed in Insert mode, before inserting it +|ModeChanged| after changing the mode + |TextChanged| after a change was made to the text in Normal mode |TextChangedI| after a change was made to the text in Insert mode when popup menu is not visible @@ -925,7 +927,22 @@ MenuPopup Just before showing the popu i Insert c Command line tl Terminal - *OptionSet* + *ModeChanged* +ModeChanged After changing the mode. The pattern is + matched against `'old_mode:new_mode'`, for + example match against `i:*` to simulate + |InsertLeave|. + The following values of |v:event| are set: + old_mode The mode before it changed. + new_mode The new mode as also returned + by |mode()|. + When ModeChanged is triggered, old_mode will + have the value of new_mode when the event was + last triggered. + Usage example to use relative line numbers + when entering visual mode: > + :autocmd ModeChanged *:v set relativenumber +< *OptionSet* OptionSet After setting an option. The pattern is matched against the long option name. || indicates what option has been set. diff --git a/src/autocmd.c b/src/autocmd.c --- a/src/autocmd.c +++ b/src/autocmd.c @@ -150,6 +150,7 @@ static struct event_name {"InsertLeavePre", EVENT_INSERTLEAVEPRE}, {"InsertCharPre", EVENT_INSERTCHARPRE}, {"MenuPopup", EVENT_MENUPOPUP}, + {"ModeChanged", EVENT_MODECHANGED}, {"OptionSet", EVENT_OPTIONSET}, {"QuickFixCmdPost", EVENT_QUICKFIXCMDPOST}, {"QuickFixCmdPre", EVENT_QUICKFIXCMDPRE}, @@ -1817,6 +1818,17 @@ has_completechanged(void) } #endif +#if defined(FEAT_EVAL) || defined(PROTO) +/* + * Return TRUE when there is a ModeChanged autocommand defined. + */ + int +has_modechanged(void) +{ + return (first_autopat[(int)EVENT_MODECHANGED] != NULL); +} +#endif + /* * Execute autocommands for "event" and file name "fname". * Return TRUE if some commands were executed. @@ -1938,7 +1950,8 @@ apply_autocmds_group( if (fname_io == NULL) { if (event == EVENT_COLORSCHEME || event == EVENT_COLORSCHEMEPRE - || event == EVENT_OPTIONSET) + || event == EVENT_OPTIONSET + || event == EVENT_MODECHANGED) autocmd_fname = NULL; else if (fname != NULL && !ends_excmd(*fname)) autocmd_fname = fname; @@ -2011,7 +2024,8 @@ apply_autocmds_group( || event == EVENT_COLORSCHEMEPRE || event == EVENT_OPTIONSET || event == EVENT_QUICKFIXCMDPOST - || event == EVENT_DIRCHANGED) + || event == EVENT_DIRCHANGED + || event == EVENT_MODECHANGED) { fname = vim_strsave(fname); autocmd_fname_full = TRUE; // don't expand it later diff --git a/src/edit.c b/src/edit.c --- a/src/edit.c +++ b/src/edit.c @@ -284,6 +284,7 @@ edit( else State = INSERT; + trigger_modechanged(); stop_insert_mode = FALSE; #ifdef FEAT_CONCEAL @@ -3681,6 +3682,7 @@ ins_esc( #endif State = NORMAL; + trigger_modechanged(); // need to position cursor again (e.g. when on a TAB ) changed_cline_bef_curs(); @@ -3811,6 +3813,7 @@ ins_insert(int replaceState) State = INSERT | (State & LANGMAP); else State = replaceState | (State & LANGMAP); + trigger_modechanged(); AppendCharToRedobuff(K_INS); showmode(); #ifdef CURSOR_SHAPE diff --git a/src/ex_docmd.c b/src/ex_docmd.c --- a/src/ex_docmd.c +++ b/src/ex_docmd.c @@ -485,6 +485,7 @@ do_exmode( else exmode_active = EXMODE_NORMAL; State = NORMAL; + trigger_modechanged(); // When using ":global /pat/ visual" and then "Q" we return to continue // the :global command. diff --git a/src/ex_getln.c b/src/ex_getln.c --- a/src/ex_getln.c +++ b/src/ex_getln.c @@ -1696,6 +1696,10 @@ getcmdline_int( // Trigger CmdlineEnter autocommands. cmdline_type = firstc == NUL ? '-' : firstc; trigger_cmd_autocmd(cmdline_type, EVENT_CMDLINEENTER); +#ifdef FEAT_EVAL + if (!debug_mode) + trigger_modechanged(); +#endif init_history(); hiscnt = get_hislen(); // set hiscnt to impossible history value @@ -2461,6 +2465,12 @@ returncmd: trigger_cmd_autocmd(cmdline_type, EVENT_CMDLINELEAVE); State = save_State; + +#ifdef FEAT_EVAL + if (!debug_mode) + trigger_modechanged(); +#endif + #ifdef HAVE_INPUT_METHOD if (b_im_ptr != NULL && *b_im_ptr != B_IMODE_LMAP) im_save_status(b_im_ptr); diff --git a/src/globals.h b/src/globals.h --- a/src/globals.h +++ b/src/globals.h @@ -1256,6 +1256,9 @@ EXTERN int listcmd_busy INIT(= FALSE); / // :bufdo is executing EXTERN int need_start_insertmode INIT(= FALSE); // start insert mode soon +#if defined(FEAT_EVAL) || defined(PROTO) +EXTERN char_u last_mode[MODE_MAX_LENGTH] INIT(= "n"); // for ModeChanged event +#endif EXTERN char_u *last_cmdline INIT(= NULL); // last command line (for ":) EXTERN char_u *repeat_cmdline INIT(= NULL); // command line for "." EXTERN char_u *new_last_cmdline INIT(= NULL); // new value for last_cmdline diff --git a/src/misc1.c b/src/misc1.c --- a/src/misc1.c +++ b/src/misc1.c @@ -630,7 +630,7 @@ ask_yesno(char_u *str, int direct) void f_mode(typval_T *argvars, typval_T *rettv) { - char_u buf[4]; + char_u buf[MODE_MAX_LENGTH]; if (in_vim9script() && check_for_opt_bool_arg(argvars, 0) == FAIL) return; @@ -2643,3 +2643,42 @@ path_with_url(char_u *fname) // "://" or ":\\" must follow return path_is_url(p); } + +/* + * Fires a ModeChanged autocmd + */ + void +trigger_modechanged() +{ +#if defined(FEAT_EVAL) || defined(PROTO) + dict_T *v_event; + typval_T rettv; + typval_T tv; + char_u *pat_pre; + char_u *pat; + + if (!has_modechanged()) + return; + + v_event = get_vim_var_dict(VV_EVENT); + + tv.v_type = VAR_UNKNOWN; + f_mode(&tv, &rettv); + (void)dict_add_string(v_event, "new_mode", rettv.vval.v_string); + (void)dict_add_string(v_event, "old_mode", last_mode); + dict_set_items_ro(v_event); + + // concatenate modes in format "old_mode:new_mode" + pat_pre = concat_str(last_mode, (char_u*)":"); + pat = concat_str(pat_pre, rettv.vval.v_string); + vim_free(pat_pre); + + apply_autocmds(EVENT_MODECHANGED, pat, NULL, FALSE, curbuf); + STRCPY(last_mode, rettv.vval.v_string); + + vim_free(rettv.vval.v_string); + vim_free(pat); + dict_free_contents(v_event); + hash_init(&v_event->dv_hashtab); +#endif +} diff --git a/src/normal.c b/src/normal.c --- a/src/normal.c +++ b/src/normal.c @@ -1386,6 +1386,7 @@ end_visual_mode_keep_button() #endif VIsual_active = FALSE; + trigger_modechanged(); setmouse(); mouse_dragging = 0; @@ -5642,6 +5643,7 @@ nv_visual(cmdarg_T *cap) { // or char/line mode VIsual_mode = cap->cmdchar; showmode(); + trigger_modechanged(); } redraw_curbuf_later(INVERTED); // update the inversion } @@ -5757,6 +5759,7 @@ n_start_visual_mode(int c) VIsual_mode = c; VIsual_active = TRUE; VIsual_reselect = TRUE; + trigger_modechanged(); // Corner case: the 0 position in a tab may change when going into // virtualedit. Recalculate curwin->w_cursor to avoid bad highlighting. diff --git a/src/proto/autocmd.pro b/src/proto/autocmd.pro --- a/src/proto/autocmd.pro +++ b/src/proto/autocmd.pro @@ -25,6 +25,7 @@ int has_insertcharpre(void); int has_cmdundefined(void); int has_textyankpost(void); int has_completechanged(void); +int has_modechanged(void); void block_autocmds(void); void unblock_autocmds(void); int is_autocmd_blocked(void); diff --git a/src/proto/misc1.pro b/src/proto/misc1.pro --- a/src/proto/misc1.pro +++ b/src/proto/misc1.pro @@ -47,4 +47,5 @@ int goto_im(void); char_u *get_isolated_shell_name(void); int path_is_url(char_u *p); int path_with_url(char_u *fname); +void trigger_modechanged(); /* vim: set ft=c : */ diff --git a/src/testdir/test_edit.vim b/src/testdir/test_edit.vim --- a/src/testdir/test_edit.vim +++ b/src/testdir/test_edit.vim @@ -1907,4 +1907,38 @@ func Test_edit_put_CTRL_E() set encoding=utf-8 endfunc +" Test for ModeChanged pattern +func Test_mode_changes() + let g:count = 0 + func! DoIt() + let g:count += 1 + endfunc + let g:index = 0 + let g:mode_seq = ['n', 'i', 'n', 'v', 'V', 'n', 'V', 'v', 'n'] + func! TestMode() + call assert_equal(g:mode_seq[g:index], get(v:event, "old_mode")) + call assert_equal(g:mode_seq[g:index + 1], get(v:event, "new_mode")) + call assert_equal(mode(), get(v:event, "new_mode")) + let g:index += 1 + endfunc + + au ModeChanged * :call TestMode() + au ModeChanged n:* :call DoIt() + call feedkeys("i\vV\", 'tnix') + call assert_equal(2, g:count) + + au ModeChanged V:v :call DoIt() + call feedkeys("Vv\", 'tnix') + call assert_equal(4, g:count) + + call assert_equal(len(g:mode_seq) - 1, g:index) + + au! ModeChanged + delfunc TestMode + unlet! g:mode_seq + unlet! g:index + delfunc DoIt + unlet! g:count +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -756,6 +756,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 3430, +/**/ 3429, /**/ 3428, diff --git a/src/vim.h b/src/vim.h --- a/src/vim.h +++ b/src/vim.h @@ -688,6 +688,8 @@ extern int (*dyn_libintl_wputenv)(const #define TERMINAL 0x2000 // Terminal mode #define MODE_ALL 0xffff +#define MODE_MAX_LENGTH 4 // max mode length returned in mode() + // all mode bits used for mapping #define MAP_ALL_MODES (0x3f | SELECTMODE | TERMINAL) @@ -1317,6 +1319,7 @@ enum auto_event EVENT_INSERTLEAVEPRE, // just before leaving Insert mode EVENT_INSERTLEAVE, // just after leaving Insert mode EVENT_MENUPOPUP, // just before popup menu is displayed + EVENT_MODECHANGED, // after changing the mode EVENT_OPTIONSET, // option was set EVENT_QUICKFIXCMDPOST, // after :make, :grep etc. EVENT_QUICKFIXCMDPRE, // before :make, :grep etc.