/// @file runtime.c
///
/// Management of runtime files (including packages)

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <uv.h>

#include "klib/kvec.h"
#include "nvim/api/private/defs.h"
#include "nvim/api/private/helpers.h"
#include "nvim/ascii_defs.h"
#include "nvim/autocmd.h"
#include "nvim/autocmd_defs.h"
#include "nvim/buffer_defs.h"
#include "nvim/charset.h"
#include "nvim/cmdexpand.h"
#include "nvim/debugger.h"
#include "nvim/errors.h"
#include "nvim/eval.h"
#include "nvim/eval/typval.h"
#include "nvim/eval/userfunc.h"
#include "nvim/eval/vars.h"
#include "nvim/ex_cmds_defs.h"
#include "nvim/ex_docmd.h"
#include "nvim/ex_eval.h"
#include "nvim/ex_eval_defs.h"
#include "nvim/garray.h"
#include "nvim/getchar.h"
#include "nvim/gettext_defs.h"
#include "nvim/globals.h"
#include "nvim/hashtab.h"
#include "nvim/hashtab_defs.h"
#include "nvim/lua/executor.h"
#include "nvim/macros_defs.h"
#include "nvim/map_defs.h"
#include "nvim/mbyte.h"
#include "nvim/mbyte_defs.h"
#include "nvim/memline.h"
#include "nvim/memory.h"
#include "nvim/memory_defs.h"
#include "nvim/message.h"
#include "nvim/option.h"
#include "nvim/option_defs.h"
#include "nvim/option_vars.h"
#include "nvim/os/fs.h"
#include "nvim/os/input.h"
#include "nvim/os/os.h"
#include "nvim/os/os_defs.h"
#include "nvim/os/stdpaths_defs.h"
#include "nvim/path.h"
#include "nvim/pos_defs.h"
#include "nvim/profile.h"
#include "nvim/regexp.h"
#include "nvim/regexp_defs.h"
#include "nvim/runtime.h"
#include "nvim/strings.h"
#include "nvim/types_defs.h"
#include "nvim/usercmd.h"
#include "nvim/vim_defs.h"
#ifdef USE_CRNL
# include "nvim/highlight.h"
#endif

/// Structure used to store info for each sourced file.
/// It is shared between do_source() and getsourceline().
/// This is required, because it needs to be handed to do_cmdline() and
/// sourcing can be done recursively.
typedef struct {
  FILE *fp;                     ///< opened file for sourcing
  char *nextline;               ///< if not NULL: line that was read ahead
  linenr_T sourcing_lnum;       ///< line number of the source file
  bool finished;                ///< ":finish" used
  bool source_from_buf_or_str;  ///< true if sourcing from a buffer or string
  int buf_lnum;                 ///< line number in the buffer or string
  garray_T buflines;            ///< lines in the buffer or string
#ifdef USE_CRNL
  int fileformat;               ///< EOL_UNKNOWN, EOL_UNIX or EOL_DOS
  bool error;                   ///< true if LF found after CR-LF
#endif
  linenr_T breakpoint;          ///< next line with breakpoint or zero
  char *fname;                  ///< name of sourced file
  int dbg_tick;                 ///< debug_tick when breakpoint was set
  int level;                    ///< top nesting level of sourced file
  vimconv_T conv;               ///< type of conversion
} source_cookie_T;

typedef struct {
  char *path;
  bool after;
  bool pack_inserted;
  TriState has_lua;
  size_t pos_in_rtp;
} SearchPathItem;

typedef kvec_t(SearchPathItem) RuntimeSearchPath;
typedef kvec_t(char *) CharVec;

#include "runtime.c.generated.h"

garray_T exestack = { 0, 0, sizeof(estack_T), 50, NULL };
garray_T script_items = { 0, 0, sizeof(scriptitem_T *), 20, NULL };

/// The names of packages that once were loaded are remembered.
static garray_T ga_loaded = { 0, 0, sizeof(char *), 4, NULL };

/// last used sequence number for sourcing scripts (current_sctx.sc_seq)
static int last_current_SID_seq = 0;

/// Initialize the execution stack.
void estack_init(void)
{
  ga_grow(&exestack, 10);
  estack_T *entry = ((estack_T *)exestack.ga_data) + exestack.ga_len;
  entry->es_type = ETYPE_TOP;
  entry->es_name = NULL;
  entry->es_lnum = 0;
  entry->es_info.ufunc = NULL;
  exestack.ga_len++;
}

/// Add an item to the execution stack.
/// @return  the new entry
estack_T *estack_push(etype_T type, char *name, linenr_T lnum)
{
  ga_grow(&exestack, 1);
  estack_T *entry = ((estack_T *)exestack.ga_data) + exestack.ga_len;
  entry->es_type = type;
  entry->es_name = name;
  entry->es_lnum = lnum;
  entry->es_info.ufunc = NULL;
  exestack.ga_len++;
  return entry;
}

/// Add a user function to the execution stack.
void estack_push_ufunc(ufunc_T *ufunc, linenr_T lnum)
{
  estack_T *entry = estack_push(ETYPE_UFUNC,
                                ufunc->uf_name_exp != NULL ? ufunc->uf_name_exp : ufunc->uf_name,
                                lnum);
  if (entry != NULL) {
    entry->es_info.ufunc = ufunc;
  }
}

/// Take an item off of the execution stack.
void estack_pop(void)
{
  if (exestack.ga_len > 1) {
    exestack.ga_len--;
  }
}

/// Get the current value for <sfile> in allocated memory.
/// @param which  ESTACK_SFILE for <sfile>, ESTACK_STACK for <stack> or
///               ESTACK_SCRIPT for <script>.
char *estack_sfile(estack_arg_T which)
{
  const estack_T *entry = ((estack_T *)exestack.ga_data) + exestack.ga_len - 1;
  if (which == ESTACK_SFILE && entry->es_type != ETYPE_UFUNC) {
    return entry->es_name != NULL ? xstrdup(entry->es_name) : NULL;
  }

  // If evaluated in a function or autocommand, return the path of the script
  // where it is defined, at script level the current script path is returned
  // instead.
  if (which == ESTACK_SCRIPT) {
    // Walk the stack backwards, starting from the current frame.
    for (int idx = exestack.ga_len - 1; idx >= 0; idx--, entry--) {
      if (entry->es_type == ETYPE_UFUNC || entry->es_type == ETYPE_AUCMD) {
        const sctx_T *const def_ctx = (entry->es_type == ETYPE_UFUNC
                                       ? &entry->es_info.ufunc->uf_script_ctx
                                       : &entry->es_info.aucmd->script_ctx);
        return def_ctx->sc_sid > 0
               ? xstrdup((SCRIPT_ITEM(def_ctx->sc_sid)->sn_name))
               : NULL;
      } else if (entry->es_type == ETYPE_SCRIPT) {
        return xstrdup(entry->es_name);
      }
    }
    return NULL;
  }

  // Give information about each stack entry up to the root.
  // For a function we compose the call stack, as it was done in the past:
  //   "function One[123]..Two[456]..Three"
  garray_T ga;
  ga_init(&ga, sizeof(char), 100);
  etype_T last_type = ETYPE_SCRIPT;
  for (int idx = 0; idx < exestack.ga_len; idx++) {
    entry = ((estack_T *)exestack.ga_data) + idx;
    if (entry->es_name != NULL) {
      String type_name = STATIC_CSTR_AS_STRING("");
      String es_name = cstr_as_string(entry->es_name);
      if (entry->es_type != last_type) {
        switch (entry->es_type) {
        case ETYPE_SCRIPT:
          type_name = STATIC_CSTR_AS_STRING("script "); break;
        case ETYPE_UFUNC:
          type_name = STATIC_CSTR_AS_STRING("function "); break;
        default:
          break;
        }
        last_type = entry->es_type;
      }
      linenr_T lnum = idx == exestack.ga_len - 1
                      ? which == ESTACK_STACK ? SOURCING_LNUM : 0
                      : entry->es_lnum;

      size_t len = es_name.size + type_name.size + 26;
      ga_grow(&ga, (int)len);
      ga_concat_len(&ga, type_name.data, type_name.size);
      ga_concat_len(&ga, es_name.data, es_name.size);
      // For the bottom entry of <sfile>: do not add the line number, it is used in
      // <slnum>.  Also leave it out when the number is not set.
      if (lnum != 0) {
        ga.ga_len += (int)vim_snprintf_safelen((char *)ga.ga_data + ga.ga_len,
                                               (size_t)(ga.ga_maxlen - ga.ga_len),
                                               "[%" PRIdLINENR "]", lnum);
      }
      if (idx != exestack.ga_len - 1) {
        GA_CONCAT_LITERAL(&ga, "..");
      }
    }
  }

  // Only NUL-terminate when not returning NULL.
  if (ga.ga_data != NULL) {
    ga_append(&ga, NUL);
  }
  return (char *)ga.ga_data;
}

static void stacktrace_push_item(list_T *const l, ufunc_T *const fp, const char *const event,
                                 const linenr_T lnum, char *const filepath)
{
  dict_T *const d = tv_dict_alloc_lock(VAR_FIXED);
  typval_T tv = {
    .v_type = VAR_DICT,
    .v_lock = VAR_LOCKED,
    .vval.v_dict = d,
  };

  if (fp != NULL) {
    tv_dict_add_func(d, S_LEN("funcref"), fp);
  }
  if (event != NULL) {
    tv_dict_add_str(d, S_LEN("event"), event);
  }
  tv_dict_add_nr(d, S_LEN("lnum"), lnum);
  tv_dict_add_str(d, S_LEN("filepath"), filepath);

  tv_list_append_tv(l, &tv);
}

/// Create the stacktrace from exestack.
list_T *stacktrace_create(void)
{
  list_T *const l = tv_list_alloc(exestack.ga_len);

  for (int i = 0; i < exestack.ga_len; i++) {
    estack_T *const entry = &((estack_T *)exestack.ga_data)[i];
    linenr_T lnum = entry->es_lnum;

    if (entry->es_type == ETYPE_SCRIPT) {
      stacktrace_push_item(l, NULL, NULL, lnum, entry->es_name);
    } else if (entry->es_type == ETYPE_UFUNC) {
      ufunc_T *const fp = entry->es_info.ufunc;
      const sctx_T sctx = fp->uf_script_ctx;
      char *filepath = sctx.sc_sid > 0 ? get_scriptname(sctx, NULL) : "";
      lnum += sctx.sc_lnum;
      stacktrace_push_item(l, fp, NULL, lnum, filepath);
    } else if (entry->es_type == ETYPE_AUCMD) {
      const sctx_T sctx = entry->es_info.aucmd->script_ctx;
      char *filepath = sctx.sc_sid > 0 ? get_scriptname(sctx, NULL) : "";
      lnum += sctx.sc_lnum;
      stacktrace_push_item(l, NULL, entry->es_name, lnum, filepath);
    }
  }
  return l;
}

/// getstacktrace() function
void f_getstacktrace(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
  tv_list_set_ret(rettv, stacktrace_create());
}

static bool runtime_search_path_valid = false;
static bool runtime_search_path_valid_thread = false;
static int *runtime_search_path_ref = NULL;
static RuntimeSearchPath runtime_search_path;
static RuntimeSearchPath runtime_search_path_thread;
static uv_mutex_t runtime_search_path_mutex;

void runtime_init(void)
{
  uv_mutex_init(&runtime_search_path_mutex);
}

/// Get DIP_ flags from the [where] argument of a :runtime command.
/// "*argp" is advanced to after the [where] argument.
static int get_runtime_cmd_flags(char **argp, size_t where_len)
{
  char *arg = *argp;

  if (where_len == 0) {
    return 0;
  }

  if (strncmp(arg, "START", where_len) == 0) {
    *argp = skipwhite(arg + where_len);
    return DIP_START + DIP_NORTP;
  }
  if (strncmp(arg, "OPT", where_len) == 0) {
    *argp = skipwhite(arg + where_len);
    return DIP_OPT + DIP_NORTP;
  }
  if (strncmp(arg, "PACK", where_len) == 0) {
    *argp = skipwhite(arg + where_len);
    return DIP_START + DIP_OPT + DIP_NORTP;
  }
  if (strncmp(arg, "ALL", where_len) == 0) {
    *argp = skipwhite(arg + where_len);
    return DIP_START + DIP_OPT;
  }

  return 0;
}

/// ":runtime [where] {name}"
void ex_runtime(exarg_T *eap)
{
  char *arg = eap->arg;
  int flags = eap->forceit ? DIP_ALL : 0;
  char *p = skiptowhite(arg);
  flags += get_runtime_cmd_flags(&arg, (size_t)(p - arg));
  assert(arg != NULL);  // suppress clang false positive
  source_runtime(arg, flags);
}

static int runtime_expand_flags;

/// Set the completion context for the :runtime command.
void set_context_in_runtime_cmd(expand_T *xp, const char *arg)
{
  char *p = skiptowhite(arg);
  runtime_expand_flags
    = *p != NUL ? get_runtime_cmd_flags((char **)&arg, (size_t)(p - arg)) : 0;
  // Skip to the last argument.
  while (*(p = skiptowhite_esc(arg)) != NUL) {
    if (runtime_expand_flags == 0) {
      // When there are multiple arguments and [where] is not specified,
      // use an unrelated non-zero flag to avoid expanding [where].
      runtime_expand_flags = DIP_ALL;
    }
    arg = skipwhite(p);
  }
  xp->xp_context = EXPAND_RUNTIME;
  xp->xp_pattern = (char *)arg;
}

/// Source all .vim and .lua files in "fnames" with .vim files being sourced first.
static bool source_callback_vim_lua(int num_fnames, char **fnames, bool all, void *cookie)
{
  bool did_one = false;

  for (int i = 0; i < num_fnames; i++) {
    if (path_with_extension(fnames[i], "vim")) {
      do_source(fnames[i], false, DOSO_NONE, cookie);
      did_one = true;
      if (!all) {
        return true;
      }
    }
  }

  for (int i = 0; i < num_fnames; i++) {
    if (path_with_extension(fnames[i], "lua")) {
      do_source(fnames[i], false, DOSO_NONE, cookie);
      did_one = true;
      if (!all) {
        return true;
      }
    }
  }

  return did_one;
}

/// Source all files in "fnames" with .vim files sourced first, .lua files
/// sourced second, and any remaining files sourced last.
static bool source_callback(int num_fnames, char **fnames, bool all, void *cookie)
{
  bool did_one = source_callback_vim_lua(num_fnames, fnames, all, cookie);

  if (!all && did_one) {
    return true;
  }

  for (int i = 0; i < num_fnames; i++) {
    if (!path_with_extension(fnames[i], "vim")
        && !path_with_extension(fnames[i], "lua")) {
      do_source(fnames[i], false, DOSO_NONE, cookie);
      did_one = true;
      if (!all) {
        return true;
      }
    }
  }

  return did_one;
}

/// Find the patterns in "name" in all directories in "path" and invoke
/// "callback(fname, cookie)".
/// "prefix" is prepended to each pattern in "name".
/// When "flags" has DIP_ALL: source all files, otherwise only the first one.
/// When "flags" has DIP_DIR: find directories instead of files.
/// When "flags" has DIP_ERR: give an error message if there is no match.
///
/// Return FAIL when no file could be sourced, OK otherwise.
int do_in_path(const char *path, const char *prefix, char *name, int flags,
               DoInRuntimepathCB callback, void *cookie)
  FUNC_ATTR_NONNULL_ARG(1, 2)
{
  bool did_one = false;

  // Make a copy of 'runtimepath'.  Invoking the callback may change the
  // value.
  char *rtp_copy = xstrdup(path);
  char *buf = xmallocz(MAXPATHL);
  {
    char *tail;
    if (p_verbose > 10 && name != NULL) {
      verbose_enter();
      if (*prefix != NUL) {
        smsg(0, _("Searching for \"%s\" under \"%s\" in \"%s\""), name, prefix, path);
      } else {
        smsg(0, _("Searching for \"%s\" in \"%s\""), name, path);
      }
      verbose_leave();
    }

    bool do_all = (flags & DIP_ALL) != 0;

    // Loop over all entries in 'runtimepath'.
    char *rtp = rtp_copy;
    while (*rtp != NUL && (do_all || !did_one)) {
      // Copy the path from 'runtimepath' to buf[].
      size_t buflen = copy_option_part(&rtp, buf, MAXPATHL, ",");

      // Skip after or non-after directories.
      if (flags & (DIP_NOAFTER | DIP_AFTER)) {
        bool is_after = path_is_after(buf, buflen);

        if ((is_after && (flags & DIP_NOAFTER))
            || (!is_after && (flags & DIP_AFTER))) {
          continue;
        }
      }

      if (name == NULL) {
        (*callback)(1, &buf, do_all, cookie);
        did_one = true;
      } else if (buflen + 2 + strlen(prefix) + strlen(name) < MAXPATHL) {
        add_pathsep(buf);
        strcat(buf, prefix);
        tail = buf + strlen(buf);

        // Loop over all patterns in "name"
        char *np = name;
        while (*np != NUL && (do_all || !did_one)) {
          // Append the pattern from "name" to buf[].
          assert(MAXPATHL >= (tail - buf));
          copy_option_part(&np, tail, (size_t)(MAXPATHL - (tail - buf)), "\t ");

          if (p_verbose > 10) {
            verbose_enter();
            smsg(0, _("Searching for \"%s\""), buf);
            verbose_leave();
          }

          int ew_flags = ((flags & DIP_DIR) ? EW_DIR : EW_FILE)
                         | ((flags & DIP_DIRFILE) ? (EW_DIR|EW_FILE) : 0);

          did_one |= gen_expand_wildcards_and_cb(1, &buf, ew_flags, do_all, callback,
                                                 cookie) == OK;
        }
      }
    }
  }
  xfree(buf);
  xfree(rtp_copy);
  if (!did_one && name != NULL) {
    char *basepath = path == p_rtp ? "runtimepath" : "packpath";

    if (flags & DIP_ERR) {
      semsg(_(e_dirnotf), basepath, name);
    } else if (p_verbose > 1) {
      verbose_enter();
      smsg(0, _("not found in '%s': \"%s\""), basepath, name);
      verbose_leave();
    }
  }

  return did_one ? OK : FAIL;
}

static RuntimeSearchPath runtime_search_path_get_cached(int *ref)
  FUNC_ATTR_NONNULL_ALL
{
  runtime_search_path_validate();

  *ref = 0;
  if (runtime_search_path_ref == NULL) {
    // cached path was unreferenced. keep a ref to
    // prevent runtime_search_path() to freeing it too early
    (*ref)++;
    runtime_search_path_ref = ref;
  }
  return runtime_search_path;
}

static RuntimeSearchPath copy_runtime_search_path(const RuntimeSearchPath src)
{
  RuntimeSearchPath dst = KV_INITIAL_VALUE;
  for (size_t j = 0; j < kv_size(src); j++) {
    SearchPathItem item = kv_A(src, j);
    kv_push(dst, ((SearchPathItem){ xstrdup(item.path), item.after, item.pack_inserted,
                                    item.has_lua, item.pos_in_rtp }));
  }

  return dst;
}

static void runtime_search_path_unref(RuntimeSearchPath path, const int *ref)
  FUNC_ATTR_NONNULL_ALL
{
  if (*ref) {
    if (runtime_search_path_ref == ref) {
      runtime_search_path_ref = NULL;
    } else {
      runtime_search_path_free(path);
    }
  }
}

/// Find the file "name" in all directories in "path" and invoke
/// "callback(fname, cookie)".
/// "name" can contain wildcards.
/// When "flags" has DIP_ALL: source all files, otherwise only the first one.
/// When "flags" has DIP_DIR: find directories instead of files.
/// When "flags" has DIP_ERR: give an error message if there is no match.
///
/// return FAIL when no file could be sourced, OK otherwise.
static int do_in_cached_path(char *name, int flags, DoInRuntimepathCB callback, void *cookie)
{
  bool did_one = false;

  char buf[MAXPATHL];

  if (p_verbose > 10 && name != NULL) {
    verbose_enter();
    smsg(0, _("Searching for \"%s\" in runtime path"), name);
    verbose_leave();
  }

  int ref;
  RuntimeSearchPath path = runtime_search_path_get_cached(&ref);

  bool do_all = (flags & DIP_ALL) != 0;

  // Loop over all entries in cached path
  for (size_t j = 0; j < kv_size(path); j++) {
    SearchPathItem item = kv_A(path, j);
    size_t buflen = strlen(item.path);

    // Skip after or non-after directories.
    if (flags & (DIP_NOAFTER | DIP_AFTER)) {
      if ((item.after && (flags & DIP_NOAFTER))
          || (!item.after && (flags & DIP_AFTER))) {
        continue;
      }
    }

    if (name == NULL) {
      (*callback)(1, &item.path, do_all, cookie);
    } else if (buflen + strlen(name) + 2 < MAXPATHL) {
      STRCPY(buf, item.path);
      add_pathsep(buf);
      char *tail = buf + strlen(buf);

      // Loop over all patterns in "name"
      char *np = name;

      while (*np != NUL && (do_all || !did_one)) {
        // Append the pattern from "name" to buf[].
        assert(MAXPATHL >= (tail - buf));
        copy_option_part(&np, tail, (size_t)(MAXPATHL - (tail - buf)), "\t ");

        if (p_verbose > 10) {
          verbose_enter();
          smsg(0, _("Searching for \"%s\""), buf);
          verbose_leave();
        }

        int ew_flags = ((flags & DIP_DIR) ? EW_DIR : EW_FILE)
                       | ((flags & DIP_DIRFILE) ? (EW_DIR|EW_FILE) : 0)
                       | EW_NOBREAK;

        // Expand wildcards, invoke the callback for each match.
        char *(pat[]) = { buf };
        did_one |= gen_expand_wildcards_and_cb(1, pat, ew_flags, do_all, callback, cookie) == OK;
      }
    }
  }

  if (!did_one && name != NULL) {
    if (flags & DIP_ERR) {
      semsg(_(e_dirnotf), "runtime path", name);
    } else if (p_verbose > 1) {
      verbose_enter();
      smsg(0, _("not found in runtime path: \"%s\""), name);
      verbose_leave();
    }
  }

  runtime_search_path_unref(path, &ref);

  return did_one ? OK : FAIL;
}

Array runtime_inspect(Arena *arena)
{
  RuntimeSearchPath path = runtime_search_path;
  Array rv = arena_array(arena, kv_size(path));

  for (size_t i = 0; i < kv_size(path); i++) {
    SearchPathItem *item = &kv_A(path, i);
    Dict entry = arena_dict(arena, 5);
    PUT_C(entry, "path", CSTR_AS_OBJ(item->path));
    if (item->after) {
      PUT_C(entry, "after", BOOLEAN_OBJ(true));
    }
    if (item->pack_inserted) {
      PUT_C(entry, "pack_inserted", BOOLEAN_OBJ(true));
    }
    if (item->has_lua != kNone) {
      PUT_C(entry, "has_lua",  BOOLEAN_OBJ(item->has_lua == kTrue));
    }
    PUT_C(entry, "pos_in_rtp", INTEGER_OBJ((Integer)item->pos_in_rtp));

    ADD_C(rv, DICT_OBJ(entry));
  }
  return rv;
}

ArrayOf(String) runtime_get_named(bool lua, Array pat, bool all, Arena *arena)
{
  int ref;
  RuntimeSearchPath path = runtime_search_path_get_cached(&ref);
  static char buf[MAXPATHL];

  ArrayOf(String) rv = runtime_get_named_common(lua, pat, all, path, buf, sizeof buf, arena);

  runtime_search_path_unref(path, &ref);
  return rv;
}

ArrayOf(String) runtime_get_named_thread(bool lua, Array pat, bool all)
{
  // TODO(bfredl): avoid contention between multiple worker threads?
  uv_mutex_lock(&runtime_search_path_mutex);
  static char buf[MAXPATHL];
  ArrayOf(String) rv = runtime_get_named_common(lua, pat, all, runtime_search_path_thread,
                                                buf, sizeof buf, NULL);
  uv_mutex_unlock(&runtime_search_path_mutex);
  return rv;
}

static ArrayOf(String) runtime_get_named_common(bool lua, Array pat, bool all,
                                                RuntimeSearchPath path, char *buf, size_t buf_len,
                                                Arena *arena)
{
  ArrayOf(String) rv = arena_array(arena, kv_size(path) * pat.size);
  for (size_t i = 0; i < kv_size(path); i++) {
    SearchPathItem *item = &kv_A(path, i);
    if (lua) {
      if (item->has_lua == kNone) {
        size_t size = (size_t)snprintf(buf, buf_len, "%s/lua/", item->path);
        item->has_lua = (size < buf_len && os_isdir(buf));
      }
      if (item->has_lua == kFalse) {
        continue;
      }
    }

    for (size_t j = 0; j < pat.size; j++) {
      Object pat_item = pat.items[j];
      if (pat_item.type == kObjectTypeString) {
        size_t size = (size_t)snprintf(buf, buf_len, "%s/%s",
                                       item->path, pat_item.data.string.data);
        if (size < buf_len) {
          if (os_file_is_readable(buf)) {
            ADD_C(rv, CSTR_TO_ARENA_OBJ(arena, buf));
            if (!all) {
              goto done;
            }
          }
        }
      }
    }
  }
done:
  return rv;
}

/// Find "name" in "path".  When found, invoke the callback function for
/// it: callback(fname, "cookie")
/// When "flags" has DIP_ALL repeat for all matches, otherwise only the first
/// one is used.
/// Returns OK when at least one match found, FAIL otherwise.
/// If "name" is NULL calls callback for each entry in "path". Cookie is
/// passed by reference in this case, setting it to NULL indicates that callback
/// has done its job.
int do_in_path_and_pp(char *path, char *name, int flags, DoInRuntimepathCB callback, void *cookie)
{
  int done = FAIL;

  if ((flags & DIP_NORTP) == 0) {
    done |= do_in_path(path, "", (name && !*name) ? NULL : name, flags, callback,
                       cookie);
  }

  if ((done == FAIL || (flags & DIP_ALL)) && (flags & DIP_START)) {
    const char *prefix
      = (flags & DIP_AFTER) ? "pack/*/start/*/after/" : "pack/*/start/*/";  // NOLINT
    done |= do_in_path(p_pp, prefix, name, flags & ~DIP_AFTER, callback, cookie);

    if (done == FAIL || (flags & DIP_ALL)) {
      prefix = (flags & DIP_AFTER) ? "start/*/after/" : "start/*/";  // NOLINT
      done |= do_in_path(p_pp, prefix, name, flags & ~DIP_AFTER, callback, cookie);
    }
  }

  if ((done == FAIL || (flags & DIP_ALL)) && (flags & DIP_OPT)) {
    done |= do_in_path(p_pp, "pack/*/opt/*/", name, flags, callback, cookie);  // NOLINT

    if (done == FAIL || (flags & DIP_ALL)) {
      done |= do_in_path(p_pp, "opt/*/", name, flags, callback, cookie);  // NOLINT
    }
  }

  return done;
}

static bool push_path(RuntimeSearchPath *search_path, Set(String) *rtp_used, char *entry,
                      bool after, size_t pos_in_rtp)
{
  String *key_alloc;
  if (set_put_ref(String, rtp_used, cstr_as_string(entry), &key_alloc)) {
    *key_alloc = cstr_to_string(entry);
    kv_push(*search_path, ((SearchPathItem){ key_alloc->data, after, false, kNone, pos_in_rtp }));
    return true;
  }
  return false;
}

static void expand_rtp_entry(RuntimeSearchPath *search_path, Set(String) *rtp_used, char *entry,
                             bool after, size_t pos_in_rtp)
{
  if (set_has(String, rtp_used, cstr_as_string(entry))) {
    return;
  }

  if (!*entry) {
    push_path(search_path, rtp_used, entry, after, pos_in_rtp);
  }

  int num_files;
  char **files;
  char *(pat[]) = { entry };
  if (gen_expand_wildcards(1, pat, &num_files, &files, EW_DIR | EW_NOBREAK) == OK) {
    for (int i = 0; i < num_files; i++) {
      // reuses position but it is ok, we need to be monotonic but not strictly
      push_path(search_path, rtp_used, files[i], after, pos_in_rtp);
    }
    FreeWild(num_files, files);
  }
}

static void expand_pack_entry(RuntimeSearchPath *search_path, Set(String) *rtp_used,
                              CharVec *after_path, char *pack_entry, size_t pack_entry_len,
                              size_t pos_in_rtp)
{
  static char buf[MAXPATHL];
  char *(start_pat[]) = { "/pack/*/start/*", "/start/*" };  // NOLINT
  for (int i = 0; i < 2; i++) {
    if (pack_entry_len + strlen(start_pat[i]) + 1 > sizeof buf) {
      continue;
    }
    xstrlcpy(buf, pack_entry, sizeof buf);
    xstrlcpy(buf + pack_entry_len, start_pat[i], sizeof buf - pack_entry_len);
    expand_rtp_entry(search_path, rtp_used, buf, false, pos_in_rtp);
    size_t after_size = strlen(buf) + 7;
    char *after = xmallocz(after_size);
    xstrlcpy(after, buf, after_size);
    xstrlcat(after, "/after", after_size);
    kv_push(*after_path, after);
  }
}

static bool path_is_after(char *buf, size_t buflen)
{
  // NOTE: we only consider dirs exactly matching "after" to be an AFTER dir.
  // vim8 considers all dirs like "foo/bar_after", "Xafter" etc, as an
  // "after" dir in SOME codepaths not not in ALL codepaths.
  return buflen >= 5
         && (!(buflen >= 6) || vim_ispathsep(buf[buflen - 6]))
         && strcmp(buf + buflen - 5, "after") == 0;
}

static RuntimeSearchPath runtime_search_path_build(void)
{
  kvec_t(String) pack_entries = KV_INITIAL_VALUE;
  Map(String, int) pack_used = MAP_INIT;
  Set(String) rtp_used = SET_INIT;
  RuntimeSearchPath search_path = KV_INITIAL_VALUE;
  CharVec after_path = KV_INITIAL_VALUE;

  static char buf[MAXPATHL];
  for (char *entry = p_pp; *entry != NUL;) {
    char *cur_entry = entry;
    size_t buflen = copy_option_part(&entry, buf, MAXPATHL, ",");

    String the_entry = { .data = cur_entry, .size = buflen };

    kv_push(pack_entries, the_entry);
    map_put(String, int)(&pack_used, the_entry, 0);
  }

  char *rtp_entry;
  for (rtp_entry = p_rtp; *rtp_entry != NUL;) {
    char *cur_entry = rtp_entry;
    size_t buflen = copy_option_part(&rtp_entry, buf, MAXPATHL, ",");

    if (path_is_after(buf, buflen)) {
      rtp_entry = cur_entry;
      break;
    }

    size_t pos_in_rtp = (size_t)(cur_entry - p_rtp);

    // fact: &rtp entries can contain wild chars
    expand_rtp_entry(&search_path, &rtp_used, buf, false, pos_in_rtp);

    handle_T *h = map_ref(String, int)(&pack_used, cstr_as_string(buf), NULL);
    if (h) {
      (*h)++;
      expand_pack_entry(&search_path, &rtp_used, &after_path, buf, buflen, pos_in_rtp);
    }
  }

  // The following entries were not explicit in rtp.
  // this is fine, but keep pos_in_rtp monotonic:
  // use the comma between two entries as a sentinel
  size_t sentinel_pos_in_rtp = (size_t)(rtp_entry - p_rtp);
  sentinel_pos_in_rtp -= (sentinel_pos_in_rtp > 0) ? 1 : 0;

  for (size_t i = 0; i < kv_size(pack_entries); i++) {
    String item = kv_A(pack_entries, i);
    handle_T h = map_get(String, int)(&pack_used, item);
    if (h == 0) {
      expand_pack_entry(&search_path, &rtp_used, &after_path, item.data, item.size,
                        sentinel_pos_in_rtp);
    }
  }

  // "after" packages
  for (size_t i = 0; i < kv_size(after_path); i++) {
    expand_rtp_entry(&search_path, &rtp_used, kv_A(after_path, i), true, sentinel_pos_in_rtp);
    xfree(kv_A(after_path, i));
  }

  // "after" dirs in rtp
  for (; *rtp_entry != NUL;) {
    char *cur_entry = rtp_entry;
    size_t buflen = copy_option_part(&rtp_entry, buf, MAXPATHL, ",");
    size_t pos_in_rtp = (size_t)(cur_entry - p_rtp);
    expand_rtp_entry(&search_path, &rtp_used, buf, path_is_after(buf, buflen), pos_in_rtp);
  }

  // strings are not owned
  kv_destroy(pack_entries);
  kv_destroy(after_path);
  map_destroy(String, &pack_used);
  set_destroy(String, &rtp_used);

  return search_path;
}

const char *did_set_runtimepackpath(optset_T *args)
{
  runtime_search_path_valid = false;
  return NULL;
}

static void runtime_search_path_free(RuntimeSearchPath path)
{
  for (size_t j = 0; j < kv_size(path); j++) {
    SearchPathItem item = kv_A(path, j);
    xfree(item.path);
  }
  kv_destroy(path);
}

void runtime_search_path_validate(void)
{
  if (!nlua_is_deferred_safe()) {
    // Cannot rebuild search path in an async context. As a plugin will invoke
    // itself asynchronously from sync code in the same plugin, the sought
    // after lua/autoload module will most likely already be in the cached path.
    // Thus prefer using the stale cache over erroring out in this situation.
    return;
  }
  if (!runtime_search_path_valid) {
    if (!runtime_search_path_ref) {
      runtime_search_path_free(runtime_search_path);
    }
    runtime_search_path = runtime_search_path_build();
    runtime_search_path_valid = true;
    runtime_search_path_ref = NULL;  // initially unowned
    update_runtime_search_path_thread(true);
  }
}

void update_runtime_search_path_thread(bool force)
{
  if (!force && !(runtime_search_path_valid && !runtime_search_path_valid_thread)) {
    return;
  }

  uv_mutex_lock(&runtime_search_path_mutex);
  runtime_search_path_free(runtime_search_path_thread);
  runtime_search_path_thread = copy_runtime_search_path(runtime_search_path);
  uv_mutex_unlock(&runtime_search_path_mutex);
  runtime_search_path_valid_thread = true;
}

/// Just like do_in_path_and_pp(), using 'runtimepath' for "path".
int do_in_runtimepath(char *name, int flags, DoInRuntimepathCB callback, void *cookie)
{
  int success = FAIL;
  if (!(flags & DIP_NORTP)) {
    success |= do_in_cached_path((name && !*name) ? NULL : name, flags, callback, cookie);
    flags = (flags & ~DIP_START) | DIP_NORTP;
  }
  // TODO(bfredl): we could integrate disabled OPT dirs into the cached path
  // which would effectivize ":packadd myoptpack" as well
  if ((flags & (DIP_START|DIP_OPT)) && (success == FAIL || (flags & DIP_ALL))) {
    success |= do_in_path_and_pp(p_rtp, name, flags, callback, cookie);
  }
  return success;
}

/// Source the file "name" from all directories in 'runtimepath'.
/// "name" can contain wildcards.
/// When "flags" has DIP_ALL: source all files, otherwise only the first one.
///
/// return FAIL when no file could be sourced, OK otherwise.
int source_runtime(char *name, int flags)
{
  return do_in_runtimepath(name, flags, source_callback, NULL);
}

/// Just like source_runtime(), but only source vim and lua files
int source_runtime_vim_lua(char *name, int flags)
{
  return do_in_runtimepath(name, flags, source_callback_vim_lua, NULL);
}

/// Just like source_runtime(), but:
/// - use "path" instead of 'runtimepath'.
/// - only source .vim and .lua files
int source_in_path_vim_lua(char *path, char *name, int flags)
{
  return do_in_path_and_pp(path, name, flags, source_callback_vim_lua, NULL);
}

/// Expand wildcards in "pats" and invoke callback matches.
///
/// @param      num_pat  is number of input patterns.
/// @param      patx     is an array of pointers to input patterns.
/// @param      flags    is a combination of EW_* flags used in
///                      expand_wildcards().
/// @param      all      invoke callback on all matches or just one
/// @param      callback called for each match.
/// @param      cookie   context for callback
///
/// @returns             OK when some files were found, FAIL otherwise.
static int gen_expand_wildcards_and_cb(int num_pat, char **pats, int flags, bool all,
                                       DoInRuntimepathCB callback, void *cookie)
{
  int num_files;
  char **files;

  if (gen_expand_wildcards(num_pat, pats, &num_files, &files, flags) != OK) {
    return FAIL;
  }

  (*callback)(num_files, files, all, cookie);

  FreeWild(num_files, files);

  return OK;
}

/// Add the package directory to 'runtimepath'
///
/// @param fname the package path
/// @param is_pack whether the added dir is a "pack/*/start/*/" style package
static int add_pack_dir_to_rtp(char *fname, bool is_pack)
{
  char *afterdir = NULL;
  int retval = FAIL;

  char *p1 = get_past_head(fname);
  char *p2 = p1;
  char *p3 = p1;
  char *p4 = p1;
  for (char *p = p1; *p; MB_PTR_ADV(p)) {
    if (vim_ispathsep_nocolon(*p)) {
      p4 = p3;
      p3 = p2;
      p2 = p1;
      p1 = p;
    }
  }

  // now we have:
  // rtp/pack/name/(start|opt)/name
  //    p4   p3   p2           p1
  //
  // find the part up to "pack" in 'runtimepath'
  p4++;  // append pathsep in order to expand symlink
  char c = *p4;
  *p4 = NUL;
  char *const ffname = fix_fname(fname);
  *p4 = c;

  if (ffname == NULL) {
    return FAIL;
  }

  // Find "ffname" in "p_rtp", ignoring '/' vs '\' differences
  // Also stop at the first "after" directory
  size_t fname_len = strlen(ffname);
  char buf[MAXPATHL];
  const char *insp = NULL;
  const char *after_insp = NULL;
  const char *entry = p_rtp;
  while (*entry != NUL) {
    const char *cur_entry = entry;
    copy_option_part((char **)&entry, buf, MAXPATHL, ",");

    char *p = strstr(buf, "after");
    bool is_after = p != NULL
                    && p > buf
                    && vim_ispathsep(p[-1])
                    && (vim_ispathsep(p[5]) || p[5] == NUL || p[5] == ',');

    if (is_after) {
      if (insp == NULL) {
        // Did not find "ffname" before the first "after" directory,
        // insert it before this entry.
        insp = cur_entry;
      }
      after_insp = cur_entry;
      break;
    }

    if (insp == NULL) {
      add_pathsep(buf);
      char *const rtp_ffname = fix_fname(buf);
      if (rtp_ffname == NULL) {
        goto theend;
      }
      if (path_fnamencmp(rtp_ffname, ffname, fname_len) == 0) {
        // Insert "ffname" after this entry (and comma).
        insp = entry;
      }
      xfree(rtp_ffname);
    }
  }

  if (insp == NULL) {
    // Both "fname" and "after" not found, append at the end.
    insp = p_rtp + strlen(p_rtp);
  }

  // check if rtp/pack/name/start/name/after exists
  afterdir = concat_fnames(fname, "after", true);
  size_t afterlen = 0;
  if (is_pack ? pack_has_entries(afterdir) : os_isdir(afterdir)) {
    afterlen = strlen(afterdir) + 1;  // add one for comma
  }

  const size_t oldlen = strlen(p_rtp);
  const size_t addlen = strlen(fname) + 1;  // add one for comma
  const size_t new_rtp_capacity = oldlen + addlen + afterlen + 1;
  // add one for NUL ------------------------------------------^
  char *const new_rtp = try_malloc(new_rtp_capacity);
  if (new_rtp == NULL) {
    goto theend;
  }

  // We now have 'rtp' parts: {keep}{keep_after}{rest}.
  // Create new_rtp, first: {keep},{fname}
  size_t keep = (size_t)(insp - p_rtp);
  size_t first_pos = keep;
  memmove(new_rtp, p_rtp, keep);
  size_t new_rtp_len = keep;
  if (*insp == NUL) {
    new_rtp[new_rtp_len++] = ',';  // add comma before
    first_pos++;
  }
  memmove(new_rtp + new_rtp_len, fname, addlen - 1);
  new_rtp_len += addlen - 1;
  if (*insp != NUL) {
    new_rtp[new_rtp_len++] = ',';  // add comma after
  }

  size_t after_pos = 0;

  if (afterlen > 0 && after_insp != NULL) {
    size_t keep_after = (size_t)(after_insp - p_rtp);

    // Add to new_rtp: {keep},{fname}{keep_after},{afterdir}
    memmove(new_rtp + new_rtp_len, p_rtp + keep, keep_after - keep);
    new_rtp_len += keep_after - keep;
    memmove(new_rtp + new_rtp_len, afterdir, afterlen - 1);
    new_rtp_len += afterlen - 1;
    new_rtp[new_rtp_len++] = ',';
    keep = keep_after;
    after_pos = keep_after;
  }

  if (p_rtp[keep] != NUL) {
    // Append rest: {keep},{fname}{keep_after},{afterdir}{rest}
    memmove(new_rtp + new_rtp_len, p_rtp + keep, oldlen - keep + 1);
  } else {
    new_rtp[new_rtp_len] = NUL;
  }

  if (afterlen > 0 && after_insp == NULL) {
    // Append afterdir when "after" was not found:
    // {keep},{fname}{rest},{afterdir}
    after_pos = xstrlcat(new_rtp, ",", new_rtp_capacity);
    xstrlcat(new_rtp, afterdir, new_rtp_capacity);
  }

  bool was_valid = runtime_search_path_valid;
  set_option_value_give_err(kOptRuntimepath, CSTR_AS_OPTVAL(new_rtp), 0);

  assert(!runtime_search_path_valid);
  // If this is the result of "packadd opt_pack", rebuilding runtime_search_pat
  // from scratch is needlessly slow. splice in the package and its afterdir instead.
  // But don't do this for "pack/*/start/*" (is_pack=true):
  // we want properly expand wildcards in a "start" bundle.
  if (was_valid && !is_pack && !runtime_search_path_ref) {
    runtime_search_path_valid = true;
    runtime_search_path_valid_thread = false;
    kv_pushp(runtime_search_path);
    ssize_t i = (ssize_t)(kv_size(runtime_search_path)) - 1;

    if (afterlen > 0) {
      kv_pushp(runtime_search_path);
      i += 1;
      for (; i >= 1; i--) {
        if (i > 1 && kv_A(runtime_search_path, i - 2).pos_in_rtp >= after_pos) {
          kv_A(runtime_search_path, i) = kv_A(runtime_search_path, i - 2);
          kv_A(runtime_search_path, i).pos_in_rtp += addlen + afterlen;
        } else {
          kv_A(runtime_search_path, i) = (SearchPathItem){ xstrdup(afterdir), true, true, kNone,
                                                           after_pos + addlen };
          i--;
          break;
        }
      }
    }

    for (; i >= 0; i--) {
      if (i > 0 && kv_A(runtime_search_path, i - 1).pos_in_rtp >= first_pos) {
        kv_A(runtime_search_path, i) = kv_A(runtime_search_path, i - 1);
        kv_A(runtime_search_path, i).pos_in_rtp += addlen;
      } else {
        kv_A(runtime_search_path, i) = (SearchPathItem){ xstrdup(fname), false, true, kNone,
                                                         first_pos };
        break;
      }
    }
  }
  xfree(new_rtp);
  retval = OK;

theend:
  xfree(ffname);
  xfree(afterdir);
  return retval;
}

/// Load scripts in "plugin" directory of the package.
/// For opt packages, also load scripts in "ftdetect" (start packages already
/// load these from filetype.lua)
static int load_pack_plugin(bool opt, char *fname)
{
  static const char plugpat[] = "%s/plugin/**/*";  // NOLINT
  static const char ftpat[] = "%s/ftdetect/*";  // NOLINT

  char *const ffname = fix_fname(fname);
  size_t len = strlen(ffname) + sizeof(plugpat);
  char *pat = xmallocz(len);

  vim_snprintf(pat, len, plugpat, ffname);
  gen_expand_wildcards_and_cb(1, &pat, EW_FILE, true, source_callback_vim_lua, NULL);

  char *cmd = xstrdup("g:did_load_filetypes");

  // If runtime/filetype.lua wasn't loaded yet, the scripts will be
  // found when it loads.
  if (opt && eval_to_number(cmd, false) > 0) {
    do_cmdline_cmd("augroup filetypedetect");
    vim_snprintf(pat, len, ftpat, ffname);
    gen_expand_wildcards_and_cb(1, &pat, EW_FILE, true, source_callback_vim_lua, NULL);
    do_cmdline_cmd("augroup END");
  }
  xfree(cmd);
  xfree(pat);
  xfree(ffname);

  return OK;
}

// used for "cookie" of add_pack_plugin()
static int APP_ADD_DIR;
static int APP_LOAD;
static int APP_BOTH;

static void add_pack_plugins(bool opt, int num_fnames, char **fnames, bool all, void *cookie)
{
  bool did_one = false;

  if (cookie != &APP_LOAD) {
    char buf[MAXPATHL];
    for (int i = 0; i < num_fnames; i++) {
      bool found = false;

      const char *p = p_rtp;
      while (*p != NUL) {
        copy_option_part((char **)&p, buf, MAXPATHL, ",");
        if (path_fnamecmp(buf, fnames[i]) == 0) {
          found = true;
          break;
        }
      }
      if (!found) {
        // directory is not yet in 'runtimepath', add it
        if (add_pack_dir_to_rtp(fnames[i], false) == FAIL) {
          return;
        }
      }
      did_one = true;
      if (!all) {
        break;
      }
    }
  }

  if (!all && did_one) {
    return;
  }

  if (cookie != &APP_ADD_DIR) {
    for (int i = 0; i < num_fnames; i++) {
      load_pack_plugin(opt, fnames[i]);
      if (!all) {
        break;
      }
    }
  }
}

static bool add_start_pack_plugins(int num_fnames, char **fnames, bool all, void *cookie)
{
  add_pack_plugins(false, num_fnames, fnames, all, cookie);
  return num_fnames > 0;
}

static bool add_opt_pack_plugins(int num_fnames, char **fnames, bool all, void *cookie)
{
  add_pack_plugins(true, num_fnames, fnames, all, cookie);
  return num_fnames > 0;
}

/// Add all packages in the "start" directory to 'runtimepath'.
void add_pack_start_dirs(void)
{
  do_in_path(p_pp, "", NULL, DIP_ALL + DIP_DIR, add_pack_start_dir, NULL);
}

static bool pack_has_entries(char *buf)
{
  int num_files;
  char **files;
  char *(pat[]) = { buf };
  if (gen_expand_wildcards(1, pat, &num_files, &files, EW_DIR) == OK) {
    FreeWild(num_files, files);
  }
  return num_files > 0;
}

static bool add_pack_start_dir(int num_fnames, char **fnames, bool all, void *cookie)
{
  static char buf[MAXPATHL];
  for (int i = 0; i < num_fnames; i++) {
    char *(start_pat[]) = { "/start/*", "/pack/*/start/*" };  // NOLINT
    for (int j = 0; j < 2; j++) {
      if (strlen(fnames[i]) + strlen(start_pat[j]) + 1 > MAXPATHL) {
        continue;
      }
      xstrlcpy(buf, fnames[i], MAXPATHL);
      xstrlcat(buf, start_pat[j], sizeof buf);
      if (pack_has_entries(buf)) {
        add_pack_dir_to_rtp(buf, true);
      }
    }

    if (!all) {
      break;
    }
  }

  return num_fnames > 1;
}

/// Load plugins from all packages in the "start" directory.
void load_start_packages(void)
{
  did_source_packages = true;
  do_in_path(p_pp, "", "pack/*/start/*", DIP_ALL + DIP_DIR,  // NOLINT
             add_start_pack_plugins, &APP_LOAD);
  do_in_path(p_pp, "", "start/*", DIP_ALL + DIP_DIR,  // NOLINT
             add_start_pack_plugins, &APP_LOAD);

  update_runtime_search_path_thread(false);
}

// ":packloadall"
// Find plugins in the package directories and source them.
void ex_packloadall(exarg_T *eap)
{
  if (!did_source_packages || eap->forceit) {
    // First do a round to add all directories to 'runtimepath', then load
    // the plugins. This allows for plugins to use an autoload directory
    // of another plugin.
    add_pack_start_dirs();
    load_start_packages();
  }
}

/// Read all the plugin files at startup
void load_plugins(void)
{
  if (p_lpl) {
    char *rtp_copy = p_rtp;
    char *const plugin_pattern = "plugin/**/*";  // NOLINT

    if (!did_source_packages) {
      rtp_copy = xstrdup(p_rtp);
      add_pack_start_dirs();
    }

    // Don't use source_runtime_vim_lua() yet so we can check for :packloadall below.
    // NB: after calling this "rtp_copy" may have been freed if it wasn't copied.
    source_in_path_vim_lua(rtp_copy, plugin_pattern, DIP_ALL | DIP_NOAFTER);
    TIME_MSG("loading rtp plugins");

    // Only source "start" packages if not done already with a :packloadall
    // command.
    if (!did_source_packages) {
      xfree(rtp_copy);
      load_start_packages();
    }
    TIME_MSG("loading packages");

    source_runtime_vim_lua(plugin_pattern, DIP_ALL | DIP_AFTER);
    TIME_MSG("loading after plugins");
  }
}

/// ":packadd[!] {name}"
void ex_packadd(exarg_T *eap)
{
  static const char plugpat[] = "pack/*/%s/%s";  // NOLINT
  int res = OK;

  const size_t len = sizeof(plugpat) + strlen(eap->arg) + 5;
  char *pat = xmallocz(len);
  void *cookie = eap->forceit ? &APP_ADD_DIR : &APP_BOTH;

  // Only look under "start" when loading packages wasn't done yet.
  if (!did_source_packages) {
    vim_snprintf(pat, len, plugpat, "start", eap->arg);
    res = do_in_path(p_pp, "", pat, DIP_ALL + DIP_DIR,
                     add_start_pack_plugins, cookie);
  }

  // Give a "not found" error if nothing was found in 'start' or 'opt'.
  vim_snprintf(pat, len, plugpat, "opt", eap->arg);
  do_in_path(p_pp, "", pat, DIP_ALL + DIP_DIR + (res == FAIL ? DIP_ERR : 0),
             add_opt_pack_plugins, cookie);

  update_runtime_search_path_thread(false);

  xfree(pat);
}

static void ExpandRTDir_int(char *pat, size_t pat_len, int flags, bool keep_ext, garray_T *gap,
                            char *dirnames[])
{
  // TODO(bfredl): this is bullshit, expandpath should not reinvent path logic.
  for (int i = 0; dirnames[i] != NULL; i++) {
    const size_t buf_len = strlen(dirnames[i]) + pat_len + 64;
    char *buf = xmalloc(buf_len);
    int glob_flags = 0;
    bool expand_dirs = false;
    // Build base pattern
    snprintf(buf, buf_len, "%s%s%s%s", *dirnames[i] ? dirnames[i] : "", *dirnames[i] ? "/" : "",
             pat, "*.{vim,lua}");

expand:
    if ((flags & DIP_NORTP) == 0) {
      globpath(p_rtp, buf, gap, glob_flags, expand_dirs);
    }

    if (flags & DIP_START) {
      // pack/*/start/*/ patterns
      snprintf(buf, buf_len, "pack/*/start/*/%s%s%s%s", *dirnames[i] ? dirnames[i] : "",  // NOLINT
               *dirnames[i] ? "/" : "", pat, expand_dirs ? "*" : "*.{vim,lua}");
      globpath(p_pp, buf, gap, glob_flags, expand_dirs);

      // start/*/ patterns
      snprintf(buf, buf_len, "start/*/%s%s%s%s", *dirnames[i] ? dirnames[i] : "",  // NOLINT
               *dirnames[i] ? "/" : "", pat, expand_dirs ? "*" : "*.{vim,lua}");
      globpath(p_pp, buf, gap, glob_flags, expand_dirs);
    }

    if (flags & DIP_OPT) {
      // pack/*/opt/*/ patterns
      snprintf(buf, buf_len, "pack/*/opt/*/%s%s%s%s", *dirnames[i] ? dirnames[i] : "",  // NOLINT
               *dirnames[i] ? "/" : "", pat, expand_dirs ? "*" : "*.{vim,lua}");
      globpath(p_pp, buf, gap, glob_flags, expand_dirs);

      // opt/*/ patterns
      snprintf(buf, buf_len, "opt/*/%s%s%s%s", *dirnames[i] ? dirnames[i] : "",  // NOLINT
               *dirnames[i] ? "/" : "", pat, expand_dirs ? "*" : "*.{vim,lua}");
      globpath(p_pp, buf, gap, glob_flags, expand_dirs);
    }

    // Second round for directories
    if (*dirnames[i] == NUL && !expand_dirs) {
      snprintf(buf, buf_len, "%s*", pat);
      glob_flags = WILD_ADD_SLASH;
      expand_dirs = true;
      goto expand;
    }

    xfree(buf);
  }

  int pat_pathsep_cnt = 0;
  for (size_t i = 0; i < pat_len; i++) {
    if (vim_ispathsep(pat[i])) {
      pat_pathsep_cnt++;
    }
  }

  for (int i = 0; i < gap->ga_len; i++) {
    char *match = ((char **)gap->ga_data)[i];
    char *s = match;
    char *e = s + strlen(s);
    if (e - s > 4 && !keep_ext && (STRNICMP(e - 4, ".vim", 4) == 0
                                   || STRNICMP(e - 4, ".lua", 4) == 0)) {
      e -= 4;
      *e = NUL;
    }

    int match_pathsep_cnt = (e > s && e[-1] == '/') ? -1 : 0;
    for (s = e; s > match; MB_PTR_BACK(match, s)) {
      if (vim_ispathsep(*s) && ++match_pathsep_cnt > pat_pathsep_cnt) {
        break;
      }
    }
    s++;
    if (s != match) {
      assert((e - s) + 1 >= 0);
      memmove(match, s, (size_t)(e - s) + 1);
    }
  }

  if (GA_EMPTY(gap)) {
    return;
  }

  // Sort and remove duplicates which can happen when specifying multiple
  // directories in dirnames.
  ga_remove_duplicate_strings(gap);
}

/// Expand color scheme, compiler or filetype names.
/// Search from 'runtimepath':
///   'runtimepath'/{dirnames}/{pat}.{vim,lua}
/// When "flags" has DIP_START: search also from "start" of 'packpath':
///   'packpath'/pack/*/start/*/{dirnames}/{pat}.{vim,lua}
/// When "flags" has DIP_OPT: search also from "opt" of 'packpath':
///   'packpath'/pack/*/opt/*/{dirnames}/{pat}.{vim,lua}
/// "dirnames" is an array with one or more directory names.
int ExpandRTDir(char *pat, int flags, int *num_file, char ***file, char *dirnames[])
{
  *num_file = 0;
  *file = NULL;

  garray_T ga;
  ga_init(&ga, (int)sizeof(char *), 10);

  ExpandRTDir_int(pat, strlen(pat), flags, false, &ga, dirnames);

  if (GA_EMPTY(&ga)) {
    return FAIL;
  }

  *file = ga.ga_data;
  *num_file = ga.ga_len;
  return OK;
}

/// Handle command line completion for :runtime command.
int expand_runtime_cmd(char *pat, int *numMatches, char ***matches)
{
  *numMatches = 0;
  *matches = NULL;

  garray_T ga;
  ga_init(&ga, sizeof(char *), 10);

  const size_t pat_len = strlen(pat);
  char *dirnames[] = { "", NULL };
  ExpandRTDir_int(pat, pat_len, runtime_expand_flags, true, &ga, dirnames);

  // Try to complete values for [where] argument when none was found.
  if (runtime_expand_flags == 0) {
    char *where_values[] = { "START", "OPT", "PACK", "ALL" };
    for (size_t i = 0; i < ARRAY_SIZE(where_values); i++) {
      if (strncmp(pat, where_values[i], pat_len) == 0) {
        GA_APPEND(char *, &ga, xstrdup(where_values[i]));
      }
    }
  }

  if (GA_EMPTY(&ga)) {
    return FAIL;
  }

  *matches = ga.ga_data;
  *numMatches = ga.ga_len;
  return OK;
}

/// Expand loadplugin names:
/// 'packpath'/pack/*/opt/{pat}
int ExpandPackAddDir(char *pat, int *num_file, char ***file)
{
  garray_T ga;

  *num_file = 0;
  *file = NULL;
  size_t pat_len = strlen(pat);
  ga_init(&ga, (int)sizeof(char *), 10);

  size_t buflen = pat_len + 26;
  char *s = xmalloc(buflen);
  snprintf(s, buflen, "pack/*/opt/%s*", pat);  // NOLINT
  globpath(p_pp, s, &ga, 0, true);
  snprintf(s, buflen, "opt/%s*", pat);  // NOLINT
  globpath(p_pp, s, &ga, 0, true);
  xfree(s);

  for (int i = 0; i < ga.ga_len; i++) {
    char *match = ((char **)ga.ga_data)[i];
    s = path_tail(match);
    memmove(match, s, strlen(s) + 1);
  }

  if (GA_EMPTY(&ga)) {
    return FAIL;
  }

  // Sort and remove duplicates which can happen when specifying multiple
  // directories in dirnames.
  ga_remove_duplicate_strings(&ga);

  *file = ga.ga_data;
  *num_file = ga.ga_len;
  return OK;
}

/// Append string with escaped commas
static char *strcpy_comma_escaped(char *dest, const char *src, const size_t len)
  FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
{
  size_t shift = 0;
  for (size_t i = 0; i < len; i++) {
    if (src[i] == ',') {
      dest[i + shift++] = '\\';
    }
    dest[i + shift] = src[i];
  }
  return &dest[len + shift];
}

/// Compute length of a ENV_SEPCHAR-separated value, doubled and with some
/// suffixes
///
/// @param[in]  val  ENV_SEPCHAR-separated array value.
/// @param[in]  common_suf_len  Length of the common suffix which is appended to
///                             each item in the array, twice.
/// @param[in]  single_suf_len  Length of the suffix which is appended to each
///                             item in the array once.
///
/// @return Length of the ENV_SEPCHAR-separated string array that contains each
///         item in the original array twice with suffixes with given length
///         (common_suf is present after each new item, single_suf is present
///         after half of the new items) and with commas after each item, commas
///         inside the values are escaped.
static inline size_t compute_double_env_sep_len(const char *const val, const size_t common_suf_len,
                                                const size_t single_suf_len)
  FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE
{
  if (val == NULL || *val == NUL) {
    return 0;
  }
  size_t ret = 0;
  const void *iter = NULL;
  do {
    size_t dir_len;
    const char *dir;
    iter = vim_env_iter(ENV_SEPCHAR, val, iter, &dir, &dir_len);
    if (dir != NULL && dir_len > 0) {
      ret += ((dir_len + memcnt(dir, ',', dir_len) + common_suf_len
               + !after_pathsep(dir, dir + dir_len)) * 2
              + single_suf_len);
    }
  } while (iter != NULL);
  return ret;
}

/// Add directories to a ENV_SEPCHAR-separated array from a colon-separated one
///
/// Commas are escaped in process. To each item PATHSEP "nvim" is appended in
/// addition to suf1 and suf2.
///
/// @param[in,out]  dest  Destination comma-separated array.
/// @param[in]  val  Source ENV_SEPCHAR-separated array.
/// @param[in]  suf1  If not NULL, suffix appended to destination. Prior to it
///                   directory separator is appended. Suffix must not contain
///                   commas.
/// @param[in]  len1  Length of the suf1.
/// @param[in]  suf2  If not NULL, another suffix appended to destination. Again
///                   with directory separator behind. Suffix must not contain
///                   commas.
/// @param[in]  len2  Length of the suf2.
/// @param[in]  forward  If true, iterate over val in forward direction.
///                      Otherwise in reverse.
///
/// @return (dest + appended_characters_length)
static inline char *add_env_sep_dirs(char *dest, const char *const val, const char *const suf1,
                                     const size_t len1, const char *const suf2, const size_t len2,
                                     const bool forward)
  FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_RET FUNC_ATTR_NONNULL_ARG(1)
{
  if (val == NULL || *val == NUL) {
    return dest;
  }
  const void *iter = NULL;
  const char *appname = get_appname(false);
  const size_t appname_len = strlen(appname);
  do {
    size_t dir_len;
    const char *dir;
    iter = (forward ? vim_env_iter : vim_env_iter_rev)(ENV_SEPCHAR, val, iter,
                                                       &dir, &dir_len);
    if (dir != NULL && dir_len > 0) {
      dest = strcpy_comma_escaped(dest, dir, dir_len);
      if (!after_pathsep(dest - 1, dest)) {
        *dest++ = PATHSEP;
      }
      memmove(dest, appname, appname_len);
      dest += appname_len;
      if (suf1 != NULL) {
        *dest++ = PATHSEP;
        memmove(dest, suf1, len1);
        dest += len1;
        if (suf2 != NULL) {
          *dest++ = PATHSEP;
          memmove(dest, suf2, len2);
          dest += len2;
        }
      }
      *dest++ = ',';
    }
  } while (iter != NULL);
  return dest;
}

/// Adds directory `dest` to a comma-separated list of directories.
///
/// Commas in the added directory are escaped.
///
/// Windows: Appends "nvim-data" instead of "nvim" if `type` is kXDGDataHome.
///
/// @see get_xdg_home
///
/// @param[in,out]  dest  Destination comma-separated array.
/// @param[in]  dir  Directory to append.
/// @param[in]  type  Decides whether to append "nvim" (Win: or "nvim-data").
/// @param[in]  suf1  If not NULL, suffix appended to destination. Prior to it
///                   directory separator is appended. Suffix must not contain
///                   commas.
/// @param[in]  len1  Length of the suf1.
/// @param[in]  suf2  If not NULL, another suffix appended to destination. Again
///                   with directory separator behind. Suffix must not contain
///                   commas.
/// @param[in]  len2  Length of the suf2.
/// @param[in]  forward  If true, iterate over val in forward direction.
///                      Otherwise in reverse.
///
/// @return (dest + appended_characters_length)
static inline char *add_dir(char *dest, const char *const dir, const size_t dir_len,
                            const XDGVarType type, const char *const suf1, const size_t len1,
                            const char *const suf2, const size_t len2)
  FUNC_ATTR_NONNULL_RET FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_WARN_UNUSED_RESULT
{
  if (dir == NULL || dir_len == 0) {
    return dest;
  }
  dest = strcpy_comma_escaped(dest, dir, dir_len);
  bool append_nvim = (type == kXDGDataHome || type == kXDGConfigHome);
  if (append_nvim) {
    if (!after_pathsep(dest - 1, dest)) {
      *dest++ = PATHSEP;
    }
    const char *appname = get_appname(false);
    size_t appname_len = strlen(appname);
    assert(appname_len < (IOSIZE - sizeof("-data")));
    xmemcpyz(IObuff, appname, appname_len);
#if defined(MSWIN)
    if (type == kXDGDataHome || type == kXDGStateHome) {
      xstrlcat(IObuff, "-data", IOSIZE);
      appname_len += 5;
    }
#endif
    xmemcpyz(dest, IObuff, appname_len);
    dest += appname_len;
    if (suf1 != NULL) {
      *dest++ = PATHSEP;
      memmove(dest, suf1, len1);
      dest += len1;
      if (suf2 != NULL) {
        *dest++ = PATHSEP;
        memmove(dest, suf2, len2);
        dest += len2;
      }
    }
  }
  *dest++ = ',';
  return dest;
}

char *get_lib_dir(void)
{
  // TODO(bfredl): too fragile? Ideally default_lib_dir would be made empty
  // in an appimage build
  if (strlen(default_lib_dir) != 0
      && os_isdir(default_lib_dir)) {
    return xstrdup(default_lib_dir);
  }

  // Find library path relative to the nvim binary: ../lib/nvim/
  char exe_name[MAXPATHL];
  vim_get_prefix_from_exepath(exe_name);
  if (append_path(exe_name, "lib/nvim", MAXPATHL) == OK) {
    return xstrdup(exe_name);
  }
  return NULL;
}

/// Determine the startup value for &runtimepath
///
/// Windows: Uses "…/nvim-data" for kXDGDataHome to avoid storing
/// configuration and data files in the same path. #4403
///
/// @param clean_arg  Nvim was started with --clean.
/// @return allocated string with the value
char *runtimepath_default(bool clean_arg)
{
  size_t rtp_size = 0;
  char *const data_home = clean_arg
                          ? NULL
                          : stdpaths_get_xdg_var(kXDGDataHome);
  char *const config_home = clean_arg
                            ? NULL
                            : stdpaths_get_xdg_var(kXDGConfigHome);
  char *const vimruntime = vim_getenv("VIMRUNTIME");
  char *const libdir = get_lib_dir();
  char *const data_dirs = stdpaths_get_xdg_var(kXDGDataDirs);
  char *const config_dirs = stdpaths_get_xdg_var(kXDGConfigDirs);
#define SITE_SIZE (sizeof("site") - 1)
#define AFTER_SIZE (sizeof("after") - 1)
  size_t data_len = 0;
  size_t config_len = 0;
  size_t vimruntime_len = 0;
  size_t libdir_len = 0;
  size_t appname_len = strlen(get_appname(false));
  if (data_home != NULL) {
    data_len = strlen(data_home);
    size_t nvim_data_size = appname_len;
#if defined(MSWIN)
    nvim_data_size += sizeof("-data") - 1;  // -1: NULL byte should be ignored
#endif
    if (data_len != 0) {
      rtp_size += ((data_len + memcnt(data_home, ',', data_len)
                    + nvim_data_size + 1 + SITE_SIZE + 1
                    + !after_pathsep(data_home, data_home + data_len)) * 2
                   + AFTER_SIZE + 1);
    }
  }
  if (config_home != NULL) {
    config_len = strlen(config_home);
    if (config_len != 0) {
      rtp_size += ((config_len + memcnt(config_home, ',', config_len)
                    + appname_len + 1
                    + !after_pathsep(config_home, config_home + config_len)) * 2
                   + AFTER_SIZE + 1);
    }
  }
  if (vimruntime != NULL) {
    vimruntime_len = strlen(vimruntime);
    if (vimruntime_len != 0) {
      rtp_size += vimruntime_len + memcnt(vimruntime, ',', vimruntime_len) + 1;
    }
  }
  if (libdir != NULL) {
    libdir_len = strlen(libdir);
    if (libdir_len != 0) {
      rtp_size += libdir_len + memcnt(libdir, ',', libdir_len) + 1;
    }
  }
  rtp_size += compute_double_env_sep_len(data_dirs,
                                         appname_len + 1 + SITE_SIZE + 1,
                                         AFTER_SIZE + 1);
  rtp_size += compute_double_env_sep_len(config_dirs, appname_len + 1,
                                         AFTER_SIZE + 1);
  char *rtp = NULL;
  if (rtp_size == 0) {
    goto freeall;
  }
  rtp = xmalloc(rtp_size);
  char *rtp_cur = rtp;
  rtp_cur = add_dir(rtp_cur, config_home, config_len, kXDGConfigHome,
                    NULL, 0, NULL, 0);
  rtp_cur = add_env_sep_dirs(rtp_cur, config_dirs, NULL, 0, NULL, 0, true);
  rtp_cur = add_dir(rtp_cur, data_home, data_len, kXDGDataHome,
                    "site", SITE_SIZE, NULL, 0);
  rtp_cur = add_env_sep_dirs(rtp_cur, data_dirs, "site", SITE_SIZE, NULL, 0,
                             true);
  rtp_cur = add_dir(rtp_cur, vimruntime, vimruntime_len, kXDGNone,
                    NULL, 0, NULL, 0);
  rtp_cur = add_dir(rtp_cur, libdir, libdir_len, kXDGNone, NULL, 0, NULL, 0);
  rtp_cur = add_env_sep_dirs(rtp_cur, data_dirs, "site", SITE_SIZE,
                             "after", AFTER_SIZE, false);
  rtp_cur = add_dir(rtp_cur, data_home, data_len, kXDGDataHome,
                    "site", SITE_SIZE, "after", AFTER_SIZE);
  rtp_cur = add_env_sep_dirs(rtp_cur, config_dirs, "after", AFTER_SIZE, NULL, 0,
                             false);
  rtp_cur = add_dir(rtp_cur, config_home, config_len, kXDGConfigHome,
                    "after", AFTER_SIZE, NULL, 0);
  // Strip trailing comma.
  rtp_cur[-1] = NUL;
  assert((size_t)(rtp_cur - rtp) == rtp_size);
#undef SITE_SIZE
#undef AFTER_SIZE
freeall:
  xfree(data_dirs);
  xfree(config_dirs);
  xfree(data_home);
  xfree(config_home);
  xfree(vimruntime);
  xfree(libdir);

  return rtp;
}

static void cmd_source(char *fname, exarg_T *eap)
{
  if (*fname != NUL && eap != NULL && eap->addr_count > 0) {
    // if a filename is specified to :source, then a range is not allowed
    emsg(_(e_norange));
    return;
  }

  if (eap != NULL && *fname == NUL) {
    if (eap->forceit) {
      // a file name is needed to source normal mode commands
      emsg(_(e_argreq));
    } else {
      // source ex commands from the current buffer
      cmd_source_buffer(eap, false);
    }
  } else if (eap != NULL && eap->forceit) {
    // ":source!": read Normal mode commands
    // Need to execute the commands directly.  This is required at least
    // for:
    // - ":g" command busy
    // - after ":argdo", ":windo" or ":bufdo"
    // - another command follows
    // - inside a loop
    openscript(fname, global_busy || listcmd_busy || eap->nextcmd != NULL
               || eap->cstack->cs_idx >= 0);

    // ":source" read ex commands
  } else if (do_source(fname, false, DOSO_NONE, NULL) == FAIL) {
    semsg(_(e_notopen), fname);
  }
}

/// ":source [{fname}]"
void ex_source(exarg_T *eap)
{
  cmd_source(eap->arg, eap);
}

/// ":options"
void ex_options(exarg_T *eap)
{
  char buf[500];
  bool multi_mods = 0;

  buf[0] = NUL;
  add_win_cmd_modifiers(buf, &cmdmod, &multi_mods);

  os_setenv("OPTWIN_CMD", buf, 1);
  cmd_source(SYS_OPTWIN_FILE, NULL);
}

/// ":source" and associated commands.
///
/// @return address holding the next breakpoint line for a source cookie
linenr_T *source_breakpoint(void *cookie)
{
  return &((source_cookie_T *)cookie)->breakpoint;
}

/// @return  the address holding the debug tick for a source cookie.
int *source_dbg_tick(void *cookie)
{
  return &((source_cookie_T *)cookie)->dbg_tick;
}

/// @return  the nesting level for a source cookie.
int source_level(void *cookie)
  FUNC_ATTR_PURE
{
  return ((source_cookie_T *)cookie)->level;
}

/// Special function to open a file without handle inheritance.
/// If possible the handle is closed on exec().
static FILE *fopen_noinh_readbin(char *filename)
{
#ifdef MSWIN
  int fd_tmp = os_open(filename, O_RDONLY | O_BINARY | O_NOINHERIT, 0);
#else
  int fd_tmp = os_open(filename, O_RDONLY, 0);
#endif

  if (fd_tmp < 0) {
    return NULL;
  }

  os_set_cloexec(fd_tmp);

  return fdopen(fd_tmp, READBIN);
}

/// Concatenate Vimscript line if it starts with a line continuation into a growarray
/// (excluding the continuation chars and leading whitespace)
///
/// @note Growsize of the growarray may be changed to speed up concatenations!
///
/// @param ga  the growarray to append to
/// @param init_growsize  the starting growsize value of the growarray
/// @param p  pointer to the beginning of the line to consider
/// @param len  the length of this line
///
/// @return true if this line did begin with a continuation (the next line
///         should also be considered, if it exists); false otherwise
static bool concat_continued_line(garray_T *const ga, const int init_growsize, const char *const p,
                                  size_t len)
  FUNC_ATTR_NONNULL_ALL
{
  const char *const line = skipwhite_len(p, len);
  len -= (size_t)(line - p);
  // Skip lines starting with '\" ', concat lines starting with '\'
  if (len >= 3 && strncmp(line, "\"\\ ", 3) == 0) {
    return true;
  } else if (len == 0 || line[0] != '\\') {
    return false;
  }
  if (ga->ga_len > init_growsize) {
    ga_set_growsize(ga, MIN(ga->ga_len, 8000));
  }
  ga_concat_len(ga, line + 1, len - 1);
  return true;
}

/// Create a new script item and allocate script-local vars. @see new_script_vars
///
/// @param  name  File name of the script. NULL for anonymous :source.
/// @param[out]  sid_out  SID of the new item.
///
/// @return  pointer to the created script item.
scriptitem_T *new_script_item(char *const name, scid_T *const sid_out)
  FUNC_ATTR_NONNULL_RET
{
  static scid_T last_current_SID = 0;
  const scid_T sid = ++last_current_SID;
  if (sid_out != NULL) {
    *sid_out = sid;
  }
  ga_grow(&script_items, sid - script_items.ga_len);
  while (script_items.ga_len < sid) {
    scriptitem_T *si = xcalloc(1, sizeof(scriptitem_T));
    script_items.ga_len++;
    SCRIPT_ITEM(script_items.ga_len) = si;
    si->sn_name = NULL;

    // Allocate the local script variables to use for this script.
    new_script_vars(script_items.ga_len);

    si->sn_prof_on = false;
  }
  SCRIPT_ITEM(sid)->sn_name = name;
  return SCRIPT_ITEM(sid);
}

/// Initialization for sourcing lines from the current buffer. Reads all the
/// lines from the buffer and stores it in the cookie grow array.
/// Returns a pointer to the name ":source buffer=<n>" on success and NULL on failure.
static char *do_source_buffer_init(source_cookie_T *sp, const exarg_T *eap, bool ex_lua)
  FUNC_ATTR_NONNULL_ALL
{
  if (curbuf == NULL) {
    return NULL;
  }

  char *fname;
  if (curbuf->b_ffname != NULL) {
    fname = xstrdup(curbuf->b_ffname);
  } else {
    if (ex_lua) {
      // Use ":{range}lua buffer=<num>" as the script name
      snprintf(IObuff, IOSIZE, ":{range}lua buffer=%d", curbuf->b_fnum);
    } else {
      // Use ":source buffer=<num>" as the script name
      snprintf(IObuff, IOSIZE, ":source buffer=%d", curbuf->b_fnum);
    }
    fname = xstrdup(IObuff);
  }

  ga_init(&sp->buflines, sizeof(char *), 100);
  // Copy the lines from the buffer into a grow array
  for (linenr_T curr_lnum = eap->line1; curr_lnum <= eap->line2; curr_lnum++) {
    GA_APPEND(char *, &sp->buflines, xstrdup(ml_get(curr_lnum)));
  }
  sp->buf_lnum = 0;
  sp->source_from_buf_or_str = true;
  // When sourcing a range of lines from a buffer, use buffer line number.
  sp->sourcing_lnum = eap->line1 - 1;

  return fname;
}

/// Initialization for sourcing lines from a string. Reads all the
/// lines from the string and stores it in the cookie grow array.
static void do_source_str_init(source_cookie_T *sp, const char *str)
  FUNC_ATTR_NONNULL_ALL
{
  ga_init(&sp->buflines, sizeof(char *), 100);
  // Copy the lines from the string into a grow array
  while (*str != NUL) {
    const char *eol = skip_to_newline(str);
    GA_APPEND(char *, &sp->buflines, xmemdupz(str, (size_t)(eol - str)));
    str = eol + (*eol != NUL);
  }
  sp->buf_lnum = 0;
  sp->source_from_buf_or_str = true;
}

void cmd_source_buffer(const exarg_T *const eap, bool ex_lua)
  FUNC_ATTR_NONNULL_ALL
{
  do_source_ext(NULL, false, DOSO_NONE, NULL, eap, ex_lua, NULL);
}

/// Executes lines in `str` as Ex commands.
///
/// @see do_source_ext()
int do_source_str(const char *str, char *traceback_name)
  FUNC_ATTR_NONNULL_ALL
{
  char *const sourcing_name = SOURCING_NAME;
  const linenr_T sourcing_lnum = SOURCING_LNUM;
  char sname_buf[256];
  if (sourcing_name != NULL) {
    snprintf(sname_buf, sizeof(sname_buf), "%s called at %s:%" PRIdLINENR,
             traceback_name, sourcing_name, sourcing_lnum);
    traceback_name = sname_buf;
  }
  return do_source_ext(traceback_name, false, DOSO_NONE, NULL, NULL, false, str);
}

/// When fname is a .lua file nlua_exec_file() is invoked to source it.
/// Otherwise reads the file `fname` and executes its lines as Ex commands.
///
/// This function may be called recursively!
///
/// @see do_source_str
///
/// @param fname        if NULL, source from the current buffer
/// @param check_other  check for .vimrc and _vimrc
/// @param is_vimrc     DOSO_ value
/// @param ret_sid      if not NULL and we loaded the script before, don't load it again
/// @param eap          used when sourcing lines from a buffer instead of a file
/// @param str          if not NULL, source from the given string
///
/// @return  FAIL if file could not be opened, OK otherwise
///
/// If a scriptitem_T was found or created "*ret_sid" is set to the SID.
static int do_source_ext(char *const fname, const bool check_other, const int is_vimrc,
                         int *const ret_sid, const exarg_T *const eap, const bool ex_lua,
                         const char *const str)
{
  source_cookie_T cookie;
  uint8_t *firstline = NULL;
  int retval = FAIL;
  int save_debug_break_level = debug_break_level;
  scriptitem_T *si = NULL;
  proftime_T wait_start;
  bool trigger_source_post = false;
  ESTACK_CHECK_DECLARATION;

  CLEAR_FIELD(cookie);
  char *fname_exp = NULL;
  if (fname == NULL) {
    assert(str == NULL);
    // sourcing lines from a buffer
    fname_exp = do_source_buffer_init(&cookie, eap, ex_lua);
    if (fname_exp == NULL) {
      return FAIL;
    }
  } else if (str != NULL) {
    do_source_str_init(&cookie, str);
    fname_exp = xstrdup(fname);
  } else {
    char *p = expand_env_save(fname);
    if (p == NULL) {
      return retval;
    }
    fname_exp = fix_fname(p);
    xfree(p);
    if (fname_exp == NULL) {
      return retval;
    }
    if (os_isdir(fname_exp)) {
      smsg(0, _("Cannot source a directory: \"%s\""), fname);
      goto theend;
    }
  }

  // See if we loaded this script before.
  int sid = str != NULL ? SID_STR : find_script_by_name(fname_exp);
  if (sid > 0 && ret_sid != NULL) {
    // Already loaded and no need to load again, return here.
    *ret_sid = sid;
    retval = OK;
    goto theend;
  }

  if (str == NULL) {
    // Apply SourceCmd autocommands, they should get the file and source it.
    if (has_autocmd(EVENT_SOURCECMD, fname_exp, NULL)
        && apply_autocmds(EVENT_SOURCECMD, fname_exp, fname_exp,
                          false, curbuf)) {
      retval = aborting() ? FAIL : OK;
      if (retval == OK) {
        // Apply SourcePost autocommands.
        apply_autocmds(EVENT_SOURCEPOST, fname_exp, fname_exp, false, curbuf);
      }
      goto theend;
    }

    // Apply SourcePre autocommands, they may get the file.
    apply_autocmds(EVENT_SOURCEPRE, fname_exp, fname_exp, false, curbuf);
  }

  if (!cookie.source_from_buf_or_str) {
    cookie.fp = fopen_noinh_readbin(fname_exp);
  }
  if (cookie.fp == NULL && check_other) {
    // Try again, replacing file name ".nvimrc" by "_nvimrc" or vice versa,
    // and ".exrc" by "_exrc" or vice versa.
    char *p = path_tail(fname_exp);
    if ((*p == '.' || *p == '_')
        && (STRICMP(p + 1, "nvimrc") == 0 || STRICMP(p + 1, "exrc") == 0)) {
      *p = (*p == '_') ? '.' : '_';
      cookie.fp = fopen_noinh_readbin(fname_exp);
    }
  }

  if (cookie.fp == NULL && !cookie.source_from_buf_or_str) {
    if (p_verbose > 1) {
      verbose_enter();
      if (SOURCING_NAME == NULL) {
        smsg(0, _("could not source \"%s\""), fname);
      } else {
        smsg(0, _("line %" PRId64 ": could not source \"%s\""),
             (int64_t)SOURCING_LNUM, fname);
      }
      verbose_leave();
    }
    goto theend;
  }

  // The file exists.
  // - In verbose mode, give a message.
  // - For a vimrc file, may want to call vimrc_found().
  if (p_verbose > 1) {
    verbose_enter();
    if (SOURCING_NAME == NULL) {
      smsg(0, _("sourcing \"%s\""), fname);
    } else {
      smsg(0, _("line %" PRId64 ": sourcing \"%s\""), (int64_t)SOURCING_LNUM, fname);
    }
    verbose_leave();
  }
  if (is_vimrc == DOSO_VIMRC) {
    vimrc_found(fname_exp, "MYVIMRC");
  }

#ifdef USE_CRNL
  // If no automatic file format: Set default to CR-NL.
  if (*p_ffs == NUL) {
    cookie.fileformat = EOL_DOS;
  } else {
    cookie.fileformat = EOL_UNKNOWN;
  }
#endif

  // Check if this script has a breakpoint.
  cookie.breakpoint = dbg_find_breakpoint(true, fname_exp, 0);
  cookie.fname = fname_exp;
  cookie.dbg_tick = debug_tick;

  cookie.level = ex_nesting_level;

  // start measuring script load time if --startuptime was passed and
  // time_fd was successfully opened afterwards.
  proftime_T rel_time;
  proftime_T start_time;
  FILE * const l_time_fd = time_fd;
  if (l_time_fd != NULL) {
    time_push(&rel_time, &start_time);
  }

  const int l_do_profiling = do_profiling;
  if (l_do_profiling == PROF_YES) {
    prof_child_enter(&wait_start);    // entering a child now
  }

  // Don't use local function variables, if called from a function.
  // Also starts profiling timer for nested script.
  funccal_entry_T funccalp_entry;
  save_funccal(&funccalp_entry);

  const sctx_T save_current_sctx = current_sctx;

  // Always use a new sequence number.
  current_sctx.sc_seq = ++last_current_SID_seq;

  if (sid > 0) {
    // loading the same script again
    si = SCRIPT_ITEM(sid);
  } else if (str == NULL) {
    // It's new, generate a new SID.
    si = new_script_item(fname_exp, &sid);
    si->sn_lua = path_with_extension(fname_exp, "lua");
    fname_exp = xstrdup(si->sn_name);  // used for autocmd
    if (ret_sid != NULL) {
      *ret_sid = sid;
    }
  }
  // Sourcing a string doesn't allocate a script item immediately.
  assert((si != NULL) == (str == NULL));

  // Don't change sc_sid to SID_STR when sourcing a string from a Lua script,
  // as keeping the current sc_sid allows more useful :verbose messages.
  if (str == NULL || !script_is_lua(current_sctx.sc_sid)) {
    current_sctx.sc_sid = sid;
    current_sctx.sc_lnum = 0;
  }

  // Keep the sourcing name/lnum, for recursive calls.
  estack_push(ETYPE_SCRIPT, si != NULL ? si->sn_name : fname_exp, 0);
  ESTACK_CHECK_SETUP;

  if (l_do_profiling == PROF_YES && si != NULL) {
    bool forceit = false;

    // Check if we do profiling for this script.
    if (!si->sn_prof_on && has_profiling(true, si->sn_name, &forceit)) {
      profile_init(si);
      si->sn_pr_force = forceit;
    }
    if (si->sn_prof_on) {
      si->sn_pr_count++;
      si->sn_pr_start = profile_start();
      si->sn_pr_children = profile_zero();
    }
  }

  cookie.conv.vc_type = CONV_NONE;              // no conversion

  // Check if treesitter detects this range as Lua (for injections like vimdoc codeblocks)
  bool ts_lua = false;
  if (fname == NULL && eap != NULL && !ex_lua
      && !strequal(curbuf->b_p_ft, "lua")
      && !(curbuf->b_fname && path_with_extension(curbuf->b_fname, "lua"))) {
    MAXSIZE_TEMP_ARRAY(args, 3);
    ADD_C(args, INTEGER_OBJ(curbuf->handle));
    ADD_C(args, INTEGER_OBJ(eap->line1));
    ADD_C(args, INTEGER_OBJ(eap->line2));
    Error err = ERROR_INIT;
    Object result = NLUA_EXEC_STATIC("return require('vim._core.util').source_is_lua(...)",
                                     args, kRetNilBool, NULL, &err);
    if (!ERROR_SET(&err) && LUARET_TRUTHY(result)) {
      ts_lua = true;
    }
    api_clear_error(&err);
  }

  if (fname == NULL
      && (ex_lua || ts_lua || strequal(curbuf->b_p_ft, "lua")
          || (curbuf->b_fname && path_with_extension(curbuf->b_fname, "lua")))) {
    // Source lines from the current buffer as lua
    nlua_exec_ga(&cookie.buflines, fname_exp);
  } else if (si != NULL && si->sn_lua) {
    // Source the file as lua
    nlua_exec_file(fname_exp);
  } else {
    // Read the first line so we can check for a UTF-8 BOM.
    firstline = (uint8_t *)getsourceline(0, (void *)&cookie, 0, true);
    if (firstline != NULL && strlen((char *)firstline) >= 3 && firstline[0] == 0xef
        && firstline[1] == 0xbb && firstline[2] == 0xbf) {
      // Found BOM; setup conversion, skip over BOM and recode the line.
      convert_setup(&cookie.conv, "utf-8", p_enc);
      char *p = string_convert(&cookie.conv, (char *)firstline + 3, NULL);
      if (p == NULL) {
        p = xstrdup((char *)firstline + 3);
      }
      xfree(firstline);
      firstline = (uint8_t *)p;
    }
    // Call do_cmdline, which will call getsourceline() to get the lines.
    do_cmdline((char *)firstline, getsourceline, (void *)&cookie,
               DOCMD_VERBOSE|DOCMD_NOWAIT|DOCMD_REPEAT);
  }
  retval = OK;

  if (l_do_profiling == PROF_YES && si != NULL) {
    // Get "si" again, "script_items" may have been reallocated.
    si = SCRIPT_ITEM(current_sctx.sc_sid);
    if (si->sn_prof_on) {
      si->sn_pr_start = profile_end(si->sn_pr_start);
      si->sn_pr_start = profile_sub_wait(wait_start, si->sn_pr_start);
      si->sn_pr_total = profile_add(si->sn_pr_total, si->sn_pr_start);
      si->sn_pr_self = profile_self(si->sn_pr_self, si->sn_pr_start,
                                    si->sn_pr_children);
    }
  }

  if (got_int) {
    emsg(_(e_interr));
  }
  ESTACK_CHECK_NOW;
  estack_pop();
  if (p_verbose > 1) {
    verbose_enter();
    smsg(0, _("finished sourcing %s"), fname);
    if (SOURCING_NAME != NULL) {
      smsg(0, _("continuing in %s"), SOURCING_NAME);
    }
    verbose_leave();
  }

  if (l_time_fd != NULL) {
    vim_snprintf(IObuff, IOSIZE, "sourcing %s", fname);
    time_msg(IObuff, &start_time);
    time_pop(rel_time);
  }

  if (!got_int) {
    trigger_source_post = true;
  }

  // After a "finish" in debug mode, need to break at first command of next
  // sourced file.
  if (save_debug_break_level > ex_nesting_level
      && debug_break_level == ex_nesting_level) {
    debug_break_level++;
  }

  current_sctx = save_current_sctx;
  restore_funccal();
  if (l_do_profiling == PROF_YES) {
    prof_child_exit(&wait_start);    // leaving a child now
  }
  if (cookie.fp != NULL) {
    fclose(cookie.fp);
  }
  if (cookie.source_from_buf_or_str) {
    ga_clear_strings(&cookie.buflines);
  }
  xfree(cookie.nextline);
  xfree(firstline);
  convert_setup(&cookie.conv, NULL, NULL);

  if (str == NULL && trigger_source_post) {
    apply_autocmds(EVENT_SOURCEPOST, fname_exp, fname_exp, false, curbuf);
  }

theend:
  xfree(fname_exp);
  return retval;
}

/// @param check_other  check for .vimrc and _vimrc
/// @param is_vimrc     DOSO_ value
int do_source(char *fname, bool check_other, int is_vimrc, int *ret_sid)
{
  return do_source_ext(fname, check_other, is_vimrc, ret_sid, NULL, false, NULL);
}

/// Checks if the script with the given script ID is a Lua script.
bool script_is_lua(scid_T sid)
{
  if (sid == SID_LUA) {
    return true;
  }
  if (!SCRIPT_ID_VALID(sid)) {
    return false;
  }
  return SCRIPT_ITEM(sid)->sn_lua;
}

/// Find an already loaded script "name".
/// If found returns its script ID.  If not found returns -1.
int find_script_by_name(char *name)
{
  assert(script_items.ga_len >= 0);
  for (int sid = script_items.ga_len; sid > 0; sid--) {
    // We used to check inode here, but that doesn't work:
    // - If a script is edited and written, it may get a different
    //   inode number, even though to the user it is the same script.
    // - If a script is deleted and another script is written, with a
    //   different name, the inode may be re-used.
    scriptitem_T *si = SCRIPT_ITEM(sid);
    if (si->sn_name != NULL && path_fnamecmp(si->sn_name, name) == 0) {
      return sid;
    }
  }
  return -1;
}

/// ":scriptnames"
void ex_scriptnames(exarg_T *eap)
{
  if (eap->addr_count > 0 || *eap->arg != NUL) {
    // :script {scriptId}: edit the script
    if (eap->addr_count > 0 && !SCRIPT_ID_VALID(eap->line2)) {
      emsg(_(e_invarg));
    } else {
      if (eap->addr_count > 0) {
        eap->arg = SCRIPT_ITEM(eap->line2)->sn_name;
      } else {
        expand_env(eap->arg, NameBuff, MAXPATHL);
        eap->arg = NameBuff;
      }
      do_exedit(eap, NULL);
    }
    return;
  }

  msg_ext_set_kind("list_cmd");
  for (int i = 1; i <= script_items.ga_len && !got_int; i++) {
    if (SCRIPT_ITEM(i)->sn_name != NULL) {
      home_replace(NULL, SCRIPT_ITEM(i)->sn_name, NameBuff, MAXPATHL, true);
      vim_snprintf(IObuff, IOSIZE, "%3d: %s", i, NameBuff);
      if (!message_filtered(IObuff)) {
        if (msg_col > 0) {
          msg_putchar('\n');
        }
        msg_outtrans(IObuff, 0, false);
        line_breakcheck();
      }
    }
  }
}

#if defined(BACKSLASH_IN_FILENAME)
/// Fix slashes in the list of script names for 'shellslash'.
void scriptnames_slash_adjust(void)
{
  for (int i = 1; i <= script_items.ga_len; i++) {
    if (SCRIPT_ITEM(i)->sn_name != NULL) {
      slash_adjust(SCRIPT_ITEM(i)->sn_name);
    }
  }
}

#endif

/// Get a pointer to a script name.  Used for ":verbose set".
/// Message appended to "Last set from "
///
/// @param should_free  if non-NULL and the script name is a file path, call
///                     home_replace_save() on it and set *should_free to true.
char *get_scriptname(sctx_T script_ctx, bool *should_free)
{
  if (should_free != NULL) {
    *should_free = false;
  }

  switch (script_ctx.sc_sid) {
  case SID_MODELINE:
    return _("modeline");
  case SID_CMDARG:
    return _("--cmd argument");
  case SID_CARG:
    return _("-c argument");
  case SID_ENV:
    return _("environment variable");
  case SID_ERROR:
    return _("error handler");
  case SID_WINLAYOUT:
    return _("changed window size");
  case SID_LUA:
    return _("Lua");
  case SID_API_CLIENT:
    snprintf(IObuff, IOSIZE, _("API client (channel id %" PRIu64 ")"), script_ctx.sc_chan);
    return IObuff;
  case SID_STR:
    return _("anonymous :source");
  default: {
    char *const sname = SCRIPT_ITEM(script_ctx.sc_sid)->sn_name;
    if (sname == NULL) {
      snprintf(IObuff, IOSIZE, _("anonymous :source (script id %d)"),
               script_ctx.sc_sid);
      return IObuff;
    }
    if (should_free != NULL) {
      *should_free = true;
      return home_replace_save(NULL, sname);
    } else {
      return sname;
    }
  }
  }
}

#if defined(EXITFREE)
void free_scriptnames(void)
{
  profile_reset();

# define FREE_SCRIPTNAME(item) \
  do { \
    scriptitem_T *_si = *(item); \
    /* the variables themselves are cleared in evalvars_clear() */ \
    xfree(_si->sn_vars); \
    xfree(_si->sn_name); \
    ga_clear(&_si->sn_prl_ga); \
    xfree(_si); \
  } while (0) \

  GA_DEEP_CLEAR(&script_items, scriptitem_T *, FREE_SCRIPTNAME);
}
#endif

void free_autoload_scriptnames(void)
{
  ga_clear_strings(&ga_loaded);
}

linenr_T get_sourced_lnum(LineGetter fgetline, void *cookie)
  FUNC_ATTR_PURE
{
  return fgetline == getsourceline
         ? ((source_cookie_T *)cookie)->sourcing_lnum
         : SOURCING_LNUM;
}

/// Return a List of script-local functions defined in the script with id "sid".
static list_T *get_script_local_funcs(scid_T sid)
{
  hashtab_T *const functbl = func_tbl_get();
  list_T *l = tv_list_alloc((ptrdiff_t)functbl->ht_used);

  // Iterate through all the functions in the global function hash table
  // looking for functions with script ID "sid".
  HASHTAB_ITER(functbl, hi, {
    const ufunc_T *const fp = HI2UF(hi);
    // Add functions with script id == "sid"
    if (fp->uf_script_ctx.sc_sid == sid) {
      const char *const name = fp->uf_name_exp != NULL ? fp->uf_name_exp : fp->uf_name;
      tv_list_append_string(l, name, -1);
    }
  });

  return l;
}

/// "getscriptinfo()" function
void f_getscriptinfo(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
  tv_list_alloc_ret(rettv, script_items.ga_len);

  if (tv_check_for_opt_dict_arg(argvars, 0) == FAIL) {
    return;
  }

  list_T *l = rettv->vval.v_list;

  regmatch_T regmatch = {
    .regprog = NULL,
    .rm_ic = p_ic,
  };
  bool filterpat = false;
  varnumber_T sid = -1;

  char *pat = NULL;
  if (argvars[0].v_type == VAR_DICT) {
    dictitem_T *sid_di = tv_dict_find(argvars[0].vval.v_dict, S_LEN("sid"));
    if (sid_di != NULL) {
      bool error = false;
      sid = tv_get_number_chk(&sid_di->di_tv, &error);
      if (error) {
        return;
      }
      if (sid <= 0) {
        semsg(_(e_invargNval), "sid", tv_get_string(&sid_di->di_tv));
        return;
      }
    } else {
      pat = tv_dict_get_string(argvars[0].vval.v_dict, "name", true);
      if (pat != NULL) {
        regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING);
      }
      if (regmatch.regprog != NULL) {
        filterpat = true;
      }
    }
  }

  for (varnumber_T i = sid > 0 ? sid : 1;
       (i == sid || sid <= 0) && i <= script_items.ga_len; i++) {
    scriptitem_T *si = SCRIPT_ITEM(i);

    if (si->sn_name == NULL) {
      continue;
    }

    if (filterpat && !vim_regexec(&regmatch, si->sn_name, 0)) {
      continue;
    }

    dict_T *d = tv_dict_alloc();
    tv_list_append_dict(l, d);
    tv_dict_add_str(d, S_LEN("name"), si->sn_name);
    tv_dict_add_nr(d, S_LEN("sid"), i);
    tv_dict_add_nr(d, S_LEN("version"), 1);
    // Vim9 autoload script (:h vim9-autoload), not applicable to Nvim.
    tv_dict_add_bool(d, S_LEN("autoload"), false);

    // When a script ID is specified, return information about only the
    // specified script, and add the script-local variables and functions.
    if (sid > 0) {
      dict_T *var_dict = tv_dict_copy(NULL, &si->sn_vars->sv_dict, true, get_copyID());
      tv_dict_add_dict(d, S_LEN("variables"), var_dict);
      tv_dict_add_list(d, S_LEN("functions"), get_script_local_funcs((scid_T)sid));
    }
  }

  vim_regfree(regmatch.regprog);
  xfree(pat);
}

/// Get one full line from a sourced file.
/// Called by do_cmdline() when it's called from do_source().
///
/// @return pointer to the line in allocated memory, or NULL for end-of-file or
///         some error.
char *getsourceline(int c, void *cookie, int indent, bool do_concat)
{
  source_cookie_T *sp = (source_cookie_T *)cookie;
  char *line;

  // If breakpoints have been added/deleted need to check for it.
  if ((sp->dbg_tick < debug_tick) && !sp->source_from_buf_or_str) {
    sp->breakpoint = dbg_find_breakpoint(true, sp->fname, SOURCING_LNUM);
    sp->dbg_tick = debug_tick;
  }
  if (do_profiling == PROF_YES) {
    script_line_end();
  }
  // Set the current sourcing line number.
  SOURCING_LNUM = sp->sourcing_lnum + 1;
  // Get current line.  If there is a read-ahead line, use it, otherwise get
  // one now.  "fp" is NULL if actually using a string.
  if (sp->finished || (!sp->source_from_buf_or_str && sp->fp == NULL)) {
    line = NULL;
  } else if (sp->nextline == NULL) {
    line = get_one_sourceline(sp);
  } else {
    line = sp->nextline;
    sp->nextline = NULL;
    sp->sourcing_lnum++;
  }
  if (line != NULL && do_profiling == PROF_YES) {
    script_line_start();
  }

  // Only concatenate lines starting with a \ when 'cpoptions' doesn't
  // contain the 'C' flag.
  if (line != NULL && do_concat && (vim_strchr(p_cpo, CPO_CONCAT) == NULL)) {
    char *p;
    // compensate for the one line read-ahead
    sp->sourcing_lnum--;

    // Get the next line and concatenate it when it starts with a
    // backslash. We always need to read the next line, keep it in
    // sp->nextline.
    // Also check for a comment in between continuation lines: "\ .
    sp->nextline = get_one_sourceline(sp);
    if (sp->nextline != NULL
        && (*(p = skipwhite(sp->nextline)) == '\\'
            || (p[0] == '"' && p[1] == '\\' && p[2] == ' '))) {
      garray_T ga;

      ga_init(&ga, (int)sizeof(char), 400);
      ga_concat(&ga, line);
      while (sp->nextline != NULL
             && concat_continued_line(&ga, 400, sp->nextline, strlen(sp->nextline))) {
        xfree(sp->nextline);
        sp->nextline = get_one_sourceline(sp);
      }
      ga_append(&ga, NUL);
      xfree(line);
      line = ga.ga_data;
    }
  }

  if (line != NULL && sp->conv.vc_type != CONV_NONE) {
    // Convert the encoding of the script line.
    char *s = string_convert(&sp->conv, line, NULL);
    if (s != NULL) {
      xfree(line);
      line = s;
    }
  }

  // Did we encounter a breakpoint?
  if (!sp->source_from_buf_or_str
      && sp->breakpoint != 0 && sp->breakpoint <= SOURCING_LNUM) {
    dbg_breakpoint(sp->fname, SOURCING_LNUM);
    // Find next breakpoint.
    sp->breakpoint = dbg_find_breakpoint(true, sp->fname, SOURCING_LNUM);
    sp->dbg_tick = debug_tick;
  }

  return line;
}

static char *get_one_sourceline(source_cookie_T *sp)
{
  garray_T ga;
  int len;
  int c;
  char *buf;
#ifdef USE_CRNL
  bool has_cr;                           // CR-LF found
#endif
  bool have_read = false;

  // use a growarray to store the sourced line
  ga_init(&ga, 1, 250);

  // Loop until there is a finished line (or end-of-file).
  sp->sourcing_lnum++;
  while (true) {
    // make room to read at least 120 (more) characters
    ga_grow(&ga, 120);
    if (sp->source_from_buf_or_str) {
      if (sp->buf_lnum >= sp->buflines.ga_len) {
        break;              // all the lines are processed
      }
      ga_concat(&ga, ((char **)sp->buflines.ga_data)[sp->buf_lnum]);
      sp->buf_lnum++;
      ga_grow(&ga, 1);
      buf = (char *)ga.ga_data;
      buf[ga.ga_len++] = NUL;
      len = ga.ga_len;
    } else {
      buf = ga.ga_data;
retry:
      errno = 0;
      if (fgets(buf + ga.ga_len, ga.ga_maxlen - ga.ga_len, sp->fp) == NULL) {
        if (errno == EINTR) {
          goto retry;
        }
        break;
      }
      len = ga.ga_len + (int)strlen(buf + ga.ga_len);
    }
#ifdef USE_CRNL
    // Ignore a trailing CTRL-Z, when in Dos mode. Only recognize the
    // CTRL-Z by its own, or after a NL.
    if ((len == 1 || (len >= 2 && buf[len - 2] == '\n'))
        && sp->fileformat == EOL_DOS
        && buf[len - 1] == Ctrl_Z) {
      buf[len - 1] = NUL;
      break;
    }
#endif

    have_read = true;
    ga.ga_len = len;

    // If the line was longer than the buffer, read more.
    if (ga.ga_maxlen - ga.ga_len == 1 && buf[len - 1] != '\n') {
      continue;
    }

    if (len >= 1 && buf[len - 1] == '\n') {     // remove trailing NL
#ifdef USE_CRNL
      has_cr = (len >= 2 && buf[len - 2] == '\r');
      if (sp->fileformat == EOL_UNKNOWN) {
        if (has_cr) {
          sp->fileformat = EOL_DOS;
        } else {
          sp->fileformat = EOL_UNIX;
        }
      }

      if (sp->fileformat == EOL_DOS) {
        if (has_cr) {               // replace trailing CR
          buf[len - 2] = '\n';
          len--;
          ga.ga_len--;
        } else {          // lines like ":map xx yy^M" will have failed
          if (!sp->error) {
            msg_source(HL_ATTR(HLF_W));
            emsg(_("W15: Warning: Wrong line separator, ^M may be missing"));
          }
          sp->error = true;
          sp->fileformat = EOL_UNIX;
        }
      }
#endif
      // The '\n' is escaped if there is an odd number of ^V's just
      // before it, first set "c" just before the 'V's and then check
      // len&c parities (is faster than ((len-c)%2 == 0)) -- Acevedo
      for (c = len - 2; c >= 0 && buf[c] == Ctrl_V; c--) {}
      if ((len & 1) != (c & 1)) {       // escaped NL, read more
        sp->sourcing_lnum++;
        continue;
      }

      buf[len - 1] = NUL;               // remove the NL
    }

    // Check for ^C here now and then, so recursive :so can be broken.
    line_breakcheck();
    break;
  }

  if (have_read) {
    return ga.ga_data;
  }

  xfree(ga.ga_data);
  return NULL;
}

/// Returns true if sourcing a script either from a file or a buffer or a string.
/// Otherwise returns false.
int sourcing_a_script(exarg_T *eap)
{
  return getline_equal(eap->ea_getline, eap->cookie, getsourceline);
}

/// ":scriptencoding": Set encoding conversion for a sourced script.
/// Without the multi-byte feature it's simply ignored.
void ex_scriptencoding(exarg_T *eap)
{
  if (!sourcing_a_script(eap)) {
    emsg(_("E167: :scriptencoding used outside of a sourced file"));
    return;
  }

  char *name = (*eap->arg != NUL) ? enc_canonize(eap->arg) : eap->arg;

  // Setup for conversion from the specified encoding to 'encoding'.
  source_cookie_T *sp = (source_cookie_T *)getline_cookie(eap->ea_getline, eap->cookie);
  convert_setup(&sp->conv, name, p_enc);

  if (name != eap->arg) {
    xfree(name);
  }
}

/// ":finish": Mark a sourced file as finished.
void ex_finish(exarg_T *eap)
{
  if (sourcing_a_script(eap)) {
    do_finish(eap, false);
  } else {
    emsg(_("E168: :finish used outside of a sourced file"));
  }
}

/// Mark a sourced file as finished.  Possibly makes the ":finish" pending.
/// Also called for a pending finish at the ":endtry" or after returning from
/// an extra do_cmdline().  "reanimate" is used in the latter case.
void do_finish(exarg_T *eap, bool reanimate)
{
  if (reanimate) {
    ((source_cookie_T *)getline_cookie(eap->ea_getline, eap->cookie))->finished = false;
  }

  // Cleanup (and deactivate) conditionals, but stop when a try conditional
  // not in its finally clause (which then is to be executed next) is found.
  // In this case, make the ":finish" pending for execution at the ":endtry".
  // Otherwise, finish normally.
  int idx = cleanup_conditionals(eap->cstack, 0, true);
  if (idx >= 0) {
    eap->cstack->cs_pending[idx] = CSTP_FINISH;
    report_make_pending(CSTP_FINISH, NULL);
  } else {
    ((source_cookie_T *)getline_cookie(eap->ea_getline, eap->cookie))->finished = true;
  }
}

/// @return  true when a sourced file had the ":finish" command: Don't give error
///          message for missing ":endif".
///          false when not sourcing a file.
bool source_finished(LineGetter fgetline, void *cookie)
{
  return getline_equal(fgetline, cookie, getsourceline)
         && ((source_cookie_T *)getline_cookie(fgetline, cookie))->finished;
}

/// Return the autoload script name for a function or variable name
/// Caller must make sure that "name" contains AUTOLOAD_CHAR.
///
/// @param[in]  name  Variable/function name.
/// @param[in]  name_len  Name length.
///
/// @return [allocated] autoload script name.
char *autoload_name(const char *const name, const size_t name_len)
  FUNC_ATTR_MALLOC FUNC_ATTR_WARN_UNUSED_RESULT
{
  // Get the script file name: replace '#' with '/', append ".vim".
  char *const scriptname = xmalloc(name_len + sizeof("autoload/.vim"));
  memcpy(scriptname, "autoload/", sizeof("autoload/") - 1);
  memcpy(scriptname + sizeof("autoload/") - 1, name, name_len);
  size_t auchar_idx = 0;
  for (size_t i = sizeof("autoload/") - 1;
       i - sizeof("autoload/") + 1 < name_len;
       i++) {
    if (scriptname[i] == AUTOLOAD_CHAR) {
      scriptname[i] = '/';
      auchar_idx = i;
    }
  }
  memcpy(scriptname + auchar_idx, ".vim", sizeof(".vim"));

  return scriptname;
}

/// If name has a package name try autoloading the script for it
///
/// @param[in]  name  Variable/function name.
/// @param[in]  name_len  Name length.
/// @param[in]  reload  If true, load script again when already loaded.
///
/// @return true if a package was loaded.
bool script_autoload(const char *const name, const size_t name_len, const bool reload)
{
  // If there is no '#' after name[0] there is no package name.
  const char *p = memchr(name, AUTOLOAD_CHAR, name_len);
  if (p == NULL || p == name) {
    return false;
  }

  bool ret = false;
  char *tofree = autoload_name(name, name_len);
  char *scriptname = tofree;

  // Find the name in the list of previously loaded package names.  Skip
  // "autoload/", it's always the same.
  int i = 0;
  for (; i < ga_loaded.ga_len; i++) {
    if (strcmp(((char **)ga_loaded.ga_data)[i] + 9, scriptname + 9) == 0) {
      break;
    }
  }
  if (!reload && i < ga_loaded.ga_len) {
    ret = false;  // Was loaded already.
  } else {
    // Remember the name if it wasn't loaded already.
    if (i == ga_loaded.ga_len) {
      GA_APPEND(char *, &ga_loaded, scriptname);
      tofree = NULL;
    }

    // Try loading the package from $VIMRUNTIME/autoload/<name>.vim
    // Use "ret_sid" to avoid loading the same script again.
    int ret_sid;
    if (do_in_runtimepath(scriptname, DIP_START, source_callback, &ret_sid) == OK) {
      ret = true;
    }
  }

  xfree(tofree);
  return ret;
}
