mirror of
https://github.com/neovim/neovim.git
synced 2026-08-17 21:14:47 -05:00
Merge #41176 :bcd fixes
This commit is contained in:
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
@@ -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