fix(startup): source init.lua from XDG_CONFIG_DIRS

Problem:

`init.lua` files in `$XDG_CONFIG_DIRS` directories were not being sourced during startup, even though the documentation states they should be searched alongside `init.vim`.

See:

e51f5e17e1/runtime/doc/starting.txt (L495-L496)

Solution:

Modify `do_user_initialization()` to search for `init.lua` in each `$XDG_CONFIG_DIRS` directory before falling back to `init.vim`, matching the behavior for `$XDG_CONFIG_HOME`. Also show `E5422` error if both `init.lua` and `init.vim` exist in the same directory.

Fixes #37405
This commit is contained in:
Jesse van der Pluijm
2026-01-15 14:19:43 +01:00
parent 6082b7f850
commit c785d5cdf0
2 changed files with 85 additions and 6 deletions

View File

@@ -2027,8 +2027,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/init.lua or init.vim).
/// 3. Sourcing other config files ($XDG_CONFIG_DIRS[1]/nvim/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
@@ -2073,6 +2073,11 @@ static bool do_user_initialization(void)
char *const config_dirs = stdpaths_get_xdg_var(kXDGConfigDirs);
if (config_dirs != NULL) {
const char vim_path_tail[] = { 'n', 'v', 'i', 'm', PATHSEP,
'i', 'n', 'i', 't', '.', 'v', 'i', 'm', NUL };
const char lua_path_tail[] = { 'n', 'v', 'i', 'm', PATHSEP,
'i', 'n', 'i', 't', '.', 'l', 'u', 'a', NUL };
const void *iter = NULL;
do {
const char *dir;
@@ -2081,12 +2086,34 @@ 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);
// Build: <xdg_dir>/<appname>/init.lua
char *init_lua = xmalloc(dir_len + sizeof(lua_path_tail) + 1);
memmove(init_lua, dir, dir_len);
init_lua[dir_len] = PATHSEP;
memmove(init_lua + dir_len + 1, lua_path_tail, sizeof(lua_path_tail));
// Build: <xdg_dir>/<appname>/init.vim
char *vimrc = xmalloc(dir_len + sizeof(vim_path_tail) + 1);
memmove(vimrc, dir, dir_len);
vimrc[dir_len] = PATHSEP;
memmove(vimrc + dir_len + 1, path_tail, sizeof(path_tail));
memmove(vimrc + dir_len + 1, vim_path_tail, sizeof(vim_path_tail));
if (os_path_exists(init_lua)
&& do_source(init_lua, true, DOSO_VIMRC, NULL)) {
if (os_path_exists(vimrc)) {
semsg(_("E5422: Conflicting configs: \"%s\" \"%s\""), init_lua, vimrc);
}
xfree(vimrc);
xfree(init_lua);
xfree(config_dirs);
do_exrc = p_exrc;
return do_exrc;
}
xfree(init_lua);
// init.vim
if (do_source(vimrc, true, DOSO_VIMRC, NULL) != FAIL) {
do_exrc = p_exrc;
if (do_exrc) {

View File

@@ -1421,6 +1421,58 @@ 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)
end)
end)
describe('runtime:', function()