Merge #33320 :bcd (buffer-local directory)

This commit is contained in:
Justin M. Keyes
2026-08-04 15:10:30 -04:00
committed by GitHub
28 changed files with 1026 additions and 377 deletions

View File

@@ -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)
<afile> 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)
<afile> is set to the new directory name.

View File

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

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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`)

View File

@@ -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

View File

@@ -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)
--- <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.
---
--- @param winnr? integer
--- @param tabnr? integer
--- @param bufnr? integer
--- @return string
function vim.fn.getcwd(winnr, tabnr) end
function vim.fn.getcwd(winnr, tabnr, bufnr) end
--- Lua: Prefer |vim.env|.
---
@@ -4445,13 +4465,15 @@ function vim.fn.has(feature) end
--- @return 0|1
function vim.fn.has_key(dict, key) end
--- 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.
--- 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)
@@ -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?
--- <Throw error if the arguments are invalid.
--- |E5000| |E5001| |E5002| |E5006| |E5007|
---
--- @param winnr? integer
--- @param tabnr? integer
--- @param bufnr? integer
--- @return 0|1
function vim.fn.haslocaldir(winnr, tabnr) end
function vim.fn.haslocaldir(winnr, tabnr, bufnr) end
--- Checks whether a mapping exists whose rhs contains {what}.
--- Returns TRUE if there is such a mapping in one of the modes

View File

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

View File

@@ -98,7 +98,7 @@ local function split(x, sep)
end
--- @param f string
--- @param params [string,string][]|true
--- @param params [string,string,string?][]|true
--- @return string
local function render_fun_sig(f, params)
local param_str --- @type string
@@ -107,7 +107,7 @@ local function render_fun_sig(f, params)
else
param_str = table.concat(
vim.tbl_map(
--- @param v [string,string]
--- @param v [string,string,string?]
--- @return string
function(v)
return luaescape(v[1])
@@ -126,8 +126,8 @@ local function render_fun_sig(f, params)
end
--- Uniquify names
--- @param params [string,string,string][]
--- @return [string,string,string][]
--- @param params [string,string,string?][]
--- @return [string,string,string?][]
local function process_params(params)
local seen = {} --- @type table<string,true>
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,

View File

@@ -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.

View File

@@ -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.

View File

@@ -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;

View File

@@ -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)

View File

@@ -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)
<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.
]=],
name = 'getcwd',
params = { { 'winnr', 'integer' }, { 'tabnr', 'integer' } },
params = { { 'winnr', 'integer' }, { 'tabnr', 'integer' }, { 'bufnr', 'integer' } },
returns = 'string',
signature = 'getcwd([{winnr} [, {tabnr}]])',
signature = 'getcwd([{winnr} [, {tabnr} [, {bufnr}]]])',
tags = { 'E5000', 'E5001', 'E5002', 'E5006', 'E5007' },
see_lua = {
'|uv.cwd()| for the global working directory; tab-local and window-local scopes differ',
},
@@ -5410,16 +5429,18 @@ M.funcs = {
see_lua = { '`dict[key] ~= nil`' },
},
haslocaldir = {
args = { 0, 2 },
args = { 0, 3 },
base = 1,
desc = [=[
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.
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)
@@ -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?
<Throw error if the arguments are invalid.
|E5000| |E5001| |E5002| |E5006| |E5007|
]=],
name = 'haslocaldir',
params = { { 'winnr', 'integer' }, { 'tabnr', 'integer' } },
params = { { 'winnr', 'integer' }, { 'tabnr', 'integer' }, { 'bufnr', 'integer' } },
returns = '0|1',
signature = 'haslocaldir([{winnr} [, {tabnr}]])',
signature = 'haslocaldir([{winnr} [, {tabnr} [, {bufnr} ]]])',
},
hasmapto = {
args = { 1, 3 },

View File

@@ -11,6 +11,7 @@
#include <sys/stat.h>
#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

View File

@@ -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();

View File

@@ -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),

View File

@@ -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";
}

View File

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

View File

@@ -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");

View File

@@ -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

View File

@@ -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;

View File

@@ -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);

View File

@@ -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'))

View File

@@ -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

View File

@@ -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)