feat(cwd)!: :lcd! (bang), rearrange :bcd/:lcd/… scope precedence #41194

Problem:
- buf-local CWD scope is lower priority than :lcd, which is weird.
  ```
  win > buf > tab > global
  ```
- No way to clear current CWD at a given scope.

Solution:
- Rerrange scope precedence to:
  ```
  buf > win > tab > global
  ```
- Introduce "bang" variants (`:bcd!`/`:lcd!`/`:tcd!`) which clears the
  local CWD for the given scope.
This commit is contained in:
Justin M. Keyes
2026-08-07 04:41:37 -04:00
committed by GitHub
parent 0e8903f44b
commit a4a544032a
15 changed files with 452 additions and 383 deletions

View File

@@ -9,7 +9,7 @@ Editing files *edit-files*
Type |gO| to see the table of contents.
==============================================================================
1. Introduction *edit-intro*
Introduction *edit-intro*
Editing a file with Vim means:
@@ -169,7 +169,7 @@ If you want to keep the changed buffer without saving it, switch on the
'hidden' is not set, check the help for the command.
==============================================================================
2. Editing a file *edit-a-file*
Editing a file *edit-a-file*
*:e* *:edit* *reload*
:e[dit][!] [++opt] [+cmd]
@@ -615,7 +615,7 @@ filename-expansion phase, so some parts of `path` are still expanded: >lua
<
==============================================================================
3. The argument list *argument-list* *arglist*
The argument list *argument-list* *arglist*
If you give more than one file name when starting Vim, this list is remembered
as the argument list. You can jump to each file in this list.
@@ -929,7 +929,7 @@ flag is used for the ":substitute" command to avoid an error for files where
"my_foo" isn't used. ":update" writes the file only if changes were made.
==============================================================================
4. Writing *writing* *save-file*
Writing *writing* *save-file*
Note: When the 'write' option is off, you are not able to write any file.
@@ -1155,7 +1155,7 @@ For MS-Windows the device is detected by its name:
The names can be in upper- or lowercase.
==============================================================================
5. Writing and quitting *write-quit*
Writing and quitting *write-quit*
*:q* *:quit*
:q[uit] Quit the current window. Quit Vim if this is the last
@@ -1268,7 +1268,7 @@ MULTIPLE WINDOWS AND BUFFERS *window-exit*
which cannot be written for another reason, Vim will not quit.
==============================================================================
6. Dialogs *edit-dialogs*
Dialogs *edit-dialogs*
*:confirm* *:conf*
:conf[irm] {command} Execute {command}, and use a dialog when an
@@ -1346,92 +1346,114 @@ use has("browsefilter"): >
endif
==============================================================================
7. The current directory *current-directory*
The current directory *current-directory*
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".
The "effective working directory" 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 a directory at a given scope.
Use |getcwd()| to get the effective directory from the current scope. The
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.
The precedence of scopes is ordered from narrowest to widest: >
*:cd* *E747* *E472*
:cd[!] On non-Unix systems when 'cdhome' is off: Print the
current directory name.
Otherwise: Change the current directory to the home
directory. Clear any window-local directory.
Use |:pwd| to print the current directory on all
systems.
buffer > window > tab > global
Use |getcwd()| to get the effective directory, and `:verbose pwd` to also see
which scope it came from.
Setting a directory at a higher scope clears the lower ones: |:tcd| clears the
window-local directory and |:cd| clears both. But the buffer-local directory
is never cleared, except by |:bcd| or |:bcd!|.
:chd[ir] *:cd* *:chd* *:chdir* *E747* *E472*
:cd Change the current directory to the home directory,
unsetting the same directories as ":cd {path}".
If 'cdhome' is disabled this only prints the name.
Use |:pwd| to print it regardless of 'cdhome'.
:cd {path} Change the current directory to {path}, resolved
against 'cdpath' (if {path} is relative). On
MS-Windows this also changes the active drive.
Clears the window-local and tab-local directories (on
the current window/tab), but not the buffer-local one.
:cd[!] {path} Change the current directory to {path}.
If {path} is relative, it is searched for in the
directories listed in 'cdpath'.
Clear any window-local directory.
Does not change the meaning of an already opened file,
because its full path name is remembered. Files from
the |arglist| may change though!
On MS-Windows this also changes the active drive.
To change to the directory of the current file: >
:cd %:h
<
Does not change the meaning of an already opened file,
because its full path name is remembered. Files from
the |arglist| may change though!
*:cd-* *E186*
:cd[!] - Change to the previous current directory (before the
previous ":cd {path}" command).
:cd - Change to previous current-directory (before the last
":cd {path}" command).
*:chd* *:chdir*
:chd[ir][!] [path] Same as |:cd|.
:tch[dir] *:tc* *:tcd* *:tch* *:tchdir*
:tc[d] [path] Like |:cd|, but only set the directory for the current
tab. Clears the window-local directory of the current
window, but not the buffer-local one.
*:tc* *:tcd*
:tc[d][!] {path} Like |:cd|, but only set the directory for the current
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
previous ":tcd {path}" command).
*:tch* *:tchdir*
:tch[dir][!] Same as |:tcd|.
*:lc* *:lcd*
:lc[d][!] {path} Like |:cd|, but only set the current directory for the
current window. The current directory for other
tabs, windows, or buffers is not changed.
*:lch* *:lchdir*
:lch[dir][!] Same as |:lcd|.
*:lcd-*
: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
Does not change the current directory for other tabs,
nor any other window or buffer with its own local
directory.
*:bcd-*
:bc[d][!] - Change to the previous buffer-local current directory
(before the last ":bcd [path]" command).
"Sticky" behavior: a new tab inherits the current
tab-local directory.
*:bch* *:bchdir*
:bch[dir][!] Same as |:bcd|.
*:tcd-*
:tc[d] - Change to previous tab-local directory (before the
last ":tcd {path}" command).
:lch[dir] *:lc* *:lcd* *:lch* *:lchdir*
:lc[d] [path] Like |:cd|, but only set the directory for the current
window. Does not change the current directory for
other tabs, windows or buffers.
The specified directory becomes the current directory
whenever that window is entered, unless its current
buffer has a buffer-local (:bcd) directory.
"Sticky" behavior: a new window inherits the current
window-local directory.
*:lcd-*
:lc[d] - Change to previous window-local directory (before the
last ":lcd {path}" command).
:bch[dir] *:bc* *:bcd* *:bch* *:bchdir*
:bc[d] [path] Like |:cd|, but only set the directory for the current
buffer. Does not change the current directory for
tabs, windows or other buffers.
Not "sticky": unlike :lcd and :tcd, a new window or
buffer (|:edit|, |:enew|, …) will not "inherit" the
current buffer-local directory. Use |:tcd| or |:lcd|
if you want "sticky" behavior.
See also |project-dir|.
*:bcd-*
:bc[d] - Change to previous buffer-local directory (before the
last ":bcd {path}" command).
*:lcd!* *:tcd!* *:bcd!*
:lc[d]! Unset the window-local current-directory.
:tc[d]! Unset the tab-local current-directory.
:bc[d]! Unset the buffer-local current-directory.
Each unsets only its own scope, so the next-higher
scope takes effect. No-op if the scope has no local
directory.
The global directory always exists, so ":cd!" is the
same as |:cd|.
*:pw* *:pwd* *E187*
:pw[d] Print the current directory name.
Also see |getcwd()|.
See also |getcwd()|.
*:pwd-verbose*
When 'verbose' is non-zero, |:pwd| will also display
what scope the current directory was set. Example: >
@@ -1444,52 +1466,9 @@ scope in the hierarchy applies.
:verbose pwd
[tabpage] /path/to/current
" 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 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.
When a |:cd| command is used, the current window and tab will lose their local
current directories and will use the global current directory from now on.
When a |:tcd| command is used, only the current window will lose its local
working directory.
After using |:cd| the full path name will be used for reading and writing
files. On some networked file systems this may cause problems. The result of
using the full path name is that the file names currently in use will remain
referring to the same file. Example: If you have a file a:test and a
directory a:vim the commands ":e test" ":cd vim" ":w" will overwrite the file
a:test and not write a:vim/test. But if you do ":w test" the file a:vim/test
will be written, because you gave a new file name and did not refer to a
filename before the ":cd".
PROJECT DIRECTORY *project-dir* *workspace-dir*
------------------------------------------------------------------------------
Project directory *project-dir* *workspace-dir*
You can use |:bcd| to assign a "workspace" (or "project directory") to each
buffer, so commands like |:make|, |:grep| and |:terminal| always run relative
@@ -1511,7 +1490,7 @@ See also:
- |terminal-osc7|
==============================================================================
8. Editing binary files *edit-binary*
Editing binary files *edit-binary*
Although Vim was made to edit text files, it is possible to edit binary
files. The |-b| Vim argument (b for binary) makes Vim do file I/O in binary
@@ -1545,7 +1524,7 @@ There are a few things to remember when editing binary files:
in the text).
==============================================================================
9. Encryption *encryption*
Encryption *encryption*
*:X* *E817* *E818* *E819* *E820*
Support for editing encrypted files has been removed.
@@ -1553,7 +1532,7 @@ Support for editing encrypted files has been removed.
https://github.com/neovim/neovim/issues/701
==============================================================================
10. Timestamps *timestamp* *timestamps*
Timestamps *timestamp* *timestamps*
Vim remembers the modification timestamp, mode and size of a file when you
begin editing it. This is used to avoid that you have two different versions
@@ -1669,7 +1648,7 @@ in the Win32 libraries that confuses Vim about the hour time difference. The
problem goes away the next day.
==============================================================================
11. File Searching *file-searching*
File Searching *file-searching*
The file searching is currently used for the 'path', 'cdpath' and 'tags'
options, for |finddir()| and |findfile()|. Other commands use |wildcards|
@@ -1768,7 +1747,7 @@ There are three different types of searching:
with depth limiter (/usr/**2) or upward search (;) notations.
==============================================================================
12. Trusted Files *trust*
Trusted Files *trust*
Nvim executes arbitrary code found on the filesystem if 'exrc' is enabled. To
prevent executing malicious code, only "trusted files" are executed. You can
@@ -1798,7 +1777,7 @@ mark a file as trusted or untrusted using the |:trust| command or the
trust or deny it.
==============================================================================
13. Log Files *log-files*
Log Files *log-files*
Nvim keeps log files in `stdpath("log")`. See |standard-path| for where that
location is on your system.

View File

@@ -251,6 +251,7 @@ EDITOR
• |:bcd| sets a buffer-local directory. |getcwd()| and |haslocaldir()| take
`bufnr` as a third parameter.
• |:lcd!| |:tcd!| |:bcd!| unsets the directory of that scope.
• |: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

@@ -1110,31 +1110,22 @@ charidx({string}, {idx} [, {countcc} [, {utf16}]]) *charidx()*
(`integer`)
chdir({dir} [, {scope}]) *chdir()*
Lua: Prefer |nvim_set_current_dir()| for the global directory;
tab-local, window-local, and return semantics differ.
Sets the |current-directory| of the given {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|
Changes the current working directory to {dir}. The scope of
the change is determined as follows:
If {scope} is not present, the current working directory is
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 not given it is decided as follows:
- If buffer-local directory (|:bcd|) is set, scope is
"buffer".
- If the window-local directory (|:lcd|) is set, scope is
"window".
- If the tabpage-local directory (|:tcd|) is set, scope is
"tabpage".
- Otherwise, scope is "global".
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|
{dir} must be a String.
If successful, returns the previous working directory. Pass
this to another chdir() to restore the directory.
If successful, returns the previous working directory.
On failure, returns an empty string.
Example: >vim
@@ -3750,44 +3741,39 @@ getcursorcharpos([{winid}]) *getcursorcharpos()*
Return: ~
(`any`)
*getcwd()* *E5000* *E5001* *E5002* *E5006* *E5007*
*getcwd()* *E5000* *E5002* *E5006* *E5007*
getcwd([{winnr} [, {tabnr} [, {bufnr}]]])
Lua: Prefer |uv.cwd()| for the global working directory;
tab-local and window-local scopes differ.
Without arguments, returns the effective |current-directory|.
With {winnr} (|window-number| or |window-ID|), {tabnr} or
{bufnr} the working directory of that scope is returned,
ignoring 'autochdir'.
With no arguments, returns the name of the effective
|current-directory|. With {winnr} or {tabnr} or {bufnr} the
working directory of that scope is returned, and 'autochdir'
is ignored.
- If {winnr} is -1: gets the tabpage directory.
- If {winnr} and {tabnr} are both -1: gets the global
directory.
- Note: Vim returns an empty string when {tabnr} is -1.
- If {bufnr} is given: gets the buffer-local directory.
({winnr} and {tabnr} must be -1.)
- An argument may be -1 only if preceding args are -1. *E5001*
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|.
getcwd(0)
getcwd(0, 0)
<
Each form reports its own scope or "wider", so e.g. {winnr}
never reports a buffer-local directory. The {bufnr} form
falls back to the global directory, because a buffer belongs
to no particular window or tabpage.
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}.
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
Examples: >vim
getcwd(-1, -1, 0) " Get current buffer's directory
getcwd(-1, -1, 3) " Get directory of buffer #3
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?`)

View File

@@ -947,30 +947,22 @@ function vim.fn.charcol(expr, winid) end
--- @return integer
function vim.fn.charidx(string, idx, countcc, utf16) end
--- Lua: Prefer |nvim_set_current_dir()| for the global directory; tab-local, window-local, and return semantics differ.
--- Sets the |current-directory| of the given {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|
---
--- Changes the current working directory to {dir}. The scope of
--- the change is determined as follows:
--- If {scope} is not present, the current working directory is
--- 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 not given it is decided as follows:
--- - If buffer-local directory (|:bcd|) is set, scope is
--- "buffer".
--- - If the window-local directory (|:lcd|) is set, scope is
--- "window".
--- - If the tabpage-local directory (|:tcd|) is set, scope is
--- "tabpage".
--- - Otherwise, scope is "global".
---
--- 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|
---
--- {dir} must be a String.
--- If successful, returns the previous working directory. Pass
--- this to another chdir() to restore the directory.
--- If successful, returns the previous working directory.
--- On failure, returns an empty string.
---
--- Example: >vim
@@ -3321,41 +3313,37 @@ function vim.fn.getcurpos(winid) end
--- @return any
function vim.fn.getcursorcharpos(winid) end
--- Lua: Prefer |uv.cwd()| for the global working directory; tab-local and window-local scopes differ.
--- Without arguments, returns the effective |current-directory|.
--- With {winnr} (|window-number| or |window-ID|), {tabnr} or
--- {bufnr} the working directory of that scope is returned,
--- ignoring 'autochdir'.
---
--- With no arguments, returns the name of the effective
--- |current-directory|. With {winnr} or {tabnr} or {bufnr} the
--- working directory of that scope is returned, and 'autochdir'
--- is ignored.
--- - If {winnr} is -1: gets the tabpage directory.
--- - If {winnr} and {tabnr} are both -1: gets the global
--- directory.
--- - Note: Vim returns an empty string when {tabnr} is -1.
--- - If {bufnr} is given: gets the buffer-local directory.
--- ({winnr} and {tabnr} must be -1.)
--- - An argument may be -1 only if preceding args are -1. *E5001*
---
--- 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|.
--- getcwd(0)
--- getcwd(0, 0)
--- <
--- Each form reports its own scope or "wider", so e.g. {winnr}
--- never reports a buffer-local directory. The {bufnr} form
--- falls back to the global directory, because a buffer belongs
--- to no particular window or tabpage.
---
--- 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}.
---
--- 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
--- Examples: >vim
--- getcwd(-1, -1, 0) " Get current buffer's directory
--- getcwd(-1, -1, 3) " Get directory of buffer #3
--- 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

View File

@@ -690,7 +690,7 @@ void nvim_set_current_dir(String dir, Error *err)
});
TRY_WRAP(err, {
changedir_func(dir.data, kCdScopeGlobal);
do_chdir(dir.data, kCdScopeGlobal);
});
}

View File

@@ -1254,28 +1254,22 @@ M.funcs = {
args = { 1, 2 },
base = 1,
desc = [=[
Changes the current working directory to {dir}. The scope of
the change is determined as follows:
If {scope} is not present, the current working directory is
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.
Sets the |current-directory| of the given {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|
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|
If {scope} is not given it is decided as follows:
- If buffer-local directory (|:bcd|) is set, scope is
"buffer".
- If the window-local directory (|:lcd|) is set, scope is
"window".
- If the tabpage-local directory (|:tcd|) is set, scope is
"tabpage".
- Otherwise, scope is "global".
{dir} must be a String.
If successful, returns the previous working directory. Pass
this to another chdir() to restore the directory.
If successful, returns the previous working directory.
On failure, returns an empty string.
Example: >vim
@@ -1291,9 +1285,6 @@ M.funcs = {
params = { { 'dir', 'string' }, { 'scope', 'string' } },
returns = 'string',
signature = 'chdir({dir} [, {scope}])',
see_lua = {
'|nvim_set_current_dir()| for the global directory; tab-local, window-local, and return semantics differ',
},
},
cindent = {
args = 1,
@@ -4098,48 +4089,43 @@ M.funcs = {
args = { 0, 3 },
base = 1,
desc = [=[
With no arguments, returns the name of the effective
|current-directory|. With {winnr} or {tabnr} or {bufnr} the
working directory of that scope is returned, and 'autochdir'
is ignored.
Without arguments, returns the effective |current-directory|.
With {winnr} (|window-number| or |window-ID|), {tabnr} or
{bufnr} the working directory of that scope is returned,
ignoring 'autochdir'.
- If {winnr} is -1: gets the tabpage directory.
- If {winnr} and {tabnr} are both -1: gets the global
directory.
- Note: Vim returns an empty string when {tabnr} is -1.
- If {bufnr} is given: gets the buffer-local directory.
({winnr} and {tabnr} must be -1.)
- An argument may be -1 only if preceding args are -1. *E5001*
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|.
getcwd(0)
getcwd(0, 0)
<
Each form reports its own scope or "wider", so e.g. {winnr}
never reports a buffer-local directory. The {bufnr} form
falls back to the global directory, because a buffer belongs
to no particular window or tabpage.
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}.
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
Examples: >vim
getcwd(-1, -1, 0) " Get current buffer's directory
getcwd(-1, -1, 3) " Get directory of buffer #3
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' }, { 'bufnr', 'integer' } },
returns = 'string',
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',
},
tags = { 'E5000', 'E5002', 'E5006', 'E5007' },
},
getenv = {
args = 1,

View File

@@ -420,15 +420,15 @@ void f_chdir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
semsg(_(e_invargNval), "scope", s);
return;
}
} else if (curwin->w_localdir != NULL) {
scope = kCdScopeWindow;
} else if (curbuf->b_localdir != NULL) {
scope = kCdScopeBuffer;
} else if (curwin->w_localdir != NULL) {
scope = kCdScopeWindow;
} else if (curtab->tp_localdir != NULL) {
scope = kCdScopeTabpage;
}
if (!changedir_func(argvars[0].vval.v_string, scope)) {
if (!do_chdir(argvars[0].vval.v_string, scope)) {
// Directory change failed
XFREE_CLEAR(rettv->vval.v_string);
}
@@ -688,7 +688,7 @@ static bool getcwd_scope_args(typval_T *argvars, CdScope default_scope, CdScope
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."));
emsg(_("E5001: Argument cannot be -1 if preceding argument is >= 0."));
return false;
}
}
@@ -748,16 +748,12 @@ static bool getcwd_scope_args(typval_T *argvars, CdScope default_scope, CdScope
}
// 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;
}
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;
@@ -797,14 +793,16 @@ void f_getcwd(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
break;
}
FALLTHROUGH;
case kCdScopeBuffer:
from = buf->b_localdir;
case kCdScopeTabpage:
from = tp->tp_localdir;
if (from) {
break;
}
FALLTHROUGH;
case kCdScopeTabpage:
from = tp->tp_localdir;
case kCdScopeBuffer:
// Only a buffer target reaches this with `scope` unchanged: a buffer belongs to no particular
// window or tabpage, so it skips those and falls back to global dir.
from = scope == kCdScopeBuffer ? buf->b_localdir : NULL;
if (from) {
break;
}

View File

@@ -6225,18 +6225,21 @@ static char **get_prevdir(CdScope scope)
/// @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.
XFREE_CLEAR(curwin->w_localdir);
// Only :bcd overwrites the buffer-local CWD.
if (scope == kCdScopeBuffer) {
XFREE_CLEAR(curbuf->b_localdir);
}
// Overwrite the window-local CWD for :cd, :tcd, :lcd.
if (scope >= kCdScopeWindow) {
XFREE_CLEAR(curwin->w_localdir);
}
// Overwrite the tab-local CWD for :cd, :tcd.
if (scope >= kCdScopeTabpage) {
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.
@@ -6252,7 +6255,11 @@ static void post_chdir(CdScope scope, bool trigger_dirchanged)
switch (scope) {
case kCdScopeGlobal:
// We are now in the global directory, no need to remember its name.
// Unless a buffer-local CWD is active, then it is in effect and must be remembered.
XFREE_CLEAR(globaldir);
if (curbuf->b_localdir != NULL) {
globaldir = xstrdup(cwd);
}
break;
case kCdScopeTabpage:
curtab->tp_localdir = xstrdup(cwd);
@@ -6275,11 +6282,13 @@ static void post_chdir(CdScope scope, bool trigger_dirchanged)
}
}
/// 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, window or buffer).
/// @return true if the directory is successfully changed.
bool changedir_func(char *new_dir, CdScope scope)
/// Performs explicit (user) chdir (:cd/:tcd/:lcd/:bcd/chdir()/nvim_set_current_dir()). Unlike
/// do_autochdir() and update_cwd(), this sets the `scope` dir and reports kCdCauseManual.
///
/// @param new_dir Directory to change to.
/// @param scope Set the directory on this scope.
/// @return true if the directory was successfully changed.
bool do_chdir(char *new_dir, CdScope scope)
{
if (new_dir == NULL || allbuf_locked()) {
return false;
@@ -6312,9 +6321,16 @@ bool changedir_func(char *new_dir, CdScope scope)
new_dir = TO_SLASH_SAVE(new_dir);
// Buffer-local CWD is never "cleared" by :lcd/:tcd/:cd, so it stays in effect.
const bool bcd_active = scope != kCdScopeBuffer && curbuf->b_localdir != NULL;
bool dir_differs = pdir == NULL || pathcmp(pdir, new_dir, -1) != 0;
if (dir_differs) {
do_autocmd_dirchanged(new_dir, scope, kCdCauseManual, true);
if (!bcd_active) {
do_autocmd_dirchanged(new_dir, scope, kCdCauseManual, true);
}
// Enter `new_dir` even if it will not apply (because of buf-local dir):
// post_chdir() reads back its resolved name.
if (vim_chdir(new_dir) != 0) {
emsg(_(e_failed));
xfree(new_dir);
@@ -6328,21 +6344,37 @@ bool changedir_func(char *new_dir, CdScope scope)
xfree(*pp);
*pp = pdir;
post_chdir(scope, dir_differs);
post_chdir(scope, dir_differs && !bcd_active);
if (bcd_active) {
os_chdir(curbuf->b_localdir);
shorten_fnames(true);
}
ctx_did_chdir();
return true;
}
/// ":lcd!", ":tcd!", ":bcd!": Clears the scope-local directory. No-op if scope has no local dir.
static void chdir_clear(CdScope scope)
{
char **localdir = &curtab->tp_localdir;
if (scope == kCdScopeBuffer) {
localdir = &curbuf->b_localdir;
} else if (scope == kCdScopeWindow) {
localdir = &curwin->w_localdir;
}
if (*localdir == NULL) {
return;
}
XFREE_CLEAR(*localdir);
update_cwd(kCdCauseManual);
ctx_did_chdir();
}
/// ":cd", ":tcd", ":lcd", ":bcd", ":chdir", ":tchdir", ":lchdir", and ":bchdir".
void ex_cd(exarg_T *eap)
{
char *new_dir = eap->arg;
// for non-UNIX ":cd" means: print current directory unless 'cdhome' is set
if (*new_dir == NUL && !p_cdh) {
ex_pwd(NULL);
return;
}
CdScope scope = kCdScopeGlobal;
switch (eap->cmdidx) {
@@ -6361,7 +6393,23 @@ void ex_cd(exarg_T *eap)
default:
break;
}
if (changedir_func(new_dir, scope)) {
// ":lcd!", ":tcd!", ":bcd!": unset that scope.
if (eap->forceit && *new_dir == NUL && scope != kCdScopeGlobal) {
chdir_clear(scope);
if (KeyTyped || p_verbose >= 5) {
ex_pwd(eap);
}
return;
}
// for non-UNIX ":cd" means: print current directory unless 'cdhome' is set
if (*new_dir == NUL && !p_cdh) {
ex_pwd(NULL);
return;
}
if (do_chdir(new_dir, scope)) {
// Echo the new current directory if the command was typed.
if (KeyTyped || p_verbose >= 5) {
ex_pwd(eap);
@@ -6380,10 +6428,10 @@ static void ex_pwd(exarg_T *eap)
char *context = "global";
if (last_chdir_reason != NULL) {
context = last_chdir_reason;
} else if (curwin->w_localdir != NULL) {
context = "window";
} else if (curbuf->b_localdir != NULL) {
context = "buffer";
} else if (curwin->w_localdir != NULL) {
context = "window";
} else if (curtab->tp_localdir != NULL) {
context = "tabpage";
}

View File

@@ -764,10 +764,8 @@ 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.
// Restore the tab-local working directory before the windows and buffers, so that 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;

View File

@@ -648,15 +648,11 @@ static int nlua_with(lua_State *L)
TRY_WRAP(&err, {
CtxSwitch cs = { 0 };
bool switched = true;
CtxSwitchFlags dirs = keepcwd ? kCtxKeepDirs : kCtxKeepCwd;
if (win) {
tabpage_T *tabpage = win_find_tabpage(win);
switched = ctx_switch(&cs, win, tabpage, NULL, kCtxNoDisplay | kCtxValidate | dirs);
} else if (buf) {
ctx_switch(&cs, NULL, NULL, buf, dirs);
} else if (keepcwd) {
ctx_switch(&cs, NULL, NULL, NULL, kCtxKeepDirs);
if (win || buf || keepcwd) {
CtxSwitchFlags dirs = keepcwd ? kCtxKeepDirs : kCtxKeepCwd;
tabpage_T *tab = win ? win_find_tabpage(win) : NULL;
switched = ctx_switch(&cs, win, tab, buf, kCtxNoDisplay | dirs | (win ? kCtxValidate : 0));
}
if (switched) {

View File

@@ -29,15 +29,11 @@ typedef enum {
kBroken,
} WorkingStatus;
/// The scope of a working-directory command like `:cd`.
///
/// 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()`.
/// The scope of a working-directory command like `:cd`. Enumerated from lowest to highest.
typedef enum {
kCdScopeInvalid = -1,
kCdScopeWindow, ///< Affects one window.
kCdScopeBuffer, ///< Affects one buffer.
kCdScopeWindow, ///< Affects one window.
kCdScopeTabpage, ///< Affects one tab page.
kCdScopeGlobal, ///< Affects the entire Nvim instance.
} CdScope;

View File

@@ -5362,12 +5362,12 @@ void update_cwd(CdCause cause)
char *new_dir;
CdScope scope;
if (curwin->w_localdir) {
new_dir = curwin->w_localdir;
scope = kCdScopeWindow;
} else if (curbuf->b_localdir) {
if (curbuf->b_localdir) {
new_dir = curbuf->b_localdir;
scope = kCdScopeBuffer;
} else if (curwin->w_localdir) {
new_dir = curwin->w_localdir;
scope = kCdScopeWindow;
} else if (curtab->tp_localdir) {
new_dir = curtab->tp_localdir;
scope = kCdScopeTabpage;

View File

@@ -72,42 +72,60 @@ describe('autocmd DirChanged and DirChangedPre', function()
eq(1, eval('g:cdprecount'))
eq(1, eval('g:cdcount'))
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'))
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'))
eq(2, eval('g:cdprecount'))
eq(2, eval('g:cdcount'))
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'))
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'))
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'))
command('bcd ' .. dirs[4])
eq({ directory = dirs[4], scope = 'buffer', changed_window = false }, eval('g:evpre'))
eq({ cwd = dirs[4], scope = 'buffer', changed_window = false }, eval('g:ev'))
eq('buffer', eval('g:amatchpre'))
eq('buffer', eval('g:amatch'))
eq(4, eval('g:cdprecount'))
eq(4, eval('g:cdcount'))
-- Buffer-local dir is NOT cleared by a wider scope.
assert_no_event('lcd ' .. dirs[1])
assert_no_event('tcd ' .. dirs[2])
assert_no_event('cd ' .. dirs[3])
-- ":bcd!" reports the scope that becomes effective, not the one that was unset.
-- The overridden ":cd" above still reset the window/tab scopes, so that is the global dir.
command('bcd!')
eq({ cwd = dirs[3], scope = 'global', changed_window = false }, eval('g:ev'))
eq('global', eval('g:amatch'))
-- Same for ":lcd!", landing on the tab-local dir.
command('tcd ' .. dirs[2])
command('lcd ' .. dirs[1])
command('lcd!')
eq({ cwd = dirs[2], scope = 'tabpage', changed_window = false }, eval('g:ev'))
eq('tabpage', eval('g:amatch'))
end)
it('DirChanged set getcwd() during event #6260', function()
command('lcd ' .. dirs[1])
eq(dirs[1], eval('g:getcwd'))
command('bcd ' .. dirs[2])
command('tcd ' .. dirs[2])
eq(dirs[2], eval('g:getcwd'))
command('tcd ' .. dirs[3])
command('cd ' .. dirs[3])
eq(dirs[3], eval('g:getcwd'))
command('cd ' .. dirs[4])
command('bcd ' .. dirs[4])
eq(dirs[4], eval('g:getcwd'))
end)
@@ -192,7 +210,8 @@ describe('autocmd DirChanged and DirChangedPre', function()
it('not triggered if directory has not changed', function()
local scopes = { lcd = 'window', bcd = 'buffer', tcd = 'tabpage', cd = 'global' }
for i, cmd in ipairs({ 'lcd', 'bcd', 'tcd', 'cd' }) do
-- ":bcd" last: a buffer-local dir overrides the other scopes.
for i, cmd in ipairs({ 'lcd', 'tcd', 'cd', 'bcd' }) do
local scope = scopes[cmd]
command(('%s %s'):format(cmd, dirs[i]))
eq({ directory = dirs[i], scope = scope, changed_window = false }, eval('g:evpre'))
@@ -207,6 +226,7 @@ describe('autocmd DirChanged and DirChangedPre', function()
end
end
command('bcd!') -- 5th event: the global dir becomes effective again.
command('set autochdir')
command(('split %s/foo'):format(dirs[2]))
@@ -214,8 +234,8 @@ describe('autocmd DirChanged and DirChangedPre', function()
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'))
eq(6, eval('g:cdprecount'))
eq(6, eval('g:cdcount'))
assert_no_event(('split %s/bar'):format(dirs[2]))
if is_os('win') then
assert_no_event(('split %s/baz'):format(win_dirs[2]))

View File

@@ -29,7 +29,7 @@ local function join(...)
return table.concat({ ... }, pathsep)
end
-- Shorthand writing to get the current working directory
-- Get the current working directory.
local cwd = function(...)
return call('getcwd', ...)
end -- effective working dir
@@ -110,16 +110,16 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
eq(1, lwd(localwin, tabnr))
command('tabnew')
-- From new tab page, original window reports global dir
-- From new tabpage, original window reports global dir
eq(globalDir, cwd(globalwin, tabnr))
eq(0, lwd(globalwin, tabnr))
-- From new tab page, local window reports as such
-- From new tabpage, local window reports as such
eq(globalDir .. pathsep .. directories.window, cwd(localwin, tabnr))
eq(1, lwd(localwin, tabnr))
end)
it('for tab page', function()
it('for tabpage', function()
local globalDir = startdir
local globaltab = call('tabpagenr')
@@ -133,7 +133,7 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
command('silent t' .. cmd .. ' ' .. directories.tab)
local localtab = call('tabpagenr')
-- From local tab page, original tab reports globalDir
-- From local tabpage, original tab reports globalDir
eq(globalDir, cwd(-1, globaltab))
eq(0, lwd(-1, globaltab))
@@ -144,7 +144,7 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
eq(1, lwd(-1, localtab))
command('tabnext')
-- From original tab page, local reports as such
-- From original tabpage, local reports as such
eq(globalDir .. pathsep .. directories.tab, cwd(-1, localtab))
eq(1, lwd(-1, localtab))
end)
@@ -183,13 +183,23 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
eq(0, blwd())
eq(globalDir, cwd())
-- getcwd({winnr}) falls through to the buffer shown in that window, not the current
-- buffer.
-- getcwd({winnr}) reports the window's own scope chain: the buffer scope is separate, and
-- visible only via the {bufnr} form.
command(('split %s%s%s2'):format(directories.buffer, pathsep, tmpfile))
local bufnr = call('winbufnr', 1)
command('wincmd p')
eq(globalDir, cwd())
eq(join(globalDir, directories.buffer), cwd(1))
eq(globalDir, cwd(2))
eq({ globalDir, globalDir }, { cwd(1), cwd(2) })
eq(join(globalDir, directories.buffer), cwd(-1, -1, bufnr))
-- The {bufnr} form skips window and tab: a buffer belongs to no particular window/tabpage.
command('lcd ' .. directories.window)
command('tcd ' .. join('..', directories.tab))
eq(
{ join(globalDir, directories.tab), join(globalDir, directories.tab) },
{ cwd(0), tcwd() }
)
eq(globalDir, cwd(-1, -1, 0))
end)
end)
@@ -199,19 +209,19 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
eq(0, lwd(-1, -1))
end)
it('works with tab-local pwd', function()
it('with tab-local dir', function()
command('silent t' .. cmd .. ' ' .. directories.tab)
eq(startdir, cwd(-1, -1))
eq(0, lwd(-1, -1))
end)
it('works with window-local pwd', function()
it('with window-local dir', function()
command('silent l' .. cmd .. ' ' .. directories.window)
eq(startdir, cwd(-1, -1))
eq(0, lwd(-1, -1))
end)
it('works with buffer-local pwd', function()
it('with buffer-local dir', function()
command(('silent b%s %s'):format(cmd, directories.buffer))
eq(startdir, cwd(-1, -1))
eq(0, lwd(-1, -1))
@@ -327,7 +337,7 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
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(globalDir, bcwd()) -- Still no buffer-directory: the buffer form skips the tab scope
eq(0, blwd())
-- Change buffer 2's buffer-local directory
@@ -369,21 +379,24 @@ for _, cmd in ipairs { 'cd', 'chdir' } do
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(globalDir, wcwd()) -- Still no window-directory
eq(0, wlwd())
-- Change window-local directory to test `:lcd`
-- :lcd sets the window-local dir WITHOUT clearing the narrower buffer-local one.
command(('silent l%s ../%s'):format(cmd, directories.window))
eq(join(globalDir, directories.window), cwd())
eq(join(globalDir, directories.buffer), bcwd())
eq(1, blwd())
eq(join(globalDir, directories.buffer), cwd())
eq({ 1, 1 }, { blwd(), wlwd() })
eq(join(globalDir, directories.window), wcwd())
-- Verify buffer has buffer-local directory in original window
-- Window-local dir applies if the buffer-local one is unset.
command(('silent b%s!'):format(cmd))
eq(0, blwd())
eq(join(globalDir, directories.window), cwd())
-- Verify other window is unaffected.
command('wincmd w')
command('b ' .. tmpfile)
eq(join(globalDir, directories.buffer), cwd())
-- Verify going to second window uses window-local directory
eq(globalDir, cwd())
command('wincmd w')
eq(join(globalDir, directories.window), cwd())
end)
@@ -424,9 +437,68 @@ for _, cmd in ipairs { 'bcd', 'bchdir' } do
eq(startdir, cwd())
eq(0, blwd())
end)
it('is not cleared or overridden by :lcd/:tcd/:cd', function()
local bufdir = join(startdir, directories.buffer)
command('edit ' .. tmpfile)
command(('%s %s'):format(cmd, directories.buffer))
-- Paths are relative to `bufdir`, which stays in effect throughout.
command('lcd ' .. join('..', directories.window))
eq({ 1, bufdir, join(startdir, directories.window) }, { blwd(), cwd(), wcwd() })
command('tcd ' .. join('..', directories.tab))
eq({ 1, bufdir, join(startdir, directories.tab) }, { blwd(), cwd(), tcwd() })
command('cd ..')
eq({ 1, bufdir, startdir }, { blwd(), cwd(), cwd(-1, -1) })
-- The overridden ":cd" still reset the window/tab scopes, so ":bcd!" lands on the global dir.
command(('%s!'):format(cmd))
eq({ 0, 0, 0, startdir }, { blwd(), wlwd(), tlwd(), cwd() })
end)
end)
end
describe(':lcd!/:tcd!/:bcd! (bang)', function()
it('clear only their own scope', function()
local bufdir = join(startdir, directories.buffer)
local windir = join(startdir, directories.window)
local tabdir = join(startdir, directories.tab)
command('tcd ' .. directories.tab)
command('lcd ' .. join('..', directories.window))
command('bcd ' .. join('..', directories.buffer))
eq({ 1, 1, 1 }, { blwd(), wlwd(), tlwd() })
eq(bufdir, cwd())
command('bcd!') -- Buffer scope gone: the window-local dir applies.
eq({ 0, 1, 1 }, { blwd(), wlwd(), tlwd() })
eq(windir, cwd())
command('lcd!') -- Window scope gone: the tab-local dir applies.
eq({ 0, 0, 1 }, { blwd(), wlwd(), tlwd() })
eq(tabdir, cwd())
command('tcd!') -- Tab scope gone: back to the global dir.
eq({ 0, 0, 0 }, { blwd(), wlwd(), tlwd() })
eq(startdir, cwd())
command('lcd!') -- No-op when the scope has no local directory.
eq(startdir, cwd())
-- :cd! does not unset.
command('lcd ' .. directories.window)
command('cd! ' .. join('..', directories.global))
eq(join(startdir, directories.global), cwd())
eq(0, wlwd()) -- ":cd" cleared it, as always.
-- Legacy: bang WITH arg ":lcd! {path}" is just ":lcd {path}".
command('lcd! ' .. join('..', directories.window))
eq({ 1, join(startdir, directories.window) }, { wlwd(), cwd() })
end)
end)
describe('cd during temp context-switch', function()
it(':bcd/:tcd/:lcd persists in target scope, does not leak into original context', function()
local exec_lua = n.exec_lua
@@ -503,7 +575,7 @@ for _, cmd in ipairs { 'getcwd', 'haslocaldir' } do
)
-- -1 preceded by an argument >= 0
local err5001 = 'Vim:E5001: Higher scope cannot be -1 if lower scope is >= 0.'
local err5001 = 'Vim:E5001: Argument cannot be -1 if preceding argument 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))

View File

@@ -104,10 +104,11 @@ func Test_chdir_func()
call assert_match('^\[global\]', trim(execute('verbose pwd')))
call chdir('.', 'tabpage')
call assert_match('^\[tabpage\]', trim(execute('verbose pwd')))
call chdir('.', 'buffer')
call assert_match('^\[buffer\]', trim(execute('verbose pwd')))
call chdir('.', 'window')
call assert_match('^\[window\]', trim(execute('verbose pwd')))
" Nvim: buffer is the narrowest scope and shadows the others, so it goes last.
call chdir('.', 'buffer')
call assert_match('^\[buffer\]', trim(execute('verbose pwd')))
" Error case
call assert_fails("call chdir('dir-abcd')", 'E344:')