diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt index f73661ec4e..02c4ce80da 100644 --- a/runtime/doc/autocmd.txt +++ b/runtime/doc/autocmd.txt @@ -615,10 +615,11 @@ DiffUpdated After diffs have been updated. Depending on *DirChanged* DirChanged After the |current-directory| was changed. The pattern can be: - "window" to trigger on `:lcd` - "tabpage" to trigger on `:tcd` - "global" to trigger on `:cd` "auto" to trigger on 'autochdir'. + "buffer" to trigger on `:bcd` + "global" to trigger on `:cd` + "tabpage" to trigger on `:tcd` + "window" to trigger on `:lcd` Sets these |v:event| keys: cwd: current working directory scope: "global", "tabpage", "window" @@ -633,7 +634,7 @@ DirChangedPre When the |current-directory| is going to be The pattern is like with |DirChanged|. Sets these |v:event| keys: directory: new working directory - scope: "global", "tabpage", "window" + scope: "global", "tabpage", "window", "buffer" changed_window: v:true if we fired the event switching window (or tab) is set to the new directory name. diff --git a/runtime/doc/editing.txt b/runtime/doc/editing.txt index 6df3e0a588..cf2e6e96ec 100644 --- a/runtime/doc/editing.txt +++ b/runtime/doc/editing.txt @@ -1387,10 +1387,11 @@ scope in the hierarchy applies. *:tc* *:tcd* :tc[d][!] {path} Like |:cd|, but only set the directory for the current - tab. The current window will also use this directory. - The current directory is not changed for windows in - other tabs and for windows in the current tab that - have their own window-local directory. + tab. The current buffer and window will also use this + directory. The current directory is not changed for + windows in other tabs and for windows in the current + tab that have their own window-local directory, or for + buffers that have their own buffer-local directory. *:tcd-* :tc[d][!] - Change to the previous current directory (before the @@ -1399,10 +1400,15 @@ scope in the hierarchy applies. *:tch* *:tchdir* :tch[dir][!] Same as |:tcd|. + *:bc* *:bcd* +:bc[d][!] {path} Like |:cd|, but only set the current directory for the + current buffer. The current directory for other + windows or buffers or tabs is not changed. + *:lc* *:lcd* :lc[d][!] {path} Like |:cd|, but only set the current directory for the current window. The current directory for other - windows or tabs is not changed. + windows, tabs, or buffers is not changed. *:lch* *:lchdir* :lch[dir][!] Same as |:lcd|. diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 47355b4c5c..18c5b7e628 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -247,6 +247,8 @@ DIAGNOSTICS EDITOR +• |:bcd| sets a buffer-local directory. |getcwd()| and |haslocaldir()| take + `bufnr` as a third parameter. • |:command-completion-customlist| can return a list of dictionaries with kind/menu/info/abbr for the popup menu. • |cmdwin| (|q:|, |q/|, |q?|, |c_CTRL-F|) is implemented as a "normal" diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index f77c8a03c6..7570978367 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -695,8 +695,8 @@ Vimscript compatibility: 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 or window - cannot be found. *E5000* *E5001* *E5002* +- |getcwd()| and |haslocaldir()| may throw errors if the tabpage, window, or + buffer cannot be found. *E5000* *E5001* *E5002* *E5006* *E5007* - |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 0f53a2592b..f47ef1e82d 100644 --- a/runtime/doc/vimfn.txt +++ b/runtime/doc/vimfn.txt @@ -1117,6 +1117,8 @@ chdir({dir} [, {scope}]) *chdir()* changed to the scope of the current directory: - If the window local directory (|:lcd|) is set, it changes the current working directory for that scope. + - If the buffer local directory (|:bcd|) is set, it + changes the current working directory for that scope. - Otherwise, if the tabpage local directory (|:tcd|) is set, it changes the current directory for that scope. - Otherwise, changes the global directory for that scope. @@ -1124,6 +1126,7 @@ chdir({dir} [, {scope}]) *chdir()* If {scope} is present, changes the current working directory for the specified scope: "window" Changes the window local directory. |:lcd| + "buffer" Changes the buffer local directory. |:bcd| "tabpage" Changes the tabpage local directory. |:tcd| "global" Changes the global directory. |:cd| @@ -3745,31 +3748,46 @@ getcursorcharpos([{winid}]) *getcursorcharpos()* Return: ~ (`any`) -getcwd([{winnr} [, {tabnr}]]) *getcwd()* +getcwd([{winnr} [, {tabnr} [, {bufnr}]]]) *getcwd()* Lua: Prefer |uv.cwd()| for the global working directory; tab-local and window-local scopes differ. With no arguments, returns the name of the effective - |current-directory|. With {winnr} or {tabnr} the working - directory of that scope is returned, and 'autochdir' is - ignored. Tabs and windows are identified by their respective - numbers, 0 means current tab or window. Missing tab number + |current-directory|. With {winnr} or {tabnr} or {bufnr} the + working directory of that scope is returned, and 'autochdir' + is ignored. + + Tabs, windows and buffers are identified by their respective + numbers, 0 means current tab or window or buffer. Missing tab + number implies 0. Thus the following are equivalent: >vim getcwd(0) getcwd(0, 0) < If {winnr} is -1 it is ignored, only the tab is resolved. {winnr} is a |window-number| or |window-ID|. - If both {winnr} and {tabnr} are -1 the global working - directory is returned. + If both {winnr} and {tabnr} are -1 and {bufnr} is missing the + global working directory is returned. Note: When {tabnr} is -1 Vim returns an empty string to signal that it is invalid, whereas Nvim returns either the global working directory if {winnr} is -1 or the working directory of the window indicated by {winnr}. - Throw error if the arguments are invalid. |E5000| |E5001| |E5002| + + If {bufnr} is provided, {winnr} and {tabnr} must be -1 and the + working directory of that buffer is returned. If {bufnr} is + -1, it is ignored, and the global working directory is + returned. + Examples of buffer usage: >vim + getcwd(-1, -1, 0) " Get current buffer's directory + getcwd(-1, -1, 3) " Get directory of buffer #3 + 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?`) • {tabnr} (`integer?`) + • {bufnr} (`integer?`) Return: ~ (`string`) @@ -4994,15 +5012,16 @@ has_key({dict}, {key}) *has_key()* Return: ~ (`0|1`) -haslocaldir([{winnr} [, {tabnr}]]) *haslocaldir()* - Checks whether the window or tabpage has set a local working - directory. Returns 1 when the window has set a local path - via |:lcd| or when {winnr} is -1 and the tabpage has set a - local path via |:tcd|, otherwise 0. +haslocaldir([{winnr} [, {tabnr} [, {bufnr} ]]]) *haslocaldir()* + Checks whether the window, tabpage or buffer has set a local + working directory. Returns 1 when the window has set a local + path via |:lcd|, or when {winnr} is -1 and the tabpage has set + a local path via |:tcd|, or when {winnr} and {tabnr} are -1 + and {bufnr} has set a local path via |:bcd|, otherwise 0. - Tabs and windows are identified by their respective numbers, - 0 means current tab or window. Missing argument implies 0. - Thus the following are equivalent: >vim + Tabs, windows and buffers are identified by their respective + numbers, 0 means current tab or window. Missing argument + implies 0. Thus the following are equivalent: >vim echo haslocaldir() echo haslocaldir(0) echo haslocaldir(0, 0) @@ -5010,11 +5029,18 @@ haslocaldir([{winnr} [, {tabnr}]]) *haslocaldir()* With {winnr} and {tabnr} use the window in that tabpage. {winnr} is a |window-number| or |window-ID|. If {winnr} is -1 it is ignored, only the tab is resolved. - Throw error if the arguments are invalid. |E5000| |E5001| |E5002| + If {bufnr} is provided, {winnr} and {tabnr} must be -1 and + only the buffer is resolved. + Examples of buffer usage: >vim + haslocaldir(-1, -1, 0) " Current buf has a local directory? + haslocaldir(-1, -1, 3) " Buf #3 has a local directory? +< Throw error if the arguments are invalid. + |E5000| |E5001| |E5002| |E5006| |E5007| Parameters: ~ • {winnr} (`integer?`) • {tabnr} (`integer?`) + • {bufnr} (`integer?`) Return: ~ (`0|1`) diff --git a/runtime/lua/vim/_meta/vimfn.gen.lua b/runtime/lua/vim/_meta/vimfn.gen.lua index 092c495939..8eb79696ea 100644 --- a/runtime/lua/vim/_meta/vimfn.gen.lua +++ b/runtime/lua/vim/_meta/vimfn.gen.lua @@ -952,6 +952,8 @@ function vim.fn.charidx(string, idx, countcc, utf16) end --- changed to the scope of the current directory: --- - If the window local directory (|:lcd|) is set, it --- changes the current working directory for that scope. +--- - If the buffer local directory (|:bcd|) is set, it +--- changes the current working directory for that scope. --- - Otherwise, if the tabpage local directory (|:tcd|) is --- set, it changes the current directory for that scope. --- - Otherwise, changes the global directory for that scope. @@ -959,6 +961,7 @@ function vim.fn.charidx(string, idx, countcc, utf16) end --- If {scope} is present, changes the current working directory --- for the specified scope: --- "window" Changes the window local directory. |:lcd| +--- "buffer" Changes the buffer local directory. |:bcd| --- "tabpage" Changes the tabpage local directory. |:tcd| --- "global" Changes the global directory. |:cd| --- @@ -3318,27 +3321,42 @@ function vim.fn.getcursorcharpos(winid) end --- Lua: Prefer |uv.cwd()| for the global working directory; tab-local and window-local scopes differ. --- --- With no arguments, returns the name of the effective ---- |current-directory|. With {winnr} or {tabnr} the working ---- directory of that scope is returned, and 'autochdir' is ---- ignored. Tabs and windows are identified by their respective ---- numbers, 0 means current tab or window. Missing tab number +--- |current-directory|. With {winnr} or {tabnr} or {bufnr} the +--- working directory of that scope is returned, and 'autochdir' +--- is ignored. +--- +--- Tabs, windows and buffers are identified by their respective +--- numbers, 0 means current tab or window or buffer. Missing tab +--- number --- implies 0. Thus the following are equivalent: >vim --- getcwd(0) --- getcwd(0, 0) --- vim +--- getcwd(-1, -1, 0) " Get current buffer's directory +--- getcwd(-1, -1, 3) " Get directory of buffer #3 +--- getcwd(-1, -1, -1) " Get global directory +--- getcwd(-1, -1) " Get global directory +--- vim +--- Tabs, windows and buffers are identified by their respective +--- numbers, 0 means current tab or window. Missing argument +--- implies 0. Thus the following are equivalent: >vim --- echo haslocaldir() --- echo haslocaldir(0) --- echo haslocaldir(0, 0) @@ -4460,12 +4479,19 @@ function vim.fn.has_key(dict, key) end --- With {winnr} and {tabnr} use the window in that tabpage. --- {winnr} is a |window-number| or |window-ID|. --- If {winnr} is -1 it is ignored, only the tab is resolved. ---- Throw error if the arguments are invalid. |E5000| |E5001| |E5002| +--- If {bufnr} is provided, {winnr} and {tabnr} must be -1 and +--- only the buffer is resolved. +--- Examples of buffer usage: >vim +--- haslocaldir(-1, -1, 0) " Current buf has a local directory? +--- haslocaldir(-1, -1, 3) " Buf #3 has a local directory? +--- b_s); // reset syntax info buf->b_flags &= ~BF_READERR; // a read error is no longer relevant + + XFREE_CLEAR(buf->b_localdir); + XFREE_CLEAR(buf->b_prevdir); return true; } @@ -1023,6 +1026,8 @@ static void free_buffer_stuff(buf_T *buf, int free_flags) map_clear_mode(buf, MAP_ALL_MODES, true, false); // clear local mappings map_clear_mode(buf, MAP_ALL_MODES, true, true); // clear local abbrevs XFREE_CLEAR(buf->b_start_fenc); + XFREE_CLEAR(buf->b_localdir); + XFREE_CLEAR(buf->b_prevdir); buf_free_callbacks(buf); } @@ -1771,6 +1776,9 @@ void set_curbuf(buf_T *buf, int action, bool update_jumplist) if (bufref_valid(&prevbufref) && prevbuf->terminal != NULL) { terminal_check_size(prevbuf->terminal); } + + // Maybe cd to buffer-local directory + fix_current_dir(false); } /// Enter a new current buffer. @@ -2023,6 +2031,8 @@ buf_T *buflist_new(char *ffname_arg, char *sfname_arg, linenr_T lnum, int flags) trigger_undo_ftplugin(buf, curwin); // It's like this buffer is deleted. Watch out for autocommands that // change curbuf! If that happens, allocate a new buffer anyway. + // We also ask it to not free the buffer-local directory so we can reuse + // it. buf_freeall(buf, BFA_WIPE | BFA_DEL); if (aborting()) { // autocmds may abort script processing xfree(ffname); @@ -2152,6 +2162,8 @@ bool curbuf_reusable(void) && curbuf->b_ffname == NULL && curbuf->b_nwindows <= 1 && !curbuf->terminal + && curbuf->b_localdir == NULL + && curbuf->b_prevdir == NULL && (curbuf->b_ml.ml_mfp == NULL || buf_is_empty(curbuf)) && !bt_quickfix(curbuf) && !curbufIsChanged()); diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index a20e3f358a..2699e1d97b 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -767,6 +767,9 @@ struct file_buffer { // The number for times the current line has been flushed in the memline. int flush_count; + + char *b_localdir; ///< Absolute path of local cwd or NULL. + char *b_prevdir; ///< Previous directory. }; // Stuff for diff mode. diff --git a/src/nvim/context.c b/src/nvim/context.c index 19d3712847..769b4b31f5 100644 --- a/src/nvim/context.c +++ b/src/nvim/context.c @@ -256,6 +256,7 @@ static void ctx_cwd_save(CtxSwitch *cs, win_T *wp, tabpage_T *tp) 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); @@ -342,9 +343,12 @@ 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, 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. + // 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; @@ -577,9 +581,15 @@ void ctx_restore(CtxSwitch *cs) 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 tp_localdir and globaldir. + // directory before restoring b_localdir, tp_localdir and globaldir. if (cwp->w_localdir != NULL) { - win_fix_current_dir(); + fix_current_dir(true); + } + 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; diff --git a/src/nvim/context_defs.h b/src/nvim/context_defs.h index 1248626936..1bf2fd75da 100644 --- a/src/nvim/context_defs.h +++ b/src/nvim/context_defs.h @@ -82,6 +82,7 @@ typedef struct { 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) diff --git a/src/nvim/errors.h b/src/nvim/errors.h index 48d02ebc61..3166029b48 100644 --- a/src/nvim/errors.h +++ b/src/nvim/errors.h @@ -20,6 +20,8 @@ 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: : 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 8834682e52..8ed0e37f67 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -1257,6 +1257,8 @@ M.funcs = { changed to the scope of the current directory: - If the window local directory (|:lcd|) is set, it changes the current working directory for that scope. + - If the buffer local directory (|:bcd|) is set, it + changes the current working directory for that scope. - Otherwise, if the tabpage local directory (|:tcd|) is set, it changes the current directory for that scope. - Otherwise, changes the global directory for that scope. @@ -1264,6 +1266,7 @@ M.funcs = { If {scope} is present, changes the current working directory for the specified scope: "window" Changes the window local directory. |:lcd| + "buffer" Changes the buffer local directory. |:bcd| "tabpage" Changes the tabpage local directory. |:tcd| "global" Changes the global directory. |:cd| @@ -4089,32 +4092,45 @@ M.funcs = { signature = 'getcursorcharpos([{winid}])', }, getcwd = { - args = { 0, 2 }, + args = { 0, 3 }, base = 1, desc = [=[ With no arguments, returns the name of the effective - |current-directory|. With {winnr} or {tabnr} the working - directory of that scope is returned, and 'autochdir' is - ignored. Tabs and windows are identified by their respective - numbers, 0 means current tab or window. Missing tab number + |current-directory|. With {winnr} or {tabnr} or {bufnr} the + working directory of that scope is returned, and 'autochdir' + is ignored. + + Tabs, windows and buffers are identified by their respective + numbers, 0 means current tab or window or buffer. Missing tab + number implies 0. Thus the following are equivalent: >vim getcwd(0) getcwd(0, 0) vim + getcwd(-1, -1, 0) " Get current buffer's directory + getcwd(-1, -1, 3) " Get directory of buffer #3 + getcwd(-1, -1, -1) " Get global directory + getcwd(-1, -1) " Get global directory + vim + Tabs, windows and buffers are identified by their respective + numbers, 0 means current tab or window. Missing argument + implies 0. Thus the following are equivalent: >vim echo haslocaldir() echo haslocaldir(0) echo haslocaldir(0, 0) @@ -5428,13 +5445,19 @@ M.funcs = { With {winnr} and {tabnr} use the window in that tabpage. {winnr} is a |window-number| or |window-ID|. If {winnr} is -1 it is ignored, only the tab is resolved. - Throw error if the arguments are invalid. |E5000| |E5001| |E5002| + If {bufnr} is provided, {winnr} and {tabnr} must be -1 and + only the buffer is resolved. + Examples of buffer usage: >vim + haslocaldir(-1, -1, 0) " Current buf has a local directory? + haslocaldir(-1, -1, 3) " Buf #3 has a local directory? + #include "auto/config.h" +#include "nvim/api/private/helpers.h" #include "nvim/ascii_defs.h" #include "nvim/buffer_defs.h" #include "nvim/cmdexpand.h" @@ -419,6 +420,8 @@ void f_chdir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) } } else if (curwin->w_localdir != NULL) { scope = kCdScopeWindow; + } else if (curbuf->b_localdir != NULL) { + scope = kCdScopeBuffer; } else if (curtab->tp_localdir != NULL) { scope = kCdScopeTabpage; } @@ -641,12 +644,12 @@ void f_fnamemodify(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) xfree(fbuf); } -/// `getcwd([{win}[, {tab}]])` function +/// `getcwd([{win}[, {tab}[, {buf}]]])` function /// /// Every scope not specified implies the currently selected scope object. /// /// @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 a string. @@ -655,50 +658,103 @@ 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, tab) we want the working directory - // of. A `-1` means to skip this scope, a `0` means the current object. - int scope_number[] = { - [kCdScopeWindow] = 0, // Number of window to look at. - [kCdScopeTabpage] = 0, // Number of tab to look at. + // 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. rettv->v_type = VAR_STRING; rettv->vval.v_string = NULL; - // Pre-conditions and scope extraction together - for (int i = MIN_CD_SCOPE; i < MAX_CD_SCOPE; i++) { + // Pre-conditions + for (; argc < 3; argc++) { // If there is no argument there are no more scopes after it, break out. - if (argvars[i].v_type == VAR_UNKNOWN) { + if (argvars[argc].v_type == VAR_UNKNOWN) { break; } - if (argvars[i].v_type != VAR_NUMBER) { + if (argvars[argc].v_type != VAR_NUMBER) { emsg(_(e_invarg)); return; } - scope_number[i] = (int)argvars[i].vval.v_number; + argv[argc] = (int)argvars[argc].vval.v_number; // It is an error for the scope number to be less than `-1`. - if (scope_number[i] < -1) { + if (argv[argc] < -1) { emsg(_(e_invarg)); return; } - // Use the narrowest scope the user requested - if (scope_number[i] >= 0 && scope == kCdScopeInvalid) { - // The scope is the current iteration step. - scope = i; - } else if (scope_number[i] < 0) { - scope = i + 1; + } + + // 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 (scope_number[kCdScopeTabpage] > 0) { - tp = find_tabpage(scope_number[kCdScopeTabpage]); + if (argv[TABPAGE_IDX] > 0) { + tp = find_tabpage(argv[TABPAGE_IDX]); if (!tp) { emsg(_("E5000: Cannot find tab number.")); return; @@ -706,19 +762,21 @@ void f_getcwd(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) } // Find the window in `tp` by number, `NULL` if none. - if (scope_number[kCdScopeWindow] >= 0) { - if (scope_number[kCdScopeTabpage] < 0) { + if (argv[WINDOW_IDX] >= 0) { + if (argv[TABPAGE_IDX] < 0) { emsg(_("E5001: Higher scope cannot be -1 if lower scope is >= 0.")); return; } - if (scope_number[kCdScopeWindow] > 0) { + 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); @@ -731,6 +789,13 @@ void f_getcwd(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) break; } FALLTHROUGH; + case kCdScopeBuffer: + assert(buf); + from = buf->b_localdir; + if (from) { + break; + } + FALLTHROUGH; case kCdScopeTabpage: assert(tp); from = tp->tp_localdir; @@ -959,7 +1024,7 @@ void f_glob2regpat(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) rettv->vval.v_string = pat == NULL ? NULL : file_pat_to_reg_pat(pat, NULL, NULL, false); } -/// `haslocaldir([{win}[, {tab}]])` function +/// `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 @@ -977,62 +1042,113 @@ void f_haslocaldir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) // 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. - int scope_number[] = { - [kCdScopeWindow] = 0, // Number of window to look at. - [kCdScopeTabpage] = 0, // Number of tab to look at. + + // 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. + win_T *win = curwin; // The window to look at. + buf_T *buf = curbuf; // The buffer to look at. rettv->v_type = VAR_NUMBER; rettv->vval.v_number = 0; - // Pre-conditions and scope extraction together - for (int i = MIN_CD_SCOPE; i < MAX_CD_SCOPE; i++) { - if (argvars[i].v_type == VAR_UNKNOWN) { + // Pre-conditions + for (; argc < 3; argc++) { + if (argvars[argc].v_type == VAR_UNKNOWN) { break; } - if (argvars[i].v_type != VAR_NUMBER) { + if (argvars[argc].v_type != VAR_NUMBER) { emsg(_(e_invarg)); return; } - scope_number[i] = (int)argvars[i].vval.v_number; - if (scope_number[i] < -1) { + argv[argc] = (int)argvars[argc].vval.v_number; + if (argv[argc] < -1) { emsg(_(e_invarg)); return; } - // Use the narrowest scope the user requested - if (scope_number[i] >= 0 && scope == kCdScopeInvalid) { - // The scope is the current iteration step. - scope = i; - } else if (scope_number[i] < 0) { - scope = i + 1; - } } - // If the user didn't specify anything, default to window scope - if (scope == kCdScopeInvalid) { - scope = MIN_CD_SCOPE; + // 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 (scope_number[kCdScopeTabpage] > 0) { - tp = find_tabpage(scope_number[kCdScopeTabpage]); - if (!tp) { - emsg(_("E5000: Cannot find tab number.")); - return; + 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 (scope_number[kCdScopeWindow] >= 0) { - if (scope_number[kCdScopeTabpage] < 0) { + if (argv[WINDOW_IDX] >= 0) { + if (argv[TABPAGE_IDX] < 0) { emsg(_("E5001: Higher scope cannot be -1 if lower scope is >= 0.")); return; } - if (scope_number[kCdScopeWindow] > 0) { + if (argv[WINDOW_IDX] > 0) { win = find_win_by_nr(&argvars[0], tp); if (!win) { emsg(_("E5002: Cannot find window number.")); @@ -1046,6 +1162,10 @@ void f_haslocaldir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) assert(win); rettv->vval.v_number = win->w_localdir ? 1 : 0; break; + case kCdScopeBuffer: + assert(buf); + rettv->vval.v_number = buf->b_localdir ? 1 : 0; + break; case kCdScopeTabpage: assert(tp); rettv->vval.v_number = tp->tp_localdir ? 1 : 0; diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 18cb5c534a..e5584a5353 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -2937,6 +2937,24 @@ int do_ecmd(int fnum, char *ffname, char *sfname, exarg_T *eap, linenr_T newlnum redraw_curbuf_later(UPD_NOT_VALID); // redraw this buffer later } + // A new buffer inherits the buffer-local directory of the buffer it was created from (unless + // autocommands already set one). + if (!oldbuf && curbuf->b_localdir == NULL + && bufref_valid(&old_curbuf) + && old_curbuf.br_buf != curbuf + && old_curbuf.br_buf->b_localdir != NULL) { + curbuf->b_localdir = xstrdup(old_curbuf.br_buf->b_localdir); + if (old_curbuf.br_buf->b_prevdir != NULL) { + curbuf->b_prevdir = xstrdup(old_curbuf.br_buf->b_prevdir); + } + } + + // If editing a buffer in the current window, make sure to update to the + // buffer's working directory. + if (oldwin == curwin) { + fix_current_dir(false); + } + // Change directories when the 'acd' option is set. do_autochdir(); diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua index bba712b334..a5767c1876 100644 --- a/src/nvim/ex_cmds.lua +++ b/src/nvim/ex_cmds.lua @@ -192,6 +192,18 @@ M.cmds = { addr_type = 'ADDR_NONE', func = 'ex_edit', }, + { + command = 'bcd', + flags = bit.bor(BANG, FILE1, TRLBAR, BUFLOCK_OK, LOCK_OK), + addr_type = 'ADDR_NONE', + func = 'ex_cd', + }, + { + command = 'bchdir', + flags = bit.bor(BANG, FILE1, TRLBAR, BUFLOCK_OK, LOCK_OK), + addr_type = 'ADDR_NONE', + func = 'ex_cd', + }, { command = 'bdelete', flags = bit.bor(BANG, RANGE, BUFNAME, COUNT, EXTRA, TRLBAR), diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index a446d2ecf7..8f9fa7fcfb 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -6212,6 +6212,9 @@ static char *get_prevdir(CdScope scope) case kCdScopeTabpage: return curtab->tp_prevdir; break; + case kCdScopeBuffer: + return curbuf->b_prevdir; + break; case kCdScopeWindow: return curwin->w_prevdir; break; @@ -6222,7 +6225,7 @@ static char *get_prevdir(CdScope scope) /// Deal with the side effects of changing the current directory. /// -/// @param scope Scope of the function call (global, tab or window). +/// @param scope Scope of the function call (global, tab, buffer or window). static void post_chdir(CdScope scope, bool trigger_dirchanged) { // Always overwrite the window-local CWD. @@ -6233,6 +6236,10 @@ static void post_chdir(CdScope scope, bool trigger_dirchanged) XFREE_CLEAR(curtab->tp_localdir); } + if (scope >= kCdScopeBuffer) { + XFREE_CLEAR(curbuf->b_localdir); + } + if (scope < kCdScopeGlobal) { char *pdir = get_prevdir(scope); // If still in global directory, set CWD as the global directory. @@ -6253,6 +6260,9 @@ static void post_chdir(CdScope scope, bool trigger_dirchanged) case kCdScopeTabpage: curtab->tp_localdir = xstrdup(cwd); break; + case kCdScopeBuffer: + curbuf->b_localdir = xstrdup(cwd); + break; case kCdScopeWindow: curwin->w_localdir = xstrdup(cwd); break; @@ -6268,9 +6278,9 @@ static void post_chdir(CdScope scope, bool trigger_dirchanged) } } -/// Change directory function used by :cd/:tcd/:lcd Ex commands and the chdir() function. +/// Change directory function used by :cd/:tcd/:bcd/:lcd Ex commands and the chdir() function. /// @param new_dir The directory to change to. -/// @param scope Scope of the function call (global, tab or window). +/// @param scope Scope of the function call (global, tab, buffer or window). /// @return true if the directory is successfully changed. bool changedir_func(char *new_dir, CdScope scope) { @@ -6325,6 +6335,9 @@ bool changedir_func(char *new_dir, CdScope scope) case kCdScopeWindow: pp = &curwin->w_prevdir; break; + case kCdScopeBuffer: + pp = &curbuf->b_prevdir; + break; default: pp = &prev_dir; } @@ -6336,7 +6349,7 @@ bool changedir_func(char *new_dir, CdScope scope) return true; } -/// ":cd", ":tcd", ":lcd", ":chdir", "tchdir" and ":lchdir". +/// ":cd", ":tcd", ":bcd", ":lcd", ":chdir", "tchdir", ":bchdir", and ":lchdir". void ex_cd(exarg_T *eap) { char *new_dir = eap->arg; @@ -6352,6 +6365,10 @@ void ex_cd(exarg_T *eap) case CMD_tchdir: scope = kCdScopeTabpage; break; + case CMD_bcd: + case CMD_bchdir: + scope = kCdScopeBuffer; + break; case CMD_lcd: case CMD_lchdir: scope = kCdScopeWindow; @@ -6380,6 +6397,8 @@ static void ex_pwd(exarg_T *eap) context = last_chdir_reason; } else if (curwin->w_localdir != NULL) { context = "window"; + } else if (curbuf->b_localdir != NULL) { + context = "buffer"; } else if (curtab->tp_localdir != NULL) { context = "tabpage"; } diff --git a/src/nvim/ex_session.c b/src/nvim/ex_session.c index 0fdf3ee146..97ce4a9e37 100644 --- a/src/nvim/ex_session.c +++ b/src/nvim/ex_session.c @@ -822,9 +822,8 @@ static int makeopens(FILE *fd, char *dirnow) return FAIL; } - // Restore the tab-local working directory if specified - // Do this before the windows, so that the window-local directory can - // override the tab-local directory. + // 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 @@ -834,6 +833,32 @@ static int makeopens(FILE *fd, char *dirnow) 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/file_search.c b/src/nvim/file_search.c index 01f2fe131f..fc4ee0b3cf 100644 --- a/src/nvim/file_search.c +++ b/src/nvim/file_search.c @@ -1854,6 +1854,9 @@ void do_autocmd_dirchanged(char *new_dir, CdScope scope, CdCause cause, bool pre case kCdScopeTabpage: snprintf(buf, sizeof(buf), "tabpage"); break; + case kCdScopeBuffer: + snprintf(buf, sizeof(buf), "buffer"); + break; case kCdScopeWindow: snprintf(buf, sizeof(buf), "window"); break; @@ -1874,6 +1877,7 @@ void do_autocmd_dirchanged(char *new_dir, CdScope scope, CdCause cause, bool pre switch (cause) { case kCdCauseManual: case kCdCauseWindow: + case kCdCauseBuffer: break; case kCdCauseAuto: snprintf(buf, sizeof(buf), "auto"); diff --git a/src/nvim/vim_defs.h b/src/nvim/vim_defs.h index 0eadcf5b03..ae33e4f2cd 100644 --- a/src/nvim/vim_defs.h +++ b/src/nvim/vim_defs.h @@ -33,23 +33,21 @@ typedef enum { /// /// Scopes are enumerated from lowest to highest. When adding a scope make sure /// to update all functions using scopes as well, such as the implementation of -/// `getcwd()`. When using scopes as limits (e.g. in loops) don't use the scopes -/// directly, use `MIN_CD_SCOPE` and `MAX_CD_SCOPE` instead. +/// `getcwd()`. typedef enum { kCdScopeInvalid = -1, kCdScopeWindow, ///< Affects one window. + kCdScopeBuffer, ///< Affects one buffer. kCdScopeTabpage, ///< Affects one tab page. kCdScopeGlobal, ///< Affects the entire Nvim instance. } CdScope; -#define MIN_CD_SCOPE kCdScopeWindow -#define MAX_CD_SCOPE kCdScopeGlobal - /// What caused the current directory to change. typedef enum { kCdCauseOther = -1, - kCdCauseManual, ///< Using `:cd`, `:tcd`, `:lcd` or `chdir()`. + kCdCauseManual, ///< Using `:cd`, `:bcd`, `:tcd`, `:lcd` or `chdir()`. kCdCauseWindow, ///< Switching to another window. + kCdCauseBuffer, ///< Switching to another buffer. kCdCauseAuto, ///< On 'autochdir'. } CdCause; diff --git a/src/nvim/window.c b/src/nvim/window.c index 5e18af5b2d..d9b01af856 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -5300,7 +5300,7 @@ static void win_enter_ext(win_T *const wp, const int flags) win_fix_cursor(get_real_state() & (MODE_NORMAL|MODE_CMDLINE|MODE_TERMINAL)); } - win_fix_current_dir(); + fix_current_dir(true); entering_window(curwin); // Careful: autocommands may close the window and make "wp" invalid @@ -5352,18 +5352,41 @@ static void win_enter_ext(win_T *const wp, const int flags) do_autochdir(); } -/// Used after making another window the current one: change directory if needed. -void win_fix_current_dir(void) +/// Used after making another window or buffer the current one: change directory if needed. +void fix_current_dir(bool caused_by_win) { - // New directory is either the local directory of the window, tab or NULL. - char *new_dir = curwin->w_localdir ? curwin->w_localdir : curtab->tp_localdir; + CdCause cause; + if (caused_by_win) { + cause = kCdCauseWindow; + } else { + cause = kCdCauseBuffer; + } + + // New directory is either the local directory of the window, buffer, tab or NULL. + char *new_dir; + CdScope scope; + + if (curwin->w_localdir) { + new_dir = curwin->w_localdir; + scope = kCdScopeWindow; + } else if (curbuf->b_localdir) { + new_dir = curbuf->b_localdir; + scope = kCdScopeBuffer; + } else if (curtab->tp_localdir) { + new_dir = curtab->tp_localdir; + scope = kCdScopeTabpage; + } else { + new_dir = NULL; + scope = kCdScopeGlobal; + } + char cwd[MAXPATHL]; if (os_dirname(cwd, MAXPATHL) != OK) { cwd[0] = NUL; } if (new_dir) { - // Window/tab has a local directory: Save current directory as global + // Window/buffer/tab has a local directory: Save current directory as global // (unless that was done already) and change to the local directory. if (globaldir == NULL) { if (cwd[0] != NUL) { @@ -5372,27 +5395,25 @@ void win_fix_current_dir(void) } bool dir_differs = pathcmp(new_dir, cwd, -1) != 0; if (!p_acd && dir_differs) { - do_autocmd_dirchanged(new_dir, curwin->w_localdir ? kCdScopeWindow : kCdScopeTabpage, - kCdCauseWindow, true); + do_autocmd_dirchanged(new_dir, scope, cause, true); } if (os_chdir(new_dir) == 0) { if (!p_acd && dir_differs) { - do_autocmd_dirchanged(new_dir, curwin->w_localdir ? kCdScopeWindow : kCdScopeTabpage, - kCdCauseWindow, false); + do_autocmd_dirchanged(new_dir, scope, cause, false); } } last_chdir_reason = NULL; shorten_fnames(true); } else if (globaldir != NULL) { - // Window doesn't have a local directory and we are not in the global + // Window nor buffer have a local directory and we are not in the global // directory: Change to the global directory. bool dir_differs = pathcmp(globaldir, cwd, -1) != 0; if (!p_acd && dir_differs) { - do_autocmd_dirchanged(globaldir, kCdScopeGlobal, kCdCauseWindow, true); + do_autocmd_dirchanged(globaldir, kCdScopeGlobal, cause, true); } if (os_chdir(globaldir) == 0) { if (!p_acd && dir_differs) { - do_autocmd_dirchanged(globaldir, kCdScopeGlobal, kCdCauseWindow, false); + do_autocmd_dirchanged(globaldir, kCdScopeGlobal, cause, false); } } XFREE_CLEAR(globaldir); diff --git a/test/functional/autocmd/dirchanged_spec.lua b/test/functional/autocmd/dirchanged_spec.lua index 4c940b293d..b743069ed2 100644 --- a/test/functional/autocmd/dirchanged_spec.lua +++ b/test/functional/autocmd/dirchanged_spec.lua @@ -17,6 +17,7 @@ describe('autocmd DirChanged and DirChangedPre', function() curdir .. '/Xtest-functional-autocmd-dirchanged.dir1', curdir .. '/Xtest-functional-autocmd-dirchanged.dir2', curdir .. '/Xtest-functional-autocmd-dirchanged.dir3', + curdir .. '/Xtest-functional-autocmd-dirchanged.dir4', } local win_dirs = { curdir .. '\\XTEST-FUNCTIONAL-AUTOCMD-DIRCHANGED.DIR1', @@ -62,32 +63,43 @@ describe('autocmd DirChanged and DirChangedPre', function() eq(1, eval('g:cdprecount')) eq(1, eval('g:cdcount')) - command('tcd ' .. dirs[2]) - eq({ directory = dirs[2], scope = 'tabpage', changed_window = false }, eval('g:evpre')) - eq({ cwd = dirs[2], scope = 'tabpage', changed_window = false }, eval('g:ev')) - eq('tabpage', eval('g:amatchpre')) - eq('tabpage', eval('g:amatch')) + command('bcd ' .. dirs[2]) + eq({ directory = dirs[2], scope = 'buffer', changed_window = false }, eval('g:evpre')) + eq({ cwd = dirs[2], scope = 'buffer', changed_window = false }, eval('g:ev')) + eq('buffer', eval('g:amatchpre')) + eq('buffer', eval('g:amatch')) eq(2, eval('g:cdprecount')) eq(2, eval('g:cdcount')) - command('cd ' .. dirs[3]) - eq({ directory = dirs[3], scope = 'global', changed_window = false }, eval('g:evpre')) - eq({ cwd = dirs[3], scope = 'global', changed_window = false }, eval('g:ev')) - eq('global', eval('g:amatchpre')) - eq('global', eval('g:amatch')) + command('tcd ' .. dirs[3]) + eq({ directory = dirs[3], scope = 'tabpage', changed_window = false }, eval('g:evpre')) + eq({ cwd = dirs[3], scope = 'tabpage', changed_window = false }, eval('g:ev')) + eq('tabpage', eval('g:amatchpre')) + eq('tabpage', eval('g:amatch')) eq(3, eval('g:cdprecount')) eq(3, eval('g:cdcount')) + + command('cd ' .. dirs[4]) + eq({ directory = dirs[4], scope = 'global', changed_window = false }, eval('g:evpre')) + eq({ cwd = dirs[4], scope = 'global', changed_window = false }, eval('g:ev')) + eq('global', eval('g:amatchpre')) + eq('global', eval('g:amatch')) + eq(4, eval('g:cdprecount')) + eq(4, eval('g:cdcount')) end) it('DirChanged set getcwd() during event #6260', function() command('lcd ' .. dirs[1]) eq(dirs[1], eval('g:getcwd')) - command('tcd ' .. dirs[2]) + command('bcd ' .. dirs[2]) eq(dirs[2], eval('g:getcwd')) - command('cd ' .. dirs[3]) + command('tcd ' .. dirs[3]) eq(dirs[3], eval('g:getcwd')) + + command('cd ' .. dirs[4]) + eq(dirs[4], eval('g:getcwd')) end) it('disallow recursion', function() @@ -216,52 +228,75 @@ describe('autocmd DirChanged and DirChangedPre', function() eq(2, eval('g:cdcount')) end - command('cd ' .. dirs[3]) - eq({ directory = dirs[3], scope = 'global', changed_window = false }, eval('g:evpre')) - eq({ cwd = dirs[3], scope = 'global', changed_window = false }, eval('g:ev')) - eq('global', eval('g:amatch')) + command('tcd ' .. dirs[3]) + eq({ directory = dirs[3], scope = 'tabpage', changed_window = false }, eval('g:evpre')) + eq({ cwd = dirs[3], scope = 'tabpage', changed_window = false }, eval('g:ev')) + eq('tabpage', eval('g:amatchpre')) + eq('tabpage', eval('g:amatch')) eq(3, eval('g:cdprecount')) eq(3, eval('g:cdcount')) command('let g:evpre = {}') command('let g:ev = {}') - command('cd ' .. dirs[3]) + command('tcd ' .. dirs[3]) eq({}, eval('g:evpre')) eq({}, eval('g:ev')) eq(3, eval('g:cdprecount')) eq(3, eval('g:cdcount')) if is_os('win') then - command('cd ' .. win_dirs[3]) + command('tcd ' .. win_dirs[3]) eq({}, eval('g:evpre')) eq({}, eval('g:ev')) eq(3, eval('g:cdprecount')) eq(3, eval('g:cdcount')) end - command('set autochdir') - - command('split ' .. dirs[1] .. '/foo') - eq({ directory = dirs[1], scope = 'window', changed_window = false }, eval('g:evpre')) - eq({ cwd = dirs[1], scope = 'window', changed_window = false }, eval('g:ev')) - eq('auto', eval('g:amatchpre')) - eq('auto', eval('g:amatch')) + command('cd ' .. dirs[4]) + eq({ directory = dirs[4], scope = 'global', changed_window = false }, eval('g:evpre')) + eq({ cwd = dirs[4], scope = 'global', changed_window = false }, eval('g:ev')) + eq('global', eval('g:amatch')) eq(4, eval('g:cdprecount')) eq(4, eval('g:cdcount')) command('let g:evpre = {}') command('let g:ev = {}') - command('split ' .. dirs[1] .. '/bar') + command('cd ' .. dirs[4]) eq({}, eval('g:evpre')) eq({}, eval('g:ev')) eq(4, eval('g:cdprecount')) eq(4, eval('g:cdcount')) if is_os('win') then - command('split ' .. win_dirs[1] .. '/baz') + command('cd ' .. dirs[4]) eq({}, eval('g:evpre')) eq({}, eval('g:ev')) eq(4, eval('g:cdprecount')) eq(4, eval('g:cdcount')) end + + command('set autochdir') + + command(('split %s/foo'):format(dirs[2])) + eq({ directory = dirs[2], scope = 'window', changed_window = false }, eval('g:evpre')) + eq({ cwd = dirs[2], scope = 'window', changed_window = false }, eval('g:ev')) + eq('auto', eval('g:amatchpre')) + eq('auto', eval('g:amatch')) + eq(5, eval('g:cdprecount')) + eq(5, eval('g:cdcount')) + command('let g:evpre = {}') + command('let g:ev = {}') + command(('split %s/bar'):format(dirs[2])) + eq({}, eval('g:evpre')) + eq({}, eval('g:ev')) + eq(5, eval('g:cdprecount')) + eq(5, eval('g:cdcount')) + + if is_os('win') then + command(('split %s/baz'):format(win_dirs[2])) + eq({}, eval('g:evpre')) + eq({}, eval('g:ev')) + eq(5, eval('g:cdprecount')) + eq(5, eval('g:cdcount')) + end end) it('are triggered by switching to win/tab with different CWD #6054', function() @@ -341,6 +376,141 @@ describe('autocmd DirChanged and DirChangedPre', function() end end) + it('are triggered by switching to buf/tab with different CWD', function() + local files = { + dirs[1] .. '/file', + dirs[2] .. '/file', + dirs[3] .. '/file', + } + + command('e ' .. files[3]) -- buffer 3 + command('e ' .. files[2]) -- buffer 2 + command('e ' .. files[1]) -- buffer 1 + + command('bcd ' .. dirs[1]) + command('b ' .. files[2]) -- Switch to buffer 2 + command('bcd ' .. dirs[2]) + command('b ' .. files[3]) -- Switch to buffer 3 + command('bcd ' .. dirs[3]) + + eq({ directory = dirs[3], scope = 'buffer', changed_window = false }, eval('g:evpre')) + eq({ cwd = dirs[3], scope = 'buffer', changed_window = false }, eval('g:ev')) + eq('buffer', eval('g:amatchpre')) + eq('buffer', eval('g:amatch')) + + eq(5, eval('g:cdprecount')) + eq(5, eval('g:cdcount')) + command('tabnew') -- tab 2 (tab-local CWD) + eq(5, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(5, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tcd ' .. dirs[2]) + command('tabnext') -- tab 1 (no tab-local CWD) + eq({ directory = dirs[3], scope = 'buffer', changed_window = true }, eval('g:evpre')) + eq({ cwd = dirs[3], scope = 'buffer', changed_window = true }, eval('g:ev')) + eq('buffer', eval('g:amatchpre')) + eq('buffer', eval('g:amatch')) + command('tabnext') -- tab 2 + eq({ directory = dirs[2], scope = 'tabpage', changed_window = true }, eval('g:evpre')) + eq({ cwd = dirs[2], scope = 'tabpage', changed_window = true }, eval('g:ev')) + eq('tabpage', eval('g:amatchpre')) + eq('tabpage', eval('g:amatch')) + eq(8, eval('g:cdprecount')) + eq(8, eval('g:cdcount')) + + command('tabnext') -- tab 1 + command('b ' .. files[2]) -- buffer 2 + eq(10, eval('g:cdprecount')) + eq(10, eval('g:cdcount')) + command('tabnext') -- tab 2 (has the *same* CWD) + eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event + + if is_os('win') then + command('tabnew') -- tab 3 + eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tcd ' .. dirs[2]) + eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tabnext') -- tab 1 + eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tabprevious') -- tab 3 + eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tabprevious') -- tab 2 + eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tabprevious') -- tab 1 + eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event + command('bcd ' .. dirs[2]) -- buffer 2 + eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tabnext') -- tab 2 + eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tabnext') -- tab 3 + eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tabnext') -- tab 1 + eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tabprevious') -- tab 3 + eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event + end + end) + + it('are triggered by switching to buf/win with different CWD', function() + command('lcd ' .. dirs[3]) -- window 3 + command(('split %s/file'):format(dirs[2])) -- window 2 + command('lcd ' .. dirs[2]) + command(('split %s/file'):format(dirs[1])) -- window 1 + command('lcd ' .. dirs[1]) + + -- All windows have a window-local CWD + + command('2wincmd w') -- window 2 + eq({ directory = dirs[2], scope = 'window', changed_window = true }, eval('g:evpre')) + eq({ cwd = dirs[2], scope = 'window', changed_window = true }, eval('g:ev')) + eq('window', eval('g:amatchpre')) + eq('window', eval('g:amatch')) + + eq(4, eval('g:cdprecount')) + eq(4, eval('g:cdcount')) + command('bcd ' .. dirs[1]) -- window 2 now has a buffer with buffer-local CWD (and no window-local CWD) + eq({ directory = dirs[1], scope = 'buffer', changed_window = false }, eval('g:evpre')) + eq({ cwd = dirs[1], scope = 'buffer', changed_window = false }, eval('g:ev')) + eq('buffer', eval('g:amatchpre')) + eq('buffer', eval('g:amatch')) + eq(5, eval('g:cdprecount')) + eq(5, eval('g:cdcount')) + command('3wincmd w') -- window 3 (window-local CWD) + eq(6, eval('g:cdprecount')) + eq(6, eval('g:cdcount')) + command('bcd ' .. dirs[2]) -- window 3 now has a buffer with buffer-local CWD (and no window-local CWD) + command('1wincmd w') -- window 1 (window-local CWD) + eq({ directory = dirs[1], scope = 'window', changed_window = true }, eval('g:evpre')) + eq({ cwd = dirs[1], scope = 'window', changed_window = true }, eval('g:ev')) + eq('window', eval('g:amatchpre')) + eq('window', eval('g:amatch')) + command('2wincmd w') -- window 2 (buffer-local CWD) + eq({ directory = dirs[1], scope = 'window', changed_window = true }, eval('g:evpre')) -- buffer-local CWD + eq({ cwd = dirs[1], scope = 'window', changed_window = true }, eval('g:ev')) + eq('window', eval('g:amatchpre')) + eq('window', eval('g:amatch')) + eq(8, eval('g:cdprecount')) + eq(8, eval('g:cdcount')) + + command('1wincmd w') -- window 1 (window-local cwd) + eq(8, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(8, eval('g:cdcount')) -- same CWD, no DirChanged event + command(('b %s/file'):format(dirs[2])) -- buffer 2 (has buffer-local cwd) + eq(8, eval('g:cdprecount')) -- no DirChangedPre event, window-local CWD has higher priority + eq(8, eval('g:cdcount')) -- no DirChanged event, window-local CWD has higher priority + end) + it('are triggered by nvim_set_current_dir()', function() request('nvim_set_current_dir', dirs[1]) eq({ directory = dirs[1], scope = 'global', changed_window = false }, eval('g:evpre')) diff --git a/test/functional/ex_cmds/cd_spec.lua b/test/functional/ex_cmds/cd_spec.lua index b6d5b195e7..2881a4634c 100644 --- a/test/functional/ex_cmds/cd_spec.lua +++ b/test/functional/ex_cmds/cd_spec.lua @@ -18,9 +18,12 @@ local mkdir = t.mkdir local directories = { tab = 'Xtest-functional-ex_cmds-cd_spec.tab', -- Tab window = 'Xtest-functional-ex_cmds-cd_spec.window', -- Window + buffer = 'Xtest-functional-ex_cmds-cd_spec.buffer', -- Buffer global = 'Xtest-functional-ex_cmds-cd_spec.global', -- New global } +local tmpfile = 'Xtest-functional-ex_cmds-cd_spec-tmpfile' + -- Shorthand writing to get the current working directory local cwd = function(...) return call('getcwd', ...) @@ -28,6 +31,9 @@ end -- effective working dir local wcwd = function() return cwd(0) end -- window dir +local bcwd = function() + return cwd(-1, -1, 0) +end -- buffer dir local tcwd = function() return cwd(-1, 0) end -- tab dir @@ -39,6 +45,9 @@ end -- effective working dir local wlwd = function() return lwd(0) end -- window dir +local blwd = function() + return lwd(-1, -1, 0) +end -- buffer dir local tlwd = function() return lwd(-1, 0) end -- tab dir @@ -134,6 +143,45 @@ for _, cmd in ipairs { 'cd', 'chdir' } do eq(globalDir .. pathsep .. directories.tab, cwd(-1, localtab)) eq(1, lwd(-1, localtab)) end) + + it('for buffer', function() + local globalDir = directories.start + -- Create two buffers + command(('e %s1'):format(tmpfile)) + command(('e %s%s%s2'):format(directories.buffer, pathsep, tmpfile)) + + -- Initially matches globalDir + eq(globalDir, cwd()) + eq(0, lwd()) + + -- Change buffer-local directory to subdirectory + command('bcd ' .. directories.buffer) + eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd()) + eq(1, blwd()) + + -- Verify Other buffer is unchanged + command('b# ') + eq(globalDir, cwd()) + eq(0, blwd()) + + -- A new buffer created with :edit inherits the buffer-local directory; editing an + -- existing buffer keeps that buffer's own directory. + command('b# ') + command(('e %s3'):format(tmpfile)) + eq(1, blwd()) + eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd()) + command(('e ..%s%s1'):format(pathsep, tmpfile)) + eq(0, blwd()) + eq(globalDir, cwd()) + + -- getcwd({winnr}) falls through to the buffer shown in that window, not the current + -- buffer. + 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(globalDir, cwd(2)) + end) end) describe('getcwd(-1, -1)', function() @@ -153,6 +201,16 @@ for _, cmd in ipairs { 'cd', 'chdir' } do eq(directories.start, 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(0, lwd(-1, -1)) + + -- Must behave the same if bufnr is -1 + eq(directories.start, cwd(-1, -1, -1)) + eq(0, lwd(-1, -1, -1)) + end) end) describe('Local directory gets inherited', function() @@ -239,6 +297,137 @@ for _, cmd in ipairs { 'cd', 'chdir' } do eq(0, tlwd()) eq(globalDir .. pathsep .. directories.window, wcwd()) end) + + it('works when mixing tab-local and buffer-local directories', function() + local globalDir = directories.start + + -- Create two buffers for testing. One in each tab + command(('e %s1'):format(tmpfile)) + command('tabnew') + command(('e %s2'):format(tmpfile)) + + -- Verify that buffer 2 has the same working directory + eq(globalDir, cwd()) + eq(globalDir, tcwd()) -- Has no tab-local directory + eq(0, tlwd()) + eq(globalDir, bcwd()) -- Has no buffer-local directory + eq(0, blwd()) + + -- 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(cwd(), tcwd()) -- Working directory matches tab directory + eq(1, tlwd()) + eq(cwd(), bcwd()) -- Still no buffer-directory + eq(0, blwd()) + + -- 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(cwd(), bcwd()) -- Has no buffer-directory + eq(1, blwd()) + + -- Verify the first tab has no local-directory + command('tabfirst') + eq(0, tlwd()) + + -- 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(0, tlwd()) -- Still no tab-local directory + + -- Verify buffer 1 did not have its directory changed + command('b#') -- Switch to buffer 1 + eq(globalDir, cwd()) + eq(0, blwd()) -- No window-buffer directory + end) + it('works when mixing window local and buffer local directories', function() + local globalDir = directories.start + -- Create a new window first and verify that is has the same working directory + command('new') + eq(globalDir, cwd()) + eq(globalDir, wcwd()) -- Has no window-local directory + eq(0, tlwd()) + eq(globalDir, bcwd()) -- Has no buffer-local directory + eq(0, blwd()) + + -- Create a buffer in current window + command('e ' .. tmpfile) + + -- 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(cwd(), bcwd()) -- Working directory matches buffer directory + eq(1, blwd()) + eq(cwd(), wcwd()) -- Still no window-directory + eq(0, wlwd()) + + -- 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(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()) + + -- Verify going to second window uses window-local directory + command('wincmd w') + eq(('%s%s%s'):format(globalDir, pathsep, 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) + + 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 + command(('%s %s'):format(cmd, directories.buffer)) + + command(':new') + eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd()) + command('wincmd x') -- close :new window + + command(':vnew') + eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), 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)) + + command(':vsplit') + eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd()) + + command(':enew') + eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd()) + end) end) end @@ -272,14 +461,23 @@ for _, cmd in ipairs { 'getcwd', 'haslocaldir' } do 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() + 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 ' .. cmd .. '(0, 0, 0)')) + eq(err118, 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 81e39889de..866950bf49 100644 --- a/test/functional/ex_cmds/mksession_spec.lua +++ b/test/functional/ex_cmds/mksession_spec.lua @@ -23,16 +23,19 @@ local file_prefix = 'Xtest-functional-ex_cmds-mksession_spec' describe(':mksession', function() local session_file = file_prefix .. '.vim' - local tab_dir = file_prefix .. '.d' + local tab_dir = file_prefix .. '.tab.d' + local buf_dir = file_prefix .. '.buf.d' before_each(function() clear() mkdir(tab_dir) + mkdir(buf_dir) end) after_each(function() os.remove(session_file) rmdir(tab_dir) + rmdir(buf_dir) end) it('restores same :terminal buf in splits', function() @@ -362,4 +365,66 @@ describe(':mksession', function() -- contains all splits eq(3, #api.nvim_tabpage_list_wins(0)) end) + + it('restores buffer-local working directories', function() + -- "'" and " " in the names must survive the session file's string quoting and escaping. + local tmpfile_base = file_prefix .. "-tmp'file" + local quote_dir = file_prefix .. ".b'uf d" + local session_file2 = session_file .. '2' + local cwd_dir = fn.getcwd() + mkdir(quote_dir) + finally(function() + rmdir(quote_dir) + os.remove(session_file2) + end) + + command(('edit %s1'):format(tmpfile_base)) + command(('edit %s2'):format(tmpfile_base)) + command('bcd ' .. fn.fnameescape(quote_dir)) + command('b# ') + command('mksession ' .. session_file) + command('set sessionoptions-=curdir') + command('mksession ' .. session_file2) + + -- Create a new test instance of Nvim. + clear() + + command('silent source ' .. session_file) + command(('b %s%s2'):format(get_pathsep(), tmpfile_base)) + eq(('%s%s%s'):format(cwd_dir, get_pathsep(), quote_dir), fn.getcwd()) + + -- Without "curdir" in 'sessionoptions' no buffer-local directory is restored. + clear() + command('silent source ' .. session_file2) + command(('b %s%s2'):format(get_pathsep(), tmpfile_base)) + eq(cwd_dir, fn.getcwd()) + eq(0, fn.haslocaldir(-1, -1, 0)) + end) + + it('restores buffer-local working directories when used with tabs', function() + local tmpfile_base = file_prefix .. '-tmpfile' + local cwd_dir = fn.getcwd() + + command(('edit %s1'):format(tmpfile_base)) + command('tabnew ') + command(('edit %s2'):format(tmpfile_base)) + command('tcd ' .. tab_dir) + command('bcd ..') + command('bcd ' .. buf_dir) + command('tabfirst') + command('mksession ' .. session_file) + + -- Create a new test instance of Nvim. + clear() + + command('source ' .. session_file) + -- First tab should have the original working directory. + command('tabnext 1') + eq(cwd_dir, fn.getcwd()) + -- Second tab should have the tab-local working directory. + command('tabnext 2') + eq(('%s%s%s'):format(cwd_dir, get_pathsep(), tab_dir), fn.getcwd(-1, 0)) + -- Buffer in second tab should have buffer-local working directory + eq(('%s%s%s'):format(cwd_dir, get_pathsep(), buf_dir), fn.getcwd(-1, -1, 0)) + end) end)