Merge #41176 :bcd fixes

This commit is contained in:
Justin M. Keyes
2026-08-06 07:51:35 -04:00
committed by GitHub
10 changed files with 368 additions and 122 deletions
+4 -1
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
@@ -1533,7 +1534,8 @@ local get_context_state = function(context)
return res
end
--- Executes function `f` with the given context specification.
--- Executes function `f` with the given `context` spec: after execution, the original state
--- indicated by the spec is restored.
---
--- Notes:
--- - Context `{ buf = buf }` has no guarantees about current window when
@@ -1567,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)
+143 -60
View File
@@ -58,6 +58,9 @@ static int _ctx_switch_depth = 0;
/// curwin saved by the outermost curwin-changing ctx_switch() (0: none).
static handle_T _ctx_saved_curwin = 0;
/// Whether an explicit :cd/:tcd/:lcd/:bcd/chdir() happened since the innermost ctx_switch().
static bool _ctx_did_chdir = false;
/// Free resources used by Context object.
///
/// param[in] ctx pointer to Context object to free.
@@ -244,30 +247,103 @@ int ctx_from_dict(Dict dict, Context *ctx, Error *err)
return types;
}
/// kCtxKeepCwd: remembers the cwd so that ctx_restore() can undo any directory change caused by
/// switching to "wp" ('autochdir', win/tab-local directories).
static void ctx_cwd_save(CtxSwitch *cs, win_T *wp, tabpage_T *tp)
/// Moves CWD state aside, so that the temporary "autocmd window" starts clean.
/// Undone by ctx_localdirs_restore().
static void ctx_win_dirs_save(CtxSwitch *cs, win_T *cw_win, buf_T *buf)
{
cs->cs_cwd_status = FAIL;
// A pooled tmp-window must not carry a stale w_localdir.
XFREE_CLEAR(cw_win->w_localdir);
cs->cs_b_localdir = buf->b_localdir;
buf->b_localdir = NULL;
cs->cs_tp_localdir = curtab->tp_localdir;
curtab->tp_localdir = NULL;
cs->cs_globaldir = globaldir;
globaldir = NULL;
}
// Getting and setting directory can be slow on some systems, only do
// this when the current or target window/tab have a local directory or
// 'acd' is set.
/// Restores the dir scopes saved in `cs`. With `persist`, a scope explicitly changed
/// (user :bcd/:tcd/:cd) keeps its new value instead.
///
/// @param cwp The discarded temp win of a hidden buf, or NULL. If given, also fix the process CWD.
/// @param tp Tabpage that owns cs_tp_localdir, or NULL if it no longer exists.
static void ctx_localdirs_restore(CtxSwitch *cs, win_T *cwp, tabpage_T *tp, bool persist)
{
const bool did_chdir = _ctx_did_chdir;
_ctx_did_chdir = persist && did_chdir;
win_T *dirs_win = win_find_by_handle(cs->cs_new_curwin);
if (dirs_win != NULL) {
xfree(dirs_win->w_localdir);
dirs_win->w_localdir = cs->cs_w_localdir;
} else {
xfree(cs->cs_w_localdir);
}
buf_T *b = bufref_valid(&cs->cs_new_curbuf) ? cs->cs_new_curbuf.br_buf : NULL;
if (b != NULL && !(persist && b->b_localdir != NULL)) {
xfree(b->b_localdir);
b->b_localdir = cs->cs_b_localdir;
} else {
xfree(cs->cs_b_localdir);
}
if (tp != NULL && !(persist && tp->tp_localdir != NULL)) {
xfree(tp->tp_localdir);
tp->tp_localdir = cs->cs_tp_localdir;
} else {
xfree(cs->cs_tp_localdir);
}
// Correct the directory before restoring globaldir: the first chdir during the switch saved
// the pre-switch cwd in `globaldir` (see `post_chdir`), which update_cwd() uses as fallback.
if (cwp != NULL && (did_chdir || cwp->w_localdir != NULL)) {
update_cwd(kCdCauseWindow);
}
// Keep-case: the globaldir set during the switch (pre-switch cwd, see `post_chdir`).
if (!(persist && cs->cs_globaldir == NULL && globaldir != NULL)) {
xfree(globaldir);
globaldir = cs->cs_globaldir;
}
}
/// Saves the dir state to be restored by ctx_dirs_restore():
/// - kCtxKeepCwd or kCtxKeepDirs: the CWD, so any directory change caused by switching to `wp`
/// ('autochdir', win/tab-local directories) can be undone.
/// - kCtxKeepDirs: also copies of the target context's dir scopes (w/b/tp-local, global).
static void ctx_dirs_save(CtxSwitch *cs, win_T *wp, tabpage_T *tp, buf_T *buf)
{
if (!(cs->cs_flags & (kCtxKeepCwd | kCtxKeepDirs))) {
return;
}
// kCtxKeepDirs: also save copies of the target context's dir scopes.
if (cs->cs_flags & kCtxKeepDirs) {
buf_T *target_buf = buf != NULL ? buf : wp->w_buffer;
cs->cs_dirs_tab = tp->handle;
cs->cs_w_localdir = wp->w_localdir == NULL ? NULL : xstrdup(wp->w_localdir);
cs->cs_b_localdir = target_buf->b_localdir == NULL ? NULL : xstrdup(target_buf->b_localdir);
cs->cs_tp_localdir = tp->tp_localdir == NULL ? NULL : xstrdup(tp->tp_localdir);
cs->cs_globaldir = globaldir == NULL ? NULL : xstrdup(globaldir);
}
// Getting and setting directory can be slow on some systems, only do this when the current or
// target window/tab have a local directory or 'acd' is set, or if kCtxKeepDirs was set.
char cwd[MAXPATHL];
if (curwin != wp
&& (curwin->w_localdir != NULL || (wp != NULL && wp->w_localdir != NULL)
|| curbuf->b_localdir != NULL || (wp != NULL && wp->w_buffer->b_localdir != NULL)
|| (curtab != tp && (curtab->tp_localdir != NULL || tp->tp_localdir != NULL))
|| p_acd)) {
cs->cs_cwd_status = os_dirname(cwd, MAXPATHL);
if (cs->cs_cwd_status == OK) {
if ((cs->cs_flags & kCtxKeepDirs)
|| (curwin != wp
&& (curwin->w_localdir != NULL || (wp != NULL && wp->w_localdir != NULL)
|| curbuf->b_localdir != NULL || (wp != NULL && wp->w_buffer->b_localdir != NULL)
|| (curtab != tp && (curtab->tp_localdir != NULL || tp->tp_localdir != NULL))
|| p_acd))) {
if (os_dirname(cwd, MAXPATHL) == OK) {
cs->cs_cwd = xstrdup(cwd); // allocated on demand: keeps CtxSwitch small
}
}
// If 'acd' is set, check we are using that directory. If yes, then
// apply 'acd' afterwards, otherwise restore the current directory.
if (cs->cs_cwd_status == OK && p_acd) {
if (cs->cs_cwd != NULL && p_acd) {
if (curbuf->b_sfname != NULL && curbuf->b_fname == curbuf->b_sfname) {
cs->cs_save_sfname = xstrdup(curbuf->b_sfname);
}
@@ -279,13 +355,27 @@ static void ctx_cwd_save(CtxSwitch *cs, win_T *wp, tabpage_T *tp)
}
}
/// kCtxKeepCwd: restores the current directory.
static void ctx_cwd_restore(CtxSwitch *cs)
/// Restores the dir state saved by ctx_dirs_save(), undoing any chdir made while switched. The
/// target window/buffer/tab may have been closed meanwhile.
static void ctx_dirs_restore(CtxSwitch *cs)
{
// kCtxKeepDirs: restore the saved dir scopes. But not for hidden buf (ctx_win).
if ((cs->cs_flags & kCtxKeepDirs) && cs->cs_ctxwin_idx < 0) {
tabpage_T *dirs_tab = NULL;
FOR_ALL_TABS(tp) {
if (tp->handle == cs->cs_dirs_tab) {
dirs_tab = tp;
break;
}
}
ctx_localdirs_restore(cs, NULL, dirs_tab, false);
}
// Restore the CWD itself.
if (cs->cs_apply_acd) {
xfree(cs->cs_save_sfname);
do_autochdir();
} else if (cs->cs_cwd_status == OK) {
} else if (cs->cs_cwd != NULL) {
os_chdir(cs->cs_cwd);
if (cs->cs_save_sfname != NULL) {
xfree(curbuf->b_sfname);
@@ -296,6 +386,11 @@ static void ctx_cwd_restore(CtxSwitch *cs)
XFREE_CLEAR(cs->cs_cwd);
}
void ctx_did_chdir(void)
{
_ctx_did_chdir = true;
}
/// Return true if `win` is an active entry in ctx_win[] (the pool of temporary scratch windows).
bool is_ctx_win(win_T *win)
{
@@ -343,16 +438,7 @@ static win_T *ctx_win_prep(CtxSwitch *cs, buf_T *buf)
buf->b_nwindows++;
win_init_empty(cw_win); // set cursor and topline to safe values
// Make sure w_localdir, b_localdir, tp_localdir, globaldir are NULL: the switched-to code runs
// in the actual cwd (no chdir on switch), and a pooled tmp-window must not carry a stale
// w_localdir.
XFREE_CLEAR(cw_win->w_localdir);
cs->cs_b_localdir = buf->b_localdir;
buf->b_localdir = NULL;
cs->cs_tp_localdir = curtab->tp_localdir;
curtab->tp_localdir = NULL;
cs->cs_globaldir = globaldir;
globaldir = NULL;
ctx_win_dirs_save(cs, cw_win, buf);
if (need_append) {
win_append(lastwin, cw_win, NULL);
@@ -411,36 +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;
@@ -458,8 +550,10 @@ 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;
}
if (flags & kCtxKeepCwd) {
ctx_cwd_save(cs, wp, tp == NULL ? curtab : tp);
// 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);
}
// Save the current state.
@@ -490,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);
@@ -580,28 +674,14 @@ void ctx_restore(CtxSwitch *cs)
vars_clear(&cwp->w_vars->dv_hashtab); // free all w: variables
hash_init(&cwp->w_vars->dv_hashtab); // re-use the hashtab
// If :lcd has been used in the autocommand window, correct current
// directory before restoring b_localdir, tp_localdir and globaldir.
if (cwp->w_localdir != NULL) {
update_cwd(kCdCauseWindow);
}
if (bufref_valid(&cs->cs_new_curbuf)) {
xfree(cs->cs_new_curbuf.br_buf->b_localdir);
cs->cs_new_curbuf.br_buf->b_localdir = cs->cs_b_localdir;
} else {
xfree(cs->cs_b_localdir);
}
xfree(curtab->tp_localdir);
curtab->tp_localdir = cs->cs_tp_localdir;
xfree(globaldir);
globaldir = cs->cs_globaldir;
ctx_localdirs_restore(cs, cwp, curtab, !(cs->cs_flags & kCtxKeepDirs));
// Buffer contents may have changed; cursor is checked below, AFTER restoring Visual state.
if (curwin->w_topline > curbuf->b_ml.ml_line_count) {
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
@@ -617,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;
@@ -633,9 +713,12 @@ void ctx_restore(CtxSwitch *cs)
if (cs->cs_flags & kCtxNoEvents) {
unblock_autocmds();
}
if (cs->cs_flags & kCtxKeepCwd) {
ctx_cwd_restore(cs);
ctx_dirs_restore(cs); // No-op if ctx_dirs_save() saved nothing.
// Re-apply the restored context's effective directory.
if (cs->cs_ctxwin_idx < 0 && _ctx_did_chdir) {
update_cwd(kCdCauseWindow);
}
_ctx_did_chdir = _ctx_did_chdir || cs->cs_did_chdir;
if (cs->cs_flags & kCtxValidate) {
// Update the status line if the cursor moved in the target window.
win_T *const wp = win_find_by_handle(cs->cs_target_win);
+29 -16
View File
@@ -45,22 +45,30 @@ typedef struct {
/// Flags for ctx_switch().
typedef enum {
/// Restore process CWD: undo incidental chdir ('autochdir', "leaked" win/tab-local CWD).
///
/// Note: this flag only exists for performance. Semantically every ctx-switch wants this, but the
/// getcwd() bookkeeping is costly for internal switches that don't run user code.
kCtxKeepCwd = 1,
/// Restore the target's full CWD state: undo all "chdir" operations on ctx_restore(), including
/// explicit :cd/:tcd/:bcd (which otherwise persist).
/// - Note: :lcd targeting a hidden buffer (temp window) is always discarded.
kCtxKeepDirs = 2,
/// Don't affect the display (no redraw; limits access to another tabpage).
kCtxNoDisplay = 1,
kCtxNoDisplay = 4,
/// Block autocommands until ctx_restore().
kCtxNoEvents = 2,
/// Undo any chdir caused by the switch ('autochdir', win/tab-local CWD) on ctx_restore().
kCtxKeepCwd = 4,
kCtxNoEvents = 8,
/// Validate cursor/Visual around the switch; update display (statusline) if the target window's
/// cursor moved.
kCtxValidate = 8,
kCtxValidate = 16,
} CtxSwitchFlags;
/// 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().
@@ -77,16 +85,21 @@ 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
// State kept across the switch:
char *cs_b_localdir; ///< saved b_localdir of the target buffer (autocmd window)
char *cs_tp_localdir; ///< saved tp_localdir (autocmd window)
char *cs_globaldir; ///< saved globaldir (autocmd window)
char *cs_cwd; ///< saved cwd (kCtxKeepCwd; allocated on demand)
int cs_cwd_status; ///< OK if cs_cwd is valid
bool cs_apply_acd; ///< re-apply 'autochdir' on ctx_restore()
char *cs_save_sfname; ///< saved b_sfname (kCtxKeepCwd)
bool cs_did_chdir; ///< saved `ctx_did_chdir` of the enclosing context
handle_T cs_dirs_tab; ///< kCtxKeepDirs: tabpage that owns cs_tp_localdir.
// Saved dir state. Two users:
// 1. hidden-buffer target always saves b/tp/globaldir (so the temp context starts dir-neutral)
// 2. kCtxKeepDirs saves copies of all four.
char *cs_w_localdir; ///< Saved w_localdir of the target window
char *cs_b_localdir; ///< Saved b_localdir of the target buffer
char *cs_tp_localdir; ///< Saved tp_localdir
char *cs_globaldir; ///< Saved globaldir
char *cs_cwd; ///< Saved CWD (kCtxKeepCwd/kCtxKeepDirs).
bool cs_apply_acd; ///< Re-apply 'autochdir' on ctx_restore().
char *cs_save_sfname; ///< Saved b_sfname (kCtxKeepCwd/kCtxKeepDirs).
} CtxSwitch;
+2
View File
@@ -414,6 +414,8 @@ void f_chdir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
scope = kCdScopeTabpage;
} else if (strcmp(s, "window") == 0) {
scope = kCdScopeWindow;
} else if (strcmp(s, "buffer") == 0) {
scope = kCdScopeBuffer;
} else {
semsg(_(e_invargNval), "scope", s);
return;
+1
View File
@@ -6329,6 +6329,7 @@ bool changedir_func(char *new_dir, CdScope scope)
*pp = pdir;
post_chdir(scope, dir_differs);
ctx_did_chdir();
return true;
}
+2 -2
View File
@@ -2471,7 +2471,7 @@ static buf_T *cmdpreview_open_buf(void)
// Rename preview buffer.
CtxSwitch aco = { 0 };
ctx_switch(&aco, NULL, NULL, cmdpreview_buf, 0);
ctx_switch(&aco, NULL, NULL, cmdpreview_buf, kCtxKeepDirs);
int retv = rename_buffer("[Preview]");
ctx_restore(&aco);
@@ -2480,7 +2480,7 @@ static buf_T *cmdpreview_open_buf(void)
}
// Temporarily switch to preview buffer to set it up for previewing.
ctx_switch(&aco, NULL, NULL, cmdpreview_buf, 0);
ctx_switch(&aco, NULL, NULL, cmdpreview_buf, kCtxKeepDirs);
buf_clear();
curbuf->b_p_ma = true;
curbuf->b_p_ul = -1;
+8 -2
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,12 +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 | kCtxKeepCwd | kCtxValidate);
switched = ctx_switch(&cs, win, tabpage, NULL, kCtxNoDisplay | kCtxValidate | dirs);
} else if (buf) {
ctx_switch(&cs, NULL, NULL, buf, 0);
ctx_switch(&cs, NULL, NULL, buf, dirs);
} else if (keepcwd) {
ctx_switch(&cs, NULL, NULL, NULL, kCtxKeepDirs);
}
if (switched) {
+82 -41
View File
@@ -23,6 +23,7 @@ local directories = {
}
local tmpfile = 'Xtest-functional-ex_cmds-cd_spec-tmpfile'
local startdir ---@type string `getcwd()` at session start (set by `before_each`)
local function join(...)
return table.concat({ ... }, pathsep)
@@ -57,29 +58,26 @@ local tlwd = function()
end -- tab dir
--local glwd = function() return eval('haslocaldir(-1, -1)') end -- global dir
local function before_test()
before_each(function()
clear()
for _, d in pairs(directories) do
mkdir(d)
end
directories.start = cwd()
end
startdir = cwd()
end)
local function remove_dirs()
after_each(function()
for _, d in pairs(directories) do
vim.uv.fs_rmdir(d)
n.rmdir(d)
end
end
end)
-- Test both the `cd` and `chdir` variants
for _, cmd in ipairs { 'cd', 'chdir' } do
describe(':' .. cmd, function()
before_each(before_test)
after_each(remove_dirs)
describe('using explicit scope', function()
it('for window', function()
local globalDir = directories.start
local globalDir = startdir
local globalwin = call('winnr')
local tabnr = call('tabpagenr')
@@ -122,7 +120,7 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
end)
it('for tab page', function()
local globalDir = directories.start
local globalDir = startdir
local globaltab = call('tabpagenr')
-- Everything matches globalDir to start
@@ -152,7 +150,7 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
end)
it('for buffer', function()
local globalDir = directories.start
local globalDir = startdir
-- Create two buffers
command(('e %s1'):format(tmpfile))
command(('e %s%s%s2'):format(directories.buffer, pathsep, tmpfile))
@@ -197,36 +195,36 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
describe('getcwd(-1, -1)', function()
it('works', function()
eq(directories.start, cwd(-1, -1))
eq(startdir, cwd(-1, -1))
eq(0, lwd(-1, -1))
end)
it('works with tab-local pwd', function()
command('silent t' .. cmd .. ' ' .. directories.tab)
eq(directories.start, cwd(-1, -1))
eq(startdir, cwd(-1, -1))
eq(0, lwd(-1, -1))
end)
it('works with window-local pwd', function()
command('silent l' .. cmd .. ' ' .. directories.window)
eq(directories.start, cwd(-1, -1))
eq(startdir, cwd(-1, -1))
eq(0, lwd(-1, -1))
end)
it('works with buffer-local pwd', function()
command(('silent b%s %s'):format(cmd, directories.buffer))
eq(directories.start, cwd(-1, -1))
eq(startdir, cwd(-1, -1))
eq(0, lwd(-1, -1))
-- Must behave the same if bufnr is -1
eq(directories.start, cwd(-1, -1, -1))
eq(startdir, cwd(-1, -1, -1))
eq(0, lwd(-1, -1, -1))
end)
end)
describe('Local directory gets inherited', function()
it('by tabs', function()
local globalDir = directories.start
local globalDir = startdir
-- Create a new tab and change directory
command('tabnew')
@@ -246,7 +244,7 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
end)
it('works', function()
local globalDir = directories.start
local globalDir = startdir
-- Create a new tab first and verify that is has the same working dir
command('tabnew')
eq(globalDir, cwd())
@@ -310,7 +308,7 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
end)
it('works when mixing tab-local and buffer-local directories', function()
local globalDir = directories.start
local globalDir = startdir
-- Create two buffers for testing. One in each tab
command(('e %s1'):format(tmpfile))
@@ -354,7 +352,7 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
eq(0, blwd()) -- No window-buffer directory
end)
it('works when mixing window local and buffer local directories', function()
local globalDir = directories.start
local globalDir = startdir
-- Create a new window first and verify that is has the same working directory
command('new')
eq(globalDir, cwd())
@@ -394,16 +392,13 @@ end
for _, cmd in ipairs { 'bcd', 'bchdir' } do
describe(':' .. cmd, function()
before_each(before_test)
after_each(remove_dirs)
it('works after deleting the only buffer', function()
command(('%s %s'):format(cmd, directories.buffer))
command('bd') -- delete buffer
end)
it('buffer-local directory is NOT sticky/inherited', function()
local bufdir = join(directories.start, directories.buffer)
local bufdir = join(startdir, directories.buffer)
command('edit ' .. tmpfile)
command(('%s %s'):format(cmd, directories.buffer))
@@ -411,12 +406,12 @@ for _, cmd in ipairs { 'bcd', 'bchdir' } do
-- A new buffer starts without a buffer-local directory.
command('new')
eq(directories.start, cwd())
eq(startdir, cwd())
eq(0, blwd())
command('close')
eq(bufdir, cwd())
command('enew')
eq(directories.start, cwd())
eq(startdir, cwd())
eq(0, blwd())
command('b# ')
eq(bufdir, cwd())
@@ -426,19 +421,74 @@ for _, cmd in ipairs { 'bcd', 'bchdir' } do
command(('%s %s'):format(cmd, directories.buffer))
eq(bufdir, cwd())
command('edit ' .. tmpfile .. '2')
eq(directories.start, cwd())
eq(startdir, cwd())
eq(0, blwd())
end)
end)
end
describe('cd during temp context-switch', function()
it(':bcd/:tcd/:lcd persists in target scope, does not leak into original context', function()
local exec_lua = n.exec_lua
local bufdir = join(startdir, directories.buffer)
local windir = join(startdir, directories.window)
local tabdir = join(startdir, directories.tab)
--- Creates a loaded, hidden buffer.
local function hidden_buf(name)
local b = call('bufadd', name)
call('bufload', b)
return b
end
--- Runs `vim.cmd[cmd](dir)` with buffer `b` as temporary curbuf.
local function cd_in_buf_call(b, cmd, dir)
exec_lua(function(b_, cmd_, d)
vim.api.nvim_buf_call(b_, function()
vim.cmd[cmd_](d)
end)
end, b, cmd, dir)
end
-- :bcd on a hidden buffer via nvim_buf_call() persists; the caller's cwd is unchanged.
local hidden = hidden_buf('Xtest-cd-hidden')
cd_in_buf_call(hidden, 'bcd', bufdir)
eq({ 1, bufdir, startdir }, { lwd(-1, -1, hidden), cwd(-1, -1, hidden), cwd() })
-- :lcd targets the temporary window, which is discarded; the caller's cwd is unchanged.
cd_in_buf_call(hidden, 'lcd', windir)
eq({ 0, startdir }, { wlwd(), cwd() })
-- :lcd via win_execute() persists on the target window; the CWD outside it is unchanged.
command('split')
call('win_execute', call('win_getid', 2), ('lcd %s'):format(windir))
eq({ 1, windir, startdir }, { lwd(2), cwd(2), cwd() })
command('only')
-- An autocmd handler targeting a hidden buffer can set its buffer-local dir; the caller's
-- cwd is unchanged.
local hidden2 = hidden_buf('Xtest-cd-hidden2')
exec_lua(function(b, d)
vim.api.nvim_create_autocmd('TermRequest', {
buffer = b,
once = true,
callback = function()
vim.cmd.bcd(d)
end,
})
vim.api.nvim_exec_autocmds('TermRequest', { buffer = b, data = { sequence = 'x' } })
end, hidden2, bufdir)
eq({ 1, bufdir, startdir }, { lwd(-1, -1, hidden2), cwd(-1, -1, hidden2), cwd() })
-- :tcd via nvim_buf_call() persists, and the tab scope claims the new cwd.
cd_in_buf_call(hidden, 'tcd', tabdir)
eq({ 1, tabdir, tabdir }, { tlwd(), tcwd(), cwd() })
end)
end)
-- Test legal parameters for 'getcwd' and 'haslocaldir'
for _, cmd in ipairs { 'getcwd', 'haslocaldir' } do
describe(cmd .. '()', function()
before_each(function()
clear()
end)
it('validation', function()
local err474 = 'Vim:E474: Invalid argument'
eq(err474, pcall_err(call, cmd, 'some string'))
@@ -472,15 +522,6 @@ for _, cmd in ipairs { 'getcwd', 'haslocaldir' } do
end
describe('getcwd()', function()
before_each(function()
clear()
mkdir(directories.global)
end)
after_each(function()
n.rmdir(directories.global)
end)
it('returns empty string if working directory does not exist', function()
skip(is_os('win'), 'N/A for Windows')
command('cd ' .. directories.global)
+95
View File
@@ -344,6 +344,27 @@ describe('vim._with', function()
]])
eq(true, out)
end)
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')
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, -- Caller's CWD is unaffected: the target is not current.
}
]]
eq({ 1, 1, 0, true }, out)
end)
end)
describe('`cwd` context', function()
@@ -381,6 +402,68 @@ 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('`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()
@@ -1305,6 +1388,18 @@ 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('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()
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({ 1, true }, out)
end)
end)
describe('`wo` context', function()
+2
View File
@@ -104,6 +104,8 @@ func Test_chdir_func()
call assert_match('^\[global\]', trim(execute('verbose pwd')))
call chdir('.', 'tabpage')
call assert_match('^\[tabpage\]', trim(execute('verbose pwd')))
call chdir('.', 'buffer')
call assert_match('^\[buffer\]', trim(execute('verbose pwd')))
call chdir('.', 'window')
call assert_match('^\[window\]', trim(execute('verbose pwd')))