commit 23bf4c0531acef4e8252f4db13fcd90ad5aa91bf
parent 2c07428966a74c76003e00e2a37bf98eb8802c93
Author: Yochem van Rosmalen <git@yochem.nl>
Date: Sun, 11 May 2025 18:00:51 +0200
feat(exrc): search in parent directories (#33889)
feat(exrc): search exrc in parent directories
Problem:
`.nvim.lua` is only loaded from current directory, which is not flexible
when working from a subfolder of the project.
Solution:
Also search parent directories for configuration file.
Diffstat:
7 files changed, 72 insertions(+), 12 deletions(-)
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
@@ -122,6 +122,7 @@ DEFAULTS
• 'statusline' default is exposed as a statusline expression (previously it
was implemented as an internal C routine).
+• Project-local configuration ('exrc') is also loaded from parent directories.
DIAGNOSTICS
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
@@ -2412,9 +2412,9 @@ A jump table for the options with a short description can be found at |Q_op|.
'exrc' 'ex' boolean (default off)
global
Enables project-local configuration. Nvim will execute any .nvim.lua,
- .nvimrc, or .exrc file found in the |current-directory|, if the file is
- in the |trust| list. Use |:trust| to manage trusted files. See also
- |vim.secure.read()|.
+ .nvimrc, or .exrc file found in the |current-directory| and all parent
+ directories (ordered upwards), if the files are in the |trust| list.
+ Use |:trust| to manage trusted files. See also |vim.secure.read()|.
Compare 'exrc' to |editorconfig|:
- 'exrc' can execute any code; editorconfig only specifies settings.
diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt
@@ -209,6 +209,10 @@ nvim.swapfile:
swapfile is owned by a running Nvim process. Shows |W325| "Ignoring
swapfile…" message.
+nvim.find_exrc:
+- VimEnter: Extend 'exrc' to also search for project-local configuration files
+ in all parent directories.
+
==============================================================================
New Features *nvim-features*
diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua
@@ -925,6 +925,29 @@ do
end
end
end
+
+ vim.api.nvim_create_autocmd('VimEnter', {
+ group = vim.api.nvim_create_augroup('nvim.find_exrc', {}),
+ desc = 'Find project-local configuration',
+ callback = function()
+ if vim.o.exrc then
+ local files = vim.fs.find(
+ { '.nvim.lua', '.nvimrc', '.exrc' },
+ { type = 'file', upward = true, limit = math.huge }
+ )
+ for _, file in ipairs(files) do
+ local trusted = vim.secure.read(file) --[[@as string|nil]]
+ if trusted then
+ if vim.endswith(file, '.lua') then
+ loadstring(trusted)()
+ else
+ vim.api.nvim_exec2(trusted, {})
+ end
+ end
+ end
+ end
+ end,
+ })
end
--- Default options
diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua
@@ -2076,9 +2076,9 @@ vim.bo.expandtab = vim.o.expandtab
vim.bo.et = vim.bo.expandtab
--- Enables project-local configuration. Nvim will execute any .nvim.lua,
---- .nvimrc, or .exrc file found in the `current-directory`, if the file is
---- in the `trust` list. Use `:trust` to manage trusted files. See also
---- `vim.secure.read()`.
+--- .nvimrc, or .exrc file found in the `current-directory` and all parent
+--- directories (ordered upwards), if the files are in the `trust` list.
+--- Use `:trust` to manage trusted files. See also `vim.secure.read()`.
---
--- Compare 'exrc' to `editorconfig`:
--- - 'exrc' can execute any code; editorconfig only specifies settings.
diff --git a/src/nvim/options.lua b/src/nvim/options.lua
@@ -2745,9 +2745,9 @@ local options = {
defaults = false,
desc = [=[
Enables project-local configuration. Nvim will execute any .nvim.lua,
- .nvimrc, or .exrc file found in the |current-directory|, if the file is
- in the |trust| list. Use |:trust| to manage trusted files. See also
- |vim.secure.read()|.
+ .nvimrc, or .exrc file found in the |current-directory| and all parent
+ directories (ordered upwards), if the files are in the |trust| list.
+ Use |:trust| to manage trusted files. See also |vim.secure.read()|.
Compare 'exrc' to |editorconfig|:
- 'exrc' can execute any code; editorconfig only specifies settings.
@@ -2765,7 +2765,7 @@ local options = {
full_name = 'exrc',
scope = { 'global' },
secure = true,
- short_desc = N_('read .nvimrc and .exrc in the current directory'),
+ short_desc = N_('read project-local configuration in parent directories'),
tags = { 'project-config', 'workspace-config' },
type = 'boolean',
varname = 'p_exrc',
diff --git a/test/functional/core/startup_spec.lua b/test/functional/core/startup_spec.lua
@@ -1108,6 +1108,7 @@ describe('user config init', function()
string.format(
[[
vim.g.exrc_file = "%s"
+ vim.g.exrc_count = (vim.g.exrc_count or 0) + 1
]],
exrc_path
)
@@ -1118,6 +1119,7 @@ describe('user config init', function()
string.format(
[[
let g:exrc_file = "%s"
+ let g:exrc_count = get(g:, 'exrc_count', 0) + 1
]],
exrc_path
)
@@ -1141,8 +1143,8 @@ describe('user config init', function()
rmdir(xstate)
end)
- for _, filename in ipairs({ '.exrc', '.nvimrc', '.nvim.lua' }) do
- it(filename .. ' in cwd', function()
+ for _, filename in ipairs({ '.exrc', '.nvimrc', '.nvim.lua', '../.nvim.lua', '../.nvimrc' }) do
+ it(filename .. ' from cwd', function()
setup_exrc_file(filename)
clear { args_rm = { '-u' }, env = xstateenv }
@@ -1185,6 +1187,36 @@ describe('user config init', function()
eq(filename, eval('g:exrc_file'))
end)
end
+
+ it('exrc from all parent directories', function()
+ -- make sure that there are not any exrc files left from previous tests
+ for _, file in ipairs({ '.exrc', '.nvimrc', '.nvim.lua', '../.nvim.lua', '../.nvimrc' }) do
+ os.remove(file)
+ end
+ setup_exrc_file('../.exrc')
+ setup_exrc_file('.nvim.lua')
+ clear { args_rm = { '-u' }, env = xstateenv }
+ local screen = Screen.new(50, 8)
+ screen._default_attr_ids = nil
+ fn.jobstart({ nvim_prog }, {
+ term = true,
+ env = {
+ VIMRUNTIME = os.getenv('VIMRUNTIME'),
+ },
+ })
+ -- current directory exrc is found first
+ screen:expect({ any = '.nvim.lua' })
+ screen:expect({ any = pesc('[i]gnore, (v)iew, (d)eny, (a)llow:') })
+ feed('ia')
+
+ -- after that the exrc in the parent directory
+ screen:expect({ any = '.exrc' })
+ screen:expect({ any = pesc('[i]gnore, (v)iew, (d)eny, (a)llow:') })
+ feed('ia')
+ clear { args_rm = { '-u' }, env = xstateenv }
+ -- a total of 2 exrc files are executed
+ eq(2, eval('g:exrc_count'))
+ end)
end)
describe('with explicitly provided config', function()