// This is an open source non-commercial project. Dear PVS-Studio, please check
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// autocmd.c: Autocommand related functions

#include "nvim/api/private/helpers.h"
#include "nvim/ascii.h"
#include "nvim/autocmd.h"
#include "nvim/buffer.h"
#include "nvim/charset.h"
#include "nvim/cursor.h"
#include "nvim/edit.h"
#include "nvim/eval.h"
#include "nvim/eval/userfunc.h"
#include "nvim/ex_docmd.h"
#include "nvim/fileio.h"
#include "nvim/getchar.h"
#include "nvim/misc1.h"
#include "nvim/option.h"
#include "nvim/regexp.h"
#include "nvim/search.h"
#include "nvim/state.h"
#include "nvim/ui_compositor.h"
#include "nvim/vim.h"

#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "auevents_name_map.generated.h"
# include "autocmd.c.generated.h"
#endif

//
// The autocommands are stored in a list for each event.
// Autocommands for the same pattern, that are consecutive, are joined
// together, to avoid having to match the pattern too often.
// The result is an array of Autopat lists, which point to AutoCmd lists:
//
// last_autopat[0]  -----------------------------+
//                                               V
// first_autopat[0] --> Autopat.next  -->  Autopat.next -->  NULL
//                      Autopat.cmds       Autopat.cmds
//                          |                    |
//                          V                    V
//                      AutoCmd.next       AutoCmd.next
//                          |                    |
//                          V                    V
//                      AutoCmd.next            NULL
//                          |
//                          V
//                         NULL
//
// last_autopat[1]  --------+
//                          V
// first_autopat[1] --> Autopat.next  -->  NULL
//                      Autopat.cmds
//                          |
//                          V
//                      AutoCmd.next
//                          |
//                          V
//                         NULL
//   etc.
//
//   The order of AutoCmds is important, this is the order in which they were
//   defined and will have to be executed.
//

// Code for automatic commands.
static AutoPatCmd *active_apc_list = NULL;  // stack of active autocommands

/// List of autocmd group names
static garray_T augroups = { 0, 0, sizeof(char_u *), 10, NULL };
#define AUGROUP_NAME(i) (((char **)augroups.ga_data)[i])
#define BUFLOCAL_PAT_LEN 25

// use get_deleted_augroup() to get this
static const char *deleted_augroup = NULL;

// The ID of the current group.  Group 0 is the default one.
static int current_augroup = AUGROUP_DEFAULT;

static int au_need_clean = false;  // need to delete marked patterns

static event_T last_event;
static int last_group;
static int autocmd_blocked = 0;  // block all autocmds

static bool autocmd_nested = false;
static bool autocmd_include_groups = false;

static char_u *old_termresponse = NULL;

static inline const char *get_deleted_augroup(void) FUNC_ATTR_ALWAYS_INLINE
{
  if (deleted_augroup == NULL) {
    deleted_augroup = _("--Deleted--");
  }
  return deleted_augroup;
}

// Show the autocommands for one AutoPat.
static void show_autocmd(AutoPat *ap, event_T event)
{
  AutoCmd *ac;

  // Check for "got_int" (here and at various places below), which is set
  // when "q" has been hit for the "--more--" prompt
  if (got_int) {
    return;
  }
  // pattern has been removed
  if (ap->pat == NULL) {
    return;
  }

  msg_putchar('\n');
  if (got_int) {
    return;
  }
  if (event != last_event || ap->group != last_group) {
    if (ap->group != AUGROUP_DEFAULT) {
      if (AUGROUP_NAME(ap->group) == NULL) {
        msg_puts_attr(get_deleted_augroup(), HL_ATTR(HLF_E));
      } else {
        msg_puts_attr(AUGROUP_NAME(ap->group), HL_ATTR(HLF_T));
      }
      msg_puts("  ");
    }
    msg_puts_attr(event_nr2name(event), HL_ATTR(HLF_T));
    last_event = event;
    last_group = ap->group;
    msg_putchar('\n');
    if (got_int) {
      return;
    }
  }
  msg_col = 4;
  msg_outtrans(ap->pat);

  for (ac = ap->cmds; ac != NULL; ac = ac->next) {
    if (ac->cmd == NULL) {  // skip removed commands
      continue;
    }
    if (msg_col >= 14) {
      msg_putchar('\n');
    }
    msg_col = 14;
    if (got_int) {
      return;
    }
    msg_outtrans(ac->cmd);
    if (p_verbose > 0) {
      last_set_msg(ac->script_ctx);
    }
    if (got_int) {
      return;
    }
    if (ac->next != NULL) {
      msg_putchar('\n');
      if (got_int) {
        return;
      }
    }
  }
}

// Mark an autocommand handler for deletion.
static void au_remove_pat(AutoPat *ap)
{
  XFREE_CLEAR(ap->pat);
  ap->buflocal_nr = -1;
  au_need_clean = true;
}

// Mark all commands for a pattern for deletion.
static void au_remove_cmds(AutoPat *ap)
{
  for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) {
    XFREE_CLEAR(ac->cmd);
  }
  au_need_clean = true;
}

// Delete one command from an autocmd pattern.
static void au_del_cmd(AutoCmd *ac)
{
  XFREE_CLEAR(ac->cmd);
  au_need_clean = true;
}

/// Cleanup autocommands and patterns that have been deleted.
/// This is only done when not executing autocommands.
static void au_cleanup(void)
{
  AutoPat *ap, **prev_ap;
  event_T event;

  if (autocmd_busy || !au_need_clean) {
    return;
  }

  // Loop over all events.
  for (event = (event_T)0; (int)event < NUM_EVENTS;
       event = (event_T)((int)event + 1)) {
    // Loop over all autocommand patterns.
    prev_ap = &(first_autopat[(int)event]);
    for (ap = *prev_ap; ap != NULL; ap = *prev_ap) {
      bool has_cmd = false;

      // Loop over all commands for this pattern.
      AutoCmd **prev_ac = &(ap->cmds);
      for (AutoCmd *ac = *prev_ac; ac != NULL; ac = *prev_ac) {
        // Remove the command if the pattern is to be deleted or when
        // the command has been marked for deletion.
        if (ap->pat == NULL || ac->cmd == NULL) {
          *prev_ac = ac->next;
          xfree(ac->cmd);
          xfree(ac);
        } else {
          has_cmd = true;
          prev_ac = &(ac->next);
        }
      }

      if (ap->pat != NULL && !has_cmd) {
        // Pattern was not marked for deletion, but all of its commands were.
        // So mark the pattern for deletion.
        au_remove_pat(ap);
      }

      // Remove the pattern if it has been marked for deletion.
      if (ap->pat == NULL) {
        if (ap->next == NULL) {
          if (prev_ap == &(first_autopat[(int)event])) {
            last_autopat[(int)event] = NULL;
          } else {
            // this depends on the "next" field being the first in
            // the struct
            last_autopat[(int)event] = (AutoPat *)prev_ap;
          }
        }
        *prev_ap = ap->next;
        vim_regfree(ap->reg_prog);
        xfree(ap);
      } else {
        prev_ap = &(ap->next);
      }
    }
  }

  au_need_clean = false;
}

// Called when buffer is freed, to remove/invalidate related buffer-local
// autocmds.
void aubuflocal_remove(buf_T *buf)
{
  AutoPat *ap;
  event_T event;
  AutoPatCmd *apc;

  // invalidate currently executing autocommands
  for (apc = active_apc_list; apc; apc = apc->next) {
    if (buf->b_fnum == apc->arg_bufnr) {
      apc->arg_bufnr = 0;
    }
  }

  // invalidate buflocals looping through events
  for (event = (event_T)0; (int)event < NUM_EVENTS;
       event = (event_T)((int)event + 1)) {
    // loop over all autocommand patterns
    for (ap = first_autopat[(int)event]; ap != NULL; ap = ap->next) {
      if (ap->buflocal_nr == buf->b_fnum) {
        au_remove_pat(ap);
        if (p_verbose >= 6) {
          verbose_enter();
          smsg(_("auto-removing autocommand: %s <buffer=%d>"),
               event_nr2name(event), buf->b_fnum);
          verbose_leave();
        }
      }
    }
  }
  au_cleanup();
}

// Add an autocmd group name.
// Return its ID.  Returns AUGROUP_ERROR (< 0) for error.
static int au_new_group(char_u *name)
{
  int i = au_find_group(name);
  if (i == AUGROUP_ERROR) {  // the group doesn't exist yet, add it.
    // First try using a free entry.
    for (i = 0; i < augroups.ga_len; i++) {
      if (AUGROUP_NAME(i) == NULL) {
        break;
      }
    }
    if (i == augroups.ga_len) {
      ga_grow(&augroups, 1);
    }

    AUGROUP_NAME(i) = xstrdup((char *)name);
    if (i == augroups.ga_len) {
      augroups.ga_len++;
    }
  }

  return i;
}

static void au_del_group(char_u *name)
{
  int i = au_find_group(name);
  if (i == AUGROUP_ERROR) {  // the group doesn't exist
    semsg(_("E367: No such group: \"%s\""), name);
  } else if (i == current_augroup) {
    emsg(_("E936: Cannot delete the current group"));
  } else {
    event_T event;
    AutoPat *ap;
    int in_use = false;

    for (event = (event_T)0; (int)event < NUM_EVENTS;
         event = (event_T)((int)event + 1)) {
      for (ap = first_autopat[(int)event]; ap != NULL; ap = ap->next) {
        if (ap->group == i && ap->pat != NULL) {
          give_warning((char_u *)_("W19: Deleting augroup that is still in use"), true);
          in_use = true;
          event = NUM_EVENTS;
          break;
        }
      }
    }
    xfree(AUGROUP_NAME(i));
    if (in_use) {
      AUGROUP_NAME(i) = (char *)get_deleted_augroup();
    } else {
      AUGROUP_NAME(i) = NULL;
    }
  }
}

/// Find the ID of an autocmd group name.
///
/// @param name augroup name
///
/// @return the ID or AUGROUP_ERROR (< 0) for error.
static int au_find_group(const char_u *name)
  FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
  for (int i = 0; i < augroups.ga_len; i++) {
    if (AUGROUP_NAME(i) != NULL && AUGROUP_NAME(i) != get_deleted_augroup()
        && STRCMP(AUGROUP_NAME(i), name) == 0) {
      return i;
    }
  }
  return AUGROUP_ERROR;
}

/// Return true if augroup "name" exists.
///
/// @param name augroup name
bool au_has_group(const char_u *name)
  FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
  return au_find_group(name) != AUGROUP_ERROR;
}

/// ":augroup {name}".
void do_augroup(char_u *arg, int del_group)
{
  if (del_group) {
    if (*arg == NUL) {
      emsg(_(e_argreq));
    } else {
      au_del_group(arg);
    }
  } else if (STRICMP(arg, "end") == 0) {  // ":aug end": back to group 0
    current_augroup = AUGROUP_DEFAULT;
  } else if (*arg) {  // ":aug xxx": switch to group xxx
    int i = au_new_group(arg);
    if (i != AUGROUP_ERROR) {
      current_augroup = i;
    }
  } else {  // ":aug": list the group names
    msg_start();
    for (int i = 0; i < augroups.ga_len; i++) {
      if (AUGROUP_NAME(i) != NULL) {
        msg_puts(AUGROUP_NAME(i));
        msg_puts("  ");
      }
    }
    msg_clr_eos();
    msg_end();
  }
}

#if defined(EXITFREE)
void free_all_autocmds(void)
{
  for (current_augroup = -1; current_augroup < augroups.ga_len;
       current_augroup++) {
    do_autocmd((char_u *)"", true);
  }

  for (int i = 0; i < augroups.ga_len; i++) {
    char *const s = ((char **)(augroups.ga_data))[i];
    if ((const char *)s != get_deleted_augroup()) {
      xfree(s);
    }
  }
  ga_clear(&augroups);
}
#endif

// Return the event number for event name "start".
// Return NUM_EVENTS if the event name was not found.
// Return a pointer to the next event name in "end".
static event_T event_name2nr(const char_u *start, char_u **end)
{
  const char_u *p;
  int i;
  int len;

  // the event name ends with end of line, '|', a blank or a comma
  for (p = start; *p && !ascii_iswhite(*p) && *p != ',' && *p != '|'; p++) {
  }
  for (i = 0; event_names[i].name != NULL; i++) {
    len = (int)event_names[i].len;
    if (len == p - start && STRNICMP(event_names[i].name, start, len) == 0) {
      break;
    }
  }
  if (*p == ',') {
    p++;
  }
  *end = (char_u *)p;
  if (event_names[i].name == NULL) {
    return NUM_EVENTS;
  }
  return event_names[i].event;
}

/// Return the name for event
///
/// @param[in]  event  Event to return name for.
///
/// @return Event name, static string. Returns "Unknown" for unknown events.
static const char *event_nr2name(event_T event)
  FUNC_ATTR_NONNULL_RET FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_CONST
{
  int i;

  for (i = 0; event_names[i].name != NULL; i++) {
    if (event_names[i].event == event) {
      return event_names[i].name;
    }
  }
  return "Unknown";
}

/// Scan over the events.  "*" stands for all events.
/// true when group name was found
static char_u *find_end_event(char_u *arg, int have_group)
{
  char_u *pat;
  char_u *p;

  if (*arg == '*') {
    if (arg[1] && !ascii_iswhite(arg[1])) {
      semsg(_("E215: Illegal character after *: %s"), arg);
      return NULL;
    }
    pat = arg + 1;
  } else {
    for (pat = arg; *pat && *pat != '|' && !ascii_iswhite(*pat); pat = p) {
      if ((int)event_name2nr(pat, &p) >= NUM_EVENTS) {
        if (have_group) {
          semsg(_("E216: No such event: %s"), pat);
        } else {
          semsg(_("E216: No such group or event: %s"), pat);
        }
        return NULL;
      }
    }
  }
  return pat;
}

/// Return true if "event" is included in 'eventignore'.
///
/// @param event event to check
static bool event_ignored(event_T event)
  FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
  char_u *p = p_ei;

  while (*p != NUL) {
    if (STRNICMP(p, "all", 3) == 0 && (p[3] == NUL || p[3] == ',')) {
      return true;
    }
    if (event_name2nr(p, &p) == event) {
      return true;
    }
  }

  return false;
}

// Return OK when the contents of p_ei is valid, FAIL otherwise.
int check_ei(void)
{
  char_u *p = p_ei;

  while (*p) {
    if (STRNICMP(p, "all", 3) == 0 && (p[3] == NUL || p[3] == ',')) {
      p += 3;
      if (*p == ',') {
        p++;
      }
    } else if (event_name2nr(p, &p) == NUM_EVENTS) {
      return FAIL;
    }
  }

  return OK;
}

// Add "what" to 'eventignore' to skip loading syntax highlighting for every
// buffer loaded into the window.  "what" must start with a comma.
// Returns the old value of 'eventignore' in allocated memory.
char_u *au_event_disable(char *what)
{
  char_u *new_ei;
  char_u *save_ei;

  save_ei = vim_strsave(p_ei);
  new_ei = vim_strnsave(p_ei, STRLEN(p_ei) + STRLEN(what));
  if (*what == ',' && *p_ei == NUL) {
    STRCPY(new_ei, what + 1);
  } else {
    STRCAT(new_ei, what);
  }
  set_string_option_direct("ei", -1, new_ei, OPT_FREE, SID_NONE);
  xfree(new_ei);

  return save_ei;
}

void au_event_restore(char_u *old_ei)
{
  if (old_ei != NULL) {
    set_string_option_direct("ei", -1, old_ei, OPT_FREE, SID_NONE);
    xfree(old_ei);
  }
}

// Implements :autocmd.
// Defines an autocmd (does not execute; cf. apply_autocmds_group).
//
// Can be used in the following ways:
//
// :autocmd <event> <pat> <cmd>     Add <cmd> to the list of commands that
//                                  will be automatically executed for <event>
//                                  when editing a file matching <pat>, in
//                                  the current group.
// :autocmd <event> <pat>           Show the autocommands associated with
//                                  <event> and <pat>.
// :autocmd <event>                 Show the autocommands associated with
//                                  <event>.
// :autocmd                         Show all autocommands.
// :autocmd! <event> <pat> <cmd>    Remove all autocommands associated with
//                                  <event> and <pat>, and add the command
//                                  <cmd>, for the current group.
// :autocmd! <event> <pat>          Remove all autocommands associated with
//                                  <event> and <pat> for the current group.
// :autocmd! <event>                Remove all autocommands associated with
//                                  <event> for the current group.
// :autocmd!                        Remove ALL autocommands for the current
//                                  group.
//
//  Multiple events and patterns may be given separated by commas.  Here are
//  some examples:
// :autocmd bufread,bufenter *.c,*.h    set tw=0 smartindent noic
// :autocmd bufleave         *          set tw=79 nosmartindent ic infercase
//
// :autocmd * *.c               show all autocommands for *.c files.
//
// Mostly a {group} argument can optionally appear before <event>.
void do_autocmd(char_u *arg_in, int forceit)
{
  char_u *arg = arg_in;
  char_u *pat;
  char_u *envpat = NULL;
  char_u *cmd;
  int need_free = false;
  int nested = false;
  bool once = false;
  int group;

  if (*arg == '|') {
    arg = (char_u *)"";
    group = AUGROUP_ALL;  // no argument, use all groups
  } else {
    // Check for a legal group name.  If not, use AUGROUP_ALL.
    group = au_get_grouparg(&arg);
  }

  // Scan over the events.
  // If we find an illegal name, return here, don't do anything.
  pat = find_end_event(arg, group != AUGROUP_ALL);
  if (pat == NULL) {
    return;
  }

  pat = skipwhite(pat);
  if (*pat == '|') {
    pat = (char_u *)"";
    cmd = (char_u *)"";
  } else {
    // Scan over the pattern.  Put a NUL at the end.
    cmd = pat;
    while (*cmd && (!ascii_iswhite(*cmd) || cmd[-1] == '\\')) {
      cmd++;
    }
    if (*cmd) {
      *cmd++ = NUL;
    }

    // Expand environment variables in the pattern.  Set 'shellslash', we want
    // forward slashes here.
    if (vim_strchr(pat, '$') != NULL || vim_strchr(pat, '~') != NULL) {
#ifdef BACKSLASH_IN_FILENAME
      int p_ssl_save = p_ssl;

      p_ssl = true;
#endif
      envpat = expand_env_save(pat);
#ifdef BACKSLASH_IN_FILENAME
      p_ssl = p_ssl_save;
#endif
      if (envpat != NULL) {
        pat = envpat;
      }
    }

    cmd = skipwhite(cmd);
    for (size_t i = 0; i < 2; i++) {
      if (*cmd != NUL) {
        // Check for "++once" flag.
        if (STRNCMP(cmd, "++once", 6) == 0 && ascii_iswhite(cmd[6])) {
          if (once) {
            semsg(_(e_duparg2), "++once");
          }
          once = true;
          cmd = skipwhite(cmd + 6);
        }

        // Check for "++nested" flag.
        if ((STRNCMP(cmd, "++nested", 8) == 0 && ascii_iswhite(cmd[8]))) {
          if (nested) {
            semsg(_(e_duparg2), "++nested");
          }
          nested = true;
          cmd = skipwhite(cmd + 8);
        }

        // Check for the old (deprecated) "nested" flag.
        if (STRNCMP(cmd, "nested", 6) == 0 && ascii_iswhite(cmd[6])) {
          if (nested) {
            semsg(_(e_duparg2), "nested");
          }
          nested = true;
          cmd = skipwhite(cmd + 6);
        }
      }
    }

    // Find the start of the commands.
    // Expand <sfile> in it.
    if (*cmd != NUL) {
      cmd = expand_sfile(cmd);
      if (cmd == NULL) {  // some error
        return;
      }
      need_free = true;
    }
  }

  // Print header when showing autocommands.
  if (!forceit && *cmd == NUL) {
    // Highlight title
    msg_puts_title(_("\n--- Autocommands ---"));
  }

  // Loop over the events.
  last_event = (event_T)-1;    // for listing the event name
  last_group = AUGROUP_ERROR;  // for listing the group name
  if (*arg == '*' || *arg == NUL || *arg == '|') {
    if (!forceit && *cmd != NUL) {
      emsg(_(e_cannot_define_autocommands_for_all_events));
    } else {
      for (event_T event = (event_T)0; event < NUM_EVENTS;
           event = (event_T)(event + 1)) {
        if (do_autocmd_event(event, pat, once, nested, cmd, forceit, group)
            == FAIL) {
          break;
        }
      }
    }
  } else {
    while (*arg && *arg != '|' && !ascii_iswhite(*arg)) {
      event_T event = event_name2nr(arg, &arg);
      assert(event < NUM_EVENTS);
      if (do_autocmd_event(event, pat, once, nested, cmd, forceit, group)
          == FAIL) {
        break;
      }
    }
  }

  if (need_free) {
    xfree(cmd);
  }
  xfree(envpat);
}

// Find the group ID in a ":autocmd" or ":doautocmd" argument.
// The "argp" argument is advanced to the following argument.
//
// Returns the group ID or AUGROUP_ALL.
static int au_get_grouparg(char_u **argp)
{
  char_u *group_name;
  char_u *p;
  char_u *arg = *argp;
  int group = AUGROUP_ALL;

  for (p = arg; *p && !ascii_iswhite(*p) && *p != '|'; p++) {
  }
  if (p > arg) {
    group_name = vim_strnsave(arg, (size_t)(p - arg));
    group = au_find_group(group_name);
    if (group == AUGROUP_ERROR) {
      group = AUGROUP_ALL;  // no match, use all groups
    } else {
      *argp = skipwhite(p);  // match, skip over group name
    }
    xfree(group_name);
  }
  return group;
}

// do_autocmd() for one event.
// Defines an autocmd (does not execute; cf. apply_autocmds_group).
//
// If *pat == NUL: do for all patterns.
// If *cmd == NUL: show entries.
// If forceit == true: delete entries.
// If group is not AUGROUP_ALL: only use this group.
static int do_autocmd_event(event_T event, char_u *pat, bool once, int nested, char_u *cmd,
                            int forceit, int group)
{
  AutoPat *ap;
  AutoPat **prev_ap;
  AutoCmd *ac;
  AutoCmd **prev_ac;
  int brace_level;
  char_u *endpat;
  int findgroup;
  int allgroups;
  int patlen;
  int is_buflocal;
  int buflocal_nr;
  char_u buflocal_pat[BUFLOCAL_PAT_LEN];  // for "<buffer=X>"

  if (group == AUGROUP_ALL) {
    findgroup = current_augroup;
  } else {
    findgroup = group;
  }
  allgroups = (group == AUGROUP_ALL && !forceit && *cmd == NUL);

  // Show or delete all patterns for an event.
  if (*pat == NUL) {
    for (ap = first_autopat[event]; ap != NULL; ap = ap->next) {
      if (forceit) {  // delete the AutoPat, if it's in the current group
        if (ap->group == findgroup) {
          au_remove_pat(ap);
        }
      } else if (group == AUGROUP_ALL || ap->group == group) {
        show_autocmd(ap, event);
      }
    }
  }

  // Loop through all the specified patterns.
  for (; *pat; pat = (*endpat == ',' ? endpat + 1 : endpat)) {
    // Find end of the pattern.
    // Watch out for a comma in braces, like "*.\{obj,o\}".
    endpat = pat;
    // ignore single comma
    if (*endpat == ',') {
      continue;
    }
    brace_level = 0;
    for (; *endpat && (*endpat != ',' || brace_level || endpat[-1] == '\\');
         endpat++) {
      if (*endpat == '{') {
        brace_level++;
      } else if (*endpat == '}') {
        brace_level--;
      }
    }
    patlen = (int)(endpat - pat);

    // detect special <buflocal[=X]> buffer-local patterns
    is_buflocal = false;
    buflocal_nr = 0;

    if (patlen >= 8 && STRNCMP(pat, "<buffer", 7) == 0
        && pat[patlen - 1] == '>') {
      // "<buffer...>": Error will be printed only for addition.
      // printing and removing will proceed silently.
      is_buflocal = true;
      if (patlen == 8) {
        // "<buffer>"
        buflocal_nr = curbuf->b_fnum;
      } else if (patlen > 9 && pat[7] == '=') {
        if (patlen == 13 && STRNICMP(pat, "<buffer=abuf>", 13) == 0) {
          // "<buffer=abuf>"
          buflocal_nr = autocmd_bufnr;
        } else if (skipdigits(pat + 8) == pat + patlen - 1) {
          // "<buffer=123>"
          buflocal_nr = atoi((char *)pat + 8);
        }
      }
    }

    if (is_buflocal) {
      // normalize pat into standard "<buffer>#N" form
      snprintf((char *)buflocal_pat,
               BUFLOCAL_PAT_LEN,
               "<buffer=%d>",
               buflocal_nr);

      pat = buflocal_pat;                  // can modify pat and patlen
      patlen = (int)STRLEN(buflocal_pat);  //   but not endpat
    }

    // Find AutoPat entries with this pattern.  When adding a command it
    // always goes at or after the last one, so start at the end.
    if (!forceit && *cmd != NUL && last_autopat[(int)event] != NULL) {
      prev_ap = &last_autopat[(int)event];
    } else {
      prev_ap = &first_autopat[(int)event];
    }
    while ((ap = *prev_ap) != NULL) {
      if (ap->pat != NULL) {
        // Accept a pattern when:
        // - a group was specified and it's that group, or a group was
        //   not specified and it's the current group, or a group was
        //   not specified and we are listing
        // - the length of the pattern matches
        // - the pattern matches.
        // For <buffer[=X]>, this condition works because we normalize
        // all buffer-local patterns.
        if ((allgroups || ap->group == findgroup) && ap->patlen == patlen
            && STRNCMP(pat, ap->pat, patlen) == 0) {
          // Remove existing autocommands.
          // If adding any new autocmd's for this AutoPat, don't
          // delete the pattern from the autopat list, append to
          // this list.
          if (forceit) {
            if (*cmd != NUL && ap->next == NULL) {
              au_remove_cmds(ap);
              break;
            }
            au_remove_pat(ap);
          } else if (*cmd == NUL) {
            // Show autocmd's for this autopat, or buflocals <buffer=X>
            show_autocmd(ap, event);
          } else if (ap->next == NULL) {
            // Add autocmd to this autopat, if it's the last one.
            break;
          }
        }
      }
      prev_ap = &ap->next;
    }

    // Add a new command.
    if (*cmd != NUL) {
      // If the pattern we want to add a command to does appear at the
      // end of the list (or not is not in the list at all), add the
      // pattern at the end of the list.
      if (ap == NULL) {
        // refuse to add buffer-local ap if buffer number is invalid
        if (is_buflocal
            && (buflocal_nr == 0 || buflist_findnr(buflocal_nr) == NULL)) {
          semsg(_("E680: <buffer=%d>: invalid buffer number "), buflocal_nr);
          return FAIL;
        }

        ap = xmalloc(sizeof(AutoPat));
        ap->pat = vim_strnsave(pat, (size_t)patlen);
        ap->patlen = patlen;

        if (is_buflocal) {
          ap->buflocal_nr = buflocal_nr;
          ap->reg_prog = NULL;
        } else {
          char_u *reg_pat;

          ap->buflocal_nr = 0;
          reg_pat = file_pat_to_reg_pat(pat, endpat, &ap->allow_dirs, true);
          if (reg_pat != NULL) {
            ap->reg_prog = vim_regcomp(reg_pat, RE_MAGIC);
          }
          xfree(reg_pat);
          if (reg_pat == NULL || ap->reg_prog == NULL) {
            xfree(ap->pat);
            xfree(ap);
            return FAIL;
          }
        }
        ap->cmds = NULL;
        *prev_ap = ap;
        last_autopat[(int)event] = ap;
        ap->next = NULL;
        if (group == AUGROUP_ALL) {
          ap->group = current_augroup;
        } else {
          ap->group = group;
        }
      }

      // Add the autocmd at the end of the AutoCmd list.
      prev_ac = &(ap->cmds);
      while ((ac = *prev_ac) != NULL) {
        prev_ac = &ac->next;
      }
      ac = xmalloc(sizeof(AutoCmd));
      ac->cmd = vim_strsave(cmd);
      ac->script_ctx = current_sctx;
      ac->script_ctx.sc_lnum += sourcing_lnum;
      ac->next = NULL;
      *prev_ac = ac;
      ac->once = once;
      ac->nested = nested;
    }
  }

  au_cleanup();  // may really delete removed patterns/commands now
  return OK;
}

/// Implementation of ":doautocmd [group] event [fname]".
/// Return OK for success, FAIL for failure;
///
/// @param do_msg  give message for no matching autocmds?
int do_doautocmd(char_u *arg, bool do_msg, bool *did_something)
{
  char_u *fname;
  int nothing_done = true;
  int group;

  if (did_something != NULL) {
    *did_something = false;
  }

  // Check for a legal group name.  If not, use AUGROUP_ALL.
  group = au_get_grouparg(&arg);

  if (*arg == '*') {
    emsg(_("E217: Can't execute autocommands for ALL events"));
    return FAIL;
  }

  // Scan over the events.
  // If we find an illegal name, return here, don't do anything.
  fname = find_end_event(arg, group != AUGROUP_ALL);
  if (fname == NULL) {
    return FAIL;
  }

  fname = skipwhite(fname);

  // Loop over the events.
  while (*arg && !ends_excmd(*arg) && !ascii_iswhite(*arg)) {
    if (apply_autocmds_group(event_name2nr(arg, &arg), fname, NULL, true, group,
                             curbuf, NULL)) {
      nothing_done = false;
    }
  }

  if (nothing_done && do_msg) {
    msg(_("No matching autocommands"));
  }
  if (did_something != NULL) {
    *did_something = !nothing_done;
  }

  return aborting() ? FAIL : OK;
}

// ":doautoall": execute autocommands for each loaded buffer.
void ex_doautoall(exarg_T *eap)
{
  int retval = OK;
  aco_save_T aco;
  char_u *arg = eap->arg;
  int call_do_modelines = check_nomodeline(&arg);
  bufref_T bufref;
  bool did_aucmd;

  // This is a bit tricky: For some commands curwin->w_buffer needs to be
  // equal to curbuf, but for some buffers there may not be a window.
  // So we change the buffer for the current window for a moment.  This
  // gives problems when the autocommands make changes to the list of
  // buffers or windows...
  FOR_ALL_BUFFERS(buf) {
    // Only do loaded buffers and skip the current buffer, it's done last.
    if (buf->b_ml.ml_mfp == NULL || buf == curbuf) {
      continue;
    }
    // Find a window for this buffer and save some values.
    aucmd_prepbuf(&aco, buf);
    set_bufref(&bufref, buf);

    // execute the autocommands for this buffer
    retval = do_doautocmd(arg, false, &did_aucmd);

    if (call_do_modelines && did_aucmd) {
      // Execute the modeline settings, but don't set window-local
      // options if we are using the current window for another
      // buffer.
      do_modelines(curwin == aucmd_win ? OPT_NOWIN : 0);
    }

    // restore the current window
    aucmd_restbuf(&aco);

    // Stop if there is some error or buffer was deleted.
    if (retval == FAIL || !bufref_valid(&bufref)) {
      retval = FAIL;
      break;
    }
  }

  // Execute autocommands for the current buffer last.
  if (retval == OK) {
    (void)do_doautocmd(arg, false, &did_aucmd);
    if (call_do_modelines && did_aucmd) {
      do_modelines(0);
    }
  }

  check_cursor();  // just in case lines got deleted
}

/// Check *argp for <nomodeline>.  When it is present return false, otherwise
/// return true and advance *argp to after it. Thus do_modelines() should be
/// called when true is returned.
///
/// @param[in,out] argp argument string
bool check_nomodeline(char_u **argp)
  FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
{
  if (STRNCMP(*argp, "<nomodeline>", 12) == 0) {
    *argp = skipwhite(*argp + 12);
    return false;
  }
  return true;
}

/// Prepare for executing autocommands for (hidden) buffer `buf`.
/// If the current buffer is not in any visible window, put it in a temporary
/// floating window `aucmd_win`.
/// Set `curbuf` and `curwin` to match `buf`.
///
/// @param aco  structure to save values in
/// @param buf  new curbuf
void aucmd_prepbuf(aco_save_T *aco, buf_T *buf)
{
  win_T *win;
  bool need_append = true;  // Append `aucmd_win` to the window list.

  // Find a window that is for the new buffer
  if (buf == curbuf) {  // be quick when buf is curbuf
    win = curwin;
  } else {
    win = NULL;
    FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
      if (wp->w_buffer == buf) {
        win = wp;
        break;
      }
    }
  }

  // Allocate the `aucmd_win` dummy floating window.
  if (win == NULL && aucmd_win == NULL) {
    win_alloc_aucmd_win();
    need_append = false;
  }
  if (win == NULL && aucmd_win_used) {
    // Strange recursive autocommand, fall back to using the current
    // window.  Expect a few side effects...
    win = curwin;
  }

  aco->save_curwin_handle = curwin->handle;
  aco->save_curbuf = curbuf;
  aco->save_prevwin_handle = prevwin == NULL ? 0 : prevwin->handle;
  if (win != NULL) {
    // There is a window for "buf" in the current tab page, make it the
    // curwin.  This is preferred, it has the least side effects (esp. if
    // "buf" is curbuf).
    aco->use_aucmd_win = false;
    curwin = win;
  } else {
    // There is no window for "buf", use "aucmd_win".  To minimize the side
    // effects, insert it in the current tab page.
    // Anything related to a window (e.g., setting folds) may have
    // unexpected results.
    aco->use_aucmd_win = true;
    aucmd_win_used = true;
    aucmd_win->w_buffer = buf;
    aucmd_win->w_s = &buf->b_s;
    buf->b_nwindows++;
    win_init_empty(aucmd_win);  // set cursor and topline to safe values

    // Make sure w_localdir and globaldir are NULL to avoid a chdir() in
    // win_enter_ext().
    XFREE_CLEAR(aucmd_win->w_localdir);
    aco->globaldir = globaldir;
    globaldir = NULL;

    block_autocmds();  // We don't want BufEnter/WinEnter autocommands.
    if (need_append) {
      win_append(lastwin, aucmd_win);
      pmap_put(handle_T)(&window_handles, aucmd_win->handle, aucmd_win);
      win_config_float(aucmd_win, aucmd_win->w_float_config);
    }
    // Prevent chdir() call in win_enter_ext(), through do_autochdir()
    int save_acd = p_acd;
    p_acd = false;
    win_enter(aucmd_win, false);
    p_acd = save_acd;
    unblock_autocmds();
    curwin = aucmd_win;
  }
  curbuf = buf;
  aco->new_curwin_handle = curwin->handle;
  set_bufref(&aco->new_curbuf, curbuf);
}

/// Cleanup after executing autocommands for a (hidden) buffer.
/// Restore the window as it was (if possible).
///
/// @param aco  structure holding saved values
void aucmd_restbuf(aco_save_T *aco)
{
  if (aco->use_aucmd_win) {
    curbuf->b_nwindows--;
    // Find "aucmd_win", it can't be closed, but it may be in another tab page.
    // Do not trigger autocommands here.
    block_autocmds();
    if (curwin != aucmd_win) {
      FOR_ALL_TAB_WINDOWS(tp, wp) {
        if (wp == aucmd_win) {
          if (tp != curtab) {
            goto_tabpage_tp(tp, true, true);
          }
          win_goto(aucmd_win);
          goto win_found;
        }
      }
    }
win_found:

    win_remove(curwin, NULL);
    pmap_del(handle_T)(&window_handles, curwin->handle);
    if (curwin->w_grid_alloc.chars != NULL) {
      ui_comp_remove_grid(&curwin->w_grid_alloc);
      ui_call_win_hide(curwin->w_grid_alloc.handle);
      grid_free(&curwin->w_grid_alloc);
    }

    aucmd_win_used = false;
    last_status(false);  // may need to remove last status line

    if (!valid_tabpage_win(curtab)) {
      // no valid window in current tabpage
      close_tabpage(curtab);
    }

    unblock_autocmds();

    win_T *const save_curwin = win_find_by_handle(aco->save_curwin_handle);
    if (save_curwin != NULL) {
      curwin = save_curwin;
    } else {
      // Hmm, original window disappeared.  Just use the first one.
      curwin = firstwin;
    }
    prevwin = win_find_by_handle(aco->save_prevwin_handle);
    vars_clear(&aucmd_win->w_vars->dv_hashtab);         // free all w: variables
    hash_init(&aucmd_win->w_vars->dv_hashtab);          // re-use the hashtab
    curbuf = curwin->w_buffer;

    xfree(globaldir);
    globaldir = aco->globaldir;

    // the buffer contents may have changed
    check_cursor();
    if (curwin->w_topline > curbuf->b_ml.ml_line_count) {
      curwin->w_topline = curbuf->b_ml.ml_line_count;
      curwin->w_topfill = 0;
    }
  } else {
    // Restore curwin.  Use the window ID, a window may have been closed
    // and the memory re-used for another one.
    win_T *const save_curwin = win_find_by_handle(aco->save_curwin_handle);
    if (save_curwin != NULL) {
      // Restore the buffer which was previously edited by curwin, if it was
      // changed, we are still the same window and the buffer is valid.
      if (curwin->handle == aco->new_curwin_handle
          && curbuf != aco->new_curbuf.br_buf
          && bufref_valid(&aco->new_curbuf)
          && aco->new_curbuf.br_buf->b_ml.ml_mfp != NULL) {
        if (curwin->w_s == &curbuf->b_s) {
          curwin->w_s = &aco->new_curbuf.br_buf->b_s;
        }
        curbuf->b_nwindows--;
        curbuf = aco->new_curbuf.br_buf;
        curwin->w_buffer = curbuf;
        curbuf->b_nwindows++;
      }

      curwin = save_curwin;
      curbuf = curwin->w_buffer;
      prevwin = win_find_by_handle(aco->save_prevwin_handle);
      // In case the autocommand moves the cursor to a position that does not
      // exist in curbuf
      check_cursor();
    }
  }
}

/// Execute autocommands for "event" and file name "fname".
///
/// @param event event that occurred
/// @param fname filename, NULL or empty means use actual file name
/// @param fname_io filename to use for <afile> on cmdline
/// @param force When true, ignore autocmd_busy
/// @param buf Buffer for <abuf>
///
/// @return true if some commands were executed.
bool apply_autocmds(event_T event, char_u *fname, char_u *fname_io, bool force, buf_T *buf)
{
  return apply_autocmds_group(event, fname, fname_io, force, AUGROUP_ALL, buf,
                              NULL);
}

/// Like apply_autocmds(), but with extra "eap" argument.  This takes care of
/// setting v:filearg.
///
/// @param event event that occurred
/// @param fname NULL or empty means use actual file name
/// @param fname_io fname to use for <afile> on cmdline
/// @param force When true, ignore autocmd_busy
/// @param buf Buffer for <abuf>
/// @param exarg Ex command arguments
///
/// @return true if some commands were executed.
bool apply_autocmds_exarg(event_T event, char_u *fname, char_u *fname_io, bool force, buf_T *buf,
                          exarg_T *eap)
{
  return apply_autocmds_group(event, fname, fname_io, force, AUGROUP_ALL, buf,
                              eap);
}

/// Like apply_autocmds(), but handles the caller's retval.  If the script
/// processing is being aborted or if retval is FAIL when inside a try
/// conditional, no autocommands are executed.  If otherwise the autocommands
/// cause the script to be aborted, retval is set to FAIL.
///
/// @param event event that occurred
/// @param fname NULL or empty means use actual file name
/// @param fname_io fname to use for <afile> on cmdline
/// @param force When true, ignore autocmd_busy
/// @param buf Buffer for <abuf>
/// @param[in,out] retval caller's retval
///
/// @return true if some autocommands were executed
bool apply_autocmds_retval(event_T event, char_u *fname, char_u *fname_io, bool force, buf_T *buf,
                           int *retval)
{
  if (should_abort(*retval)) {
    return false;
  }

  bool did_cmd = apply_autocmds_group(event, fname, fname_io, force,
                                      AUGROUP_ALL, buf, NULL);
  if (did_cmd && aborting()) {
    *retval = FAIL;
  }
  return did_cmd;
}

/// Return true if "event" autocommand is defined.
///
/// @param event the autocommand to check
bool has_event(event_T event) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
  return first_autopat[event] != NULL;
}

/// Return true when there is a CursorHold/CursorHoldI autocommand defined for
/// the current mode.
bool has_cursorhold(void) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
  return has_event((get_real_state() == NORMAL_BUSY ? EVENT_CURSORHOLD : EVENT_CURSORHOLDI));
  // return first_autopat[] != NULL;
}

/// Return true if the CursorHold/CursorHoldI event can be triggered.
bool trigger_cursorhold(void) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
  int state;

  if (!did_cursorhold && has_cursorhold() && reg_recording == 0
      && typebuf.tb_len == 0 && !ins_compl_active()) {
    state = get_real_state();
    if (state == NORMAL_BUSY || (state & INSERT) != 0) {
      return true;
    }
  }
  return false;
}

/// Execute autocommands for "event" and file name "fname".
///
/// @param event event that occurred
/// @param fname filename, NULL or empty means use actual file name
/// @param fname_io filename to use for <afile> on cmdline,
///                 NULL means use `fname`.
/// @param force When true, ignore autocmd_busy
/// @param group autocmd group ID or AUGROUP_ALL
/// @param buf Buffer for <abuf>
/// @param eap Ex command arguments
///
/// @return true if some commands were executed.
static bool apply_autocmds_group(event_T event, char_u *fname, char_u *fname_io, bool force,
                                 int group, buf_T *buf, exarg_T *eap)
{
  char_u *sfname = NULL;  // short file name
  char_u *tail;
  bool save_changed;
  buf_T *old_curbuf;
  bool retval = false;
  char_u *save_sourcing_name;
  linenr_T save_sourcing_lnum;
  char_u *save_autocmd_fname;
  int save_autocmd_bufnr;
  char_u *save_autocmd_match;
  int save_autocmd_busy;
  int save_autocmd_nested;
  static int nesting = 0;
  AutoPatCmd patcmd;
  AutoPat *ap;
  char_u *save_cmdarg;
  long save_cmdbang;
  static int filechangeshell_busy = false;
  proftime_T wait_time;
  bool did_save_redobuff = false;
  save_redo_T save_redo;
  const bool save_KeyTyped = KeyTyped;

  // Quickly return if there are no autocommands for this event or
  // autocommands are blocked.
  if (event == NUM_EVENTS || first_autopat[(int)event] == NULL
      || is_autocmd_blocked()) {
    goto BYPASS_AU;
  }

  // When autocommands are busy, new autocommands are only executed when
  // explicitly enabled with the "nested" flag.
  if (autocmd_busy && !(force || autocmd_nested)) {
    goto BYPASS_AU;
  }

  // Quickly return when immediately aborting on error, or when an interrupt
  // occurred or an exception was thrown but not caught.
  if (aborting()) {
    goto BYPASS_AU;
  }

  // FileChangedShell never nests, because it can create an endless loop.
  if (filechangeshell_busy
      && (event == EVENT_FILECHANGEDSHELL
          || event == EVENT_FILECHANGEDSHELLPOST)) {
    goto BYPASS_AU;
  }

  // Ignore events in 'eventignore'.
  if (event_ignored(event)) {
    goto BYPASS_AU;
  }

  // Allow nesting of autocommands, but restrict the depth, because it's
  // possible to create an endless loop.
  if (nesting == 10) {
    emsg(_("E218: autocommand nesting too deep"));
    goto BYPASS_AU;
  }

  // Check if these autocommands are disabled.  Used when doing ":all" or
  // ":ball".
  if ((autocmd_no_enter && (event == EVENT_WINENTER || event == EVENT_BUFENTER))
      || (autocmd_no_leave
          && (event == EVENT_WINLEAVE || event == EVENT_BUFLEAVE))) {
    goto BYPASS_AU;
  }

  // Save the autocmd_* variables and info about the current buffer.
  save_autocmd_fname = autocmd_fname;
  save_autocmd_bufnr = autocmd_bufnr;
  save_autocmd_match = autocmd_match;
  save_autocmd_busy = autocmd_busy;
  save_autocmd_nested = autocmd_nested;
  save_changed = curbuf->b_changed;
  old_curbuf = curbuf;

  // Set the file name to be used for <afile>.
  // Make a copy to avoid that changing a buffer name or directory makes it
  // invalid.
  if (fname_io == NULL) {
    if (event == EVENT_COLORSCHEME || event == EVENT_COLORSCHEMEPRE
        || event == EVENT_OPTIONSET) {
      autocmd_fname = NULL;
    } else if (fname != NULL && !ends_excmd(*fname)) {
      autocmd_fname = fname;
    } else if (buf != NULL) {
      autocmd_fname = buf->b_ffname;
    } else {
      autocmd_fname = NULL;
    }
  } else {
    autocmd_fname = fname_io;
  }
  if (autocmd_fname != NULL) {
    // Allocate MAXPATHL for when eval_vars() resolves the fullpath.
    autocmd_fname = vim_strnsave(autocmd_fname, MAXPATHL);
  }

  // Set the buffer number to be used for <abuf>.
  if (buf == NULL) {
    autocmd_bufnr = 0;
  } else {
    autocmd_bufnr = buf->b_fnum;
  }

  // When the file name is NULL or empty, use the file name of buffer "buf".
  // Always use the full path of the file name to match with, in case
  // "allow_dirs" is set.
  if (fname == NULL || *fname == NUL) {
    if (buf == NULL) {
      fname = NULL;
    } else {
      if (event == EVENT_SYNTAX) {
        fname = buf->b_p_syn;
      } else if (event == EVENT_FILETYPE) {
        fname = buf->b_p_ft;
      } else {
        if (buf->b_sfname != NULL) {
          sfname = vim_strsave(buf->b_sfname);
        }
        fname = buf->b_ffname;
      }
    }
    if (fname == NULL) {
      fname = (char_u *)"";
    }
    fname = vim_strsave(fname);  // make a copy, so we can change it
  } else {
    sfname = vim_strsave(fname);
    // Don't try expanding the following events.
    if (event == EVENT_CMDLINECHANGED || event == EVENT_CMDLINEENTER
        || event == EVENT_CMDLINELEAVE || event == EVENT_CMDWINENTER
        || event == EVENT_CMDWINLEAVE || event == EVENT_CMDUNDEFINED
        || event == EVENT_COLORSCHEME || event == EVENT_COLORSCHEMEPRE
        || event == EVENT_DIRCHANGED || event == EVENT_FILETYPE
        || event == EVENT_FUNCUNDEFINED || event == EVENT_OPTIONSET
        || event == EVENT_QUICKFIXCMDPOST || event == EVENT_QUICKFIXCMDPRE
        || event == EVENT_REMOTEREPLY || event == EVENT_SPELLFILEMISSING
        || event == EVENT_SYNTAX || event == EVENT_SIGNAL
        || event == EVENT_TABCLOSED || event == EVENT_WINCLOSED) {
      fname = vim_strsave(fname);
    } else {
      fname = (char_u *)FullName_save((char *)fname, false);
    }
  }
  if (fname == NULL) {  // out of memory
    xfree(sfname);
    retval = false;
    goto BYPASS_AU;
  }

#ifdef BACKSLASH_IN_FILENAME
  // Replace all backslashes with forward slashes. This makes the
  // autocommand patterns portable between Unix and Windows.
  if (sfname != NULL) {
    forward_slash(sfname);
  }
  forward_slash(fname);
#endif

  // Set the name to be used for <amatch>.
  autocmd_match = fname;

  // Don't redraw while doing autocommands.
  RedrawingDisabled++;
  save_sourcing_name = sourcing_name;
  sourcing_name = NULL;  // don't free this one
  save_sourcing_lnum = sourcing_lnum;
  sourcing_lnum = 0;  // no line number here

  const sctx_T save_current_sctx = current_sctx;

  if (do_profiling == PROF_YES) {
    prof_child_enter(&wait_time);  // doesn't count for the caller itself
  }

  // Don't use local function variables, if called from a function.
  funccal_entry_T funccal_entry;
  save_funccal(&funccal_entry);

  // When starting to execute autocommands, save the search patterns.
  if (!autocmd_busy) {
    save_search_patterns();
    if (!ins_compl_active()) {
      saveRedobuff(&save_redo);
      did_save_redobuff = true;
    }
    did_filetype = keep_filetype;
  }

  // Note that we are applying autocmds.  Some commands need to know.
  autocmd_busy = true;
  filechangeshell_busy = (event == EVENT_FILECHANGEDSHELL);
  nesting++;  // see matching decrement below

  // Remember that FileType was triggered.  Used for did_filetype().
  if (event == EVENT_FILETYPE) {
    did_filetype = true;
  }

  tail = path_tail(fname);

  // Find first autocommand that matches
  patcmd.curpat = first_autopat[(int)event];
  patcmd.nextcmd = NULL;
  patcmd.group = group;
  patcmd.fname = fname;
  patcmd.sfname = sfname;
  patcmd.tail = tail;
  patcmd.event = event;
  patcmd.arg_bufnr = autocmd_bufnr;
  patcmd.next = NULL;
  auto_next_pat(&patcmd, false);

  // found one, start executing the autocommands
  if (patcmd.curpat != NULL) {
    // add to active_apc_list
    patcmd.next = active_apc_list;
    active_apc_list = &patcmd;

    // set v:cmdarg (only when there is a matching pattern)
    save_cmdbang = (long)get_vim_var_nr(VV_CMDBANG);
    if (eap != NULL) {
      save_cmdarg = set_cmdarg(eap, NULL);
      set_vim_var_nr(VV_CMDBANG, (long)eap->forceit);
    } else {
      save_cmdarg = NULL;  // avoid gcc warning
    }
    retval = true;
    // mark the last pattern, to avoid an endless loop when more patterns
    // are added when executing autocommands
    for (ap = patcmd.curpat; ap->next != NULL; ap = ap->next) {
      ap->last = false;
    }
    ap->last = true;

    if (nesting == 1) {
      // make sure cursor and topline are valid
      check_lnums(true);
    }

    // Execute the autocmd. The `getnextac` callback handles iteration.
    do_cmdline(NULL, getnextac, (void *)&patcmd,
               DOCMD_NOWAIT | DOCMD_VERBOSE | DOCMD_REPEAT);

    if (nesting == 1) {
      // restore cursor and topline, unless they were changed
      reset_lnums();
    }


    if (eap != NULL) {
      (void)set_cmdarg(NULL, save_cmdarg);
      set_vim_var_nr(VV_CMDBANG, save_cmdbang);
    }
    // delete from active_apc_list
    if (active_apc_list == &patcmd) {  // just in case
      active_apc_list = patcmd.next;
    }
  }

  RedrawingDisabled--;
  autocmd_busy = save_autocmd_busy;
  filechangeshell_busy = false;
  autocmd_nested = save_autocmd_nested;
  xfree(sourcing_name);
  sourcing_name = save_sourcing_name;
  sourcing_lnum = save_sourcing_lnum;
  xfree(autocmd_fname);
  autocmd_fname = save_autocmd_fname;
  autocmd_bufnr = save_autocmd_bufnr;
  autocmd_match = save_autocmd_match;
  current_sctx = save_current_sctx;
  restore_funccal();
  if (do_profiling == PROF_YES) {
    prof_child_exit(&wait_time);
  }
  KeyTyped = save_KeyTyped;
  xfree(fname);
  xfree(sfname);
  nesting--;  // see matching increment above

  // When stopping to execute autocommands, restore the search patterns and
  // the redo buffer. Free any buffers in the au_pending_free_buf list and
  // free any windows in the au_pending_free_win list.
  if (!autocmd_busy) {
    restore_search_patterns();
    if (did_save_redobuff) {
      restoreRedobuff(&save_redo);
    }
    did_filetype = false;
    while (au_pending_free_buf != NULL) {
      buf_T *b = au_pending_free_buf->b_next;

      xfree(au_pending_free_buf);
      au_pending_free_buf = b;
    }
    while (au_pending_free_win != NULL) {
      win_T *w = au_pending_free_win->w_next;

      xfree(au_pending_free_win);
      au_pending_free_win = w;
    }
  }

  // Some events don't set or reset the Changed flag.
  // Check if still in the same buffer!
  if (curbuf == old_curbuf
      && (event == EVENT_BUFREADPOST || event == EVENT_BUFWRITEPOST
          || event == EVENT_FILEAPPENDPOST || event == EVENT_VIMLEAVE
          || event == EVENT_VIMLEAVEPRE)) {
    if (curbuf->b_changed != save_changed) {
      need_maketitle = true;
    }
    curbuf->b_changed = save_changed;
  }

  au_cleanup();  // may really delete removed patterns/commands now

BYPASS_AU:
  // When wiping out a buffer make sure all its buffer-local autocommands
  // are deleted.
  if (event == EVENT_BUFWIPEOUT && buf != NULL) {
    aubuflocal_remove(buf);
  }

  if (retval == OK && event == EVENT_FILETYPE) {
    au_did_filetype = true;
  }

  return retval;
}

// Block triggering autocommands until unblock_autocmd() is called.
// Can be used recursively, so long as it's symmetric.
void block_autocmds(void)
{
  // Remember the value of v:termresponse.
  if (!is_autocmd_blocked()) {
    old_termresponse = get_vim_var_str(VV_TERMRESPONSE);
  }
  autocmd_blocked++;
}

void unblock_autocmds(void)
{
  autocmd_blocked--;

  // When v:termresponse was set while autocommands were blocked, trigger
  // the autocommands now.  Esp. useful when executing a shell command
  // during startup (nvim -d).
  if (!is_autocmd_blocked()
      && get_vim_var_str(VV_TERMRESPONSE) != old_termresponse) {
    apply_autocmds(EVENT_TERMRESPONSE, NULL, NULL, false, curbuf);
  }
}

bool is_autocmd_blocked(void)
  FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
  return autocmd_blocked != 0;
}

/// Find next autocommand pattern that matches.
/// stop when 'last' flag is set
void auto_next_pat(AutoPatCmd *apc, int stop_at_last)
{
  AutoPat *ap;
  AutoCmd *cp;
  char *s;

  XFREE_CLEAR(sourcing_name);

  for (ap = apc->curpat; ap != NULL && !got_int; ap = ap->next) {
    apc->curpat = NULL;

    // Only use a pattern when it has not been removed, has commands and
    // the group matches. For buffer-local autocommands only check the
    // buffer number.
    if (ap->pat != NULL && ap->cmds != NULL
        && (apc->group == AUGROUP_ALL || apc->group == ap->group)) {
      // execution-condition
      if (ap->buflocal_nr == 0
          ? match_file_pat(NULL,
                           &ap->reg_prog,
                           apc->fname,
                           apc->sfname,
                           apc->tail,
                           ap->allow_dirs)
          : ap->buflocal_nr == apc->arg_bufnr) {
        const char *const name = event_nr2name(apc->event);
        s = _("%s Autocommands for \"%s\"");

        const size_t sourcing_name_len
          = (STRLEN(s) + strlen(name) + (size_t)ap->patlen + 1);

        sourcing_name = xmalloc(sourcing_name_len);
        snprintf((char *)sourcing_name, sourcing_name_len, s, name,
                 (char *)ap->pat);
        if (p_verbose >= 8) {
          verbose_enter();
          smsg(_("Executing %s"), sourcing_name);
          verbose_leave();
        }

        apc->curpat = ap;
        apc->nextcmd = ap->cmds;
        // mark last command
        for (cp = ap->cmds; cp->next != NULL; cp = cp->next) {
          cp->last = false;
        }
        cp->last = true;
      }
      line_breakcheck();
      if (apc->curpat != NULL) {  // found a match
        break;
      }
    }
    if (stop_at_last && ap->last) {
      break;
    }
  }
}

/// Get next autocommand command.
/// Called by do_cmdline() to get the next line for ":if".
/// @return allocated string, or NULL for end of autocommands.
char_u *getnextac(int c, void *cookie, int indent, bool do_concat)
{
  AutoPatCmd *acp = (AutoPatCmd *)cookie;
  char_u *retval;
  AutoCmd *ac;

  // Can be called again after returning the last line.
  if (acp->curpat == NULL) {
    return NULL;
  }

  // repeat until we find an autocommand to execute
  for (;;) {
    // skip removed commands
    while (acp->nextcmd != NULL && acp->nextcmd->cmd == NULL) {
      if (acp->nextcmd->last) {
        acp->nextcmd = NULL;
      } else {
        acp->nextcmd = acp->nextcmd->next;
      }
    }

    if (acp->nextcmd != NULL) {
      break;
    }

    // at end of commands, find next pattern that matches
    if (acp->curpat->last) {
      acp->curpat = NULL;
    } else {
      acp->curpat = acp->curpat->next;
    }
    if (acp->curpat != NULL) {
      auto_next_pat(acp, true);
    }
    if (acp->curpat == NULL) {
      return NULL;
    }
  }

  ac = acp->nextcmd;

  if (p_verbose >= 9) {
    verbose_enter_scroll();
    smsg(_("autocommand %s"), ac->cmd);
    msg_puts("\n");  // don't overwrite this either
    verbose_leave_scroll();
  }
  retval = vim_strsave(ac->cmd);
  // Remove one-shot ("once") autocmd in anticipation of its execution.
  if (ac->once) {
    au_del_cmd(ac);
  }
  autocmd_nested = ac->nested;
  current_sctx = ac->script_ctx;
  if (ac->last) {
    acp->nextcmd = NULL;
  } else {
    acp->nextcmd = ac->next;
  }

  return retval;
}

/// Return true if there is a matching autocommand for "fname".
/// To account for buffer-local autocommands, function needs to know
/// in which buffer the file will be opened.
///
/// @param event event that occurred.
/// @param sfname filename the event occurred in.
/// @param buf buffer the file is open in
bool has_autocmd(event_T event, char_u *sfname, buf_T *buf) FUNC_ATTR_WARN_UNUSED_RESULT
{
  AutoPat *ap;
  char_u *fname;
  char_u *tail = path_tail(sfname);
  bool retval = false;

  fname = (char_u *)FullName_save((char *)sfname, false);
  if (fname == NULL) {
    return false;
  }

#ifdef BACKSLASH_IN_FILENAME
  // Replace all backslashes with forward slashes. This makes the
  // autocommand patterns portable between Unix and Windows.
  sfname = vim_strsave(sfname);
  forward_slash(sfname);
  forward_slash(fname);
#endif

  for (ap = first_autopat[(int)event]; ap != NULL; ap = ap->next) {
    if (ap->pat != NULL && ap->cmds != NULL
        && (ap->buflocal_nr == 0
            ? match_file_pat(NULL,
                             &ap->reg_prog,
                             fname,
                             sfname,
                             tail,
                             ap->allow_dirs)
            : buf != NULL && ap->buflocal_nr == buf->b_fnum)) {
      retval = true;
      break;
    }
  }

  xfree(fname);
#ifdef BACKSLASH_IN_FILENAME
  xfree(sfname);
#endif

  return retval;
}

// Function given to ExpandGeneric() to obtain the list of autocommand group
// names.
char_u *get_augroup_name(expand_T *xp, int idx)
{
  if (idx == augroups.ga_len) {  // add "END" add the end
    return (char_u *)"END";
  }
  if (idx >= augroups.ga_len) {  // end of list
    return NULL;
  }
  if (AUGROUP_NAME(idx) == NULL || AUGROUP_NAME(idx) == get_deleted_augroup()) {
    // skip deleted entries
    return (char_u *)"";
  }
  return (char_u *)AUGROUP_NAME(idx);
}

/// @param doautocmd  true for :doauto*, false for :autocmd
char_u *set_context_in_autocmd(expand_T *xp, char_u *arg, int doautocmd)
{
  char_u *p;
  int group;

  // check for a group name, skip it if present
  autocmd_include_groups = false;
  p = arg;
  group = au_get_grouparg(&arg);

  // If there only is a group name that's what we expand.
  if (*arg == NUL && group != AUGROUP_ALL && !ascii_iswhite(arg[-1])) {
    arg = p;
    group = AUGROUP_ALL;
  }

  // skip over event name
  for (p = arg; *p != NUL && !ascii_iswhite(*p); p++) {
    if (*p == ',') {
      arg = p + 1;
    }
  }
  if (*p == NUL) {
    if (group == AUGROUP_ALL) {
      autocmd_include_groups = true;
    }
    xp->xp_context = EXPAND_EVENTS;  // expand event name
    xp->xp_pattern = arg;
    return NULL;
  }

  // skip over pattern
  arg = skipwhite(p);
  while (*arg && (!ascii_iswhite(*arg) || arg[-1] == '\\')) {
    arg++;
  }
  if (*arg) {
    return arg;  // expand (next) command
  }

  if (doautocmd) {
    xp->xp_context = EXPAND_FILES;  // expand file names
  } else {
    xp->xp_context = EXPAND_NOTHING;  // pattern is not expanded
  }
  return NULL;
}

// Function given to ExpandGeneric() to obtain the list of event names.
char_u *get_event_name(expand_T *xp, int idx)
{
  if (idx < augroups.ga_len) {  // First list group names, if wanted
    if (!autocmd_include_groups || AUGROUP_NAME(idx) == NULL
        || AUGROUP_NAME(idx) == get_deleted_augroup()) {
      return (char_u *)"";  // skip deleted entries
    }
    return (char_u *)AUGROUP_NAME(idx);
  }
  return (char_u *)event_names[idx - augroups.ga_len].name;
}

/// Check whether given autocommand is supported
///
/// @param[in]  event  Event to check.
///
/// @return True if it is, false otherwise.
bool autocmd_supported(const char *const event)
  FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
  char_u *p;
  return event_name2nr((const char_u *)event, &p) != NUM_EVENTS;
}

/// Return true if an autocommand is defined for a group, event and
/// pattern:  The group can be omitted to accept any group.
/// `event` and `pattern` can be omitted to accept any event and pattern.
/// Buffer-local patterns <buffer> or <buffer=N> are accepted.
/// Used for:
///   exists("#Group") or
///   exists("#Group#Event") or
///   exists("#Group#Event#pat") or
///   exists("#Event") or
///   exists("#Event#pat")
///
/// @param arg autocommand string
bool au_exists(const char *const arg) FUNC_ATTR_WARN_UNUSED_RESULT
{
  event_T event;
  AutoPat *ap;
  buf_T *buflocal_buf = NULL;
  int group;
  bool retval = false;

  // Make a copy so that we can change the '#' chars to a NUL.
  char *const arg_save = xstrdup(arg);
  char *p = strchr(arg_save, '#');
  if (p != NULL) {
    *p++ = NUL;
  }

  // First, look for an autocmd group name.
  group = au_find_group((char_u *)arg_save);
  char *event_name;
  if (group == AUGROUP_ERROR) {
    // Didn't match a group name, assume the first argument is an event.
    group = AUGROUP_ALL;
    event_name = arg_save;
  } else {
    if (p == NULL) {
      // "Group": group name is present and it's recognized
      retval = true;
      goto theend;
    }

    // Must be "Group#Event" or "Group#Event#pat".
    event_name = p;
    p = strchr(event_name, '#');
    if (p != NULL) {
      *p++ = NUL;  // "Group#Event#pat"
    }
  }

  char *pattern = p;  // "pattern" is NULL when there is no pattern.

  // Find the index (enum) for the event name.
  event = event_name2nr((char_u *)event_name, (char_u **)&p);

  // return false if the event name is not recognized
  if (event == NUM_EVENTS) {
    goto theend;
  }

  // Find the first autocommand for this event.
  // If there isn't any, return false;
  // If there is one and no pattern given, return true;
  ap = first_autopat[(int)event];
  if (ap == NULL) {
    goto theend;
  }

  // if pattern is "<buffer>", special handling is needed which uses curbuf
  // for pattern "<buffer=N>, fnamecmp() will work fine
  if (pattern != NULL && STRICMP(pattern, "<buffer>") == 0) {
    buflocal_buf = curbuf;
  }

  // Check if there is an autocommand with the given pattern.
  for (; ap != NULL; ap = ap->next) {
    // only use a pattern when it has not been removed and has commands.
    // For buffer-local autocommands, fnamecmp() works fine.
    if (ap->pat != NULL && ap->cmds != NULL
        && (group == AUGROUP_ALL || ap->group == group)
        && (pattern == NULL
            || (buflocal_buf == NULL
                ? fnamecmp(ap->pat, (char_u *)pattern) == 0
                : ap->buflocal_nr == buflocal_buf->b_fnum))) {
      retval = true;
      break;
    }
  }

theend:
  xfree(arg_save);
  return retval;
}
