Files
neovim/src/nvim/runtime.c
Ghjuvan Lacambre d34c64e342 feat: $NVIM_APPNAME #22128
This commit implements the ability to control all of the XDG paths
Neovim should use. This is done by setting an environment variable named
NVIM_APPNAME. For example, setting $NVIM_APPNAME makes Neovim look for
its configuration directory in $XDG_CONFIG_HOME/$NVIM_APPNAME instead of
$XDG_CONFIG_HOME/nvim.

If NVIM_APPNAME is not set or is an empty string, "nvim" will be used as
default.

The usecase for this feature is to enable an easy way to switch from
configuration to configuration. One might argue that the various $XDG
environment variables can already be used for this usecase. However,
setting $XDG environment variables also affects tools spawned by Neovim.
For example, while setting $XDG_CONFIG_HOME will enable Neovim to use a
different configuration directory, it will also prevent Git from finding
its "default" configuration.

Closes https://github.com/neovim/neovim/issues/21691
2023-02-16 04:15:02 -08:00

2600 lines
77 KiB
C

// This is an open source non-commercial project. Dear PVS-Studio, please check
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
/// @file runtime.c
///
/// Management of runtime files (including packages)
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <uv.h>
#include "nvim/api/private/defs.h"
#include "nvim/api/private/helpers.h"
#include "nvim/ascii.h"
#include "nvim/autocmd.h"
#include "nvim/buffer_defs.h"
#include "nvim/charset.h"
#include "nvim/cmdexpand.h"
#include "nvim/debugger.h"
#include "nvim/eval.h"
#include "nvim/eval/userfunc.h"
#include "nvim/ex_cmds_defs.h"
#include "nvim/ex_docmd.h"
#include "nvim/ex_eval.h"
#include "nvim/garray.h"
#include "nvim/getchar.h"
#include "nvim/gettext.h"
#include "nvim/globals.h"
#include "nvim/lua/executor.h"
#include "nvim/macros.h"
#include "nvim/map.h"
#include "nvim/mbyte.h"
#include "nvim/memline.h"
#include "nvim/memory.h"
#include "nvim/message.h"
#include "nvim/option.h"
#include "nvim/os/input.h"
#include "nvim/os/os.h"
#include "nvim/os/stdpaths_defs.h"
#include "nvim/path.h"
#include "nvim/profile.h"
#include "nvim/runtime.h"
#include "nvim/strings.h"
#include "nvim/usercmd.h"
#include "nvim/vim.h"
/// 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.
struct source_cookie {
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
int finished; ///< ":finish" used
#if defined(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
};
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "runtime.c.generated.h"
#endif
garray_T exestack = { 0, 0, sizeof(estack_T), 50, NULL };
garray_T script_items = { 0, 0, sizeof(scriptitem_T *), 20, NULL };
/// 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) {
if (entry->es_name == NULL) {
return NULL;
}
return xstrdup(entry->es_name);
}
// 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) {
size_t len = strlen(entry->es_name) + 15;
char *type_name = "";
if (entry->es_type != last_type) {
switch (entry->es_type) {
case ETYPE_SCRIPT:
type_name = "script "; break;
case ETYPE_UFUNC:
type_name = "function "; break;
default:
type_name = ""; break;
}
last_type = entry->es_type;
}
len += strlen(type_name);
ga_grow(&ga, (int)len);
linenr_T lnum = idx == exestack.ga_len - 1
? which == ESTACK_STACK ? SOURCING_LNUM : 0
: entry->es_lnum;
char *dots = idx == exestack.ga_len - 1 ? "" : "..";
if (lnum == 0) {
// 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.
vim_snprintf((char *)ga.ga_data + ga.ga_len, len, "%s%s%s",
type_name, entry->es_name, dots);
} else {
vim_snprintf((char *)ga.ga_data + ga.ga_len, len, "%s%s[%" PRIdLINENR "]%s",
type_name, entry->es_name, lnum, dots);
}
ga.ga_len += (int)strlen((char *)ga.ga_data + ga.ga_len);
}
}
return (char *)ga.ga_data;
}
static bool runtime_search_path_valid = 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));
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;
xp->xp_context = EXPAND_RUNTIME;
xp->xp_pattern = (char *)arg;
}
static void source_callback(char *fname, void *cookie)
{
(void)do_source(fname, false, DOSO_NONE);
}
/// 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.
int do_in_path(char *path, char *name, int flags, DoInRuntimepathCB callback, void *cookie)
{
int num_files;
char **files;
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();
smsg(_("Searching for \"%s\" in \"%s\""), name, path);
verbose_leave();
}
// Loop over all entries in 'runtimepath'.
char *rtp = rtp_copy;
while (*rtp != NUL && ((flags & DIP_ALL) || !did_one)) {
// Copy the path from 'runtimepath' to buf[].
copy_option_part(&rtp, buf, MAXPATHL, ",");
size_t buflen = strlen(buf);
// 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)(buf, cookie);
did_one = true;
} else if (buflen + strlen(name) + 2 < MAXPATHL) {
add_pathsep(buf);
tail = buf + strlen(buf);
// Loop over all patterns in "name"
char *np = name;
while (*np != NUL && ((flags & DIP_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(_("Searching for \"%s\""), buf);
verbose_leave();
}
int ew_flags = ((flags & DIP_DIR) ? EW_DIR : EW_FILE)
| (flags & DIP_DIRFILE) ? (EW_DIR|EW_FILE) : 0;
// Expand wildcards, invoke the callback for each match.
if (gen_expand_wildcards(1, &buf, &num_files, &files, ew_flags) == OK) {
for (int i = 0; i < num_files; i++) {
(*callback)(files[i], cookie);
did_one = true;
if (!(flags & DIP_ALL)) {
break;
}
}
FreeWild(num_files, files);
}
}
}
}
}
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(_("not found in '%s': \"%s\""), basepath, name);
verbose_leave();
}
}
return did_one ? OK : FAIL;
}
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;
}
RuntimeSearchPath copy_runtime_search_path(const RuntimeSearchPath src)
{
RuntimeSearchPath dst = KV_INITIAL_VALUE;
for (size_t j = 0; j < kv_size(src); j++) {
SearchPathItem src_item = kv_A(src, j);
kv_push(dst, ((SearchPathItem){ xstrdup(src_item.path), src_item.after, src_item.has_lua }));
}
return dst;
}
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.
int do_in_cached_path(char *name, int flags, DoInRuntimepathCB callback, void *cookie)
{
char *tail;
int num_files;
char **files;
bool did_one = false;
char buf[MAXPATHL];
if (p_verbose > 10 && name != NULL) {
verbose_enter();
smsg(_("Searching for \"%s\" in runtime path"), name);
verbose_leave();
}
int ref;
RuntimeSearchPath path = runtime_search_path_get_cached(&ref);
// 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)(item.path, cookie);
} else if (buflen + strlen(name) + 2 < MAXPATHL) {
STRCPY(buf, item.path);
add_pathsep(buf);
tail = buf + strlen(buf);
// Loop over all patterns in "name"
char *np = name;
while (*np != NUL && ((flags & DIP_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(_("Searching for \"%s\""), buf);
verbose_leave();
}
int ew_flags = ((flags & DIP_DIR) ? EW_DIR : EW_FILE)
| (flags & DIP_DIRFILE) ? (EW_DIR|EW_FILE) : 0;
// Expand wildcards, invoke the callback for each match.
char *(pat[]) = { buf };
if (gen_expand_wildcards(1, pat, &num_files, &files, ew_flags) == OK) {
for (int i = 0; i < num_files; i++) {
(*callback)(files[i], cookie);
did_one = true;
if (!(flags & DIP_ALL)) {
break;
}
}
FreeWild(num_files, files);
}
}
}
}
if (!did_one && name != NULL) {
if (flags & DIP_ERR) {
semsg(_(e_dirnotf), "runtime path", name);
} else if (p_verbose > 1) {
verbose_enter();
smsg(_("not found in runtime path: \"%s\""), name);
verbose_leave();
}
}
runtime_search_path_unref(path, &ref);
return did_one ? OK : FAIL;
}
Array runtime_inspect(void)
{
RuntimeSearchPath path = runtime_search_path;
Array rv = ARRAY_DICT_INIT;
for (size_t i = 0; i < kv_size(path); i++) {
SearchPathItem *item = &kv_A(path, i);
Array entry = ARRAY_DICT_INIT;
ADD(entry, STRING_OBJ(cstr_to_string(item->path)));
ADD(entry, BOOLEAN_OBJ(item->after));
if (item->has_lua != kNone) {
ADD(entry, BOOLEAN_OBJ(item->has_lua == kTrue));
}
ADD(rv, ARRAY_OBJ(entry));
}
return rv;
}
ArrayOf(String) runtime_get_named(bool lua, Array pat, bool all)
{
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);
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);
uv_mutex_unlock(&runtime_search_path_mutex);
return rv;
}
ArrayOf(String) runtime_get_named_common(bool lua, Array pat, bool all,
RuntimeSearchPath path, char *buf, size_t buf_len)
{
ArrayOf(String) rv = ARRAY_DICT_INIT;
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(rv, STRING_OBJ(cstr_to_string(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)) {
char *start_dir = "pack/*/start/*/%s%s"; // NOLINT
size_t len = strlen(start_dir) + strlen(name) + 6;
char *s = xmallocz(len); // TODO(bfredl): get rid of random allocations
char *suffix = (flags & DIP_AFTER) ? "after/" : "";
vim_snprintf(s, len, start_dir, suffix, name);
done |= do_in_path(p_pp, s, flags & ~DIP_AFTER, callback, cookie);
xfree(s);
if (done == FAIL || (flags & DIP_ALL)) {
start_dir = "start/*/%s%s"; // NOLINT
len = strlen(start_dir) + strlen(name) + 6;
s = xmallocz(len);
vim_snprintf(s, len, start_dir, suffix, name);
done |= do_in_path(p_pp, s, flags & ~DIP_AFTER, callback, cookie);
xfree(s);
}
}
if ((done == FAIL || (flags & DIP_ALL)) && (flags & DIP_OPT)) {
char *opt_dir = "pack/*/opt/*/%s"; // NOLINT
size_t len = strlen(opt_dir) + strlen(name);
char *s = xmallocz(len);
vim_snprintf(s, len, opt_dir, name);
done |= do_in_path(p_pp, s, flags, callback, cookie);
xfree(s);
if (done == FAIL || (flags & DIP_ALL)) {
opt_dir = "opt/*/%s"; // NOLINT
len = strlen(opt_dir) + strlen(name);
s = xmallocz(len);
vim_snprintf(s, len, opt_dir, name);
done |= do_in_path(p_pp, s, flags, callback, cookie);
xfree(s);
}
}
return done;
}
static void push_path(RuntimeSearchPath *search_path, Map(String, handle_T) *rtp_used, char *entry,
bool after)
{
handle_T h = map_get(String, handle_T)(rtp_used, cstr_as_string(entry));
if (h == 0) {
char *allocated = xstrdup(entry);
map_put(String, handle_T)(rtp_used, cstr_as_string(allocated), 1);
kv_push(*search_path, ((SearchPathItem){ allocated, after, kNone }));
}
}
static void expand_rtp_entry(RuntimeSearchPath *search_path, Map(String, handle_T) *rtp_used,
char *entry, bool after)
{
if (map_get(String, handle_T)(rtp_used, cstr_as_string(entry))) {
return;
}
if (!*entry) {
push_path(search_path, rtp_used, entry, after);
}
int num_files;
char **files;
char *(pat[]) = { entry };
if (gen_expand_wildcards(1, pat, &num_files, &files, EW_DIR) == OK) {
for (int i = 0; i < num_files; i++) {
push_path(search_path, rtp_used, files[i], after);
}
FreeWild(num_files, files);
}
}
static void expand_pack_entry(RuntimeSearchPath *search_path, Map(String, handle_T) *rtp_used,
CharVec *after_path, char *pack_entry, size_t pack_entry_len)
{
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);
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;
}
RuntimeSearchPath runtime_search_path_build(void)
{
kvec_t(String) pack_entries = KV_INITIAL_VALUE;
// TODO(bfredl): these should just be sets, when Set(String) is do merge to
// master.
Map(String, handle_T) pack_used = MAP_INIT;
Map(String, handle_T) rtp_used = MAP_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;
copy_option_part(&entry, buf, MAXPATHL, ",");
String the_entry = { .data = cur_entry, .size = strlen(buf) };
kv_push(pack_entries, the_entry);
map_put(String, handle_T)(&pack_used, the_entry, 0);
}
char *rtp_entry;
for (rtp_entry = p_rtp; *rtp_entry != NUL;) {
char *cur_entry = rtp_entry;
copy_option_part(&rtp_entry, buf, MAXPATHL, ",");
size_t buflen = strlen(buf);
if (path_is_after(buf, buflen)) {
rtp_entry = cur_entry;
break;
}
// fact: &rtp entries can contain wild chars
expand_rtp_entry(&search_path, &rtp_used, buf, false);
handle_T *h = map_ref(String, handle_T)(&pack_used, cstr_as_string(buf), false);
if (h) {
(*h)++;
expand_pack_entry(&search_path, &rtp_used, &after_path, buf, buflen);
}
}
for (size_t i = 0; i < kv_size(pack_entries); i++) {
String item = kv_A(pack_entries, i);
handle_T h = map_get(String, handle_T)(&pack_used, item);
if (h == 0) {
expand_pack_entry(&search_path, &rtp_used, &after_path, item.data, item.size);
}
}
// "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);
xfree(kv_A(after_path, i));
}
// "after" dirs in rtp
for (; *rtp_entry != NUL;) {
copy_option_part(&rtp_entry, buf, MAXPATHL, ",");
expand_rtp_entry(&search_path, &rtp_used, buf, path_is_after(buf, strlen(buf)));
}
// strings are not owned
kv_destroy(pack_entries);
kv_destroy(after_path);
map_destroy(String, handle_T)(&pack_used);
map_destroy(String, handle_T)(&rtp_used);
return search_path;
}
void runtime_search_path_invalidate(void)
{
runtime_search_path_valid = false;
}
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
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);
}
}
/// 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 use "path" instead of 'runtimepath'.
int source_in_path(char *path, char *name, int flags)
{
return do_in_path_and_pp(path, name, flags, source_callback, NULL);
}
// Expand wildcards in "pat" and invoke do_source()/nlua_exec_file()
// for each match.
static void source_all_matches(char *pat)
{
int num_files;
char **files;
if (gen_expand_wildcards(1, &pat, &num_files, &files, EW_FILE) != OK) {
return;
}
for (int i = 0; i < num_files; i++) {
(void)do_source(files[i], false, DOSO_NONE);
}
FreeWild(num_files, files);
}
/// 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 *p;
char *buf = NULL;
char *afterdir = NULL;
int retval = FAIL;
char *p1 = get_past_head(fname);
char *p2 = p1;
char *p3 = p1;
char *p4 = p1;
for (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/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);
buf = try_malloc(MAXPATHL);
if (buf == NULL) {
goto theend;
}
const char *insp = NULL;
const char *after_insp = NULL;
for (const char *entry = (const char *)p_rtp; *entry != NUL;) {
const char *cur_entry = entry;
copy_option_part((char **)&entry, buf, MAXPATHL, ",");
if (insp == NULL) {
add_pathsep(buf);
char *const rtp_ffname = fix_fname(buf);
if (rtp_ffname == NULL) {
goto theend;
}
bool match = path_fnamencmp(rtp_ffname, ffname, fname_len) == 0;
xfree(rtp_ffname);
if (match) {
// Insert "ffname" after this entry (and comma).
insp = entry;
}
}
if ((p = strstr(buf, "after")) != NULL
&& p > buf
&& vim_ispathsep(p[-1])
&& (vim_ispathsep(p[5]) || p[5] == NUL || p[5] == ',')) {
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) {
// Both "fname" and "after" not found, append at the end.
insp = (const char *)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 - (const char *)p_rtp);
memmove(new_rtp, p_rtp, keep);
size_t new_rtp_len = keep;
if (*insp == NUL) {
new_rtp[new_rtp_len++] = ','; // add comma before
}
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
}
if (afterlen > 0 && after_insp != NULL) {
size_t keep_after = (size_t)(after_insp - (const char *)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;
}
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}
xstrlcat(new_rtp, ",", new_rtp_capacity);
xstrlcat(new_rtp, afterdir, new_rtp_capacity);
}
set_option_value_give_err("rtp", 0L, new_rtp, 0);
xfree(new_rtp);
retval = OK;
theend:
xfree(buf);
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.vim)
static int load_pack_plugin(bool opt, char *fname)
{
static const char *ftpat = "%s/ftdetect/*.vim"; // NOLINT
char *const ffname = fix_fname(fname);
size_t len = strlen(ffname) + strlen(ftpat);
char *pat = xmallocz(len);
vim_snprintf(pat, len, "%s/plugin/**/*.vim", ffname); // NOLINT
source_all_matches(pat);
vim_snprintf(pat, len, "%s/plugin/**/*.lua", ffname); // NOLINT
source_all_matches(pat);
char *cmd = xstrdup("g:did_load_filetypes");
// If runtime/filetype.vim wasn't loaded yet, the scripts will be
// found when it loads.
if (opt && eval_to_number(cmd) > 0) {
do_cmdline_cmd("augroup filetypedetect");
vim_snprintf(pat, len, ftpat, ffname);
source_all_matches(pat);
vim_snprintf((char *)pat, len, "%s/ftdetect/*.lua", ffname); // NOLINT
source_all_matches(pat);
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_plugin(bool opt, char *fname, void *cookie)
{
if (cookie != &APP_LOAD) {
char *buf = xmalloc(MAXPATHL);
bool found = false;
const char *p = (const char *)p_rtp;
while (*p != NUL) {
copy_option_part((char **)&p, buf, MAXPATHL, ",");
if (path_fnamecmp(buf, fname) == 0) {
found = true;
break;
}
}
xfree(buf);
if (!found) {
// directory is not yet in 'runtimepath', add it
if (add_pack_dir_to_rtp(fname, false) == FAIL) {
return;
}
}
}
if (cookie != &APP_ADD_DIR) {
load_pack_plugin(opt, fname);
}
}
static void add_start_pack_plugin(char *fname, void *cookie)
{
add_pack_plugin(false, fname, cookie);
}
static void add_opt_pack_plugin(char *fname, void *cookie)
{
add_pack_plugin(true, fname, cookie);
}
/// 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 void add_pack_start_dir(char *fname, void *cookie)
{
static char buf[MAXPATHL];
char *(start_pat[]) = { "/start/*", "/pack/*/start/*" }; // NOLINT
for (int i = 0; i < 2; i++) {
if (strlen(fname) + strlen(start_pat[i]) + 1 > MAXPATHL) {
continue;
}
xstrlcpy(buf, fname, MAXPATHL);
xstrlcat(buf, start_pat[i], sizeof buf);
if (pack_has_entries(buf)) {
add_pack_dir_to_rtp(buf, true);
}
}
}
/// 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_plugin, &APP_LOAD);
do_in_path(p_pp, "start/*", DIP_ALL + DIP_DIR, // NOLINT
add_start_pack_plugin, &APP_LOAD);
}
// ":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_vim = "plugin/**/*.vim"; // NOLINT
char *const plugin_pattern_lua = "plugin/**/*.lua"; // NOLINT
if (!did_source_packages) {
rtp_copy = xstrdup(p_rtp);
add_pack_start_dirs();
}
// don't use source_runtime() yet so we can check for :packloadall below
source_in_path(rtp_copy, plugin_pattern_vim, DIP_ALL | DIP_NOAFTER);
source_in_path(rtp_copy, plugin_pattern_lua, 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(plugin_pattern_vim, DIP_ALL | DIP_AFTER);
source_runtime(plugin_pattern_lua, 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;
// Round 1: use "start", round 2: use "opt".
for (int round = 1; round <= 2; round++) {
// Only look under "start" when loading packages wasn't done yet.
if (round == 1 && did_source_packages) {
continue;
}
const size_t len = strlen(plugpat) + strlen(eap->arg) + 5;
char *pat = xmallocz(len);
vim_snprintf(pat, len, plugpat, round == 1 ? "start" : "opt", eap->arg);
// The first round don't give a "not found" error, in the second round
// only when nothing was found in the first round.
res =
do_in_path(p_pp, pat, DIP_ALL + DIP_DIR + (round == 2 && res == FAIL ? DIP_ERR : 0),
round == 1 ? add_start_pack_plugin : add_opt_pack_plugin,
eap->forceit ? &APP_ADD_DIR : &APP_BOTH);
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 + 31;
char *const buf = xmalloc(buf_len);
char *const tail = buf + 15;
const size_t tail_buflen = buf_len - 15;
int glob_flags = 0;
bool expand_dirs = false;
if (*dirnames[i] == NUL) { // empty dir used for :runtime
snprintf(tail, tail_buflen, "%s*.\\(vim\\|lua\\)", pat);
} else {
snprintf(tail, tail_buflen, "%s/%s*.\\(vim\\|lua\\)", dirnames[i], pat);
}
expand:
if ((flags & DIP_NORTP) == 0) {
globpath(p_rtp, tail, gap, glob_flags, expand_dirs);
}
if (flags & DIP_START) {
memcpy(tail - 15, "pack/*/start/*/", 15); // NOLINT
globpath(p_pp, tail - 15, gap, glob_flags, expand_dirs);
memcpy(tail - 8, "start/*/", 8); // NOLINT
globpath(p_pp, tail - 8, gap, glob_flags, expand_dirs);
}
if (flags & DIP_OPT) {
memcpy(tail - 13, "pack/*/opt/*/", 13); // NOLINT
globpath(p_pp, tail - 13, gap, glob_flags, expand_dirs);
memcpy(tail - 6, "opt/*/", 6); // NOLINT
globpath(p_pp, tail - 6, gap, glob_flags, expand_dirs);
}
if (*dirnames[i] == NUL && !expand_dirs) {
// expand dir names in another round
snprintf(tail, tail_buflen, "%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();
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();
size_t appname_len = strlen(appname);
assert(appname_len < (IOSIZE - sizeof("-data")));
xstrlcpy(IObuff, appname, appname_len + 1);
#if defined(MSWIN)
if (type == kXDGDataHome || type == kXDGStateHome) {
STRCAT(IObuff, "-data");
appname_len += 5;
}
#endif
xstrlcpy(dest, IObuff, appname_len + 1);
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" _PATHSEPSTR "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;
const char *appname = get_appname();
size_t appname_len = strlen(appname);
if (data_home != NULL) {
data_len = strlen(data_home);
size_t nvim_data_size = appname_len;
#if defined(MSWIN)
nvim_data_size += sizeof("-data");
#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 (eap != NULL && *fname == NUL) {
cmd_source_buffer(eap);
} 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) == 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;
(void)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 &((struct source_cookie *)cookie)->breakpoint;
}
/// @return the address holding the debug tick for a source cookie.
int *source_dbg_tick(void *cookie)
{
return &((struct source_cookie *)cookie)->dbg_tick;
}
/// @return the nesting level for a source cookie.
int source_level(void *cookie)
FUNC_ATTR_PURE
{
return ((struct source_cookie *)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;
}
(void)os_set_cloexec(fd_tmp);
return fdopen(fd_tmp, READBIN);
}
/// Concatenate VimL 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((char *)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;
}
typedef struct {
linenr_T curr_lnum;
const linenr_T final_lnum;
} GetBufferLineCookie;
typedef struct {
char *buf;
size_t offset;
} GetStrLineCookie;
/// Get one full line from a sourced string (in-memory, no file).
/// Called by do_cmdline() when it's called from do_source_str().
///
/// @return pointer to allocated line, or NULL for end-of-file or
/// some error.
static char *get_str_line(int c, void *cookie, int indent, bool do_concat)
{
GetStrLineCookie *p = cookie;
if (strlen(p->buf) <= p->offset) {
return NULL;
}
const char *line = p->buf + p->offset;
const char *eol = skip_to_newline(line);
garray_T ga;
ga_init(&ga, sizeof(char), 400);
ga_concat_len(&ga, line, (size_t)(eol - line));
if (do_concat && vim_strchr(p_cpo, CPO_CONCAT) == NULL) {
while (eol[0] != NUL) {
line = eol + 1;
const char *const next_eol = skip_to_newline(line);
if (!concat_continued_line(&ga, 400, line, (size_t)(next_eol - line))) {
break;
}
eol = (char *)next_eol;
}
}
ga_append(&ga, NUL);
p->offset = (size_t)(eol - p->buf) + 1;
return ga.ga_data;
}
/// 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)
{
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);
}
static int source_using_linegetter(void *cookie, LineGetter fgetline, const char *traceback_name)
{
char *save_sourcing_name = SOURCING_NAME;
linenr_T save_sourcing_lnum = SOURCING_LNUM;
char sourcing_name_buf[256];
char *sname;
if (save_sourcing_name == NULL) {
sname = (char *)traceback_name;
} else {
snprintf((char *)sourcing_name_buf, sizeof(sourcing_name_buf),
"%s called at %s:%" PRIdLINENR, traceback_name, save_sourcing_name,
save_sourcing_lnum);
sname = sourcing_name_buf;
}
estack_push(ETYPE_SCRIPT, sname, 0);
const sctx_T save_current_sctx = current_sctx;
if (current_sctx.sc_sid != SID_LUA) {
current_sctx.sc_sid = SID_STR;
}
current_sctx.sc_seq = 0;
current_sctx.sc_lnum = save_sourcing_lnum;
funccal_entry_T entry;
save_funccal(&entry);
int retval = do_cmdline(NULL, fgetline, cookie,
DOCMD_VERBOSE | DOCMD_NOWAIT | DOCMD_REPEAT);
estack_pop();
current_sctx = save_current_sctx;
restore_funccal();
return retval;
}
static void cmd_source_buffer(const exarg_T *const eap)
FUNC_ATTR_NONNULL_ALL
{
if (curbuf == NULL) {
return;
}
garray_T ga;
ga_init(&ga, sizeof(char), 400);
const linenr_T final_lnum = eap->line2;
// Copy the contents to be executed.
for (linenr_T curr_lnum = eap->line1; curr_lnum <= final_lnum; curr_lnum++) {
// Adjust growsize to current length to speed up concatenating many lines.
if (ga.ga_len > 400) {
ga_set_growsize(&ga, MIN(ga.ga_len, 8000));
}
ga_concat(&ga, ml_get(curr_lnum));
ga_append(&ga, NL);
}
((char *)ga.ga_data)[ga.ga_len - 1] = NUL;
const GetStrLineCookie cookie = {
.buf = ga.ga_data,
.offset = 0,
};
if (curbuf->b_fname
&& path_with_extension((const char *)curbuf->b_fname, "lua")) {
nlua_source_using_linegetter(get_str_line, (void *)&cookie,
":source (no file)");
} else {
source_using_linegetter((void *)&cookie, get_str_line,
":source (no file)");
}
ga_clear(&ga);
}
/// Executes lines in `src` as Ex commands.
///
/// @see do_source()
int do_source_str(const char *cmd, const char *traceback_name)
{
GetStrLineCookie cookie = {
.buf = (char *)cmd,
.offset = 0,
};
return source_using_linegetter((void *)&cookie, get_str_line, traceback_name);
}
/// 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
/// @param check_other check for .vimrc and _vimrc
/// @param is_vimrc DOSO_ value
///
/// @return FAIL if file could not be opened, OK otherwise
int do_source(char *fname, int check_other, int is_vimrc)
{
struct source_cookie cookie;
char *p;
char *fname_exp;
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;
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(_("Cannot source a directory: \"%s\""), fname);
goto theend;
}
// 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);
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.
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) {
if (p_verbose > 1) {
verbose_enter();
if (SOURCING_NAME == NULL) {
smsg(_("could not source \"%s\""), fname);
} else {
smsg(_("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(_("sourcing \"%s\""), fname);
} else {
smsg(_("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;
}
cookie.error = false;
#endif
cookie.nextline = NULL;
cookie.sourcing_lnum = 0;
cookie.finished = false;
// Check if this script has a breakpoint.
cookie.breakpoint = dbg_find_breakpoint(true, fname_exp, (linenr_T)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;
si = get_current_script_id(&fname_exp, &current_sctx);
// Keep the sourcing name/lnum, for recursive calls.
estack_push(ETYPE_SCRIPT, si->sn_name, 0);
if (l_do_profiling == PROF_YES) {
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
if (path_with_extension((const char *)fname_exp, "lua")) {
const sctx_T current_sctx_backup = current_sctx;
current_sctx.sc_sid = SID_LUA;
current_sctx.sc_lnum = 0;
// Source the file as lua
nlua_exec_file((const char *)fname_exp);
current_sctx = current_sctx_backup;
} 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);
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) {
// 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_pop();
if (p_verbose > 1) {
verbose_enter();
smsg(_("finished sourcing %s"), fname);
if (SOURCING_NAME != NULL) {
smsg(_("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
}
fclose(cookie.fp);
xfree(cookie.nextline);
xfree(firstline);
convert_setup(&cookie.conv, NULL, NULL);
if (trigger_source_post) {
apply_autocmds(EVENT_SOURCEPOST, fname_exp, fname_exp, false, curbuf);
}
theend:
xfree(fname_exp);
return retval;
}
/// Check if fname was sourced before to finds its SID.
/// If it's new, generate a new SID.
///
/// @param[in,out] fnamep pointer to file path of script
/// @param[out] ret_sctx sctx of this script
scriptitem_T *get_current_script_id(char **fnamep, sctx_T *ret_sctx)
{
static int last_current_SID_seq = 0;
sctx_T script_sctx = { .sc_seq = ++last_current_SID_seq,
.sc_lnum = 0,
.sc_sid = 0 };
scriptitem_T *si = NULL;
assert(script_items.ga_len >= 0);
for (script_sctx.sc_sid = script_items.ga_len; script_sctx.sc_sid > 0; script_sctx.sc_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.
si = SCRIPT_ITEM(script_sctx.sc_sid);
if (si->sn_name != NULL && path_fnamecmp(si->sn_name, *fnamep) == 0) {
// Found it!
break;
}
}
if (script_sctx.sc_sid == 0) {
si = new_script_item(*fnamep, &script_sctx.sc_sid);
*fnamep = xstrdup(si->sn_name);
}
if (ret_sctx != NULL) {
*ret_sctx = script_sctx;
}
return si;
}
/// ":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;
}
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)) {
msg_putchar('\n');
msg_outtrans(IObuff);
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 "
char *get_scriptname(LastSet last_set, bool *should_free)
{
*should_free = false;
switch (last_set.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 ")"), last_set.channel_id);
return IObuff;
case SID_STR:
return _("anonymous :source");
default: {
char *const sname = SCRIPT_ITEM(last_set.script_ctx.sc_sid)->sn_name;
if (sname == NULL) {
snprintf(IObuff, IOSIZE, _("anonymous :source (script id %d)"),
last_set.script_ctx.sc_sid);
return IObuff;
}
*should_free = true;
return home_replace_save(NULL, 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
linenr_T get_sourced_lnum(LineGetter fgetline, void *cookie)
FUNC_ATTR_PURE
{
return fgetline == getsourceline
? ((struct source_cookie *)cookie)->sourcing_lnum
: SOURCING_LNUM;
}
/// 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)
{
struct source_cookie *sp = (struct source_cookie *)cookie;
char *line;
// If breakpoints have been added/deleted need to check for it.
if (sp->dbg_tick < debug_tick) {
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.
if (sp->finished) {
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) {
char *s;
// Convert the encoding of the script line.
s = string_convert(&sp->conv, line, NULL);
if (s != NULL) {
xfree(line);
line = s;
}
}
// Did we encounter a breakpoint?
if (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(struct source_cookie *sp)
{
garray_T ga;
int len;
int c;
char *buf;
#ifdef USE_CRNL
int 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++;
for (;;) {
// make room to read at least 120 (more) characters
ga_grow(&ga, 120);
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;
}
/// ":scriptencoding": Set encoding conversion for a sourced script.
/// Without the multi-byte feature it's simply ignored.
void ex_scriptencoding(exarg_T *eap)
{
struct source_cookie *sp;
char *name;
if (!getline_equal(eap->getline, eap->cookie, getsourceline)) {
emsg(_("E167: :scriptencoding used outside of a sourced file"));
return;
}
if (*eap->arg != NUL) {
name = enc_canonize(eap->arg);
} else {
name = eap->arg;
}
// Setup for conversion from the specified encoding to 'encoding'.
sp = (struct source_cookie *)getline_cookie(eap->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 (getline_equal(eap->getline, eap->cookie, getsourceline)) {
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, int reanimate)
{
int idx;
if (reanimate) {
((struct source_cookie *)getline_cookie(eap->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.
idx = cleanup_conditionals(eap->cstack, 0, true);
if (idx >= 0) {
eap->cstack->cs_pending[idx] = CSTP_FINISH;
report_make_pending(CSTP_FINISH, NULL);
} else {
((struct source_cookie *)getline_cookie(eap->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)
&& ((struct source_cookie *)getline_cookie(fgetline, cookie))->finished;
}