feat(cwd): support explicit chdir (:bcd/:tcd/…) in temp context

Problem:
- Explicit `:bcd` (etc.) persists from `nvim_buf_call()` but not from an
  autocmd handler targeting a hidden buf (`LspAttach`, `TermRequest`, …),
  which needs a `vim.schedule()` workaround.
- `vim._with()` is supposed to work as a "sandbox", discarding
  side-effects, but it leaks CWD changes: `:lcd` from a `win` context,
  any chdir from a visible-buffer context.

Solution:
- Explicit :cd/:tcd/:bcd during a temp context persists by default.
  - "Ambient" directory changes ('autochdir', existing win-local CWD,
    etc.) are still undone, as before.
- Add `kCtxKeepDirs`: snapshot/restore the target's full CWD state
  (w/b/tp-local, global, cwd). Used by `vim._with()` and `'inccommand'`,
  which must not leak state.
This commit is contained in:
Justin M. Keyes
2026-08-05 11:20:05 +02:00
parent 93696ca903
commit 1c1dc0558f
10 changed files with 300 additions and 106 deletions

View File

@@ -344,6 +344,32 @@ describe('vim._with', function()
]])
eq(true, out)
end)
it('restores CWD state', function()
local out = exec_lua [[
local other_buf, cur_buf = setup_buffers()
local cwd = fn.getcwd()
local dir = vim.fs.joinpath(cwd, 'test')
-- ":bcd" on the target buffer is discarded: hidden target, (nested) visible target, and
-- when the callback errors.
vim._with({ buf = other_buf }, function()
vim.cmd.bcd(dir)
vim._with({ buf = cur_buf }, function()
vim.cmd.bcd(dir)
end)
end)
pcall(vim._with, { buf = other_buf }, function()
vim.cmd.bcd(dir)
error('oops')
end)
return {
fn.haslocaldir(-1, -1, other_buf),
fn.haslocaldir(-1, -1, cur_buf),
fn.getcwd() == cwd,
}
]]
eq({ 0, 0, true }, out)
end)
end)
describe('`cwd` context', function()
@@ -381,6 +407,19 @@ describe('vim._with', function()
]]
eq({ true, true, true, true }, out)
end)
it('does not modify global CWD', function()
local out = exec_lua [[
local other_buf, _ = setup_buffers()
local cwd = fn.getcwd()
-- Activate a window-local dir, so that the global dir must be remembered.
vim.cmd.lcd(vim.fs.joinpath(cwd, 'test'))
local lcd_cwd = fn.getcwd() -- Not necessarily `cwd .. '/test'`: symlinks are resolved.
vim._with({ buf = other_buf, cwd = vim.fs.joinpath(cwd, 'src') }, function() end)
return { fn.getcwd() == lcd_cwd, fn.getcwd(-1, -1) == cwd }
]]
eq({ true, true }, out)
end)
end)
describe('`emsg_silent` context', function()
@@ -1305,6 +1344,19 @@ describe('vim._with', function()
exec_lua('vim._with({ win = ... }, function() vim.cmd.wincmd "J" end)', t2_move_win)
eq({ 'col', { { 'leaf', t2_other_win }, { 'leaf', t2_move_win } } }, fn.winlayout(2))
end)
it('restores CWD state', function()
local out = exec_lua [[
local other_win, cur_win = setup_windows()
local cwd = fn.getcwd()
-- ":lcd" on the target window is discarded.
vim._with({ win = other_win }, function()
vim.cmd.lcd(vim.fs.joinpath(cwd, 'test'))
end)
return { fn.haslocaldir(fn.win_id2win(other_win)), fn.getcwd() == cwd }
]]
eq({ 0, true }, out)
end)
end)
describe('`wo` context', function()