mirror of
https://github.com/neovim/neovim.git
synced 2026-08-27 01:21:48 +00:00
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:
@@ -1533,9 +1533,13 @@ 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:
|
||||
--- - 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
|
||||
|
||||
@@ -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);
|
||||
@@ -437,6 +523,8 @@ bool ctx_switch(CtxSwitch *cs, win_T *wp, tabpage_T *tp, buf_T *buf, CtxSwitchFl
|
||||
cs->cs_flags = flags;
|
||||
cs->cs_mode = buf != NULL ? kCtxSwitchBuf : kCtxSwitchWin;
|
||||
cs->cs_ctxwin_idx = -1;
|
||||
cs->cs_did_chdir = _ctx_did_chdir;
|
||||
_ctx_did_chdir = false;
|
||||
|
||||
// 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
|
||||
@@ -458,8 +546,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 targets with a real window: hidden-buffer targets are
|
||||
// handled by the ctx_win machinery instead (see ctx_win_prep()).
|
||||
if (buf == NULL || wp != NULL) {
|
||||
ctx_dirs_save(cs, wp, tp == NULL ? curtab : tp, buf);
|
||||
}
|
||||
|
||||
// Save the current state.
|
||||
@@ -580,21 +670,7 @@ 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) {
|
||||
@@ -633,9 +709,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);
|
||||
|
||||
@@ -45,15 +45,22 @@ 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).
|
||||
@@ -77,16 +84,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 ctx_win[], 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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6329,6 +6329,7 @@ bool changedir_func(char *new_dir, CdScope scope)
|
||||
*pp = pdir;
|
||||
|
||||
post_chdir(scope, dir_differs);
|
||||
ctx_did_chdir();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -648,9 +648,10 @@ static int nlua_with(lua_State *L)
|
||||
|
||||
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 | kCtxKeepDirs);
|
||||
} else if (buf) {
|
||||
ctx_switch(&cs, NULL, NULL, buf, 0);
|
||||
ctx_switch(&cs, NULL, NULL, buf, kCtxKeepDirs);
|
||||
}
|
||||
|
||||
if (switched) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user