refactor(editor): cleanup change-directory (:bcd) logic

This commit is contained in:
Justin M. Keyes
2026-08-04 15:16:17 +02:00
parent ea3868bcf9
commit db2e86fba4
13 changed files with 274 additions and 418 deletions

View File

@@ -1348,15 +1348,16 @@ use has("browsefilter"): >
==============================================================================
7. The current directory *current-directory*
There are three current-directory "scopes": global, tab and window. You can
use |:cd|, |:tcd| and |:lcd| to change to another directory. This affects
anything that operates on relative paths, e.g. ":edit ./foo.txt" or shell
commands like ":!ls" or ":te ls".
There are four current-directory "scopes": global, tab, window and buffer.
You can use |:cd|, |:tcd|, |:lcd| and |:bcd| to change to another directory.
This affects anything that operates on relative paths, e.g. ":edit ./foo.txt"
or shell commands like ":!ls" or ":te ls".
Use |getcwd()| to get the effective directory from the current scope. The
window-local working directory takes precedence over the tab-local working
directory, which in turn takes precedence over the global working directory.
If a local working directory (tab or window) does not exist, the next-higher
window-local working directory takes precedence over the buffer-local working
directory, which takes precedence over the tab-local working directory, which
in turn takes precedence over the global working directory. If a local
working directory (window, buffer or tab) does not exist, the next-higher
scope in the hierarchy applies.
*:cd* *E747* *E472*

View File

@@ -696,7 +696,7 @@ Working directory (Vim implemented some of these after Nvim):
- |DirChanged| and |DirChangedPre| can be triggered when switching to another
window or tab.
- |getcwd()| and |haslocaldir()| may throw errors if the tabpage, window, or
buffer cannot be found. *E5000* *E5001* *E5002* *E5006* *E5007*
buffer cannot be found.
- |haslocaldir()| checks for tab-local directory if and only if -1 is passed as
window number, and its only possible returns values are 0 and 1.
- `getcwd(-1)` is equivalent to `getcwd(-1, 0)` instead of returning the global

View File

@@ -3748,7 +3748,8 @@ getcursorcharpos([{winid}]) *getcursorcharpos()*
Return: ~
(`any`)
getcwd([{winnr} [, {tabnr} [, {bufnr}]]]) *getcwd()*
*getcwd()* *E5000* *E5001* *E5002* *E5006* *E5007*
getcwd([{winnr} [, {tabnr} [, {bufnr}]]])
Lua: Prefer |uv.cwd()| for the global working directory;
tab-local and window-local scopes differ.
@@ -3782,7 +3783,6 @@ getcwd([{winnr} [, {tabnr} [, {bufnr}]]]) *getcwd()*
getcwd(-1, -1, -1) " Get global directory
getcwd(-1, -1) " Get global directory
< Throw error if the arguments are invalid.
|E5000| |E5001| |E5002| |E5006| |E5007|
Parameters: ~
• {winnr} (`integer?`)

View File

@@ -3350,7 +3350,6 @@ function vim.fn.getcursorcharpos(winid) end
--- getcwd(-1, -1, -1) " Get global directory
--- getcwd(-1, -1) " Get global directory
--- <Throw error if the arguments are invalid.
--- |E5000| |E5001| |E5002| |E5006| |E5007|
---
--- @param winnr? integer
--- @param tabnr? integer

View File

@@ -38,9 +38,6 @@ local dup_allowed = {
E482 = 3,
E484 = 2,
E488 = 2,
E5000 = 2,
E5001 = 2,
E5002 = 2,
E5009 = 3,
E502 = 2,
E503 = 3,

View File

@@ -20,8 +20,6 @@ EXTERN const char e_invalid_buffer_name_str[] INIT(= N_("E158: Invalid buffer na
EXTERN const char e_command_too_recursive[] INIT(= N_("E169: Command too recursive"));
EXTERN const char e_buffer_nr_invalid_buffer_number[] INIT(= N_("E680: <buffer=%d>: invalid buffer number"));
EXTERN const char e_buffer_is_not_loaded[] INIT(= N_("E681: Buffer is not loaded"));
EXTERN const char e_getcwd_buffer_scope_requires_minus_one[] INIT(= N_("E5006: Window and tab scope must be -1 when using buffer scope"));
EXTERN const char e_cannot_find_buffer_number[] INIT(= N_("E5007: Cannot find buffer number."));
EXTERN const char e_endif[] INIT(= N_("E171: Missing :endif"));
EXTERN const char e_endtry[] INIT(= N_("E600: Missing :endtry"));
EXTERN const char e_endwhile[] INIT(= N_("E170: Missing :endwhile"));

View File

@@ -4125,12 +4125,12 @@ M.funcs = {
getcwd(-1, -1, -1) " Get global directory
getcwd(-1, -1) " Get global directory
<Throw error if the arguments are invalid.
|E5000| |E5001| |E5002| |E5006| |E5007|
]=],
name = 'getcwd',
params = { { 'winnr', 'integer' }, { 'tabnr', 'integer' }, { 'bufnr', 'integer' } },
returns = 'string',
signature = 'getcwd([{winnr} [, {tabnr} [, {bufnr}]]])',
tags = { 'E5000', 'E5001', 'E5002', 'E5006', 'E5007' },
see_lua = {
'|uv.cwd()| for the global working directory; tab-local and window-local scopes differ',
},

View File

@@ -644,6 +644,117 @@ void f_fnamemodify(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
xfree(fbuf);
}
/// Resolves the `[{winnr}[, {tabnr}[, {bufnr}]]]` arguments of `getcwd()`/`haslocaldir()` to the
/// requested scope and the scope objects to inspect. In an argument, `0` means the current object
/// and `-1` skips the scope (requesting the next broader one). Note that the argument order
/// differs from the `CdScope` order.
///
/// @param default_scope Scope when no arguments were given.
///
/// @return false if an error message was emitted.
static bool getcwd_scope_args(typval_T *argvars, CdScope default_scope, CdScope *scope, win_T **win,
tabpage_T **tp, buf_T **buf)
{
enum { kWinArg, kTabArg, kBufArg, };
int argv[] = { 0, 0, 0 };
int argc = 0;
*scope = default_scope;
*win = curwin;
*tp = curtab;
*buf = curbuf;
for (; argc < 3; argc++) {
// If there is no argument there are no more scopes after it, break out.
if (argvars[argc].v_type == VAR_UNKNOWN) {
break;
}
if (argvars[argc].v_type != VAR_NUMBER) {
emsg(_(e_invarg));
return false;
}
argv[argc] = (int)argvars[argc].vval.v_number;
// It is an error for the scope number to be less than `-1`.
if (argv[argc] < -1) {
emsg(_(e_invarg));
return false;
}
}
// Narrowest requested scope. Imagine X >= 0:
switch (argc) {
case 0:
break;
case 1:
if (argv[kWinArg] >= 0) {
*scope = kCdScopeWindow; // (X)
} else {
*scope = kCdScopeTabpage; // (-1)
}
break;
case 2:
if (argv[kWinArg] >= 0) {
*scope = kCdScopeWindow; // (X, ...)
} else if (argv[kTabArg] >= 0) {
*scope = kCdScopeTabpage; // (-1, X)
} else {
*scope = kCdScopeGlobal; // (-1, -1)
}
break;
case 3:
if (argv[kBufArg] >= 0) {
*scope = kCdScopeBuffer; // (-1, -1, X)
} else {
*scope = kCdScopeGlobal; // (..., ..., -1)
}
break;
}
if (*scope == kCdScopeBuffer) {
if (argv[kWinArg] >= 0 || argv[kTabArg] >= 0) {
emsg(_("E5006: Window and tab scope must be -1 when using buffer scope"));
return false;
}
if (argv[kBufArg] > 0) {
Error err = ERROR_INIT;
*buf = find_buffer_by_handle(argv[kBufArg], &err);
if (ERROR_SET(&err)) {
emsg(_("E5007: Cannot find buffer number."));
xfree(err.msg);
return false;
}
}
}
// Find the tabpage by number.
if (argv[kTabArg] > 0) {
*tp = find_tabpage(argv[kTabArg]);
if (*tp == NULL) {
emsg(_("E5000: Cannot find tab number."));
return false;
}
}
// Find the window in `tp` by number.
if (argv[kWinArg] >= 0) {
if (argv[kTabArg] < 0) {
emsg(_("E5001: Higher scope cannot be -1 if lower scope is >= 0."));
return false;
}
if (argv[kWinArg] > 0) {
*win = find_win_by_nr(&argvars[0], *tp);
if (*win == NULL) {
emsg(_("E5002: Cannot find window number."));
return false;
}
}
// Window scope may fall through to the buffer shown in that window.
*buf = (*win)->w_buffer;
}
return true;
}
/// `getcwd([{win}[, {tab}[, {buf}]]])` function
///
/// Every scope not specified implies the currently selected scope object.
@@ -655,149 +766,36 @@ void f_fnamemodify(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
/// @post The return value will be a string.
void f_getcwd(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
// Possible scope of working directory to return.
CdScope scope = kCdScopeInvalid;
// Numbers of the scope objects (window, buffer, tab) we want the working
// directory of. A `-1` means to skip this scope, a `0` means the current object.
// getcwd() takes arguments in this order: (window, tab, buffer)
// Note that this is different from the order of CdScope
enum {
WINDOW_IDX = 0,
TABPAGE_IDX = 1,
BUFFER_IDX = 2,
};
int argv[] = { // arguments passed to getcwd().
0, // Number of window to look at.
0, // Number of tab to look at.
0, // Number of buffer to look at.
};
int argc = 0; // number of arguments passed to getcwd().
char *cwd = NULL; // Current working directory to print
char *from = NULL; // The original string to copy
tabpage_T *tp = curtab; // The tabpage to look at.
win_T *win = curwin; // The window to look at.
buf_T *buf = curbuf; // The buffer to look at.
CdScope scope;
win_T *win;
tabpage_T *tp;
buf_T *buf;
rettv->v_type = VAR_STRING;
rettv->vval.v_string = NULL;
// Pre-conditions
for (; argc < 3; argc++) {
// If there is no argument there are no more scopes after it, break out.
if (argvars[argc].v_type == VAR_UNKNOWN) {
break;
}
if (argvars[argc].v_type != VAR_NUMBER) {
emsg(_(e_invarg));
return;
}
argv[argc] = (int)argvars[argc].vval.v_number;
// It is an error for the scope number to be less than `-1`.
if (argv[argc] < -1) {
emsg(_(e_invarg));
return;
}
// Without arguments (kCdScopeInvalid), the effective working directory is returned.
if (!getcwd_scope_args(argvars, kCdScopeInvalid, &scope, &win, &tp, &buf)) {
return;
}
// Scope extraction
// Imagine X >= 0
switch (argc) {
case 0:
scope = kCdScopeInvalid; // getcwd()
break;
case 1:
if (argv[WINDOW_IDX] > -1) {
scope = kCdScopeWindow; // getcwd(X)
} else {
scope = kCdScopeTabpage; // getcwd(-1)
}
break;
case 2:
if (argv[WINDOW_IDX] > -1) {
scope = kCdScopeWindow; // getcwd(X, ...)
} else if (argv[TABPAGE_IDX] > -1) {
scope = kCdScopeTabpage; // getcwd(-1, X)
} else {
scope = kCdScopeGlobal; // getcwd(-1, -1)
}
break;
case 3:
if (argv[BUFFER_IDX] > -1) {
scope = kCdScopeBuffer; // getcwd(..., ..., X)
} else {
scope = kCdScopeGlobal; // getcwd(..., ..., -1)
}
break;
}
// getcwd(-1, -1, X)
if (scope == kCdScopeBuffer) {
if (argv[WINDOW_IDX] >= 0 || argv[TABPAGE_IDX] >= 0) {
emsg(_(e_getcwd_buffer_scope_requires_minus_one));
return;
}
if (argv[BUFFER_IDX] > 0) {
Error err = ERROR_INIT;
buf = find_buffer_by_handle(argv[BUFFER_IDX], &err);
if (ERROR_SET(&err)) {
emsg(_(e_cannot_find_buffer_number));
xfree(err.msg);
return;
}
}
}
// Find the tabpage by number
if (argv[TABPAGE_IDX] > 0) {
tp = find_tabpage(argv[TABPAGE_IDX]);
if (!tp) {
emsg(_("E5000: Cannot find tab number."));
return;
}
}
// Find the window in `tp` by number, `NULL` if none.
if (argv[WINDOW_IDX] >= 0) {
if (argv[TABPAGE_IDX] < 0) {
emsg(_("E5001: Higher scope cannot be -1 if lower scope is >= 0."));
return;
}
if (argv[WINDOW_IDX] > 0) {
win = find_win_by_nr(&argvars[0], tp);
if (!win) {
emsg(_("E5002: Cannot find window number."));
return;
}
}
// Window scope may fall through to the buffer shown in that window.
buf = win->w_buffer;
}
cwd = xmalloc(MAXPATHL);
char *from = NULL; // The local directory to copy.
char *cwd = xmalloc(MAXPATHL);
switch (scope) {
case kCdScopeWindow:
assert(win);
from = win->w_localdir;
if (from) {
break;
}
FALLTHROUGH;
case kCdScopeBuffer:
assert(buf);
from = buf->b_localdir;
if (from) {
break;
}
FALLTHROUGH;
case kCdScopeTabpage:
assert(tp);
from = tp->tp_localdir;
if (from) {
break;
@@ -1027,148 +1025,37 @@ void f_glob2regpat(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
/// `haslocaldir([{win}[, {tab}[, {buf}]]])` function
///
/// Returns `1` if the scope object has a local directory, `0` otherwise. If a
/// scope object is not specified the current one is implied. This function
/// share a lot of code with `f_getcwd`.
/// scope object is not specified the current one is implied.
///
/// @pre The arguments must be of type number.
/// @pre There may not be more than two arguments.
/// @pre There may not be more than three arguments.
/// @pre An argument may not be -1 if preceding arguments are not all -1.
///
/// @post The return value will be either the number `1` or `0`.
void f_haslocaldir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
// Possible scope of working directory to return.
CdScope scope = kCdScopeInvalid;
// Numbers of the scope objects (window, tab) we want the working directory
// of. A `-1` means to skip this scope, a `0` means the current object.
// haslocaldir() takes arguments in this order: (window, tab, buffer)
// Note that this is different from the order of CdScope
enum {
WINDOW_IDX = 0,
TABPAGE_IDX = 1,
BUFFER_IDX = 2,
};
int argv[] = { // arguments passed to haslocaldir().
0, // Number of window to look at.
0, // Number of tab to look at.
0, // Number of buffer to look at.
};
int argc = 0; // number of arguments passed to haslocaldir.
tabpage_T *tp = curtab; // The tabpage to look at.
win_T *win = curwin; // The window to look at.
buf_T *buf = curbuf; // The buffer to look at.
CdScope scope;
win_T *win;
tabpage_T *tp;
buf_T *buf;
rettv->v_type = VAR_NUMBER;
rettv->vval.v_number = 0;
// Pre-conditions
for (; argc < 3; argc++) {
if (argvars[argc].v_type == VAR_UNKNOWN) {
break;
}
if (argvars[argc].v_type != VAR_NUMBER) {
emsg(_(e_invarg));
return;
}
argv[argc] = (int)argvars[argc].vval.v_number;
if (argv[argc] < -1) {
emsg(_(e_invarg));
return;
}
}
// Scope extraction
// Imagine X >= 0
switch (argc) {
case 0:
// If the user didn't specify anything, default to window scope
scope = kCdScopeWindow; // haslocaldir()
break;
case 1:
if (argv[0] > -1) {
scope = kCdScopeWindow; // haslocaldir(X)
} else {
scope = kCdScopeTabpage; // haslocaldir(-1)
}
break;
case 2:
if (argv[0] > -1) {
scope = kCdScopeWindow; // haslocaldir(X, ...)
} else if (argv[1] > -1) {
scope = kCdScopeTabpage; // haslocaldir(-1, X)
} else {
scope = kCdScopeGlobal; // haslocaldir(-1, -1)
}
break;
case 3:
if (argv[2] > -1) {
scope = kCdScopeBuffer; // haslocaldir(..., ..., X)
} else {
scope = kCdScopeGlobal; // haslocaldir(..., ..., -1)
}
break;
}
// haslocaldir(-1, -1, X)
if (scope == kCdScopeBuffer) {
if (argv[WINDOW_IDX] >= 0 || argv[TABPAGE_IDX] >= 0) {
emsg(_(e_getcwd_buffer_scope_requires_minus_one));
return;
}
if (argv[BUFFER_IDX] > 0) {
Error err = ERROR_INIT;
buf = find_buffer_by_handle(argv[BUFFER_IDX], &err);
if (ERROR_SET(&err)) {
emsg(_(e_cannot_find_buffer_number));
xfree(err.msg);
return;
}
}
}
// Find the tabpage by number
if (argv[TABPAGE_IDX] >= 0) {
if (argv[TABPAGE_IDX] > 0) {
tp = find_tabpage(argv[TABPAGE_IDX]);
if (!tp) {
emsg(_("E5000: Cannot find tab number."));
return;
}
}
}
// Find the window in `tp` by number, `NULL` if none.
if (argv[WINDOW_IDX] >= 0) {
if (argv[TABPAGE_IDX] < 0) {
emsg(_("E5001: Higher scope cannot be -1 if lower scope is >= 0."));
return;
}
if (argv[WINDOW_IDX] > 0) {
win = find_win_by_nr(&argvars[0], tp);
if (!win) {
emsg(_("E5002: Cannot find window number."));
return;
}
}
// Without arguments, default to window scope.
if (!getcwd_scope_args(argvars, kCdScopeWindow, &scope, &win, &tp, &buf)) {
return;
}
switch (scope) {
case kCdScopeWindow:
assert(win);
rettv->vval.v_number = win->w_localdir ? 1 : 0;
rettv->vval.v_number = win->w_localdir != NULL;
break;
case kCdScopeBuffer:
assert(buf);
rettv->vval.v_number = buf->b_localdir ? 1 : 0;
rettv->vval.v_number = buf->b_localdir != NULL;
break;
case kCdScopeTabpage:
assert(tp);
rettv->vval.v_number = tp->tp_localdir ? 1 : 0;
rettv->vval.v_number = tp->tp_localdir != NULL;
break;
case kCdScopeGlobal:
// The global scope never has a local directory

View File

@@ -6205,21 +6205,18 @@ void free_cd_dir(void)
#endif
/// Get the previous directory for the given chdir scope.
static char *get_prevdir(CdScope scope)
/// Gets the previous-directory slot for the given chdir scope.
static char **get_prevdir(CdScope scope)
{
switch (scope) {
case kCdScopeTabpage:
return curtab->tp_prevdir;
break;
return &curtab->tp_prevdir;
case kCdScopeBuffer:
return curbuf->b_prevdir;
break;
return &curbuf->b_prevdir;
case kCdScopeWindow:
return curwin->w_prevdir;
break;
return &curwin->w_prevdir;
default:
return prev_dir;
return &prev_dir;
}
}
@@ -6241,7 +6238,7 @@ static void post_chdir(CdScope scope, bool trigger_dirchanged)
}
if (scope < kCdScopeGlobal) {
char *pdir = get_prevdir(scope);
char *pdir = *get_prevdir(scope);
// If still in global directory, set CWD as the global directory.
if (globaldir == NULL && pdir != NULL) {
globaldir = xstrdup(pdir);
@@ -6291,7 +6288,7 @@ bool changedir_func(char *new_dir, CdScope scope)
char *pdir = NULL;
// ":cd -": Change to previous directory
if (strcmp(new_dir, "-") == 0) {
pdir = get_prevdir(scope);
pdir = *get_prevdir(scope);
if (pdir == NULL) {
emsg(_("E186: No previous directory"));
return false;
@@ -6327,20 +6324,7 @@ bool changedir_func(char *new_dir, CdScope scope)
}
xfree(new_dir);
char **pp;
switch (scope) {
case kCdScopeTabpage:
pp = &curtab->tp_prevdir;
break;
case kCdScopeWindow:
pp = &curwin->w_prevdir;
break;
case kCdScopeBuffer:
pp = &curbuf->b_prevdir;
break;
default:
pp = &prev_dir;
}
char **pp = get_prevdir(scope);
xfree(*pp);
*pp = pdir;

View File

@@ -675,6 +675,31 @@ static int makeopens(FILE *fd, char *dirnow)
return FAIL;
}
// Restore buffer-local CWD (:bcd) via one-shot BufEnter handlers: there is no reliable point
// during session-load where all buffers are entered. Each buffer's directory is applied when the
// buffer is first entered, which may happen after session-load for buffers that stay hidden.
if (ssop_flags & kOptSsopFlagCurdir) {
FOR_ALL_BUFFERS(buf) {
if (buf->b_localdir == NULL || buf->b_fname == NULL || !buf->b_p_bl
|| (only_save_windows && buf->b_nwindows == 0)
|| (buf->b_help && !(ssop_flags & kOptSsopFlagHelp))
|| (bt_terminal(buf) && !(ssop_flags & kOptSsopFlagTerminal))) {
continue;
}
// `bufadd()` finds the buffer by exact (literal) name.
if (fputs("lua vim.api.nvim_create_autocmd('BufEnter', { once = true, "
"buffer = vim.fn.bufadd([==[", fd) < 0
|| fputs(ses_get_fname(buf, &ssop_flags), fd) < 0
|| fputs("]==]), callback = function() vim.cmd.bcd({ [==[", fd) < 0
|| fputs(buf->b_localdir, fd) < 0
|| fputs("]==], magic = { file = false, bar = false } }) end })\n", fd) < 0) {
return FAIL;
}
// Cwd may change when a handler fires: filenames must be written absolute from here on.
did_lcd = true;
}
}
if (ssop_flags & kOptSsopFlagResize) {
// Note: after the restore we still check it worked!
if (fprintf(fd, "set lines=%" PRId64 " columns=%" PRId64 "\n",
@@ -739,6 +764,23 @@ static int makeopens(FILE *fd, char *dirnow)
tab_topframe = topframe;
}
// Restore the tab-local working directory while the tab still shows its empty "placeholder"
// buffer: ":tcd" clears the b_localdir of the current buffer, so emitting it before any file is
// loaded keeps it away from directories restored by the ":bcd" handlers above. Do this before
// the windows and buffers, so the win-local / buf-local dir can override the tab-local dir.
if ((ssop_flags & kOptSsopFlagCurdir) && tp->tp_localdir != NULL) {
if (need_tabnext && put_line(fd, "tabnext") == FAIL) {
return FAIL;
}
need_tabnext = false;
if (fputs("tcd ", fd) < 0
|| ses_put_fname(fd, tp->tp_localdir, &ssop_flags) == FAIL
|| put_eol(fd) == FAIL) {
return FAIL;
}
did_lcd = true;
}
// Before creating the window layout, try loading one file. If this
// is aborted we don't end up with a number of useless windows.
// This may have side effects! (e.g., compressed or network file).
@@ -822,43 +864,6 @@ static int makeopens(FILE *fd, char *dirnow)
return FAIL;
}
// Restore the tab-local working directory if specified. Do this before the buffers and windows,
// so the buf-local / win-local directory can override the tab-local directory.
if ((ssop_flags & kOptSsopFlagCurdir) && tp->tp_localdir != NULL) {
if (fputs("tcd ", fd) < 0
|| ses_put_fname(fd, tp->tp_localdir, &ssop_flags) == FAIL
|| put_eol(fd) == FAIL) {
return FAIL;
}
did_lcd = true;
}
// Restore buffer-local CWD (:bcd) via a one-shot BufEnter handler: there is no reliable point
// during session-load where all buffers are entered. Re-applied per tab, AFTER the ":tcd"
// above: ":tcd" clears the b_localdir of the then-current buffer, which may be a buffer whose
// autocmd already fired.
if (ssop_flags & kOptSsopFlagCurdir) {
FOR_ALL_BUFFERS(buf) {
if (buf->b_localdir == NULL || buf->b_fname == NULL || !buf->b_p_bl
|| (only_save_windows && buf->b_nwindows == 0)
|| (buf->b_help && !(ssop_flags & kOptSsopFlagHelp))
|| (bt_terminal(buf) && !(ssop_flags & kOptSsopFlagTerminal))) {
continue;
}
// `bufadd()` finds the buffer by exact (literal) name.
if (fputs("lua vim.api.nvim_create_autocmd('BufEnter', { once = true, "
"buffer = vim.fn.bufadd([=[", fd) < 0
|| fputs(ses_get_fname(buf, &ssop_flags), fd) < 0
|| fputs("]=]), callback = function() vim.cmd.bcd({ [=[", fd) < 0
|| fputs(buf->b_localdir, fd) < 0
|| fputs("]=], magic = { file = false, bar = false } }) end })\n", fd) < 0) {
return FAIL;
}
// Cwd may change when the autocmd fires: filenames must be written absolute from here on.
did_lcd = true;
}
}
// Restore the view of the window (options, file, cursor, etc.).
for (win_T *wp = tab_firstwin; wp != NULL; wp = wp->w_next) {
if (!ses_do_win(wp)) {

View File

@@ -5355,12 +5355,7 @@ static void win_enter_ext(win_T *const wp, const int flags)
/// Used after making another window or buffer the current one: change directory if needed.
void fix_current_dir(bool caused_by_win)
{
CdCause cause;
if (caused_by_win) {
cause = kCdCauseWindow;
} else {
cause = kCdCauseBuffer;
}
CdCause cause = caused_by_win ? kCdCauseWindow : kCdCauseBuffer;
// New directory is either the local directory of the window, buffer, tab or NULL.
char *new_dir;

View File

@@ -24,6 +24,10 @@ local directories = {
local tmpfile = 'Xtest-functional-ex_cmds-cd_spec-tmpfile'
local function join(...)
return table.concat({ ... }, pathsep)
end
-- Shorthand writing to get the current working directory
local cwd = function(...)
return call('getcwd', ...)
@@ -53,22 +57,25 @@ local tlwd = function()
end -- tab dir
--local glwd = function() return eval('haslocaldir(-1, -1)') end -- global dir
local function before_test()
clear()
for _, d in pairs(directories) do
mkdir(d)
end
directories.start = cwd()
end
local function remove_dirs()
for _, d in pairs(directories) do
vim.uv.fs_rmdir(d)
end
end
-- Test both the `cd` and `chdir` variants
for _, cmd in ipairs { 'cd', 'chdir' } do
describe(':' .. cmd, function()
before_each(function()
clear()
for _, d in pairs(directories) do
mkdir(d)
end
directories.start = cwd()
end)
after_each(function()
for _, d in pairs(directories) do
vim.uv.fs_rmdir(d)
end
end)
before_each(before_test)
after_each(remove_dirs)
describe('using explicit scope', function()
it('for window', function()
@@ -156,7 +163,15 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
-- Change buffer-local directory to subdirectory
command('bcd ' .. directories.buffer)
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
eq(join(globalDir, directories.buffer), cwd())
eq(1, blwd())
-- ":bcd -" changes to the previous directory, still buffer-scoped.
command('bcd -')
eq(globalDir, cwd())
eq(1, blwd())
command('bcd -')
eq(join(globalDir, directories.buffer), cwd())
eq(1, blwd())
-- Verify Other buffer is unchanged
@@ -169,7 +184,7 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
command('b# ')
command(('e %s3'):format(tmpfile))
eq(1, blwd())
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
eq(join(globalDir, directories.buffer), cwd())
command(('e ..%s%s1'):format(pathsep, tmpfile))
eq(0, blwd())
eq(globalDir, cwd())
@@ -179,7 +194,7 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
command(('split %s%s%s2'):format(directories.buffer, pathsep, tmpfile))
command('wincmd p')
eq(globalDir, cwd())
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd(1))
eq(join(globalDir, directories.buffer), cwd(1))
eq(globalDir, cwd(2))
end)
end)
@@ -315,7 +330,7 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
-- Change tab-local working directory and verify it is different
command(('silent t%s %s'):format(cmd, directories.tab))
eq(('%s%s%s'):format(globalDir, pathsep, directories.tab), cwd())
eq(join(globalDir, directories.tab), cwd())
eq(cwd(), tcwd()) -- Working directory matches tab directory
eq(1, tlwd())
eq(cwd(), bcwd()) -- Still no buffer-directory
@@ -323,8 +338,8 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
-- Change buffer 2's buffer-local directory
command(('silent b%s ..%s%s'):format(cmd, pathsep, directories.buffer))
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
eq(('%s%s%s'):format(globalDir, pathsep, directories.tab), tcwd())
eq(join(globalDir, directories.buffer), cwd())
eq(join(globalDir, directories.tab), tcwd())
eq(cwd(), bcwd()) -- Has no buffer-directory
eq(1, blwd())
@@ -334,7 +349,7 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
-- Verify buffer 2 has buffer-local directory even in first tab
command(('b %s2'):format(tmpfile)) -- Switch to buffer 2
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), bcwd())
eq(join(globalDir, directories.buffer), bcwd())
eq(0, tlwd()) -- Still no tab-local directory
-- Verify buffer 1 did not have its directory changed
@@ -357,7 +372,7 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
-- Change buffer-local working directory and verify it is different
command(('silent b%s %s'):format(cmd, directories.buffer))
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
eq(join(globalDir, directories.buffer), cwd())
eq(cwd(), bcwd()) -- Working directory matches buffer directory
eq(1, blwd())
eq(cwd(), wcwd()) -- Still no window-directory
@@ -365,68 +380,52 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
-- Change window-local directory to test `:lcd`
command(('silent l%s ../%s'):format(cmd, directories.window))
eq(('%s%s%s'):format(globalDir, pathsep, directories.window), cwd())
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), bcwd())
eq(join(globalDir, directories.window), cwd())
eq(join(globalDir, directories.buffer), bcwd())
eq(1, blwd())
-- Verify buffer has buffer-local directory in original window
command('wincmd w')
command('b ' .. tmpfile)
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
eq(join(globalDir, directories.buffer), cwd())
-- Verify going to second window uses window-local directory
command('wincmd w')
eq(('%s%s%s'):format(globalDir, pathsep, directories.window), cwd())
eq(join(globalDir, directories.window), cwd())
end)
end)
end
for _, cmd in ipairs { 'bcd', 'bchdir' } do
describe(':' .. cmd, function()
before_each(function()
clear()
for _, d in pairs(directories) do
mkdir(d)
end
directories.start = cwd()
end)
after_each(function()
for _, d in pairs(directories) do
vim.uv.fs_rmdir(d)
end
end)
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('makes :new use the buffer-local directory', function()
local globalDir = directories.start
it('makes :new/:vnew/:enew use the buffer-local directory', function()
local bufdir = join(directories.start, directories.buffer)
command(('%s %s'):format(cmd, directories.buffer))
command(':new')
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
eq(bufdir, cwd())
command('wincmd x') -- close :new window
command(':vnew')
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
eq(bufdir, cwd())
command('wincmd x') -- close :vnew window
command(':enew')
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
end)
it('makes :enew use the buffer-local directory in a split', function()
local globalDir = directories.start
command(('%s %s'):format(cmd, directories.buffer))
eq(bufdir, cwd())
-- Also in a split.
command(':vsplit')
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
eq(bufdir, cwd())
command(':enew')
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
eq(bufdir, cwd())
end)
end)
end
@@ -438,46 +437,34 @@ for _, cmd in ipairs { 'getcwd', 'haslocaldir' } do
clear()
end)
-- Test invalid argument types
local err474 = 'Vim(call):E474: Invalid argument'
it('fails on string', function()
eq(err474, pcall_err(command, 'call ' .. cmd .. '("some string")'))
end)
it('fails on float', function()
eq(err474, pcall_err(command, 'call ' .. cmd .. '(1.0)'))
end)
it('fails on list', function()
eq(err474, pcall_err(command, 'call ' .. cmd .. '([1, 2])'))
end)
it('fails on dictionary', function()
eq(err474, pcall_err(command, 'call ' .. cmd .. '({"key": "value"})'))
end)
it('fails on funcref', function()
eq(err474, pcall_err(command, 'call ' .. cmd .. '(function("tr"))'))
end)
it('validation', function()
local err474 = 'Vim(call):E474: Invalid argument'
eq(err474, pcall_err(command, ('call %s("some string")'):format(cmd)))
eq(err474, pcall_err(command, ('call %s(1.0)'):format(cmd)))
eq(err474, pcall_err(command, ('call %s([1, 2])'):format(cmd)))
eq(err474, pcall_err(command, ('call %s({"key": "value"})'):format(cmd)))
eq(err474, pcall_err(command, ('call %s(function("tr"))'):format(cmd)))
eq(err474, pcall_err(command, ('call %s(-2)'):format(cmd)))
-- Test invalid numbers
it('fails on number less than -1', function()
eq(err474, pcall_err(command, 'call ' .. cmd .. '(-2)'))
end)
local err5001 = 'Vim(call):E5001: Higher scope cannot be -1 if lower scope is >= 0.'
local err5006 = 'Vim(call):E5006: Window and tab scope must be -1 when using buffer scope'
local err5007 = 'Vim(call):E5007: Cannot find buffer number.'
it('fails on -1 if previous arg is >=0', function()
eq(err5001, pcall_err(command, 'call ' .. cmd .. '(0, -1)'))
end)
it('fails when 1st and 2nd args != -1 when 3rd arg > -1', function()
-- -1 preceded by an argument >= 0
eq(
'Vim(call):E5001: Higher scope cannot be -1 if lower scope is >= 0.',
pcall_err(command, ('call %s(0, -1)'):format(cmd))
)
-- Buffer scope requires window and tab arguments of -1.
local err5006 = 'Vim(call):E5006: Window and tab scope must be -1 when using buffer scope'
eq(err5006, pcall_err(command, ('call %s(0, 0, 0)'):format(cmd)))
eq(err5006, pcall_err(command, ('call %s(1, 2, 3)'):format(cmd)))
end)
it('fails when passing a bufnr that does not exist', function()
eq(err5007, pcall_err(command, ('call %s(-1, -1, 99999)'):format(cmd)))
end)
-- Test wrong number of arguments
local err118 = 'Vim(call):E118: Too many arguments for function: ' .. cmd
it('fails to parse more than one argument', function()
eq(err118, pcall_err(command, ('call %s(0, 0, 0, 0)'):format(cmd)))
-- Nonexistent buffer.
eq(
'Vim(call):E5007: Cannot find buffer number.',
pcall_err(command, ('call %s(-1, -1, 99999)'):format(cmd))
)
-- Too many arguments.
eq(
('Vim(call):E118: Too many arguments for function: %s'):format(cmd),
pcall_err(command, ('call %s(0, 0, 0, 0)'):format(cmd))
)
end)
end)
end

View File

@@ -418,6 +418,9 @@ describe(':mksession', function()
clear()
command('source ' .. session_file)
-- The ":tcd" during restore must not clear the buffer-local directory of the tab's
-- first-loaded buffer: it must be set before that tab is ever (re)visited.
eq(1, fn.haslocaldir(-1, -1, fn.bufnr(('%s2'):format(tmpfile_base))))
-- First tab should have the original working directory.
command('tabnext 1')
eq(cwd_dir, fn.getcwd())