From db2e86fba4f732dab43f70dd7f6d0a089ea9e98c Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 4 Aug 2026 15:16:17 +0200 Subject: [PATCH] refactor(editor): cleanup change-directory (:bcd) logic --- runtime/doc/editing.txt | 15 +- runtime/doc/vim_diff.txt | 2 +- runtime/doc/vimfn.txt | 4 +- runtime/lua/vim/_meta/vimfn.gen.lua | 1 - scripts/linterrcodes.lua | 3 - src/nvim/errors.h | 2 - src/nvim/eval.lua | 2 +- src/nvim/eval/fs.c | 377 ++++++++------------- src/nvim/ex_docmd.c | 34 +- src/nvim/ex_session.c | 79 +++-- src/nvim/window.c | 7 +- test/functional/ex_cmds/cd_spec.lua | 163 ++++----- test/functional/ex_cmds/mksession_spec.lua | 3 + 13 files changed, 274 insertions(+), 418 deletions(-) diff --git a/runtime/doc/editing.txt b/runtime/doc/editing.txt index cf2e6e96ec..04f88d8058 100644 --- a/runtime/doc/editing.txt +++ b/runtime/doc/editing.txt @@ -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* diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index 7570978367..698be950d0 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -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 diff --git a/runtime/doc/vimfn.txt b/runtime/doc/vimfn.txt index f47ef1e82d..a7e840d093 100644 --- a/runtime/doc/vimfn.txt +++ b/runtime/doc/vimfn.txt @@ -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?`) diff --git a/runtime/lua/vim/_meta/vimfn.gen.lua b/runtime/lua/vim/_meta/vimfn.gen.lua index 8eb79696ea..6112415d09 100644 --- a/runtime/lua/vim/_meta/vimfn.gen.lua +++ b/runtime/lua/vim/_meta/vimfn.gen.lua @@ -3350,7 +3350,6 @@ function vim.fn.getcursorcharpos(winid) end --- getcwd(-1, -1, -1) " Get global directory --- getcwd(-1, -1) " Get global directory --- : 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")); diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index 8ed0e37f67..9384b03933 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -4125,12 +4125,12 @@ M.funcs = { getcwd(-1, -1, -1) " Get global directory getcwd(-1, -1) " Get global directory = 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 diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 8f9fa7fcfb..71412d548d 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -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; diff --git a/src/nvim/ex_session.c b/src/nvim/ex_session.c index 97ce4a9e37..4320e431b0 100644 --- a/src/nvim/ex_session.c +++ b/src/nvim/ex_session.c @@ -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)) { diff --git a/src/nvim/window.c b/src/nvim/window.c index d9b01af856..a4cdf68784 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -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; diff --git a/test/functional/ex_cmds/cd_spec.lua b/test/functional/ex_cmds/cd_spec.lua index 2881a4634c..2057866d09 100644 --- a/test/functional/ex_cmds/cd_spec.lua +++ b/test/functional/ex_cmds/cd_spec.lua @@ -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 diff --git a/test/functional/ex_cmds/mksession_spec.lua b/test/functional/ex_cmds/mksession_spec.lua index 866950bf49..2c115f605e 100644 --- a/test/functional/ex_cmds/mksession_spec.lua +++ b/test/functional/ex_cmds/mksession_spec.lua @@ -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())