fix(cwd): nvim_win_set_buf changes global CWD #41328

Problem:
nvim_win_set_buf() on a non-current win, while the current win has
a win-local dir, changes the global CWD:

    :vsplit | lcd ..
    :call nvim_win_set_buf(other_win, buf)
    :wincmd l
    :verbose pwd
    [global] /parent        " expected: the initial cwd

Analysis:
`globaldir` is where to return when no local dir applies; NULL means the
process CWD is already there. Switching to a window with no local dir
makes update_cwd() chdir back to `globaldir` and clear it. kCtxKeepCwd
restores the process CWD but not that bookkeeping, so the restored
window-local dir is mistaken for the global one.

Solution:
Save/restore `globaldir` with the CWD.
This commit is contained in:
Justin M. Keyes
2026-08-15 11:45:44 -04:00
committed by GitHub
parent 9cca923ab4
commit cd2db7913a
3 changed files with 43 additions and 10 deletions

View File

@@ -311,13 +311,14 @@ static void ctx_localdirs_restore(CtxSwitch *cs, win_T *cwp, tabpage_T *tp, bool
if (!(persist && cs->cs_globaldir == NULL && globaldir != NULL)) {
xfree(globaldir);
globaldir = cs->cs_globaldir;
cs->cs_globaldir = NULL;
}
}
/// 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).
/// - kCtxKeepCwd or kCtxKeepDirs: the CWD and `globaldir`, 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).
static void ctx_dirs_save(CtxSwitch *cs, win_T *wp, tabpage_T *tp, buf_T *buf)
FUNC_ATTR_NONNULL_ARG(1, 2, 3)
{
@@ -325,6 +326,9 @@ static void ctx_dirs_save(CtxSwitch *cs, win_T *wp, tabpage_T *tp, buf_T *buf)
return;
}
// `globaldir` is where to return when no local dir applies (NULL: the CWD is already there).
cs->cs_globaldir = globaldir == NULL ? NULL : xstrdup(globaldir);
// 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;
@@ -332,7 +336,6 @@ static void ctx_dirs_save(CtxSwitch *cs, win_T *wp, tabpage_T *tp, buf_T *buf)
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
@@ -367,8 +370,12 @@ static void ctx_dirs_save(CtxSwitch *cs, win_T *wp, tabpage_T *tp, buf_T *buf)
/// 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) {
if (cs->cs_ctxwin_idx >= 0) {
return; // Hidden-buffer target: ctx_localdirs_restore() already restored dirs.
}
// kCtxKeepDirs: restore the saved dir scopes.
if (cs->cs_flags & kCtxKeepDirs) {
tabpage_T *dirs_tab = NULL;
FOR_ALL_TABS(tp) {
if (tp->handle == cs->cs_dirs_tab) {
@@ -377,7 +384,13 @@ static void ctx_dirs_restore(CtxSwitch *cs)
}
}
ctx_localdirs_restore(cs, NULL, dirs_tab, false);
} else if (cs->cs_cwd != NULL && !_ctx_did_chdir) {
// Pairs with the CWD restore below.
xfree(globaldir);
globaldir = cs->cs_globaldir;
cs->cs_globaldir = NULL;
}
XFREE_CLEAR(cs->cs_globaldir);
// Restore the CWD itself.
if (cs->cs_apply_acd) {

View File

@@ -106,9 +106,10 @@ typedef struct {
// State kept across the switch:
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:
// Saved dir state. Three users:
// 1. hidden-buffer target always saves b/tp/globaldir (so the temp context starts dir-neutral)
// 2. kCtxKeepDirs saves copies of all four.
// 2. kCtxKeepCwd saves globaldir.
// 3. kCtxKeepDirs also saves w/b/tp-localdir.
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