feat(vim._with): keepcwd

This commit is contained in:
Justin M. Keyes
2026-08-06 13:16:48 +02:00
parent 1c1dc0558f
commit eb19a52b7c
5 changed files with 92 additions and 40 deletions

View File

@@ -1480,6 +1480,7 @@ end
--- @field go? table<string, any>
--- @field hide? boolean
--- @field keepalt? boolean
--- @field keepcwd? boolean
--- @field keepjumps? boolean
--- @field keepmarks? boolean
--- @field keeppatterns? boolean
@@ -1537,9 +1538,6 @@ end
--- indicated by the spec is restored.
---
--- Notes:
--- - If `buf`/`win` is specified, CWD state (win/buf/tab-local dirs) is restored after execution.
--- Any :cd/:tcd/:lcd/:bcd during execution is undone.
--- - TODO: allow opt-out? Workaround: use nvim_buf_call()/nvim_win_call().
--- - Context `{ buf = buf }` has no guarantees about current window when
--- inside context.
--- - Context `{ buf = buf, win = win }` is yet not allowed, but this seems
@@ -1571,6 +1569,7 @@ function vim._with(context, f)
vim.validate('context.go', context.go, 'table', true)
vim.validate('context.hide', context.hide, 'boolean', true)
vim.validate('context.keepalt', context.keepalt, 'boolean', true)
vim.validate('context.keepcwd', context.keepcwd, 'boolean', true)
vim.validate('context.keepjumps', context.keepjumps, 'boolean', true)
vim.validate('context.keepmarks', context.keepmarks, 'boolean', true)
vim.validate('context.keeppatterns', context.keeppatterns, 'boolean', true)

View File

@@ -497,38 +497,42 @@ win_T *ctx_saved_curwin(void)
return _ctx_saved_curwin == 0 ? NULL : win_find_by_handle(_ctx_saved_curwin);
}
/// Prepares a temporary window or buffer as a temporary execution context. ctx_restore() MUST be
/// called afterwards, also when this returns false.
/// Prepares a temporary execution context. ctx_restore() MUST be called afterwards, also when this
/// returns false.
///
/// - Passing `wp` makes that window the curwin (in tabpage `tp`, or NULL for current tabpage).
/// - (Legacy: switch_win(), switch_win_noblock(), win_execute_before().)
/// - Passing `buf`, enters a window showing `buf` in the current tabpage, or prepares a temporary
/// "autocmd window" for it (never switches tabpage).
/// - (Legacy: aucmd_prepbuf().)
/// - Passing neither: only CWD state is saved; flags must include `kCtxKeepDirs`.
///
/// The switch itself never triggers autocommands; whether autocommands can fire _while_ switched
/// (until ctx_restore()) is the caller's choice via kCtxNoEvents.
///
/// @param wp Target window, or NULL to target a buffer.
/// @param wp Target window, or NULL.
/// @param tp Tabpage of `wp`, or NULL to not switch tabpage.
/// @param buf Target buffer, or NULL to target a window.
/// @param buf Target buffer, or NULL.
/// @param flags kCtx flags.
///
/// @return false if switching failed (only possible for a window target).
bool ctx_switch(CtxSwitch *cs, win_T *wp, tabpage_T *tp, buf_T *buf, CtxSwitchFlags flags)
{
assert((wp == NULL) != (buf == NULL));
// Exactly one target, or none with kCtxKeepDirs (which only saves the CWD state).
assert(((wp == NULL) != (buf == NULL)) || (wp == NULL && (flags & kCtxKeepDirs)));
assert(buf == NULL || tp == NULL); // a buffer target never switches tabpage
CLEAR_POINTER(cs);
cs->cs_flags = flags;
cs->cs_mode = buf != NULL ? kCtxSwitchBuf : kCtxSwitchWin;
cs->cs_mode = buf != NULL ? kCtxSwitchBuf : wp != NULL ? kCtxSwitchWin : kCtxSwitchDirs;
cs->cs_ctxwin_idx = -1;
cs->cs_did_chdir = _ctx_did_chdir;
_ctx_did_chdir = false;
if (cs->cs_mode == kCtxSwitchDirs) {
wp = curwin; // No target: "switch" to curwin, i.e. stay put.
}
// Resolve the target window. A buffer target prefers a window already showing "buf" in the
// current tabpage (least side effects, esp. if "buf" is curbuf); when there is none, an autocmd
// window is prepared below, after the save (entering it changes curwin and prevwin).
// Resolve the target window. A buffer target prefers a window already showing it, in the current
// tabpage (minimizes side effects); else a ctx_win is prepared below (ctx_win_prep).
if (buf != NULL) {
if (buf == curbuf) { // be quick when buf is curbuf
wp = curwin;
@@ -546,8 +550,8 @@ bool ctx_switch(CtxSwitch *cs, win_T *wp, tabpage_T *tp, buf_T *buf, CtxSwitchFl
cs->cs_target_win = wp->handle;
cs->cs_target_old_pos = wp->w_cursor;
}
// The CWD-state snapshot is only for targets with a real window: hidden-buffer targets are
// handled by the ctx_win machinery instead (see ctx_win_prep()).
// The CWD-state snapshot is only for a real window target; hidden-buffer target is handled by the
// ctx_win machinery (ctx_win_prep).
if (buf == NULL || wp != NULL) {
ctx_dirs_save(cs, wp, tp == NULL ? curtab : tp, buf);
}
@@ -580,8 +584,8 @@ bool ctx_switch(CtxSwitch *cs, win_T *wp, tabpage_T *tp, buf_T *buf, CtxSwitchFl
if (buf != NULL) {
if (wp == NULL) {
// No window shows `buf`: prepare a temp window. Anything related to a window (e.g., setting
// folds) may have unexpected results.
// Hidden buffer (`buf` not visible in any window): prepare a temp window.
// Window behavior (e.g., setting folds) may have unexpected results.
wp = ctx_win_prep(cs, buf);
// Leave the window we entered "from".
leaving_window(curwin);
@@ -677,7 +681,7 @@ void ctx_restore(CtxSwitch *cs)
curwin->w_topline = curbuf->b_ml.ml_line_count;
curwin->w_topfill = 0;
}
} else {
} else if (cs->cs_mode == kCtxSwitchBuf) {
// Restore the buffer previously edited by curwin.
if (curwin->handle == cs->cs_new_curwin
&& curbuf != cs->cs_new_curbuf.br_buf
@@ -693,7 +697,7 @@ void ctx_restore(CtxSwitch *cs)
}
ctx_restore_curwin(cs, NULL);
}
} // Else: only save CWD state.
if (!cs->cs_same_win) {
Visual.active = cs->cs_visual_active;

View File

@@ -65,9 +65,10 @@ typedef enum {
/// What ctx_switch() switched (set internally).
enum {
kCtxSwitchNone = 0, ///< zero-initialized: ctx_restore() is a no-op
kCtxSwitchWin, ///< window target
kCtxSwitchBuf, ///< buffer target
kCtxSwitchNone = 0, ///< Zero-initialized: ctx_restore() is a no-op.
kCtxSwitchWin, ///< Window target.
kCtxSwitchBuf, ///< Buffer target.
kCtxSwitchDirs, ///< No target: only CWD state is saved.
};
/// Context before a temporary switch of current window/buffer. Undone by ctx_restore().
@@ -84,7 +85,7 @@ typedef struct {
// Temporary location (ctx_switch()):
handle_T cs_new_curwin; ///< ID of new curwin
bufref_T cs_new_curbuf; ///< new curbuf
int cs_ctxwin_idx; ///< "autocmd" window in ctx_win[], or -1.
int cs_ctxwin_idx; ///< "autocmd" window in the ctx_win pool, or -1.
// Target tracking (kCtxValidate):
handle_T cs_target_win; ///< the window switched to
pos_T cs_target_old_pos; ///< its cursor before the switch

View File

@@ -586,6 +586,7 @@ static int nlua_with(lua_State *L)
int flags = 0;
buf_T *buf = NULL;
win_T *win = NULL;
bool keepcwd = false;
int log_level = -1;
#define APPLY_FLAG(key, flag) \
@@ -604,6 +605,8 @@ static int nlua_with(lua_State *L)
buf = handle_get_buffer((int)luaL_checkinteger(L, -1));
} else if (strequal("win", k)) {
win = handle_get_window((int)luaL_checkinteger(L, -1));
} else if (strequal("keepcwd", k)) {
keepcwd = v;
} else if (strequal("log_level", k)) {
log_level = (int)luaL_checkinteger(L, -1);
} else {
@@ -645,13 +648,15 @@ static int nlua_with(lua_State *L)
TRY_WRAP(&err, {
CtxSwitch cs = { 0 };
bool switched = true;
CtxSwitchFlags dirs = keepcwd ? kCtxKeepDirs : kCtxKeepCwd;
if (win) {
tabpage_T *tabpage = win_find_tabpage(win);
switched = ctx_switch(&cs, win, tabpage, NULL,
kCtxNoDisplay | kCtxValidate | kCtxKeepDirs);
switched = ctx_switch(&cs, win, tabpage, NULL, kCtxNoDisplay | kCtxValidate | dirs);
} else if (buf) {
ctx_switch(&cs, NULL, NULL, buf, kCtxKeepDirs);
ctx_switch(&cs, NULL, NULL, buf, dirs);
} else if (keepcwd) {
ctx_switch(&cs, NULL, NULL, NULL, kCtxKeepDirs);
}
if (switched) {

View File

@@ -345,30 +345,25 @@ describe('vim._with', function()
eq(true, out)
end)
it('restores CWD state', function()
it('keeps ":bcd" on the target buffer', function()
local out = exec_lua [[
local other_buf, cur_buf = setup_buffers()
local err_buf = api.nvim_create_buf(false, true)
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._with({ buf = other_buf }, function() vim.cmd.bcd(dir) end)
pcall(vim._with, { buf = err_buf }, function()
vim.cmd.bcd(dir)
error('oops')
end)
return {
fn.haslocaldir(-1, -1, other_buf),
fn.haslocaldir(-1, -1, err_buf),
fn.haslocaldir(-1, -1, cur_buf),
fn.getcwd() == cwd,
fn.getcwd() == cwd, -- Caller's CWD is unaffected: the target is not current.
}
]]
eq({ 0, 0, true }, out)
eq({ 1, 1, 0, true }, out)
end)
end)
@@ -422,6 +417,55 @@ describe('vim._with', function()
end)
end)
describe('`keepcwd` context', function()
it('undoes chdir at every scope', function()
local out = exec_lua [[
local cwd = fn.getcwd()
local dir = vim.fs.joinpath(cwd, 'test')
vim._with({ keepcwd = true }, function()
vim.cmd.cd(dir)
vim.cmd.tcd(dir)
vim.cmd.bcd(dir)
vim.cmd.lcd(dir)
end)
return {
fn.haslocaldir(), -- window
fn.haslocaldir(-1, 0), -- tabpage
fn.haslocaldir(-1, -1, 0), -- buffer
fn.getcwd() == cwd,
fn.getcwd(-1, -1) == cwd, -- global
}
]]
eq({ 0, 0, 0, true, true }, out)
end)
it('discards ":bcd"/":lcd" that a `buf`/`win` context would keep', function()
local out = exec_lua [[
local other_buf, _ = setup_buffers()
local other_win, _ = setup_windows()
local cwd = fn.getcwd()
local dir = vim.fs.joinpath(cwd, 'test')
vim._with({ buf = other_buf, keepcwd = true }, function() vim.cmd.bcd(dir) end)
vim._with({ win = other_win, keepcwd = true }, function() vim.cmd.lcd(dir) end)
return {
fn.haslocaldir(-1, -1, other_buf),
fn.haslocaldir(fn.win_id2win(other_win)),
fn.getcwd() == cwd,
}
]]
eq({ 0, 0, true }, out)
end)
it('restores nothing else: the callback may switch window', function()
local out = exec_lua [[
local other_win, _ = setup_windows()
vim._with({ keepcwd = true }, function() api.nvim_set_current_win(other_win) end)
return api.nvim_get_current_win() == other_win
]]
eq(true, out)
end)
end)
describe('`emsg_silent` context', function()
pending('works', function()
local ok = pcall(
@@ -1345,17 +1389,16 @@ describe('vim._with', function()
eq({ 'col', { { 'leaf', t2_other_win }, { 'leaf', t2_move_win } } }, fn.winlayout(2))
end)
it('restores CWD state', function()
it('keeps ":lcd" on the target window, but restores the CWD', 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)
eq({ 1, true }, out)
end)
end)