commit 7720e52a0b4aa973540934ab6dcf4703dbde3b9f
parent ba6440c106f99b5d68980784805e8a60a295eafe
Author: Justin M. Keyes <justinkz@gmail.com>
Date: Fri, 6 Feb 2026 13:28:43 -0500
Merge #37424 $XDG_CONFIG_DIRS with init.lua, $NVIM_APPNAME
Diffstat:
3 files changed, 145 insertions(+), 19 deletions(-)
diff --git a/src/nvim/errors.h b/src/nvim/errors.h
@@ -216,6 +216,8 @@ EXTERN const char e_diff_anchors_with_hidden_windows[] INIT( = N_("E1562: Diff a
EXTERN const char e_trustfile[] INIT(= N_("E5570: Cannot update trust file: %s"));
EXTERN const char e_cannot_read_from_str_2[] INIT(= N_("E282: Cannot read from \"%s\""));
+EXTERN const char e_conflicting_configs[] INIT(= N_("E5422: Conflicting configs: \"%s\" \"%s\""));
+
EXTERN const char e_unknown_option2[] INIT(= N_("E355: Unknown option: %s"));
EXTERN const char top_bot_msg[] INIT(= N_("search hit TOP, continuing at BOTTOM"));
diff --git a/src/nvim/main.c b/src/nvim/main.c
@@ -1981,15 +1981,17 @@ static void exe_commands(mparm_T *parmp)
///
/// Does one of the following things, stops after whichever succeeds:
///
-/// 1. Source system vimrc file from $XDG_CONFIG_DIRS/nvim/sysinit.vim
+/// 1. Source system vimrc file from $XDG_CONFIG_DIRS/$NVIM_APPNAME/sysinit.vim
/// 2. Source system vimrc file from $VIM
static void do_system_initialization(void)
{
char *const config_dirs = stdpaths_get_xdg_var(kXDGConfigDirs);
if (config_dirs != NULL) {
const void *iter = NULL;
- const char path_tail[] = {
- 'n', 'v', 'i', 'm', PATHSEP,
+ const char *appname = get_appname(false);
+ size_t appname_len = strlen(appname);
+ const char sysinit_suffix[] = {
+ PATHSEP,
's', 'y', 's', 'i', 'n', 'i', 't', '.', 'v', 'i', 'm', NUL
};
do {
@@ -1999,13 +2001,15 @@ static void do_system_initialization(void)
if (dir == NULL || dir_len == 0) {
break;
}
- char *vimrc = xmalloc(dir_len + sizeof(path_tail) + 1);
+ size_t path_len = dir_len + 1 + appname_len + sizeof(sysinit_suffix);
+ char *vimrc = xmalloc(path_len);
memcpy(vimrc, dir, dir_len);
if (vimrc[dir_len - 1] != PATHSEP) {
vimrc[dir_len] = PATHSEP;
dir_len += 1;
}
- memcpy(vimrc + dir_len, path_tail, sizeof(path_tail));
+ memcpy(vimrc + dir_len, appname, appname_len);
+ memcpy(vimrc + dir_len + appname_len, sysinit_suffix, sizeof(sysinit_suffix));
if (do_source(vimrc, false, DOSO_NONE, NULL) != FAIL) {
xfree(vimrc);
xfree(config_dirs);
@@ -2027,8 +2031,8 @@ static void do_system_initialization(void)
/// Does one of the following things, stops after whichever succeeds:
///
/// 1. Execution of VIMINIT environment variable.
-/// 2. Sourcing user vimrc file ($XDG_CONFIG_HOME/nvim/init.vim).
-/// 3. Sourcing other vimrc files ($XDG_CONFIG_DIRS[1]/nvim/init.vim, …).
+/// 2. Sourcing user config file ($XDG_CONFIG_HOME/$NVIM_APPNAME/init.lua or init.vim).
+/// 3. Sourcing other config files ($XDG_CONFIG_DIRS[1]/$NVIM_APPNAME/init.lua or init.vim, …).
/// 4. Execution of EXINIT environment variable.
///
/// @return True if it is needed to attempt to source exrc file according to
@@ -2049,8 +2053,7 @@ static bool do_user_initialization(void)
if (os_path_exists(init_lua_path)
&& do_source(init_lua_path, true, DOSO_VIMRC, NULL)) {
if (os_path_exists(user_vimrc)) {
- semsg(_("E5422: Conflicting configs: \"%s\" \"%s\""), init_lua_path,
- user_vimrc);
+ semsg(e_conflicting_configs, init_lua_path, user_vimrc);
}
xfree(user_vimrc);
@@ -2073,6 +2076,9 @@ static bool do_user_initialization(void)
char *const config_dirs = stdpaths_get_xdg_var(kXDGConfigDirs);
if (config_dirs != NULL) {
+ const char *appname = get_appname(false);
+ size_t appname_len = strlen(appname);
+
const void *iter = NULL;
do {
const char *dir;
@@ -2081,22 +2087,49 @@ static bool do_user_initialization(void)
if (dir == NULL || dir_len == 0) {
break;
}
- const char path_tail[] = { 'n', 'v', 'i', 'm', PATHSEP,
- 'i', 'n', 'i', 't', '.', 'v', 'i', 'm', NUL };
- char *vimrc = xmalloc(dir_len + sizeof(path_tail) + 1);
- memmove(vimrc, dir, dir_len);
- vimrc[dir_len] = PATHSEP;
- memmove(vimrc + dir_len + 1, path_tail, sizeof(path_tail));
- if (do_source(vimrc, true, DOSO_VIMRC, NULL) != FAIL) {
+
+ // Build: <xdg_dir>/<appname>/init.lua
+ const char init_lua_suffix[] = { PATHSEP, 'i', 'n', 'i', 't', '.', 'l', 'u', 'a', NUL };
+ size_t init_lua_len = dir_len + 1 + appname_len + sizeof(init_lua_suffix);
+ char *init_lua = xmalloc(init_lua_len);
+ memcpy(init_lua, dir, dir_len);
+ init_lua[dir_len] = PATHSEP;
+ memcpy(init_lua + dir_len + 1, appname, appname_len);
+ memcpy(init_lua + dir_len + 1 + appname_len, init_lua_suffix, sizeof(init_lua_suffix));
+
+ // Build: <xdg_dir>/<appname>/init.vim
+ const char init_vim_suffix[] = { PATHSEP, 'i', 'n', 'i', 't', '.', 'v', 'i', 'm', NUL };
+ size_t init_vim_len = dir_len + 1 + appname_len + sizeof(init_vim_suffix);
+ char *init_vim = xmalloc(init_vim_len);
+ memcpy(init_vim, dir, dir_len);
+ init_vim[dir_len] = PATHSEP;
+ memcpy(init_vim + dir_len + 1, appname, appname_len);
+ memcpy(init_vim + dir_len + 1 + appname_len, init_vim_suffix, sizeof(init_vim_suffix));
+
+ if (os_path_exists(init_lua)
+ && do_source(init_lua, true, DOSO_VIMRC, NULL)) {
+ if (os_path_exists(init_vim)) {
+ semsg(e_conflicting_configs, init_lua, init_vim);
+ }
+
+ xfree(init_vim);
+ xfree(init_lua);
+ xfree(config_dirs);
+ do_exrc = p_exrc;
+ return do_exrc;
+ }
+ xfree(init_lua);
+
+ if (do_source(init_vim, true, DOSO_VIMRC, NULL) != FAIL) {
do_exrc = p_exrc;
if (do_exrc) {
- do_exrc = (path_full_compare(VIMRC_FILE, vimrc, false, true) != kEqualFiles);
+ do_exrc = (path_full_compare(VIMRC_FILE, init_vim, false, true) != kEqualFiles);
}
- xfree(vimrc);
+ xfree(init_vim);
xfree(config_dirs);
return do_exrc;
}
- xfree(vimrc);
+ xfree(init_vim);
} while (iter != NULL);
xfree(config_dirs);
}
diff --git a/test/functional/core/startup_spec.lua b/test/functional/core/startup_spec.lua
@@ -1192,6 +1192,22 @@ describe('sysinit', function()
eval('printf("loaded %d xdg %d vim %d", g:loaded, get(g:, "xdg", 0), get(g:, "vim", 0))')
)
end)
+
+ it('respects NVIM_APPNAME in XDG_CONFIG_DIRS', function()
+ local appname = 'mysysinitapp'
+ mkdir(xdgdir .. pathsep .. appname)
+ write_file(
+ table.concat({ xdgdir, appname, 'sysinit.vim' }, pathsep),
+ [[let g:appname_sysinit = 1]]
+ )
+ clear {
+ args_rm = { '-u' },
+ env = { HOME = xhome, XDG_CONFIG_DIRS = xdgdir, NVIM_APPNAME = appname },
+ }
+ eq(1, eval('g:appname_sysinit'))
+ -- Should not load from nvim/ subdir (which has the default sysinit.vim from before_each)
+ eq(0, eval('get(g:, "xdg", 0)'))
+ end)
end)
describe('user config init', function()
@@ -1421,6 +1437,81 @@ describe('user config init', function()
)
end)
end)
+
+ describe('from XDG_CONFIG_DIRS', function()
+ local xdgdir = 'Xxdgconfigdirs'
+
+ before_each(function()
+ -- Remove init.lua from XDG_CONFIG_HOME so nvim falls back to XDG_CONFIG_DIRS
+ os.remove(init_lua_path)
+ rmdir(xdgdir)
+ mkdir_p(xdgdir .. pathsep .. 'nvim')
+ end)
+
+ after_each(function()
+ rmdir(xdgdir)
+ end)
+
+ it('loads init.lua from XDG_CONFIG_DIRS when no config in XDG_CONFIG_HOME', function()
+ write_file(
+ table.concat({ xdgdir, 'nvim', 'init.lua' }, pathsep),
+ [[vim.g.xdg_config_dirs_lua = 1]]
+ )
+ clear {
+ args_rm = { '-u' },
+ env = { XDG_CONFIG_HOME = xconfig, XDG_DATA_HOME = xdata, XDG_CONFIG_DIRS = xdgdir },
+ }
+ eq(1, eval('g:xdg_config_dirs_lua'))
+ eq(
+ fn.fnamemodify(table.concat({ xdgdir, 'nvim', 'init.lua' }, pathsep), ':p'),
+ eval('$MYVIMRC')
+ )
+ end)
+
+ it('prefers init.lua over init.vim, shows E5422', function()
+ write_file(table.concat({ xdgdir, 'nvim', 'init.lua' }, pathsep), [[vim.g.xdg_lua = 1]])
+ write_file(table.concat({ xdgdir, 'nvim', 'init.vim' }, pathsep), [[let g:xdg_vim = 1]])
+ clear {
+ args_rm = { '-u' },
+ env = { XDG_CONFIG_HOME = xconfig, XDG_DATA_HOME = xdata, XDG_CONFIG_DIRS = xdgdir },
+ }
+ eq(1, eval('g:xdg_lua'))
+ eq(0, eval('get(g:, "xdg_vim", 0)'))
+ t.matches('E5422: Conflicting configs:', eval('v:errmsg'))
+ end)
+
+ it('falls back to init.vim when no init.lua', function()
+ write_file(table.concat({ xdgdir, 'nvim', 'init.vim' }, pathsep), [[let g:xdg_vim = 1]])
+ clear {
+ args_rm = { '-u' },
+ env = { XDG_CONFIG_HOME = xconfig, XDG_DATA_HOME = xdata, XDG_CONFIG_DIRS = xdgdir },
+ }
+ eq(1, eval('g:xdg_vim'))
+ end)
+
+ it('respects NVIM_APPNAME', function()
+ local appname = 'mytestapp'
+ mkdir_p(xdgdir .. pathsep .. appname)
+ -- Also create nvim/ with a config that should NOT be loaded
+ write_file(table.concat({ xdgdir, 'nvim', 'init.lua' }, pathsep), [[vim.g.wrong = 1]])
+ write_file(table.concat({ xdgdir, appname, 'init.lua' }, pathsep), [[vim.g.appname_lua = 1]])
+ clear {
+ args_rm = { '-u' },
+ env = {
+ XDG_CONFIG_HOME = xconfig,
+ XDG_DATA_HOME = xdata,
+ XDG_CONFIG_DIRS = xdgdir,
+ NVIM_APPNAME = appname,
+ },
+ }
+ eq(1, eval('g:appname_lua'))
+ eq(0, eval('get(g:, "wrong", 0)'))
+ eq(
+ fn.fnamemodify(table.concat({ xdgdir, appname, 'init.lua' }, pathsep), ':p'),
+ eval('$MYVIMRC')
+ )
+ end)
+ end)
end)
describe('runtime:', function()