diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt index f73661ec4e..98d903134d 100644 --- a/runtime/doc/autocmd.txt +++ b/runtime/doc/autocmd.txt @@ -615,13 +615,14 @@ 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'. + "auto" triggered on 'autochdir'. + "buffer" triggered on `:bcd`. + "global" triggered on `:cd`. + "tabpage" triggered on `:tcd`. + "window" triggered on `:lcd`. Sets these |v:event| keys: cwd: current 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. @@ -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..952801d486 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 (tab, window or buffer) does not exist, the next-higher scope in the hierarchy applies. *:cd* *E747* *E472* @@ -1387,10 +1388,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 window and buffer 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 @@ -1402,7 +1404,7 @@ scope in the hierarchy applies. *: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. + tabs, windows, or buffers is not changed. *:lch* *:lchdir* :lch[dir][!] Same as |:lcd|. @@ -1411,6 +1413,22 @@ scope in the hierarchy applies. :lc[d][!] - Change to the previous current directory (before the previous ":lcd {path}" command). + *:bc* *:bcd* +:bc[d][!] [path] Like |:cd|, but only set the current directory for the + current buffer. The current directory for other + tabs, windows, or buffers is not changed. + + Unlike :lcd and :tcd, this is not "sticky": a new + buffer or window will not "inherit" a buffer-local + directory. + + *:bcd-* +:bc[d][!] - Change to the previous buffer-local current directory + (before the last ":bcd [path]" command). + + *:bch* *:bchdir* +:bch[dir][!] Same as |:bcd|. + *:pw* *:pwd* *E187* :pw[d] Print the current directory name. Also see |getcwd()|. @@ -1422,24 +1440,36 @@ scope in the hierarchy applies. :verbose pwd [global] /path/to/current - " Set by :lcd - :verbose pwd - [window] /path/to/current - " Set by :tcd :verbose pwd [tabpage] /path/to/current -So long as no |:lcd| or |:tcd| command has been used, all windows share the -same current directory. Using a command to jump to another window doesn't + " Set by :lcd + :verbose pwd + [window] /path/to/current + + " Set by :bcd + :verbose pwd + [buffer] /path/to/current + +So long as |:tcd|, |:lcd| or |:bcd| has not been used, all buffers and windows +share the same directory. Using a command to jump to another window doesn't change anything for the current directory. When |:lcd| has been used for a window, the specified directory becomes the current directory for that window. Windows where the |:lcd| command has not -been used stick to the global or tab-local directory. When jumping to another -window the current directory is changed to the last specified local current -directory. If none was specified, the global or tab-local directory is used. -When creating a new window it inherits the local directory of the current window. +been used stick to the buffer-local, tab-local or global directory. When +jumping to another window the current directory is changed to the last +specified window-local directory. If none was specified, the buffer-local, +tab-local or global directory is used. When creating a new window it inherits +the current window-local directory ("sticky" behavior). + +When |:bcd| has been used for a buffer, the specified directory becomes the +current directory whenever that buffer is entered, unless the window has its +own window-local directory. The buffer-local directory is never inherited +(not "sticky"): a buffer created with |:edit|, |:enew|, etc., ignores the +current buffer-local (:bcd) directory, if any. Use |:lcd| if you want "sticky" +behavior. When changing tabs the same behaviour applies. If the current tab has no local working directory the global working directory is used. diff --git a/runtime/doc/index.txt b/runtime/doc/index.txt index ee667bed3e..e7b4470ba8 100644 --- a/runtime/doc/index.txt +++ b/runtime/doc/index.txt @@ -1181,6 +1181,8 @@ Tag Command Action ~ |:ball| :ba[ll] open a window for each buffer in the buffer list |:badd| :bad[d] add buffer to the buffer list |:balt| :balt like ":badd" but also set the alternate file +|:bcd| :bc[d] change directory for buffer +|:bchdir| :bch[dir] change directory for buffer |:bdelete| :bd[elete] remove a buffer from the buffer list |:belowright| :bel[owright] make split window appear right or below |:bfirst| :bf[irst] go to first buffer in the buffer list 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/options.txt b/runtime/doc/options.txt index c8f1d868a8..8086129f68 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -1323,7 +1323,7 @@ A jump table for the options with a short description can be found at |Q_op|. 'cdhome' 'cdh' boolean (default on on Unix, off on Windows) global Disallowed in |modeline|. |no-modeline-option| - When on, |:cd|, |:tcd| and |:lcd| without an argument changes the + When on, |:cd|, |:tcd|, |:lcd| and |:bcd| without an argument changes the current working directory to the |$HOME| directory like in Unix. When off, those commands just print the current directory name. @@ -1332,7 +1332,7 @@ A jump table for the options with a short description can be found at |Q_op|. global Disallowed in |modeline|. |no-modeline-option| This is a list of directories which will be searched when using the - |:cd|, |:tcd| and |:lcd| commands, provided that the directory being + |:cd|, |:tcd|, |:lcd| and |:bcd| commands, provided that the directory being searched for has a relative path, not an absolute part starting with "/", "./" or "../", the 'cdpath' option is not used then. The 'cdpath' option's value has the same form and semantics as @@ -2058,7 +2058,7 @@ A jump table for the options with a short description can be found at |Q_op|. following occurrence. *cpo-~* ~ When included, don't resolve symbolic links when - changing directory with |:cd|, |:lcd|, or |:tcd|. + changing directory with |:cd|, |:tcd|, |:lcd|, or |:bcd|. This preserves the symbolic link path in buffer names and when displaying the current directory. When excluded (default), symbolic links are resolved to diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index f77c8a03c6..ee33bf2242 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -312,6 +312,7 @@ Command-line: is open. The |cmdwin-char| is shown via 'statuscolumn'. Commands: +- |:bcd| - |:checkhealth| - |:connect| - |:detach| @@ -695,8 +696,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. - |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..806ee13d2e 100644 --- a/runtime/doc/vimfn.txt +++ b/runtime/doc/vimfn.txt @@ -1117,12 +1117,15 @@ 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. If {scope} is present, changes the current working directory for the specified scope: + "buffer" Changes the buffer local directory. |:bcd| "window" Changes the window local directory. |:lcd| "tabpage" Changes the tabpage local directory. |:tcd| "global" Changes the global directory. |:cd| @@ -3745,31 +3748,49 @@ getcursorcharpos([{winid}]) *getcursorcharpos()* Return: ~ (`any`) -getcwd([{winnr} [, {tabnr}]]) *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. 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 - implies 0. Thus the following are equivalent: >vim + |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/window/buffer. Missing {tabnr} + implies 0 (missing {bufnr} does not; see below). 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, then + the buffer-local working directory is returned. + + An argument may be -1 only if all preceding arguments are -1. + + 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. Parameters: ~ • {winnr} (`integer?`) • {tabnr} (`integer?`) + • {bufnr} (`integer?`) Return: ~ (`string`) @@ -4994,14 +5015,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 tabpage, window 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. + Tabs, windows and buffers are identified by their respective + numbers, 0 means current tab/window/buffer. Missing {winnr} + or {tabnr} implies 0 (missing {bufnr} does not; see below). Thus the following are equivalent: >vim echo haslocaldir() echo haslocaldir(0) @@ -5010,11 +5033,19 @@ 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. An argument may be -1 only if + all preceding arguments are -1. + 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/options.gen.lua b/runtime/lua/vim/_meta/options.gen.lua index dd2d1a7468..6932bbfca5 100644 --- a/runtime/lua/vim/_meta/options.gen.lua +++ b/runtime/lua/vim/_meta/options.gen.lua @@ -734,7 +734,7 @@ vim.o.cmp = vim.o.casemap vim.go.casemap = vim.o.casemap vim.go.cmp = vim.go.casemap ---- When on, `:cd`, `:tcd` and `:lcd` without an argument changes the +--- When on, `:cd`, `:tcd`, `:lcd` and `:bcd` without an argument changes the --- current working directory to the `$HOME` directory like in Unix. --- When off, those commands just print the current directory name. --- @@ -745,7 +745,7 @@ vim.go.cdhome = vim.o.cdhome vim.go.cdh = vim.go.cdhome --- This is a list of directories which will be searched when using the ---- `:cd`, `:tcd` and `:lcd` commands, provided that the directory being +--- `:cd`, `:tcd`, `:lcd` and `:bcd` commands, provided that the directory being --- searched for has a relative path, not an absolute part starting with --- "/", "./" or "../", the 'cdpath' option is not used then. --- The 'cdpath' option's value has the same form and semantics as @@ -1572,7 +1572,7 @@ vim.bo.ci = vim.bo.copyindent --- following occurrence. --- *cpo-~* --- ~ When included, don't resolve symbolic links when ---- changing directory with `:cd`, `:lcd`, or `:tcd`. +--- changing directory with `:cd`, `:tcd`, `:lcd`, or `:bcd`. --- This preserves the symbolic link path in buffer names --- and when displaying the current directory. When --- excluded (default), symbolic links are resolved to diff --git a/runtime/lua/vim/_meta/vimfn.gen.lua b/runtime/lua/vim/_meta/vimfn.gen.lua index 092c495939..8444b6d6f4 100644 --- a/runtime/lua/vim/_meta/vimfn.gen.lua +++ b/runtime/lua/vim/_meta/vimfn.gen.lua @@ -952,12 +952,15 @@ 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. --- --- If {scope} is present, changes the current working directory --- for the specified scope: +--- "buffer" Changes the buffer local directory. |:bcd| --- "window" Changes the window local directory. |:lcd| --- "tabpage" Changes the tabpage local directory. |:tcd| --- "global" Changes the global directory. |:cd| @@ -3318,27 +3321,44 @@ 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 ---- implies 0. Thus the following are equivalent: >vim +--- |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/window/buffer. Missing {tabnr} +--- implies 0 (missing {bufnr} does not; see below). 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 --- echo haslocaldir() --- echo haslocaldir(0) @@ -4460,12 +4482,20 @@ 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. An argument may be -1 only if +--- all preceding arguments are -1. +--- 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? +--- local sfx = 1 @@ -192,7 +192,7 @@ local function get_api_meta() sees[#sees + 1] = see.desc end - local params = {} --- @type [string,string][] + local params = {} --- @type [string,string,string?][] for _, p in ipairs(fun.params) do params[#params + 1] = { p.name, diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index b02a53736b..db94778e36 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -938,6 +938,9 @@ bool buf_freeall(buf_T *buf, int flags) } syntax_clear(&buf->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 + update_cwd(kCdCauseBuffer); } /// Enter a new current buffer. 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..911f2dd116 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(); + update_cwd(kCdCauseWindow); + } + if (bufref_valid(&cs->cs_new_curbuf)) { + xfree(cs->cs_new_curbuf.br_buf->b_localdir); + cs->cs_new_curbuf.br_buf->b_localdir = cs->cs_b_localdir; + } else { + xfree(cs->cs_b_localdir); } xfree(curtab->tp_localdir); curtab->tp_localdir = cs->cs_tp_localdir; 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/eval.lua b/src/nvim/eval.lua index 0321db3384..3181857b53 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -14,7 +14,7 @@ --- @field generics? string[] Used to write `---@generic` annotations over a function. --- @field signature? string --- @field desc? string ---- @field params [string, string, string][] +--- @field params [string, string, string?][] Each entry is `{name, type, desc?}`; `desc` is only present in API-meta entries. --- @field notes? string[] --- @field see? string[] --- @@ -1257,12 +1257,15 @@ 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. If {scope} is present, changes the current working directory for the specified scope: + "buffer" Changes the buffer local directory. |:bcd| "window" Changes the window local directory. |:lcd| "tabpage" Changes the tabpage local directory. |:tcd| "global" Changes the global directory. |:cd| @@ -4089,32 +4092,48 @@ 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 - implies 0. Thus the following are equivalent: >vim + |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/window/buffer. Missing {tabnr} + implies 0 (missing {bufnr} does not; see below). 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 echo haslocaldir() echo haslocaldir(0) @@ -5428,13 +5449,20 @@ 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. An argument may be -1 only if + all preceding arguments are -1. + 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,98 +644,164 @@ void f_fnamemodify(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) xfree(fbuf); } -/// `getcwd([{win}[, {tab}]])` function +/// Resolves the `[{winnr}[, {tabnr}[, {bufnr}]]]` arguments of `getcwd()`/`haslocaldir()` to the +/// requested scope and the scope objects to inspect. In an argument, `0` means the current object +/// and `-1` skips the scope (requesting the next broader one). Note that the argument order +/// differs from the `CdScope` order. +/// +/// @param default_scope Scope when no arguments were given. +/// +/// @return false if an error message was emitted. +static bool getcwd_scope_args(typval_T *argvars, CdScope default_scope, CdScope *scope, win_T **win, + tabpage_T **tp, buf_T **buf) +{ + enum { kWinArg, kTabArg, kBufArg, }; + int argv[] = { 0, 0, 0 }; + int argc = 0; + + *scope = default_scope; + *win = curwin; + *tp = curtab; + *buf = curbuf; + + for (; argc < 3; argc++) { + // If there is no argument there are no more scopes after it, break out. + if (argvars[argc].v_type == VAR_UNKNOWN) { + break; + } + if (argvars[argc].v_type != VAR_NUMBER) { + emsg(_(e_invarg)); + return false; + } + argv[argc] = (int)argvars[argc].vval.v_number; + // It is an error for the scope number to be less than `-1`. + if (argv[argc] < -1) { + emsg(_(e_invarg)); + return false; + } + } + + // An argument may only be -1 if all preceding arguments are -1: `(2, 3, -1)` is an error. + bool explicit_scope = false; + for (int i = 0; i < argc; i++) { + explicit_scope = explicit_scope || argv[i] >= 0; + if (explicit_scope && argv[i] < 0) { + emsg(_("E5001: Higher scope cannot be -1 if lower scope is >= 0.")); + return false; + } + } + + // Narrowest requested scope. Imagine X >= 0: + switch (argc) { + case 0: + break; + case 1: + if (argv[kWinArg] >= 0) { + *scope = kCdScopeWindow; // (X) + } else { + *scope = kCdScopeTabpage; // (-1) + } + break; + case 2: + if (argv[kWinArg] >= 0) { + *scope = kCdScopeWindow; // (X, ...) + } else if (argv[kTabArg] >= 0) { + *scope = kCdScopeTabpage; // (-1, X) + } else { + *scope = kCdScopeGlobal; // (-1, -1) + } + break; + case 3: + if (argv[kBufArg] >= 0) { + *scope = kCdScopeBuffer; // (-1, -1, X) + } else { + *scope = kCdScopeGlobal; // (-1, -1, -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[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. /// /// @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. 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. - }; - - 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. + CdScope scope; + win_T *win; + tabpage_T *tp; + buf_T *buf; 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++) { - // If there is no argument there are no more scopes after it, break out. - if (argvars[i].v_type == VAR_UNKNOWN) { - break; - } - if (argvars[i].v_type != VAR_NUMBER) { - emsg(_(e_invarg)); - return; - } - scope_number[i] = (int)argvars[i].vval.v_number; - // It is an error for the scope number to be less than `-1`. - if (scope_number[i] < -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; - } + // Without arguments (kCdScopeInvalid), the effective working directory is returned. + if (!getcwd_scope_args(argvars, kCdScopeInvalid, &scope, &win, &tp, &buf)) { + 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; - } - } - - // Find the window in `tp` by number, `NULL` if none. - if (scope_number[kCdScopeWindow] >= 0) { - if (scope_number[kCdScopeTabpage] < 0) { - emsg(_("E5001: Higher scope cannot be -1 if lower scope is >= 0.")); - return; - } - - if (scope_number[kCdScopeWindow] > 0) { - win = find_win_by_nr(&argvars[0], tp); - if (!win) { - emsg(_("E5002: Cannot find window number.")); - return; - } - } - } - - 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: + from = buf->b_localdir; + if (from) { + break; + } + FALLTHROUGH; case kCdScopeTabpage: - assert(tp); from = tp->tp_localdir; if (from) { break; @@ -959,96 +1028,40 @@ 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 -/// 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. - int scope_number[] = { - [kCdScopeWindow] = 0, // Number of window to look at. - [kCdScopeTabpage] = 0, // Number of tab to look at. - }; - - tabpage_T *tp = curtab; // The tabpage to look at. - win_T *win = curwin; // The window 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 and scope extraction together - for (int i = MIN_CD_SCOPE; i < MAX_CD_SCOPE; i++) { - if (argvars[i].v_type == VAR_UNKNOWN) { - break; - } - if (argvars[i].v_type != VAR_NUMBER) { - emsg(_(e_invarg)); - return; - } - scope_number[i] = (int)argvars[i].vval.v_number; - if (scope_number[i] < -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; - } - - // 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; - } - } - - // Find the window in `tp` by number, `NULL` if none. - if (scope_number[kCdScopeWindow] >= 0) { - if (scope_number[kCdScopeTabpage] < 0) { - emsg(_("E5001: Higher scope cannot be -1 if lower scope is >= 0.")); - return; - } - - if (scope_number[kCdScopeWindow] > 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: + 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_cmds.c b/src/nvim/ex_cmds.c index 18cb5c534a..782dd9b9d8 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -2937,6 +2937,9 @@ 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 } + // Update to the working directory of the new current buffer. + update_cwd(kCdCauseBuffer); + // 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..1da6864edd 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -6205,24 +6205,24 @@ 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; case kCdScopeWindow: - return curwin->w_prevdir; - break; + return &curwin->w_prevdir; default: - return prev_dir; + return &prev_dir; } } /// 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, window or buffer). static void post_chdir(CdScope scope, bool trigger_dirchanged) { // Always overwrite the window-local CWD. @@ -6233,8 +6233,12 @@ 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); + char *pdir = *get_prevdir(scope); // If still in global directory, set CWD as the global directory. if (globaldir == NULL && pdir != NULL) { globaldir = xstrdup(pdir); @@ -6253,6 +6257,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 +6275,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/:lcd/:bcd 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, window or buffer). /// @return true if the directory is successfully changed. bool changedir_func(char *new_dir, CdScope scope) { @@ -6281,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; @@ -6317,17 +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; - default: - pp = &prev_dir; - } + char **pp = get_prevdir(scope); xfree(*pp); *pp = pdir; @@ -6336,7 +6333,7 @@ bool changedir_func(char *new_dir, CdScope scope) return true; } -/// ":cd", ":tcd", ":lcd", ":chdir", "tchdir" and ":lchdir". +/// ":cd", ":tcd", ":lcd", ":bcd", ":chdir", ":tchdir", ":lchdir", and ":bchdir". void ex_cd(exarg_T *eap) { char *new_dir = eap->arg; @@ -6352,6 +6349,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 +6381,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..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,18 +864,6 @@ 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. - 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 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/options.lua b/src/nvim/options.lua index bf487a6d59..a070861233 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -1093,7 +1093,7 @@ local options = { doc = [[on on Unix, off on Windows]], }, desc = [=[ - When on, |:cd|, |:tcd| and |:lcd| without an argument changes the + When on, |:cd|, |:tcd|, |:lcd| and |:bcd| without an argument changes the current working directory to the |$HOME| directory like in Unix. When off, those commands just print the current directory name. ]=], @@ -1113,7 +1113,7 @@ local options = { deny_duplicates = true, desc = [=[ This is a list of directories which will be searched when using the - |:cd|, |:tcd| and |:lcd| commands, provided that the directory being + |:cd|, |:tcd|, |:lcd| and |:bcd| commands, provided that the directory being searched for has a relative path, not an absolute part starting with "/", "./" or "../", the 'cdpath' option is not used then. The 'cdpath' option's value has the same form and semantics as @@ -2119,7 +2119,7 @@ local options = { following occurrence. *cpo-~* ~ When included, don't resolve symbolic links when - changing directory with |:cd|, |:lcd|, or |:tcd|. + changing directory with |:cd|, |:tcd|, |:lcd|, or |:bcd|. This preserves the symbolic link path in buffer names and when displaying the current directory. When excluded (default), symbolic links are resolved to diff --git a/src/nvim/vim_defs.h b/src/nvim/vim_defs.h index 0eadcf5b03..41015e226e 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`, `:tcd`, `:lcd`, `:bcd` 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..d880fc2f8d 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(); + update_cwd(kCdCauseWindow); entering_window(curwin); // Careful: autocommands may close the window and make "wp" invalid @@ -5352,18 +5352,37 @@ 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) +/// Applies the effective current-directory of the current window (its window-local, buffer-local, +/// tab-local or the global directory). Called after setting curbuf/curwin. +/// +/// @param cause What caused the switch, reported by the DirChanged event. +void update_cwd(CdCause cause) { - // 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; + // 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 +5391,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..6a7f7b9a6c 100644 --- a/test/functional/autocmd/dirchanged_spec.lua +++ b/test/functional/autocmd/dirchanged_spec.lua @@ -6,7 +6,6 @@ local describe, it, before_each, setup, teardown = local clear = n.clear local command = n.command local eq = t.eq -local pcall_err = t.pcall_err local eval = n.eval local request = n.request local is_os = t.is_os @@ -17,6 +16,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 +62,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() @@ -150,7 +161,7 @@ describe('autocmd DirChanged and DirChangedPre', function() eq('E344:', string.match(err3, 'E%d*:')) end) - it("are triggered by 'autochdir'", function() + it("triggered by 'autochdir'", function() command('set autochdir') command('split ' .. dirs[1] .. '/foo') @@ -169,7 +180,7 @@ describe('autocmd DirChanged and DirChangedPre', function() eq(2, eval('g:cdcount')) end) - it('do not trigger if directory has not changed', function() + it('not triggered if directory has not changed', function() command('lcd ' .. dirs[1]) eq({ directory = dirs[1], scope = 'window', changed_window = false }, eval('g:evpre')) eq({ cwd = dirs[1], scope = 'window', changed_window = false }, eval('g:ev')) @@ -216,55 +227,78 @@ 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() + it('triggered by switching to win/tab with different CWD #6054', function() command('lcd ' .. dirs[3]) -- window 3 command('split ' .. dirs[2] .. '/foo') -- window 2 command('lcd ' .. dirs[2]) @@ -341,7 +375,143 @@ describe('autocmd DirChanged and DirChangedPre', function() end end) - it('are triggered by nvim_set_current_dir()', function() + it('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: its new empty buffer has no local CWD, reverts to global + eq({ cwd = curdir, scope = 'global', changed_window = false }, eval('g:ev')) + eq(6, eval('g:cdprecount')) + eq(6, eval('g:cdcount')) + 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(9, eval('g:cdprecount')) + eq(9, eval('g:cdcount')) + + command('tabnext') -- tab 1 + command('b ' .. files[2]) -- buffer 2 + eq(11, eval('g:cdprecount')) + eq(11, eval('g:cdcount')) + command('tabnext') -- tab 2 (has the *same* CWD) + eq(11, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(11, eval('g:cdcount')) -- same CWD, no DirChanged event + + if is_os('win') then + command('tabnew') -- tab 3 + eq(11, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(11, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tcd ' .. dirs[2]) + eq(11, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(11, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tabnext') -- tab 1 + eq(11, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(11, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tabprevious') -- tab 3 + eq(11, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(11, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tabprevious') -- tab 2 + eq(11, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(11, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tabprevious') -- tab 1 + eq(11, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(11, eval('g:cdcount')) -- same CWD, no DirChanged event + command('bcd ' .. dirs[2]) -- buffer 2 + eq(11, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(11, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tabnext') -- tab 2 + eq(11, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(11, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tabnext') -- tab 3 + eq(11, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(11, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tabnext') -- tab 1 + eq(11, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(11, eval('g:cdcount')) -- same CWD, no DirChanged event + command('tabprevious') -- tab 3 + eq(11, eval('g:cdprecount')) -- same CWD, no DirChangedPre event + eq(11, eval('g:cdcount')) -- same CWD, no DirChanged event + end + end) + + it('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('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')) eq({ cwd = dirs[1], scope = 'global', changed_window = false }, eval('g:ev')) diff --git a/test/functional/ex_cmds/cd_spec.lua b/test/functional/ex_cmds/cd_spec.lua index b6d5b195e7..6c8ff1ceb4 100644 --- a/test/functional/ex_cmds/cd_spec.lua +++ b/test/functional/ex_cmds/cd_spec.lua @@ -18,9 +18,16 @@ 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' + +local function join(...) + return table.concat({ ... }, pathsep) +end + -- Shorthand writing to get the current working directory local cwd = function(...) return call('getcwd', ...) @@ -28,6 +35,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,27 +49,33 @@ 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 --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() @@ -134,6 +150,49 @@ 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(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 + command('b# ') + eq(globalDir, cwd()) + eq(0, blwd()) + + -- A new buffer created with :edit does not inherit the buffer-local directory. + command('b# ') + command(('e %s3'):format(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(join(globalDir, directories.buffer), cwd(1)) + eq(globalDir, cwd(2)) + end) end) describe('getcwd(-1, -1)', function() @@ -153,6 +212,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 +308,127 @@ 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(join(globalDir, 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(join(globalDir, directories.buffer), cwd()) + eq(join(globalDir, 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(join(globalDir, 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(join(globalDir, 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(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(join(globalDir, directories.buffer), cwd()) + + -- Verify going to second window uses window-local directory + command('wincmd w') + eq(join(globalDir, directories.window), cwd()) + end) + end) +end + +for _, cmd in ipairs { 'bcd', 'bchdir' } do + describe(':' .. cmd, function() + before_each(before_test) + after_each(remove_dirs) + + it('works after deleting the only buffer', function() + command(('%s %s'):format(cmd, directories.buffer)) + command('bd') -- delete buffer + end) + + it('buffer-local directory is NOT sticky/inherited', function() + local bufdir = join(directories.start, directories.buffer) + + command('edit ' .. tmpfile) + command(('%s %s'):format(cmd, directories.buffer)) + eq(bufdir, cwd()) + + -- A new buffer starts without a buffer-local directory. + command('new') + eq(directories.start, cwd()) + eq(0, blwd()) + command('close') + eq(bufdir, cwd()) + command('enew') + eq(directories.start, cwd()) + eq(0, blwd()) + command('b# ') + eq(bufdir, cwd()) + + -- Recycling an empty unnamed buffer (:edit) drops its directory with it. + command('enew') + command(('%s %s'):format(cmd, directories.buffer)) + eq(bufdir, cwd()) + command('edit ' .. tmpfile .. '2') + eq(directories.start, cwd()) + eq(0, blwd()) + end) end) end @@ -249,37 +439,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:E474: Invalid argument' + eq(err474, pcall_err(call, cmd, 'some string')) + eq(err474, pcall_err(call, cmd, 1.5)) + eq(err474, pcall_err(call, cmd, { 1, 2 })) + eq(err474, pcall_err(call, cmd, { key = 'value' })) + eq(err474, pcall_err(call, cmd, -2)) + -- Funcref is not representable over RPC. + eq( + 'Vim(call):E474: Invalid argument', + pcall_err(command, ('call %s(function("tr"))'):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.' - it('fails on -1 if previous arg is >=0', function() - eq(err5001, pcall_err(command, 'call ' .. cmd .. '(0, -1)')) - 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)')) + -- -1 preceded by an argument >= 0 + local err5001 = 'Vim:E5001: Higher scope cannot be -1 if lower scope is >= 0.' + eq(err5001, pcall_err(call, cmd, 0, -1)) + eq(err5001, pcall_err(call, cmd, 2, 3, -1)) + eq(err5001, pcall_err(call, cmd, -1, 0, -1)) + eq(err5001, pcall_err(call, cmd, 0, -1, 0)) + -- Buffer scope requires window and tab args to be -1. + local err5006 = 'Vim:E5006: Window and tab scope must be -1 when using buffer scope' + eq(err5006, pcall_err(call, cmd, 0, 0, 0)) + eq(err5006, pcall_err(call, cmd, 1, 2, 3)) + eq('Vim:E5007: Cannot find buffer number.', pcall_err(call, cmd, -1, -1, 99999)) + eq( + ('Vim:E118: Too many arguments for function: %s'):format(cmd), + pcall_err(call, cmd, 0, 0, 0, 0) + ) end) end) end diff --git a/test/functional/ex_cmds/mksession_spec.lua b/test/functional/ex_cmds/mksession_spec.lua index 81e39889de..2c115f605e 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,69 @@ 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) + -- 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()) + -- 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)