From 23bf4c0531acef4e8252f4db13fcd90ad5aa91bf Mon Sep 17 00:00:00 2001 From: Yochem van Rosmalen Date: Sun, 11 May 2025 18:00:51 +0200 Subject: [PATCH] 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. --- runtime/doc/news.txt | 1 + runtime/doc/options.txt | 6 ++--- runtime/doc/vim_diff.txt | 4 +++ runtime/lua/vim/_defaults.lua | 23 +++++++++++++++++ runtime/lua/vim/_meta/options.lua | 6 ++--- src/nvim/options.lua | 8 +++--- test/functional/core/startup_spec.lua | 36 +++++++++++++++++++++++++-- 7 files changed, 72 insertions(+), 12 deletions(-) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 11a9854800..4f59555ac4 100644 --- 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 index f39b2583ce..5fabe6efe9 100644 --- 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 index 43267db497..9f77836ea8 100644 --- 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 index adec7bda93..d0b3fea389 100644 --- 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 index c6b9d7a875..79035d420d 100644 --- 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 index 6b206e0445..8e5b374938 100644 --- 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 index 0564b06ac6..463553d4a0 100644 --- 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()