feat(editor): :bcd changes buffer-local directory

Problem:
No way to set a buffer-local directory.
Use-cases:
- "Root dir" for LSP (and the "project" concept).
- `:terminal` OSC 7

Solution:
Add `:bcd` command.

- Extend `getcwd()` to take a third arg; `getcwd(-1, -1, bunfr)` returns
  the buffer-local working directory.
- Buffer-local directories have less priority than window-local
  ones, and more priority than tab-local ones.

Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
This commit is contained in:
saher
2025-04-02 18:17:15 +03:00
committed by Justin M. Keyes
parent 838f130e6a
commit ea3868bcf9
23 changed files with 940 additions and 178 deletions

View File

@@ -615,10 +615,11 @@ DiffUpdated After diffs have been updated. Depending on
*DirChanged*
DirChanged After the |current-directory| was changed.
The pattern can be:
"window" to trigger on `:lcd`
"tabpage" to trigger on `:tcd`
"global" to trigger on `:cd`
"auto" to trigger on 'autochdir'.
"buffer" to trigger on `:bcd`
"global" to trigger on `:cd`
"tabpage" to trigger on `:tcd`
"window" to trigger on `:lcd`
Sets these |v:event| keys:
cwd: current working directory
scope: "global", "tabpage", "window"
@@ -633,7 +634,7 @@ DirChangedPre When the |current-directory| is going to be
The pattern is like with |DirChanged|.
Sets these |v:event| keys:
directory: new working directory
scope: "global", "tabpage", "window"
scope: "global", "tabpage", "window", "buffer"
changed_window: v:true if we fired the event
switching window (or tab)
<afile> is set to the new directory name.

View File

@@ -1387,10 +1387,11 @@ scope in the hierarchy applies.
*:tc* *:tcd*
:tc[d][!] {path} Like |:cd|, but only set the directory for the current
tab. The current window will also use this directory.
The current directory is not changed for windows in
other tabs and for windows in the current tab that
have their own window-local directory.
tab. The current buffer and window will also use this
directory. The current directory is not changed for
windows in other tabs and for windows in the current
tab that have their own window-local directory, or for
buffers that have their own buffer-local directory.
*:tcd-*
:tc[d][!] - Change to the previous current directory (before the
@@ -1399,10 +1400,15 @@ scope in the hierarchy applies.
*:tch* *:tchdir*
:tch[dir][!] Same as |:tcd|.
*:bc* *:bcd*
:bc[d][!] {path} Like |:cd|, but only set the current directory for the
current buffer. The current directory for other
windows or buffers or tabs is not changed.
*:lc* *:lcd*
:lc[d][!] {path} Like |:cd|, but only set the current directory for the
current window. The current directory for other
windows or tabs is not changed.
windows, tabs, or buffers is not changed.
*:lch* *:lchdir*
:lch[dir][!] Same as |:lcd|.

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

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

View File

@@ -1117,6 +1117,8 @@ chdir({dir} [, {scope}]) *chdir()*
changed to the scope of the current directory:
- If the window local directory (|:lcd|) is set, it
changes the current working directory for that scope.
- If the buffer local directory (|:bcd|) is set, it
changes the current working directory for that scope.
- Otherwise, if the tabpage local directory (|:tcd|) is
set, it changes the current directory for that scope.
- Otherwise, changes the global directory for that scope.
@@ -1124,6 +1126,7 @@ chdir({dir} [, {scope}]) *chdir()*
If {scope} is present, changes the current working directory
for the specified scope:
"window" Changes the window local directory. |:lcd|
"buffer" Changes the buffer local directory. |:bcd|
"tabpage" Changes the tabpage local directory. |:tcd|
"global" Changes the global directory. |:cd|
@@ -3745,31 +3748,46 @@ getcursorcharpos([{winid}]) *getcursorcharpos()*
Return: ~
(`any`)
getcwd([{winnr} [, {tabnr}]]) *getcwd()*
getcwd([{winnr} [, {tabnr} [, {bufnr}]]]) *getcwd()*
Lua: Prefer |uv.cwd()| for the global working directory;
tab-local and window-local scopes differ.
With no arguments, returns the name of the effective
|current-directory|. With {winnr} or {tabnr} the working
directory of that scope is returned, and 'autochdir' is
ignored. Tabs and windows are identified by their respective
numbers, 0 means current tab or window. Missing tab number
|current-directory|. With {winnr} or {tabnr} or {bufnr} the
working directory of that scope is returned, and 'autochdir'
is ignored.
Tabs, windows and buffers are identified by their respective
numbers, 0 means current tab or window or buffer. Missing tab
number
implies 0. Thus the following are equivalent: >vim
getcwd(0)
getcwd(0, 0)
< If {winnr} is -1 it is ignored, only the tab is resolved.
{winnr} is a |window-number| or |window-ID|.
If both {winnr} and {tabnr} are -1 the global working
directory is returned.
If both {winnr} and {tabnr} are -1 and {bufnr} is missing the
global working directory is returned.
Note: When {tabnr} is -1 Vim returns an empty string to
signal that it is invalid, whereas Nvim returns either the
global working directory if {winnr} is -1 or the working
directory of the window indicated by {winnr}.
Throw error if the arguments are invalid. |E5000| |E5001| |E5002|
If {bufnr} is provided, {winnr} and {tabnr} must be -1 and the
working directory of that buffer is returned. If {bufnr} is
-1, it is ignored, and the global working directory is
returned.
Examples of buffer usage: >vim
getcwd(-1, -1, 0) " Get current buffer's directory
getcwd(-1, -1, 3) " Get directory of buffer #3
getcwd(-1, -1, -1) " Get global directory
getcwd(-1, -1) " Get global directory
< Throw error if the arguments are invalid.
|E5000| |E5001| |E5002| |E5006| |E5007|
Parameters: ~
• {winnr} (`integer?`)
• {tabnr} (`integer?`)
• {bufnr} (`integer?`)
Return: ~
(`string`)
@@ -4994,15 +5012,16 @@ has_key({dict}, {key}) *has_key()*
Return: ~
(`0|1`)
haslocaldir([{winnr} [, {tabnr}]]) *haslocaldir()*
Checks whether the window or tabpage has set a local working
directory. Returns 1 when the window has set a local path
via |:lcd| or when {winnr} is -1 and the tabpage has set a
local path via |:tcd|, otherwise 0.
haslocaldir([{winnr} [, {tabnr} [, {bufnr} ]]]) *haslocaldir()*
Checks whether the window, tabpage or buffer has set a local
working directory. Returns 1 when the window has set a local
path via |:lcd|, or when {winnr} is -1 and the tabpage has set
a local path via |:tcd|, or when {winnr} and {tabnr} are -1
and {bufnr} has set a local path via |:bcd|, otherwise 0.
Tabs and windows are identified by their respective numbers,
0 means current tab or window. Missing argument implies 0.
Thus the following are equivalent: >vim
Tabs, windows and buffers are identified by their respective
numbers, 0 means current tab or window. Missing argument
implies 0. Thus the following are equivalent: >vim
echo haslocaldir()
echo haslocaldir(0)
echo haslocaldir(0, 0)
@@ -5010,11 +5029,18 @@ haslocaldir([{winnr} [, {tabnr}]]) *haslocaldir()*
With {winnr} and {tabnr} use the window in that tabpage.
{winnr} is a |window-number| or |window-ID|.
If {winnr} is -1 it is ignored, only the tab is resolved.
Throw error if the arguments are invalid. |E5000| |E5001| |E5002|
If {bufnr} is provided, {winnr} and {tabnr} must be -1 and
only the buffer is resolved.
Examples of buffer usage: >vim
haslocaldir(-1, -1, 0) " Current buf has a local directory?
haslocaldir(-1, -1, 3) " Buf #3 has a local directory?
< Throw error if the arguments are invalid.
|E5000| |E5001| |E5002| |E5006| |E5007|
Parameters: ~
• {winnr} (`integer?`)
• {tabnr} (`integer?`)
• {bufnr} (`integer?`)
Return: ~
(`0|1`)

View File

@@ -952,6 +952,8 @@ function vim.fn.charidx(string, idx, countcc, utf16) end
--- changed to the scope of the current directory:
--- - If the window local directory (|:lcd|) is set, it
--- changes the current working directory for that scope.
--- - If the buffer local directory (|:bcd|) is set, it
--- changes the current working directory for that scope.
--- - Otherwise, if the tabpage local directory (|:tcd|) is
--- set, it changes the current directory for that scope.
--- - Otherwise, changes the global directory for that scope.
@@ -959,6 +961,7 @@ function vim.fn.charidx(string, idx, countcc, utf16) end
--- If {scope} is present, changes the current working directory
--- for the specified scope:
--- "window" Changes the window local directory. |:lcd|
--- "buffer" Changes the buffer local directory. |:bcd|
--- "tabpage" Changes the tabpage local directory. |:tcd|
--- "global" Changes the global directory. |:cd|
---
@@ -3318,27 +3321,42 @@ function vim.fn.getcursorcharpos(winid) end
--- Lua: Prefer |uv.cwd()| for the global working directory; tab-local and window-local scopes differ.
---
--- With no arguments, returns the name of the effective
--- |current-directory|. With {winnr} or {tabnr} the working
--- directory of that scope is returned, and 'autochdir' is
--- ignored. Tabs and windows are identified by their respective
--- numbers, 0 means current tab or window. Missing tab number
--- |current-directory|. With {winnr} or {tabnr} or {bufnr} the
--- working directory of that scope is returned, and 'autochdir'
--- is ignored.
---
--- Tabs, windows and buffers are identified by their respective
--- numbers, 0 means current tab or window or buffer. Missing tab
--- number
--- implies 0. Thus the following are equivalent: >vim
--- getcwd(0)
--- getcwd(0, 0)
--- <If {winnr} is -1 it is ignored, only the tab is resolved.
--- {winnr} is a |window-number| or |window-ID|.
--- If both {winnr} and {tabnr} are -1 the global working
--- directory is returned.
--- If both {winnr} and {tabnr} are -1 and {bufnr} is missing the
--- global working directory is returned.
--- Note: When {tabnr} is -1 Vim returns an empty string to
--- signal that it is invalid, whereas Nvim returns either the
--- global working directory if {winnr} is -1 or the working
--- directory of the window indicated by {winnr}.
--- Throw error if the arguments are invalid. |E5000| |E5001| |E5002|
---
--- If {bufnr} is provided, {winnr} and {tabnr} must be -1 and the
--- working directory of that buffer is returned. If {bufnr} is
--- -1, it is ignored, and the global working directory is
--- returned.
--- Examples of buffer usage: >vim
--- getcwd(-1, -1, 0) " Get current buffer's directory
--- getcwd(-1, -1, 3) " Get directory of buffer #3
--- getcwd(-1, -1, -1) " Get global directory
--- getcwd(-1, -1) " Get global directory
--- <Throw error if the arguments are invalid.
--- |E5000| |E5001| |E5002| |E5006| |E5007|
---
--- @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,14 +4463,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 window, tabpage or buffer has set a local
--- working directory. Returns 1 when the window has set a local
--- path via |:lcd|, or when {winnr} is -1 and the tabpage has set
--- a local path via |:tcd|, or when {winnr} and {tabnr} are -1
--- and {bufnr} has set a local path via |:bcd|, otherwise 0.
---
--- Tabs and windows are identified by their respective numbers,
--- 0 means current tab or window. Missing argument implies 0.
--- Thus the following are equivalent: >vim
--- Tabs, windows and buffers are identified by their respective
--- numbers, 0 means current tab or window. Missing argument
--- implies 0. Thus the following are equivalent: >vim
--- echo haslocaldir()
--- echo haslocaldir(0)
--- echo haslocaldir(0, 0)
@@ -4460,12 +4479,19 @@ function vim.fn.has_key(dict, key) end
--- With {winnr} and {tabnr} use the window in that tabpage.
--- {winnr} is a |window-number| or |window-ID|.
--- If {winnr} is -1 it is ignored, only the tab is resolved.
--- Throw error if the arguments are invalid. |E5000| |E5001| |E5002|
--- If {bufnr} is provided, {winnr} and {tabnr} must be -1 and
--- only the buffer is resolved.
--- Examples of buffer usage: >vim
--- haslocaldir(-1, -1, 0) " Current buf has a local directory?
--- haslocaldir(-1, -1, 3) " Buf #3 has a local directory?
--- <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

@@ -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
fix_current_dir(false);
}
/// Enter a new current buffer.
@@ -2023,6 +2031,8 @@ buf_T *buflist_new(char *ffname_arg, char *sfname_arg, linenr_T lnum, int flags)
trigger_undo_ftplugin(buf, curwin);
// It's like this buffer is deleted. Watch out for autocommands that
// change curbuf! If that happens, allocate a new buffer anyway.
// We also ask it to not free the buffer-local directory so we can reuse
// it.
buf_freeall(buf, BFA_WIPE | BFA_DEL);
if (aborting()) { // autocmds may abort script processing
xfree(ffname);
@@ -2152,6 +2162,8 @@ bool curbuf_reusable(void)
&& curbuf->b_ffname == NULL
&& curbuf->b_nwindows <= 1
&& !curbuf->terminal
&& curbuf->b_localdir == NULL
&& curbuf->b_prevdir == NULL
&& (curbuf->b_ml.ml_mfp == NULL || buf_is_empty(curbuf))
&& !bt_quickfix(curbuf)
&& !curbufIsChanged());

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();
fix_current_dir(true);
}
if (bufref_valid(&cs->cs_new_curbuf)) {
xfree(cs->cs_new_curbuf.br_buf->b_localdir);
cs->cs_new_curbuf.br_buf->b_localdir = cs->cs_b_localdir;
} else {
xfree(cs->cs_b_localdir);
}
xfree(curtab->tp_localdir);
curtab->tp_localdir = cs->cs_tp_localdir;

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

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

View File

@@ -1257,6 +1257,8 @@ M.funcs = {
changed to the scope of the current directory:
- If the window local directory (|:lcd|) is set, it
changes the current working directory for that scope.
- If the buffer local directory (|:bcd|) is set, it
changes the current working directory for that scope.
- Otherwise, if the tabpage local directory (|:tcd|) is
set, it changes the current directory for that scope.
- Otherwise, changes the global directory for that scope.
@@ -1264,6 +1266,7 @@ M.funcs = {
If {scope} is present, changes the current working directory
for the specified scope:
"window" Changes the window local directory. |:lcd|
"buffer" Changes the buffer local directory. |:bcd|
"tabpage" Changes the tabpage local directory. |:tcd|
"global" Changes the global directory. |:cd|
@@ -4089,32 +4092,45 @@ M.funcs = {
signature = 'getcursorcharpos([{winid}])',
},
getcwd = {
args = { 0, 2 },
args = { 0, 3 },
base = 1,
desc = [=[
With no arguments, returns the name of the effective
|current-directory|. With {winnr} or {tabnr} the working
directory of that scope is returned, and 'autochdir' is
ignored. Tabs and windows are identified by their respective
numbers, 0 means current tab or window. Missing tab number
|current-directory|. With {winnr} or {tabnr} or {bufnr} the
working directory of that scope is returned, and 'autochdir'
is ignored.
Tabs, windows and buffers are identified by their respective
numbers, 0 means current tab or window or buffer. Missing tab
number
implies 0. Thus the following are equivalent: >vim
getcwd(0)
getcwd(0, 0)
<If {winnr} is -1 it is ignored, only the tab is resolved.
{winnr} is a |window-number| or |window-ID|.
If both {winnr} and {tabnr} are -1 the global working
directory is returned.
If both {winnr} and {tabnr} are -1 and {bufnr} is missing the
global working directory is returned.
Note: When {tabnr} is -1 Vim returns an empty string to
signal that it is invalid, whereas Nvim returns either the
global working directory if {winnr} is -1 or the working
directory of the window indicated by {winnr}.
Throw error if the arguments are invalid. |E5000| |E5001| |E5002|
If {bufnr} is provided, {winnr} and {tabnr} must be -1 and the
working directory of that buffer is returned. If {bufnr} is
-1, it is ignored, and the global working directory is
returned.
Examples of buffer usage: >vim
getcwd(-1, -1, 0) " Get current buffer's directory
getcwd(-1, -1, 3) " Get directory of buffer #3
getcwd(-1, -1, -1) " Get global directory
getcwd(-1, -1) " Get global directory
<Throw error if the arguments are invalid.
|E5000| |E5001| |E5002| |E5006| |E5007|
]=],
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}]]])',
see_lua = {
'|uv.cwd()| for the global working directory; tab-local and window-local scopes differ',
},
@@ -5410,17 +5426,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 window, tabpage or buffer has set a local
working directory. Returns 1 when the window has set a local
path via |:lcd|, or when {winnr} is -1 and the tabpage has set
a local path via |:tcd|, or when {winnr} and {tabnr} are -1
and {bufnr} has set a local path via |:bcd|, otherwise 0.
Tabs and windows are identified by their respective numbers,
0 means current tab or window. Missing argument implies 0.
Thus the following are equivalent: >vim
Tabs, windows and buffers are identified by their respective
numbers, 0 means current tab or window. Missing argument
implies 0. Thus the following are equivalent: >vim
echo haslocaldir()
echo haslocaldir(0)
echo haslocaldir(0, 0)
@@ -5428,13 +5445,19 @@ M.funcs = {
With {winnr} and {tabnr} use the window in that tabpage.
{winnr} is a |window-number| or |window-ID|.
If {winnr} is -1 it is ignored, only the tab is resolved.
Throw error if the arguments are invalid. |E5000| |E5001| |E5002|
If {bufnr} is provided, {winnr} and {tabnr} must be -1 and
only the buffer is resolved.
Examples of buffer usage: >vim
haslocaldir(-1, -1, 0) " Current buf has a local directory?
haslocaldir(-1, -1, 3) " Buf #3 has a local directory?
<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,12 +644,12 @@ void f_fnamemodify(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
xfree(fbuf);
}
/// `getcwd([{win}[, {tab}]])` function
/// `getcwd([{win}[, {tab}[, {buf}]]])` function
///
/// Every scope not specified implies the currently selected scope object.
///
/// @pre The arguments must be of type number.
/// @pre There may not be more than two arguments.
/// @pre There may not be more than three arguments.
/// @pre An argument may not be -1 if preceding arguments are not all -1.
///
/// @post The return value will be a string.
@@ -655,50 +658,103 @@ void f_getcwd(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
// Possible scope of working directory to return.
CdScope scope = kCdScopeInvalid;
// Numbers of the scope objects (window, tab) we want the working directory
// of. A `-1` means to skip this scope, a `0` means the current object.
int scope_number[] = {
[kCdScopeWindow] = 0, // Number of window to look at.
[kCdScopeTabpage] = 0, // Number of tab to look at.
// Numbers of the scope objects (window, buffer, tab) we want the working
// directory of. A `-1` means to skip this scope, a `0` means the current object.
// getcwd() takes arguments in this order: (window, tab, buffer)
// Note that this is different from the order of CdScope
enum {
WINDOW_IDX = 0,
TABPAGE_IDX = 1,
BUFFER_IDX = 2,
};
int argv[] = { // arguments passed to getcwd().
0, // Number of window to look at.
0, // Number of tab to look at.
0, // Number of buffer to look at.
};
int argc = 0; // number of arguments passed to getcwd().
char *cwd = NULL; // Current working directory to print
char *from = NULL; // The original string to copy
tabpage_T *tp = curtab; // The tabpage to look at.
win_T *win = curwin; // The window to look at.
buf_T *buf = curbuf; // The buffer to look at.
rettv->v_type = VAR_STRING;
rettv->vval.v_string = NULL;
// Pre-conditions and scope extraction together
for (int i = MIN_CD_SCOPE; i < MAX_CD_SCOPE; i++) {
// Pre-conditions
for (; argc < 3; argc++) {
// If there is no argument there are no more scopes after it, break out.
if (argvars[i].v_type == VAR_UNKNOWN) {
if (argvars[argc].v_type == VAR_UNKNOWN) {
break;
}
if (argvars[i].v_type != VAR_NUMBER) {
if (argvars[argc].v_type != VAR_NUMBER) {
emsg(_(e_invarg));
return;
}
scope_number[i] = (int)argvars[i].vval.v_number;
argv[argc] = (int)argvars[argc].vval.v_number;
// It is an error for the scope number to be less than `-1`.
if (scope_number[i] < -1) {
if (argv[argc] < -1) {
emsg(_(e_invarg));
return;
}
// Use the narrowest scope the user requested
if (scope_number[i] >= 0 && scope == kCdScopeInvalid) {
// The scope is the current iteration step.
scope = i;
} else if (scope_number[i] < 0) {
scope = i + 1;
}
// Scope extraction
// Imagine X >= 0
switch (argc) {
case 0:
scope = kCdScopeInvalid; // getcwd()
break;
case 1:
if (argv[WINDOW_IDX] > -1) {
scope = kCdScopeWindow; // getcwd(X)
} else {
scope = kCdScopeTabpage; // getcwd(-1)
}
break;
case 2:
if (argv[WINDOW_IDX] > -1) {
scope = kCdScopeWindow; // getcwd(X, ...)
} else if (argv[TABPAGE_IDX] > -1) {
scope = kCdScopeTabpage; // getcwd(-1, X)
} else {
scope = kCdScopeGlobal; // getcwd(-1, -1)
}
break;
case 3:
if (argv[BUFFER_IDX] > -1) {
scope = kCdScopeBuffer; // getcwd(..., ..., X)
} else {
scope = kCdScopeGlobal; // getcwd(..., ..., -1)
}
break;
}
// getcwd(-1, -1, X)
if (scope == kCdScopeBuffer) {
if (argv[WINDOW_IDX] >= 0 || argv[TABPAGE_IDX] >= 0) {
emsg(_(e_getcwd_buffer_scope_requires_minus_one));
return;
}
if (argv[BUFFER_IDX] > 0) {
Error err = ERROR_INIT;
buf = find_buffer_by_handle(argv[BUFFER_IDX], &err);
if (ERROR_SET(&err)) {
emsg(_(e_cannot_find_buffer_number));
xfree(err.msg);
return;
}
}
}
// Find the tabpage by number
if (scope_number[kCdScopeTabpage] > 0) {
tp = find_tabpage(scope_number[kCdScopeTabpage]);
if (argv[TABPAGE_IDX] > 0) {
tp = find_tabpage(argv[TABPAGE_IDX]);
if (!tp) {
emsg(_("E5000: Cannot find tab number."));
return;
@@ -706,19 +762,21 @@ void f_getcwd(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
}
// Find the window in `tp` by number, `NULL` if none.
if (scope_number[kCdScopeWindow] >= 0) {
if (scope_number[kCdScopeTabpage] < 0) {
if (argv[WINDOW_IDX] >= 0) {
if (argv[TABPAGE_IDX] < 0) {
emsg(_("E5001: Higher scope cannot be -1 if lower scope is >= 0."));
return;
}
if (scope_number[kCdScopeWindow] > 0) {
if (argv[WINDOW_IDX] > 0) {
win = find_win_by_nr(&argvars[0], tp);
if (!win) {
emsg(_("E5002: Cannot find window number."));
return;
}
}
// Window scope may fall through to the buffer shown in that window.
buf = win->w_buffer;
}
cwd = xmalloc(MAXPATHL);
@@ -731,6 +789,13 @@ void f_getcwd(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
break;
}
FALLTHROUGH;
case kCdScopeBuffer:
assert(buf);
from = buf->b_localdir;
if (from) {
break;
}
FALLTHROUGH;
case kCdScopeTabpage:
assert(tp);
from = tp->tp_localdir;
@@ -959,7 +1024,7 @@ void f_glob2regpat(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
rettv->vval.v_string = pat == NULL ? NULL : file_pat_to_reg_pat(pat, NULL, NULL, false);
}
/// `haslocaldir([{win}[, {tab}]])` function
/// `haslocaldir([{win}[, {tab}[, {buf}]]])` function
///
/// Returns `1` if the scope object has a local directory, `0` otherwise. If a
/// scope object is not specified the current one is implied. This function
@@ -977,62 +1042,113 @@ void f_haslocaldir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
// Numbers of the scope objects (window, tab) we want the working directory
// of. A `-1` means to skip this scope, a `0` means the current object.
int scope_number[] = {
[kCdScopeWindow] = 0, // Number of window to look at.
[kCdScopeTabpage] = 0, // Number of tab to look at.
// haslocaldir() takes arguments in this order: (window, tab, buffer)
// Note that this is different from the order of CdScope
enum {
WINDOW_IDX = 0,
TABPAGE_IDX = 1,
BUFFER_IDX = 2,
};
int argv[] = { // arguments passed to haslocaldir().
0, // Number of window to look at.
0, // Number of tab to look at.
0, // Number of buffer to look at.
};
int argc = 0; // number of arguments passed to haslocaldir.
tabpage_T *tp = curtab; // The tabpage to look at.
win_T *win = curwin; // The window to look at.
win_T *win = curwin; // The window to look at.
buf_T *buf = curbuf; // The buffer to look at.
rettv->v_type = VAR_NUMBER;
rettv->vval.v_number = 0;
// Pre-conditions and scope extraction together
for (int i = MIN_CD_SCOPE; i < MAX_CD_SCOPE; i++) {
if (argvars[i].v_type == VAR_UNKNOWN) {
// Pre-conditions
for (; argc < 3; argc++) {
if (argvars[argc].v_type == VAR_UNKNOWN) {
break;
}
if (argvars[i].v_type != VAR_NUMBER) {
if (argvars[argc].v_type != VAR_NUMBER) {
emsg(_(e_invarg));
return;
}
scope_number[i] = (int)argvars[i].vval.v_number;
if (scope_number[i] < -1) {
argv[argc] = (int)argvars[argc].vval.v_number;
if (argv[argc] < -1) {
emsg(_(e_invarg));
return;
}
// Use the narrowest scope the user requested
if (scope_number[i] >= 0 && scope == kCdScopeInvalid) {
// The scope is the current iteration step.
scope = i;
} else if (scope_number[i] < 0) {
scope = i + 1;
}
}
// If the user didn't specify anything, default to window scope
if (scope == kCdScopeInvalid) {
scope = MIN_CD_SCOPE;
// Scope extraction
// Imagine X >= 0
switch (argc) {
case 0:
// If the user didn't specify anything, default to window scope
scope = kCdScopeWindow; // haslocaldir()
break;
case 1:
if (argv[0] > -1) {
scope = kCdScopeWindow; // haslocaldir(X)
} else {
scope = kCdScopeTabpage; // haslocaldir(-1)
}
break;
case 2:
if (argv[0] > -1) {
scope = kCdScopeWindow; // haslocaldir(X, ...)
} else if (argv[1] > -1) {
scope = kCdScopeTabpage; // haslocaldir(-1, X)
} else {
scope = kCdScopeGlobal; // haslocaldir(-1, -1)
}
break;
case 3:
if (argv[2] > -1) {
scope = kCdScopeBuffer; // haslocaldir(..., ..., X)
} else {
scope = kCdScopeGlobal; // haslocaldir(..., ..., -1)
}
break;
}
// haslocaldir(-1, -1, X)
if (scope == kCdScopeBuffer) {
if (argv[WINDOW_IDX] >= 0 || argv[TABPAGE_IDX] >= 0) {
emsg(_(e_getcwd_buffer_scope_requires_minus_one));
return;
}
if (argv[BUFFER_IDX] > 0) {
Error err = ERROR_INIT;
buf = find_buffer_by_handle(argv[BUFFER_IDX], &err);
if (ERROR_SET(&err)) {
emsg(_(e_cannot_find_buffer_number));
xfree(err.msg);
return;
}
}
}
// Find the tabpage by number
if (scope_number[kCdScopeTabpage] > 0) {
tp = find_tabpage(scope_number[kCdScopeTabpage]);
if (!tp) {
emsg(_("E5000: Cannot find tab number."));
return;
if (argv[TABPAGE_IDX] >= 0) {
if (argv[TABPAGE_IDX] > 0) {
tp = find_tabpage(argv[TABPAGE_IDX]);
if (!tp) {
emsg(_("E5000: Cannot find tab number."));
return;
}
}
}
// Find the window in `tp` by number, `NULL` if none.
if (scope_number[kCdScopeWindow] >= 0) {
if (scope_number[kCdScopeTabpage] < 0) {
if (argv[WINDOW_IDX] >= 0) {
if (argv[TABPAGE_IDX] < 0) {
emsg(_("E5001: Higher scope cannot be -1 if lower scope is >= 0."));
return;
}
if (scope_number[kCdScopeWindow] > 0) {
if (argv[WINDOW_IDX] > 0) {
win = find_win_by_nr(&argvars[0], tp);
if (!win) {
emsg(_("E5002: Cannot find window number."));
@@ -1046,6 +1162,10 @@ void f_haslocaldir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
assert(win);
rettv->vval.v_number = win->w_localdir ? 1 : 0;
break;
case kCdScopeBuffer:
assert(buf);
rettv->vval.v_number = buf->b_localdir ? 1 : 0;
break;
case kCdScopeTabpage:
assert(tp);
rettv->vval.v_number = tp->tp_localdir ? 1 : 0;

View File

@@ -2937,6 +2937,24 @@ int do_ecmd(int fnum, char *ffname, char *sfname, exarg_T *eap, linenr_T newlnum
redraw_curbuf_later(UPD_NOT_VALID); // redraw this buffer later
}
// A new buffer inherits the buffer-local directory of the buffer it was created from (unless
// autocommands already set one).
if (!oldbuf && curbuf->b_localdir == NULL
&& bufref_valid(&old_curbuf)
&& old_curbuf.br_buf != curbuf
&& old_curbuf.br_buf->b_localdir != NULL) {
curbuf->b_localdir = xstrdup(old_curbuf.br_buf->b_localdir);
if (old_curbuf.br_buf->b_prevdir != NULL) {
curbuf->b_prevdir = xstrdup(old_curbuf.br_buf->b_prevdir);
}
}
// If editing a buffer in the current window, make sure to update to the
// buffer's working directory.
if (oldwin == curwin) {
fix_current_dir(false);
}
// Change directories when the 'acd' option is set.
do_autochdir();

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

@@ -6212,6 +6212,9 @@ static char *get_prevdir(CdScope scope)
case kCdScopeTabpage:
return curtab->tp_prevdir;
break;
case kCdScopeBuffer:
return curbuf->b_prevdir;
break;
case kCdScopeWindow:
return curwin->w_prevdir;
break;
@@ -6222,7 +6225,7 @@ static char *get_prevdir(CdScope scope)
/// Deal with the side effects of changing the current directory.
///
/// @param scope Scope of the function call (global, tab or window).
/// @param scope Scope of the function call (global, tab, buffer or window).
static void post_chdir(CdScope scope, bool trigger_dirchanged)
{
// Always overwrite the window-local CWD.
@@ -6233,6 +6236,10 @@ static void post_chdir(CdScope scope, bool trigger_dirchanged)
XFREE_CLEAR(curtab->tp_localdir);
}
if (scope >= kCdScopeBuffer) {
XFREE_CLEAR(curbuf->b_localdir);
}
if (scope < kCdScopeGlobal) {
char *pdir = get_prevdir(scope);
// If still in global directory, set CWD as the global directory.
@@ -6253,6 +6260,9 @@ static void post_chdir(CdScope scope, bool trigger_dirchanged)
case kCdScopeTabpage:
curtab->tp_localdir = xstrdup(cwd);
break;
case kCdScopeBuffer:
curbuf->b_localdir = xstrdup(cwd);
break;
case kCdScopeWindow:
curwin->w_localdir = xstrdup(cwd);
break;
@@ -6268,9 +6278,9 @@ static void post_chdir(CdScope scope, bool trigger_dirchanged)
}
}
/// Change directory function used by :cd/:tcd/:lcd Ex commands and the chdir() function.
/// Change directory function used by :cd/:tcd/:bcd/:lcd Ex commands and the chdir() function.
/// @param new_dir The directory to change to.
/// @param scope Scope of the function call (global, tab or window).
/// @param scope Scope of the function call (global, tab, buffer or window).
/// @return true if the directory is successfully changed.
bool changedir_func(char *new_dir, CdScope scope)
{
@@ -6325,6 +6335,9 @@ bool changedir_func(char *new_dir, CdScope scope)
case kCdScopeWindow:
pp = &curwin->w_prevdir;
break;
case kCdScopeBuffer:
pp = &curbuf->b_prevdir;
break;
default:
pp = &prev_dir;
}
@@ -6336,7 +6349,7 @@ bool changedir_func(char *new_dir, CdScope scope)
return true;
}
/// ":cd", ":tcd", ":lcd", ":chdir", "tchdir" and ":lchdir".
/// ":cd", ":tcd", ":bcd", ":lcd", ":chdir", "tchdir", ":bchdir", and ":lchdir".
void ex_cd(exarg_T *eap)
{
char *new_dir = eap->arg;
@@ -6352,6 +6365,10 @@ void ex_cd(exarg_T *eap)
case CMD_tchdir:
scope = kCdScopeTabpage;
break;
case CMD_bcd:
case CMD_bchdir:
scope = kCdScopeBuffer;
break;
case CMD_lcd:
case CMD_lchdir:
scope = kCdScopeWindow;
@@ -6380,6 +6397,8 @@ static void ex_pwd(exarg_T *eap)
context = last_chdir_reason;
} else if (curwin->w_localdir != NULL) {
context = "window";
} else if (curbuf->b_localdir != NULL) {
context = "buffer";
} else if (curtab->tp_localdir != NULL) {
context = "tabpage";
}

View File

@@ -822,9 +822,8 @@ static int makeopens(FILE *fd, char *dirnow)
return FAIL;
}
// Restore the tab-local working directory if specified
// Do this before the windows, so that the window-local directory can
// override the tab-local directory.
// Restore the tab-local working directory if specified. Do this before the buffers and windows,
// so the buf-local / win-local directory can override the tab-local directory.
if ((ssop_flags & kOptSsopFlagCurdir) && tp->tp_localdir != NULL) {
if (fputs("tcd ", fd) < 0
|| ses_put_fname(fd, tp->tp_localdir, &ssop_flags) == FAIL
@@ -834,6 +833,32 @@ static int makeopens(FILE *fd, char *dirnow)
did_lcd = true;
}
// Restore buffer-local CWD (:bcd) via a one-shot BufEnter handler: there is no reliable point
// during session-load where all buffers are entered. Re-applied per tab, AFTER the ":tcd"
// above: ":tcd" clears the b_localdir of the then-current buffer, which may be a buffer whose
// autocmd already fired.
if (ssop_flags & kOptSsopFlagCurdir) {
FOR_ALL_BUFFERS(buf) {
if (buf->b_localdir == NULL || buf->b_fname == NULL || !buf->b_p_bl
|| (only_save_windows && buf->b_nwindows == 0)
|| (buf->b_help && !(ssop_flags & kOptSsopFlagHelp))
|| (bt_terminal(buf) && !(ssop_flags & kOptSsopFlagTerminal))) {
continue;
}
// `bufadd()` finds the buffer by exact (literal) name.
if (fputs("lua vim.api.nvim_create_autocmd('BufEnter', { once = true, "
"buffer = vim.fn.bufadd([=[", fd) < 0
|| fputs(ses_get_fname(buf, &ssop_flags), fd) < 0
|| fputs("]=]), callback = function() vim.cmd.bcd({ [=[", fd) < 0
|| fputs(buf->b_localdir, fd) < 0
|| fputs("]=], magic = { file = false, bar = false } }) end })\n", fd) < 0) {
return FAIL;
}
// Cwd may change when the autocmd fires: filenames must be written absolute from here on.
did_lcd = true;
}
}
// Restore the view of the window (options, file, cursor, etc.).
for (win_T *wp = tab_firstwin; wp != NULL; wp = wp->w_next) {
if (!ses_do_win(wp)) {

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

@@ -33,23 +33,21 @@ typedef enum {
///
/// Scopes are enumerated from lowest to highest. When adding a scope make sure
/// to update all functions using scopes as well, such as the implementation of
/// `getcwd()`. When using scopes as limits (e.g. in loops) don't use the scopes
/// directly, use `MIN_CD_SCOPE` and `MAX_CD_SCOPE` instead.
/// `getcwd()`.
typedef enum {
kCdScopeInvalid = -1,
kCdScopeWindow, ///< Affects one window.
kCdScopeBuffer, ///< Affects one buffer.
kCdScopeTabpage, ///< Affects one tab page.
kCdScopeGlobal, ///< Affects the entire Nvim instance.
} CdScope;
#define MIN_CD_SCOPE kCdScopeWindow
#define MAX_CD_SCOPE kCdScopeGlobal
/// What caused the current directory to change.
typedef enum {
kCdCauseOther = -1,
kCdCauseManual, ///< Using `:cd`, `:tcd`, `:lcd` or `chdir()`.
kCdCauseManual, ///< Using `:cd`, `:bcd`, `:tcd`, `:lcd` or `chdir()`.
kCdCauseWindow, ///< Switching to another window.
kCdCauseBuffer, ///< Switching to another buffer.
kCdCauseAuto, ///< On 'autochdir'.
} CdCause;

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();
fix_current_dir(true);
entering_window(curwin);
// Careful: autocommands may close the window and make "wp" invalid
@@ -5352,18 +5352,41 @@ static void win_enter_ext(win_T *const wp, const int flags)
do_autochdir();
}
/// Used after making another window the current one: change directory if needed.
void win_fix_current_dir(void)
/// Used after making another window or buffer the current one: change directory if needed.
void fix_current_dir(bool caused_by_win)
{
// New directory is either the local directory of the window, tab or NULL.
char *new_dir = curwin->w_localdir ? curwin->w_localdir : curtab->tp_localdir;
CdCause cause;
if (caused_by_win) {
cause = kCdCauseWindow;
} else {
cause = kCdCauseBuffer;
}
// New directory is either the local directory of the window, buffer, tab or NULL.
char *new_dir;
CdScope scope;
if (curwin->w_localdir) {
new_dir = curwin->w_localdir;
scope = kCdScopeWindow;
} else if (curbuf->b_localdir) {
new_dir = curbuf->b_localdir;
scope = kCdScopeBuffer;
} else if (curtab->tp_localdir) {
new_dir = curtab->tp_localdir;
scope = kCdScopeTabpage;
} else {
new_dir = NULL;
scope = kCdScopeGlobal;
}
char cwd[MAXPATHL];
if (os_dirname(cwd, MAXPATHL) != OK) {
cwd[0] = NUL;
}
if (new_dir) {
// Window/tab has a local directory: Save current directory as global
// Window/buffer/tab has a local directory: Save current directory as global
// (unless that was done already) and change to the local directory.
if (globaldir == NULL) {
if (cwd[0] != NUL) {
@@ -5372,27 +5395,25 @@ void win_fix_current_dir(void)
}
bool dir_differs = pathcmp(new_dir, cwd, -1) != 0;
if (!p_acd && dir_differs) {
do_autocmd_dirchanged(new_dir, curwin->w_localdir ? kCdScopeWindow : kCdScopeTabpage,
kCdCauseWindow, true);
do_autocmd_dirchanged(new_dir, scope, cause, true);
}
if (os_chdir(new_dir) == 0) {
if (!p_acd && dir_differs) {
do_autocmd_dirchanged(new_dir, curwin->w_localdir ? kCdScopeWindow : kCdScopeTabpage,
kCdCauseWindow, false);
do_autocmd_dirchanged(new_dir, scope, cause, false);
}
}
last_chdir_reason = NULL;
shorten_fnames(true);
} else if (globaldir != NULL) {
// Window doesn't have a local directory and we are not in the global
// Window nor buffer have a local directory and we are not in the global
// directory: Change to the global directory.
bool dir_differs = pathcmp(globaldir, cwd, -1) != 0;
if (!p_acd && dir_differs) {
do_autocmd_dirchanged(globaldir, kCdScopeGlobal, kCdCauseWindow, true);
do_autocmd_dirchanged(globaldir, kCdScopeGlobal, cause, true);
}
if (os_chdir(globaldir) == 0) {
if (!p_acd && dir_differs) {
do_autocmd_dirchanged(globaldir, kCdScopeGlobal, kCdCauseWindow, false);
do_autocmd_dirchanged(globaldir, kCdScopeGlobal, cause, false);
}
}
XFREE_CLEAR(globaldir);

View File

@@ -17,6 +17,7 @@ describe('autocmd DirChanged and DirChangedPre', function()
curdir .. '/Xtest-functional-autocmd-dirchanged.dir1',
curdir .. '/Xtest-functional-autocmd-dirchanged.dir2',
curdir .. '/Xtest-functional-autocmd-dirchanged.dir3',
curdir .. '/Xtest-functional-autocmd-dirchanged.dir4',
}
local win_dirs = {
curdir .. '\\XTEST-FUNCTIONAL-AUTOCMD-DIRCHANGED.DIR1',
@@ -62,32 +63,43 @@ describe('autocmd DirChanged and DirChangedPre', function()
eq(1, eval('g:cdprecount'))
eq(1, eval('g:cdcount'))
command('tcd ' .. dirs[2])
eq({ directory = dirs[2], scope = 'tabpage', changed_window = false }, eval('g:evpre'))
eq({ cwd = dirs[2], scope = 'tabpage', changed_window = false }, eval('g:ev'))
eq('tabpage', eval('g:amatchpre'))
eq('tabpage', eval('g:amatch'))
command('bcd ' .. dirs[2])
eq({ directory = dirs[2], scope = 'buffer', changed_window = false }, eval('g:evpre'))
eq({ cwd = dirs[2], scope = 'buffer', changed_window = false }, eval('g:ev'))
eq('buffer', eval('g:amatchpre'))
eq('buffer', eval('g:amatch'))
eq(2, eval('g:cdprecount'))
eq(2, eval('g:cdcount'))
command('cd ' .. dirs[3])
eq({ directory = dirs[3], scope = 'global', changed_window = false }, eval('g:evpre'))
eq({ cwd = dirs[3], scope = 'global', changed_window = false }, eval('g:ev'))
eq('global', eval('g:amatchpre'))
eq('global', eval('g:amatch'))
command('tcd ' .. dirs[3])
eq({ directory = dirs[3], scope = 'tabpage', changed_window = false }, eval('g:evpre'))
eq({ cwd = dirs[3], scope = 'tabpage', changed_window = false }, eval('g:ev'))
eq('tabpage', eval('g:amatchpre'))
eq('tabpage', eval('g:amatch'))
eq(3, eval('g:cdprecount'))
eq(3, eval('g:cdcount'))
command('cd ' .. dirs[4])
eq({ directory = dirs[4], scope = 'global', changed_window = false }, eval('g:evpre'))
eq({ cwd = dirs[4], scope = 'global', changed_window = false }, eval('g:ev'))
eq('global', eval('g:amatchpre'))
eq('global', eval('g:amatch'))
eq(4, eval('g:cdprecount'))
eq(4, eval('g:cdcount'))
end)
it('DirChanged set getcwd() during event #6260', function()
command('lcd ' .. dirs[1])
eq(dirs[1], eval('g:getcwd'))
command('tcd ' .. dirs[2])
command('bcd ' .. dirs[2])
eq(dirs[2], eval('g:getcwd'))
command('cd ' .. dirs[3])
command('tcd ' .. dirs[3])
eq(dirs[3], eval('g:getcwd'))
command('cd ' .. dirs[4])
eq(dirs[4], eval('g:getcwd'))
end)
it('disallow recursion', function()
@@ -216,52 +228,75 @@ describe('autocmd DirChanged and DirChangedPre', function()
eq(2, eval('g:cdcount'))
end
command('cd ' .. dirs[3])
eq({ directory = dirs[3], scope = 'global', changed_window = false }, eval('g:evpre'))
eq({ cwd = dirs[3], scope = 'global', changed_window = false }, eval('g:ev'))
eq('global', eval('g:amatch'))
command('tcd ' .. dirs[3])
eq({ directory = dirs[3], scope = 'tabpage', changed_window = false }, eval('g:evpre'))
eq({ cwd = dirs[3], scope = 'tabpage', changed_window = false }, eval('g:ev'))
eq('tabpage', eval('g:amatchpre'))
eq('tabpage', eval('g:amatch'))
eq(3, eval('g:cdprecount'))
eq(3, eval('g:cdcount'))
command('let g:evpre = {}')
command('let g:ev = {}')
command('cd ' .. dirs[3])
command('tcd ' .. dirs[3])
eq({}, eval('g:evpre'))
eq({}, eval('g:ev'))
eq(3, eval('g:cdprecount'))
eq(3, eval('g:cdcount'))
if is_os('win') then
command('cd ' .. win_dirs[3])
command('tcd ' .. win_dirs[3])
eq({}, eval('g:evpre'))
eq({}, eval('g:ev'))
eq(3, eval('g:cdprecount'))
eq(3, eval('g:cdcount'))
end
command('set autochdir')
command('split ' .. dirs[1] .. '/foo')
eq({ directory = dirs[1], scope = 'window', changed_window = false }, eval('g:evpre'))
eq({ cwd = dirs[1], scope = 'window', changed_window = false }, eval('g:ev'))
eq('auto', eval('g:amatchpre'))
eq('auto', eval('g:amatch'))
command('cd ' .. dirs[4])
eq({ directory = dirs[4], scope = 'global', changed_window = false }, eval('g:evpre'))
eq({ cwd = dirs[4], scope = 'global', changed_window = false }, eval('g:ev'))
eq('global', eval('g:amatch'))
eq(4, eval('g:cdprecount'))
eq(4, eval('g:cdcount'))
command('let g:evpre = {}')
command('let g:ev = {}')
command('split ' .. dirs[1] .. '/bar')
command('cd ' .. dirs[4])
eq({}, eval('g:evpre'))
eq({}, eval('g:ev'))
eq(4, eval('g:cdprecount'))
eq(4, eval('g:cdcount'))
if is_os('win') then
command('split ' .. win_dirs[1] .. '/baz')
command('cd ' .. dirs[4])
eq({}, eval('g:evpre'))
eq({}, eval('g:ev'))
eq(4, eval('g:cdprecount'))
eq(4, eval('g:cdcount'))
end
command('set autochdir')
command(('split %s/foo'):format(dirs[2]))
eq({ directory = dirs[2], scope = 'window', changed_window = false }, eval('g:evpre'))
eq({ cwd = dirs[2], scope = 'window', changed_window = false }, eval('g:ev'))
eq('auto', eval('g:amatchpre'))
eq('auto', eval('g:amatch'))
eq(5, eval('g:cdprecount'))
eq(5, eval('g:cdcount'))
command('let g:evpre = {}')
command('let g:ev = {}')
command(('split %s/bar'):format(dirs[2]))
eq({}, eval('g:evpre'))
eq({}, eval('g:ev'))
eq(5, eval('g:cdprecount'))
eq(5, eval('g:cdcount'))
if is_os('win') then
command(('split %s/baz'):format(win_dirs[2]))
eq({}, eval('g:evpre'))
eq({}, eval('g:ev'))
eq(5, eval('g:cdprecount'))
eq(5, eval('g:cdcount'))
end
end)
it('are triggered by switching to win/tab with different CWD #6054', function()
@@ -341,6 +376,141 @@ describe('autocmd DirChanged and DirChangedPre', function()
end
end)
it('are triggered by switching to buf/tab with different CWD', function()
local files = {
dirs[1] .. '/file',
dirs[2] .. '/file',
dirs[3] .. '/file',
}
command('e ' .. files[3]) -- buffer 3
command('e ' .. files[2]) -- buffer 2
command('e ' .. files[1]) -- buffer 1
command('bcd ' .. dirs[1])
command('b ' .. files[2]) -- Switch to buffer 2
command('bcd ' .. dirs[2])
command('b ' .. files[3]) -- Switch to buffer 3
command('bcd ' .. dirs[3])
eq({ directory = dirs[3], scope = 'buffer', changed_window = false }, eval('g:evpre'))
eq({ cwd = dirs[3], scope = 'buffer', changed_window = false }, eval('g:ev'))
eq('buffer', eval('g:amatchpre'))
eq('buffer', eval('g:amatch'))
eq(5, eval('g:cdprecount'))
eq(5, eval('g:cdcount'))
command('tabnew') -- tab 2 (tab-local CWD)
eq(5, eval('g:cdprecount')) -- same CWD, no DirChangedPre event
eq(5, eval('g:cdcount')) -- same CWD, no DirChanged event
command('tcd ' .. dirs[2])
command('tabnext') -- tab 1 (no tab-local CWD)
eq({ directory = dirs[3], scope = 'buffer', changed_window = true }, eval('g:evpre'))
eq({ cwd = dirs[3], scope = 'buffer', changed_window = true }, eval('g:ev'))
eq('buffer', eval('g:amatchpre'))
eq('buffer', eval('g:amatch'))
command('tabnext') -- tab 2
eq({ directory = dirs[2], scope = 'tabpage', changed_window = true }, eval('g:evpre'))
eq({ cwd = dirs[2], scope = 'tabpage', changed_window = true }, eval('g:ev'))
eq('tabpage', eval('g:amatchpre'))
eq('tabpage', eval('g:amatch'))
eq(8, eval('g:cdprecount'))
eq(8, eval('g:cdcount'))
command('tabnext') -- tab 1
command('b ' .. files[2]) -- buffer 2
eq(10, eval('g:cdprecount'))
eq(10, eval('g:cdcount'))
command('tabnext') -- tab 2 (has the *same* CWD)
eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event
eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event
if is_os('win') then
command('tabnew') -- tab 3
eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event
eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event
command('tcd ' .. dirs[2])
eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event
eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event
command('tabnext') -- tab 1
eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event
eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event
command('tabprevious') -- tab 3
eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event
eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event
command('tabprevious') -- tab 2
eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event
eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event
command('tabprevious') -- tab 1
eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event
eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event
command('bcd ' .. dirs[2]) -- buffer 2
eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event
eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event
command('tabnext') -- tab 2
eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event
eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event
command('tabnext') -- tab 3
eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event
eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event
command('tabnext') -- tab 1
eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event
eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event
command('tabprevious') -- tab 3
eq(10, eval('g:cdprecount')) -- same CWD, no DirChangedPre event
eq(10, eval('g:cdcount')) -- same CWD, no DirChanged event
end
end)
it('are triggered by switching to buf/win with different CWD', function()
command('lcd ' .. dirs[3]) -- window 3
command(('split %s/file'):format(dirs[2])) -- window 2
command('lcd ' .. dirs[2])
command(('split %s/file'):format(dirs[1])) -- window 1
command('lcd ' .. dirs[1])
-- All windows have a window-local CWD
command('2wincmd w') -- window 2
eq({ directory = dirs[2], scope = 'window', changed_window = true }, eval('g:evpre'))
eq({ cwd = dirs[2], scope = 'window', changed_window = true }, eval('g:ev'))
eq('window', eval('g:amatchpre'))
eq('window', eval('g:amatch'))
eq(4, eval('g:cdprecount'))
eq(4, eval('g:cdcount'))
command('bcd ' .. dirs[1]) -- window 2 now has a buffer with buffer-local CWD (and no window-local CWD)
eq({ directory = dirs[1], scope = 'buffer', changed_window = false }, eval('g:evpre'))
eq({ cwd = dirs[1], scope = 'buffer', changed_window = false }, eval('g:ev'))
eq('buffer', eval('g:amatchpre'))
eq('buffer', eval('g:amatch'))
eq(5, eval('g:cdprecount'))
eq(5, eval('g:cdcount'))
command('3wincmd w') -- window 3 (window-local CWD)
eq(6, eval('g:cdprecount'))
eq(6, eval('g:cdcount'))
command('bcd ' .. dirs[2]) -- window 3 now has a buffer with buffer-local CWD (and no window-local CWD)
command('1wincmd w') -- window 1 (window-local CWD)
eq({ directory = dirs[1], scope = 'window', changed_window = true }, eval('g:evpre'))
eq({ cwd = dirs[1], scope = 'window', changed_window = true }, eval('g:ev'))
eq('window', eval('g:amatchpre'))
eq('window', eval('g:amatch'))
command('2wincmd w') -- window 2 (buffer-local CWD)
eq({ directory = dirs[1], scope = 'window', changed_window = true }, eval('g:evpre')) -- buffer-local CWD
eq({ cwd = dirs[1], scope = 'window', changed_window = true }, eval('g:ev'))
eq('window', eval('g:amatchpre'))
eq('window', eval('g:amatch'))
eq(8, eval('g:cdprecount'))
eq(8, eval('g:cdcount'))
command('1wincmd w') -- window 1 (window-local cwd)
eq(8, eval('g:cdprecount')) -- same CWD, no DirChangedPre event
eq(8, eval('g:cdcount')) -- same CWD, no DirChanged event
command(('b %s/file'):format(dirs[2])) -- buffer 2 (has buffer-local cwd)
eq(8, eval('g:cdprecount')) -- no DirChangedPre event, window-local CWD has higher priority
eq(8, eval('g:cdcount')) -- no DirChanged event, window-local CWD has higher priority
end)
it('are triggered by nvim_set_current_dir()', function()
request('nvim_set_current_dir', dirs[1])
eq({ directory = dirs[1], scope = 'global', changed_window = false }, eval('g:evpre'))

View File

@@ -18,9 +18,12 @@ local mkdir = t.mkdir
local directories = {
tab = 'Xtest-functional-ex_cmds-cd_spec.tab', -- Tab
window = 'Xtest-functional-ex_cmds-cd_spec.window', -- Window
buffer = 'Xtest-functional-ex_cmds-cd_spec.buffer', -- Buffer
global = 'Xtest-functional-ex_cmds-cd_spec.global', -- New global
}
local tmpfile = 'Xtest-functional-ex_cmds-cd_spec-tmpfile'
-- Shorthand writing to get the current working directory
local cwd = function(...)
return call('getcwd', ...)
@@ -28,6 +31,9 @@ end -- effective working dir
local wcwd = function()
return cwd(0)
end -- window dir
local bcwd = function()
return cwd(-1, -1, 0)
end -- buffer dir
local tcwd = function()
return cwd(-1, 0)
end -- tab dir
@@ -39,6 +45,9 @@ end -- effective working dir
local wlwd = function()
return lwd(0)
end -- window dir
local blwd = function()
return lwd(-1, -1, 0)
end -- buffer dir
local tlwd = function()
return lwd(-1, 0)
end -- tab dir
@@ -134,6 +143,45 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
eq(globalDir .. pathsep .. directories.tab, cwd(-1, localtab))
eq(1, lwd(-1, localtab))
end)
it('for buffer', function()
local globalDir = directories.start
-- Create two buffers
command(('e %s1'):format(tmpfile))
command(('e %s%s%s2'):format(directories.buffer, pathsep, tmpfile))
-- Initially matches globalDir
eq(globalDir, cwd())
eq(0, lwd())
-- Change buffer-local directory to subdirectory
command('bcd ' .. directories.buffer)
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
eq(1, blwd())
-- Verify Other buffer is unchanged
command('b# ')
eq(globalDir, cwd())
eq(0, blwd())
-- A new buffer created with :edit inherits the buffer-local directory; editing an
-- existing buffer keeps that buffer's own directory.
command('b# ')
command(('e %s3'):format(tmpfile))
eq(1, blwd())
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
command(('e ..%s%s1'):format(pathsep, tmpfile))
eq(0, blwd())
eq(globalDir, cwd())
-- getcwd({winnr}) falls through to the buffer shown in that window, not the current
-- buffer.
command(('split %s%s%s2'):format(directories.buffer, pathsep, tmpfile))
command('wincmd p')
eq(globalDir, cwd())
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd(1))
eq(globalDir, cwd(2))
end)
end)
describe('getcwd(-1, -1)', function()
@@ -153,6 +201,16 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
eq(directories.start, cwd(-1, -1))
eq(0, lwd(-1, -1))
end)
it('works with buffer-local pwd', function()
command(('silent b%s %s'):format(cmd, directories.buffer))
eq(directories.start, cwd(-1, -1))
eq(0, lwd(-1, -1))
-- Must behave the same if bufnr is -1
eq(directories.start, cwd(-1, -1, -1))
eq(0, lwd(-1, -1, -1))
end)
end)
describe('Local directory gets inherited', function()
@@ -239,6 +297,137 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
eq(0, tlwd())
eq(globalDir .. pathsep .. directories.window, wcwd())
end)
it('works when mixing tab-local and buffer-local directories', function()
local globalDir = directories.start
-- Create two buffers for testing. One in each tab
command(('e %s1'):format(tmpfile))
command('tabnew')
command(('e %s2'):format(tmpfile))
-- Verify that buffer 2 has the same working directory
eq(globalDir, cwd())
eq(globalDir, tcwd()) -- Has no tab-local directory
eq(0, tlwd())
eq(globalDir, bcwd()) -- Has no buffer-local directory
eq(0, blwd())
-- Change tab-local working directory and verify it is different
command(('silent t%s %s'):format(cmd, directories.tab))
eq(('%s%s%s'):format(globalDir, pathsep, directories.tab), cwd())
eq(cwd(), tcwd()) -- Working directory matches tab directory
eq(1, tlwd())
eq(cwd(), bcwd()) -- Still no buffer-directory
eq(0, blwd())
-- Change buffer 2's buffer-local directory
command(('silent b%s ..%s%s'):format(cmd, pathsep, directories.buffer))
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
eq(('%s%s%s'):format(globalDir, pathsep, directories.tab), tcwd())
eq(cwd(), bcwd()) -- Has no buffer-directory
eq(1, blwd())
-- Verify the first tab has no local-directory
command('tabfirst')
eq(0, tlwd())
-- Verify buffer 2 has buffer-local directory even in first tab
command(('b %s2'):format(tmpfile)) -- Switch to buffer 2
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), bcwd())
eq(0, tlwd()) -- Still no tab-local directory
-- Verify buffer 1 did not have its directory changed
command('b#') -- Switch to buffer 1
eq(globalDir, cwd())
eq(0, blwd()) -- No window-buffer directory
end)
it('works when mixing window local and buffer local directories', function()
local globalDir = directories.start
-- Create a new window first and verify that is has the same working directory
command('new')
eq(globalDir, cwd())
eq(globalDir, wcwd()) -- Has no window-local directory
eq(0, tlwd())
eq(globalDir, bcwd()) -- Has no buffer-local directory
eq(0, blwd())
-- Create a buffer in current window
command('e ' .. tmpfile)
-- Change buffer-local working directory and verify it is different
command(('silent b%s %s'):format(cmd, directories.buffer))
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
eq(cwd(), bcwd()) -- Working directory matches buffer directory
eq(1, blwd())
eq(cwd(), wcwd()) -- Still no window-directory
eq(0, wlwd())
-- Change window-local directory to test `:lcd`
command(('silent l%s ../%s'):format(cmd, directories.window))
eq(('%s%s%s'):format(globalDir, pathsep, directories.window), cwd())
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), bcwd())
eq(1, blwd())
-- Verify buffer has buffer-local directory in original window
command('wincmd w')
command('b ' .. tmpfile)
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
-- Verify going to second window uses window-local directory
command('wincmd w')
eq(('%s%s%s'):format(globalDir, pathsep, directories.window), cwd())
end)
end)
end
for _, cmd in ipairs { 'bcd', 'bchdir' } do
describe(':' .. cmd, function()
before_each(function()
clear()
for _, d in pairs(directories) do
mkdir(d)
end
directories.start = cwd()
end)
after_each(function()
for _, d in pairs(directories) do
vim.uv.fs_rmdir(d)
end
end)
it('works after deleting the only buffer', function()
command(('%s %s'):format(cmd, directories.buffer))
command('bd') -- delete buffer
end)
it('makes :new use the buffer-local directory', function()
local globalDir = directories.start
command(('%s %s'):format(cmd, directories.buffer))
command(':new')
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
command('wincmd x') -- close :new window
command(':vnew')
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
command('wincmd x') -- close :vnew window
command(':enew')
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
end)
it('makes :enew use the buffer-local directory in a split', function()
local globalDir = directories.start
command(('%s %s'):format(cmd, directories.buffer))
command(':vsplit')
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
command(':enew')
eq(('%s%s%s'):format(globalDir, pathsep, directories.buffer), cwd())
end)
end)
end
@@ -272,14 +461,23 @@ for _, cmd in ipairs { 'getcwd', 'haslocaldir' } do
eq(err474, pcall_err(command, 'call ' .. cmd .. '(-2)'))
end)
local err5001 = 'Vim(call):E5001: Higher scope cannot be -1 if lower scope is >= 0.'
local err5006 = 'Vim(call):E5006: Window and tab scope must be -1 when using buffer scope'
local err5007 = 'Vim(call):E5007: Cannot find buffer number.'
it('fails on -1 if previous arg is >=0', function()
eq(err5001, pcall_err(command, 'call ' .. cmd .. '(0, -1)'))
end)
it('fails when 1st and 2nd args != -1 when 3rd arg > -1', function()
eq(err5006, pcall_err(command, ('call %s(0, 0, 0)'):format(cmd)))
eq(err5006, pcall_err(command, ('call %s(1, 2, 3)'):format(cmd)))
end)
it('fails when passing a bufnr that does not exist', function()
eq(err5007, pcall_err(command, ('call %s(-1, -1, 99999)'):format(cmd)))
end)
-- Test wrong number of arguments
local err118 = 'Vim(call):E118: Too many arguments for function: ' .. cmd
it('fails to parse more than one argument', function()
eq(err118, pcall_err(command, 'call ' .. cmd .. '(0, 0, 0)'))
eq(err118, pcall_err(command, ('call %s(0, 0, 0, 0)'):format(cmd)))
end)
end)
end

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,66 @@ describe(':mksession', function()
-- contains all splits
eq(3, #api.nvim_tabpage_list_wins(0))
end)
it('restores buffer-local working directories', function()
-- "'" and " " in the names must survive the session file's string quoting and escaping.
local tmpfile_base = file_prefix .. "-tmp'file"
local quote_dir = file_prefix .. ".b'uf d"
local session_file2 = session_file .. '2'
local cwd_dir = fn.getcwd()
mkdir(quote_dir)
finally(function()
rmdir(quote_dir)
os.remove(session_file2)
end)
command(('edit %s1'):format(tmpfile_base))
command(('edit %s2'):format(tmpfile_base))
command('bcd ' .. fn.fnameescape(quote_dir))
command('b# ')
command('mksession ' .. session_file)
command('set sessionoptions-=curdir')
command('mksession ' .. session_file2)
-- Create a new test instance of Nvim.
clear()
command('silent source ' .. session_file)
command(('b %s%s2'):format(get_pathsep(), tmpfile_base))
eq(('%s%s%s'):format(cwd_dir, get_pathsep(), quote_dir), fn.getcwd())
-- Without "curdir" in 'sessionoptions' no buffer-local directory is restored.
clear()
command('silent source ' .. session_file2)
command(('b %s%s2'):format(get_pathsep(), tmpfile_base))
eq(cwd_dir, fn.getcwd())
eq(0, fn.haslocaldir(-1, -1, 0))
end)
it('restores buffer-local working directories when used with tabs', function()
local tmpfile_base = file_prefix .. '-tmpfile'
local cwd_dir = fn.getcwd()
command(('edit %s1'):format(tmpfile_base))
command('tabnew ')
command(('edit %s2'):format(tmpfile_base))
command('tcd ' .. tab_dir)
command('bcd ..')
command('bcd ' .. buf_dir)
command('tabfirst')
command('mksession ' .. session_file)
-- Create a new test instance of Nvim.
clear()
command('source ' .. session_file)
-- First tab should have the original working directory.
command('tabnext 1')
eq(cwd_dir, fn.getcwd())
-- Second tab should have the tab-local working directory.
command('tabnext 2')
eq(('%s%s%s'):format(cwd_dir, get_pathsep(), tab_dir), fn.getcwd(-1, 0))
-- Buffer in second tab should have buffer-local working directory
eq(('%s%s%s'):format(cwd_dir, get_pathsep(), buf_dir), fn.getcwd(-1, -1, 0))
end)
end)