]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0908: cannot use a {} block in a nested :autocmd v9.2.0908
authorHirohito Higashi <h.east.727@gmail.com>
Tue, 4 Aug 2026 19:50:18 +0000 (19:50 +0000)
committerChristian Brabandt <cb@256bit.org>
Tue, 4 Aug 2026 19:50:18 +0000 (19:50 +0000)
Problem:  At script level a "{" block is only recognized when it is the
          whole argument of :autocmd or :command, so an :autocmd that is
          the command of another :autocmd cannot use a block.  In a :def
          function a trailing "{" is accepted instead, and there any
          command ending in "{", such as "normal! {", is mistaken for the
          start of a block (lacygoill).
Solution: Locate the block by following the argument of the command,
          descending into a nested :autocmd or :command, and use that
          everywhere a block needs to be recognized (Hirohito Higashi).

fixes:  #20918
closes: #20933

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Hirohito Higashi <h.east.727@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
12 files changed:
runtime/doc/autocmd.txt
src/autocmd.c
src/ex_docmd.c
src/proto/autocmd.pro
src/proto/ex_docmd.pro
src/testdir/test_autocmd.vim
src/testdir/test_usercommands.vim
src/testdir/test_vim9_script.vim
src/usercmd.c
src/userfunc.c
src/version.c
src/vim9cmds.c

index b3d1deb25f55a371bb6b9dc7d0faf22b56f44429..7a1df5c46a96520e84a9ac21c779649bafa01a3d 100644 (file)
@@ -1,4 +1,4 @@
-*autocmd.txt*  For Vim version 9.2.  Last change: 2026 May 23
+*autocmd.txt*  For Vim version 9.2.  Last change: 2026 Aug 04
 
 
                  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -103,6 +103,14 @@ triggered.
                  setlocal matchpairs+=<:>
                  /<start
                }
+<This also works when the `:autocmd` is itself the command of another
+`:autocmd` or `:command`: >
+       au FileType xml au BufWinEnter * ++once {
+                 setlocal matchpairs+=<:>
+               }
+<Note that the "{" must be the whole command, a command that happens to end in
+"{", such as "normal! {", does not start a block.  Nesting the commands works,
+nesting the blocks themselves does not, see |:command-repl|.
 
 The |autocmd_add()| function can be used to add a list of autocmds and autocmd
 groups from a Vim script.  It is preferred if you have anything that would
index 98014cf03746fafc857505ef64b42cb0dc3b512f..7de6947c684335fed4381fc620d85beb172ca598 100644 (file)
@@ -806,6 +806,56 @@ find_end_event(
     return pat;
 }
 
+/*
+ * Find the command of an ":autocmd" with argument "arg", the part after the
+ * events, the pattern and any "++once"/"++nested".  Returns NULL when there is
+ * no command.  Does not modify "arg" and gives no error messages.
+ */
+    char_u *
+au_find_cmd_arg(char_u *arg)
+{
+    char_u     *pat;
+    char_u     *cmd;
+    int                group;
+
+    if (*arg == '|')
+       return NULL;
+
+    // Errors are reported when the autocommand is actually defined.
+    ++emsg_off;
+    group = au_get_grouparg(&arg);
+    pat = group == AUGROUP_ERROR
+                         ? NULL : find_end_event(arg, group != AUGROUP_ALL);
+    --emsg_off;
+    if (pat == NULL)
+       return NULL;
+
+    pat = skipwhite(pat);
+    if (*pat == NUL || *pat == '|')
+       return NULL;
+
+    // White space in the pattern can be escaped with a backslash.
+    cmd = pat;
+    while (*cmd != NUL
+           && (!VIM_ISWHITE(*cmd) || (cmd > pat && *(cmd - 1) == '\\')))
+       ++cmd;
+    cmd = skipwhite(cmd);
+
+    // "++once" and "++nested" can come in any order.
+    for (int i = 0; i < 2; ++i)
+    {
+       if (STRNCMP(cmd, "++once", 6) == 0 && VIM_ISWHITE(cmd[6]))
+           cmd = skipwhite(cmd + 6);
+       if (STRNCMP(cmd, "++nested", 8) == 0 && VIM_ISWHITE(cmd[8]))
+           cmd = skipwhite(cmd + 8);
+       if (!in_vim9script() && STRNCMP(cmd, "nested", 6) == 0
+                                                      && VIM_ISWHITE(cmd[6]))
+           cmd = skipwhite(cmd + 6);
+    }
+
+    return *cmd == NUL ? NULL : cmd;
+}
+
 /*
  * Return TRUE if "event" is included in 'eventignore(win)'.
  */
index b23504a85bd993bf65ca97e588fbac206e0d5e18..f5771cb615bec4fc3183ec7249ef0fc82f7087cc 100644 (file)
@@ -2838,6 +2838,68 @@ checkforcmd_noparen(
     return checkforcmd_opt(pp, cmd, len, TRUE);
 }
 
+/*
+ * Find the replacement text of a ":command", the part after the attributes and
+ * the command name.  Returns NULL when there is none.  Does not modify "arg"
+ * and gives no error messages.
+ */
+    static char_u *
+find_ucmd_repl(char_u *arg)
+{
+    char_u     *p = arg;
+
+    // Skip over the attributes.
+    while (*p == '-')
+       p = skipwhite(skiptowhite(p));
+
+    // Skip over the command name.
+    if (!ASCII_ISALPHA(*p))
+       return NULL;
+    while (ASCII_ISALNUM(*p))
+       ++p;
+
+    return *p == NUL ? NULL : skipwhite(p);
+}
+
+/*
+ * Find the "{" in "line" that starts a block for ":command" or ":autocmd".
+ * That is the case when the command argument is "{" at the end of the line,
+ * also when the command is nested in another ":command" or ":autocmd".
+ * Returns NULL when the line does not start such a block.
+ */
+    char_u *
+find_cmd_block_start(char_u *line)
+{
+    char_u     *p = skipwhite(line);
+
+    for (;;)
+    {
+       char_u  *arg = p;
+
+       if (*p == '{' && ends_excmd2(p, skipwhite(p + 1)))
+           return p;
+
+       if (checkforcmd_noparen(&arg, "autocmd", 2))
+       {
+           if (*arg == '!')
+               arg = skipwhite(arg + 1);
+           p = au_find_cmd_arg(arg);
+       }
+       else if (checkforcmd_noparen(&arg, "command", 3))
+       {
+           if (*arg == '!')
+               arg = skipwhite(arg + 1);
+           p = find_ucmd_repl(arg);
+       }
+       else
+           return NULL;
+
+       if (p == NULL)
+           return NULL;
+       p = skipwhite(p);
+    }
+}
+
 /*
  * Parse and skip over command modifiers:
  * - update eap->cmd
index abff7fd4143ba633a9031c8c388d858b2485b714..143c731d28ac5112e84b6c388d80ac857779a8e2 100644 (file)
@@ -5,6 +5,7 @@ void do_augroup(char_u *arg, int del_group);
 void autocmd_init(void);
 void free_all_autocmds(void);
 int is_aucmd_win(win_T *win);
+char_u *au_find_cmd_arg(char_u *arg);
 int event_ignored(event_T event, char_u *ei);
 int check_ei(char_u *ei);
 char_u *au_event_disable(char *what);
index df44d02004521e3802790a1c3a16d13c7126b081..299820077a523c7db9c413f9ed107863ab9123e1 100644 (file)
@@ -10,6 +10,7 @@ char *ex_errmsg(char *msg, char_u *arg);
 char *ex_range_without_command(exarg_T *eap);
 int checkforcmd(char_u **pp, char *cmd, int len);
 int checkforcmd_noparen(char_u **pp, char *cmd, int len);
+char_u *find_cmd_block_start(char_u *line);
 int parse_command_modifiers(exarg_T *eap, char **errormsg, cmdmod_T *cmod, int skip_only);
 int has_cmdmod(cmdmod_T *cmod, int ignore_silent);
 int cmdmod_error(int ignore_silent);
index 7e36eec53e7b005b3dd65739b5f1a308e581ee49..b76cec4f3f6466088ce5521fbd9afee06ecb89fc 100644 (file)
@@ -4070,6 +4070,92 @@ func Test_autocmd_with_block()
   augroup END
 endfunc
 
+" Test for a {} block at script level in an :autocmd nested in another one
+func Test_autocmd_nested_block()
+  let lines =<< trim END
+      vim9script
+      autocmd CursorHold * autocmd BufReadPre * ++once {
+            g:nested_block = 'yes'
+          }
+  END
+  call writefile(lines, 'XautoNestedBlock', 'D')
+  source XautoNestedBlock
+
+  doautocmd CursorHold
+  doautocmd BufReadPre
+  call assert_equal('yes', g:nested_block)
+
+  unlet g:nested_block
+  au! CursorHold
+  au! BufReadPre
+endfunc
+
+" Test for a {} block in an :autocmd nested two levels deep
+func Test_autocmd_nested_block_twice()
+  let lines =<< trim END
+      vim9script
+      autocmd CursorHold * autocmd CursorHoldI * autocmd BufReadPre * ++once {
+            g:nested_twice = 'yes'
+          }
+  END
+  call writefile(lines, 'XautoNestedTwice', 'D')
+  source XautoNestedTwice
+
+  doautocmd CursorHold
+  doautocmd CursorHoldI
+  doautocmd BufReadPre
+  call assert_equal('yes', g:nested_twice)
+
+  unlet g:nested_twice
+  au! CursorHold
+  au! CursorHoldI
+  au! BufReadPre
+endfunc
+
+" Only the :autocmd owning the block uses Vim9 syntax, the one it is nested in
+" keeps the syntax of the script.  The old "nested" is valid in legacy script
+" but an error in Vim9 script, so it tells the two apart.
+func Test_autocmd_nested_block_legacy_script()
+  let lines =<< trim END
+      autocmd CursorHold * autocmd BufReadPre * nested {
+            g:legacy_block = 'yes'
+          }
+  END
+  call writefile(lines, 'XautoNestedLegacy', 'D')
+  source XautoNestedLegacy
+
+  doautocmd CursorHold
+  doautocmd BufReadPre
+  call assert_equal('yes', g:legacy_block)
+
+  unlet g:legacy_block
+  au! CursorHold
+  au! BufReadPre
+endfunc
+
+" A trailing "{" that is an argument of the command does not start a block,
+" the lines after it are not swallowed
+func Test_autocmd_trailing_curly_no_block()
+  let lines =<< trim END
+      vim9script
+      autocmd CursorHold * normal! {
+      g:after_autocmd = 'reached'
+  END
+  call writefile(lines, 'XautoTrailingCurly', 'D')
+  source XautoTrailingCurly
+  call assert_equal('reached', g:after_autocmd)
+
+  new
+  call setline(1, ['one', '', 'two'])
+  call cursor(3, 1)
+  doautocmd CursorHold
+  call assert_equal(2, line('.'))
+
+  bwipe!
+  unlet g:after_autocmd
+  au! CursorHold
+endfunc
+
 func Test_closing_autocmd_window()
   let lines =<< trim END
       edit Xa.txt
index e505e2d4c31d0ddb8ced0421a47b55ca54ad1aab..7f8fea4216ffa446bb4d1520bae49a8e87ab1e92 100644 (file)
@@ -927,6 +927,42 @@ func Test_usercmd_custom()
   delfunc T2
 endfunc
 
+" Test for a {} block in a command nested in :command or :autocmd
+func Test_usercmd_nested_block()
+  command DefineIt command DoNested {
+        g:didnested = 'yes'
+      }
+  DefineIt
+  DoNested
+  call assert_equal('yes', g:didnested)
+  unlet g:didnested
+  delcommand DoNested
+  delcommand DefineIt
+
+  " a command defined by an autocmd
+  autocmd CursorHold * command DoFromAu {
+        g:didfromau = 'yes'
+      }
+  doautocmd CursorHold
+  DoFromAu
+  call assert_equal('yes', g:didfromau)
+  unlet g:didfromau
+  delcommand DoFromAu
+  au! CursorHold
+
+  " a trailing "{" that is an argument of the command is not a block
+  let lines =<< trim END
+      vim9script
+      command NoBlock normal! {
+      g:after_command = 'reached'
+  END
+  call writefile(lines, 'XcmdTrailingCurly', 'D')
+  source XcmdTrailingCurly
+  call assert_equal('reached', g:after_command)
+  unlet g:after_command
+  delcommand NoBlock
+endfunc
+
 func Test_usercmd_with_block()
   command DoSomething {
         g:didit = 'yes'  # comment
index b86acd071ea01f09203cf5871f6e0eeff3d99283..c82c01a0fa0b171ac126e124844939255c731db1 100644 (file)
@@ -485,6 +485,47 @@ def Test_command_block()
   unlet g:someVar
 enddef
 
+" Test for a {} block in an :autocmd nested in another :autocmd
+def Test_nested_autocmd_block_in_def()
+  au CursorHold * autocmd BufNew *.xml {
+        g:nestedVar = 'nested'
+      }
+  doautocmd CursorHold
+  split other.xml
+  assert_equal('nested', g:nestedVar)
+
+  bwipe!
+  au! CursorHold
+  au! BufNew *.xml
+  unlet g:nestedVar
+enddef
+
+" A trailing "{" that is an argument of the command does not start a block.
+" Use a separate script, when the "{" is taken for a block the rest of this
+" file would be swallowed until a line starting with "}".
+def Test_autocmd_trailing_curly_no_block_in_def()
+  var lines =<< trim END
+      vim9script
+      def Setup()
+        au CursorHold * normal! {
+        g:afterCurly = 'reached'
+      enddef
+      Setup()
+  END
+  v9.CheckScriptSuccess(lines)
+  assert_equal('reached', g:afterCurly)
+
+  new
+  setline(1, ['one', '', 'two'])
+  cursor(3, 1)
+  doautocmd CursorHold
+  assert_equal(2, line('.'))
+
+  bwipe!
+  unlet g:afterCurly
+  au! CursorHold
+enddef
+
 " Test for using heredoc in a :command command block
 def Test_command_block_heredoc()
   var lines =<< trim CODE
index 412ac0ae7a4ff777f4a8a27ea32a34b9e30c303d..9dcd0f8066993046dd02e3081c12cedae5619203 100644 (file)
@@ -1322,16 +1322,16 @@ fail:
 }
 
 /*
- * If "p" starts with "{" then read a block of commands until "}".
+ * If "p" starts a block of commands, read it until "}".
  * Used for ":command" and ":autocmd".
  */
     char_u *
 may_get_cmd_block(exarg_T *eap, char_u *p, char_u **tofree, int *flags)
 {
     char_u *retp = p;
+    char_u *block = find_cmd_block_start(p);
 
-    if (*p == '{' && ends_excmd2(eap->arg, skipwhite(p + 1))
-                                                   && eap->ea_getline != NULL)
+    if (block != NULL && eap->ea_getline != NULL)
     {
        garray_T    ga;
        char_u      *line = NULL;
@@ -1364,7 +1364,10 @@ may_get_cmd_block(exarg_T *eap, char_u *p, char_u **tofree, int *flags)
        if (retp == NULL)
            retp = p;
        ga_clear_strings(&ga);
-       *flags |= UC_VIM9;
+       // Only the command owning the block uses Vim9 syntax.  A command with
+       // the block nested in it keeps the syntax of its script.
+       if (block == p)
+           *flags |= UC_VIM9;
     }
     return retp;
 }
index 4dddf621d5e0945a86ea15741739378803e7dede..0cc9e31e33aeaf88f4dd0a119bc534924658fbe0 100644 (file)
@@ -1257,14 +1257,7 @@ get_function_body(
                        --end;
                    is_block = end > p + 2 && end[-1] == '=' && end[0] == '>';
                    if (!is_block)
-                   {
-                       char_u *s = p;
-
-                       // check for line starting with "au" for :autocmd or
-                       // "com" for :command, these can use a {} block
-                       is_block = checkforcmd_noparen(&s, "autocmd", 2)
-                                     || checkforcmd_noparen(&s, "command", 3);
-                   }
+                       is_block = find_cmd_block_start(p) != NULL;
 
                    if (is_block)
                    {
index 5a748f4996d96409ecbde5b7e844a237403313f1..bc604af0b3b1543b23b693522f3bb1a16429e6e9 100644 (file)
@@ -763,6 +763,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    908,
 /**/
     907,
 /**/
index 1a8ff15f48a3ee0840ce560e34f7020510b8dc12..01643b83d9c0f47190ca11a0d0af0e42d6050f4d 100644 (file)
@@ -2345,30 +2345,18 @@ compile_exec(char_u *line_arg, exarg_T *eap, cctx_T *cctx)
        }
        else if (eap->cmdidx == CMD_command || eap->cmdidx == CMD_autocmd)
        {
-           // If there is a trailing '{' read lines until the '}'
-           p = eap->arg + STRLEN(eap->arg) - 1;
-           while (p > eap->arg && VIM_ISWHITE(*p))
-               --p;
-           if (*p == '{')
+           exarg_T ea;
+           int     flags = 0;  // unused
+           int     start_lnum = SOURCING_LNUM;
+
+           CLEAR_FIELD(ea);
+           ea.arg = eap->arg;
+           fill_exarg_from_cctx(&ea, cctx);
+           p = may_get_cmd_block(&ea, line, &tofree, &flags);
+           if (tofree != NULL)
            {
-               exarg_T ea;
-               int     flags = 0;  // unused
-               int     start_lnum = SOURCING_LNUM;
-
-               CLEAR_FIELD(ea);
-               ea.arg = eap->arg;
-               fill_exarg_from_cctx(&ea, cctx);
-               (void)may_get_cmd_block(&ea, p, &tofree, &flags);
-               if (tofree != NULL)
-               {
-                   *p = NUL;
-                   line = concat_str(line, tofree);
-                   if (line == NULL)
-                       goto theend;
-                   vim_free(tofree);
-                   tofree = line;
-                   SOURCING_LNUM = start_lnum;
-               }
+               line = p;
+               SOURCING_LNUM = start_lnum;
            }
        }
     }