feat: ignore swapfile for running Nvim processes #25336

Problem:
The swapfile "E325: ATTENTION" dialog is displayed when editing a file
already open in another (running) Nvim. Usually this behavior is
annoying and irrelevant:
- "Recover" and the other options ("Open readonly", "Quit", "Abort") are
  almost never wanted.
- swapfiles are less relevant for "multi-Nvim" since 'autoread' is
  enabled by default.
  - Even less relevant if user enables 'autowrite'.

Solution:
Define a default SwapExists handler which does the following:
1. If the swapfile is owned by a running Nvim process, automatically
   chooses "(E)dit anyway" (caveat: this creates a new, extra swapfile,
   which is mostly harmless and ignored except by `:recover` or `nvim -r`.
2. Shows a 1-line "ignoring swapfile..." message.
3. Users can disable the default SwapExists handler via `autocmd! nvim_swapfile`.
This commit is contained in:
Justin M. Keyes
2023-10-04 06:31:25 -07:00
committed by GitHub
parent 1e7e9ee91f
commit 29fe883aa9
15 changed files with 267 additions and 178 deletions

View File

@ -118,7 +118,7 @@ manually. Mostly the screen will not scroll up, thus there is no hit-enter
prompt. When one command outputs two messages this can happen anyway. prompt. When one command outputs two messages this can happen anyway.
============================================================================== ==============================================================================
3. Removing autocommands *autocmd-remove* 3. Removing autocommands *autocmd!* *autocmd-remove*
:au[tocmd]! [group] {event} {aupat} [++once] [++nested] {cmd} :au[tocmd]! [group] {event} {aupat} [++once] [++nested] {cmd}
Remove all autocommands associated with {event} and Remove all autocommands associated with {event} and

View File

@ -7843,8 +7843,8 @@ swapinfo({fname}) *swapinfo()*
user user name user user name
host host name host host name
fname original file name fname original file name
pid PID of the Vim process that created the swap pid PID of the Nvim process that created the swap
file file, or zero if not running.
mtime last modification time in seconds mtime last modification time in seconds
inode Optional: INODE number of the file inode Optional: INODE number of the file
dirty 1 if file was modified, 0 if not dirty 1 if file was modified, 0 if not

View File

@ -169,33 +169,26 @@ If you want to keep the changed buffer without saving it, switch on the
2. Editing a file *edit-a-file* 2. Editing a file *edit-a-file*
*:e* *:edit* *reload* *:e* *:edit* *reload*
:e[dit] [++opt] [+cmd] Edit the current file. This is useful to re-edit the :e[dit][!] [++opt] [+cmd]
Edit the current file. This is useful to re-edit the
current file, when it has been changed outside of Vim. current file, when it has been changed outside of Vim.
This fails when changes have been made to the current
buffer and 'autowriteall' isn't set or the file can't
be written.
Also see |++opt| and |+cmd|.
*:edit!* *discard* *:edit!* *discard*
:e[dit]! [++opt] [+cmd] If [!] is given, unsaved changes in the current buffer
Edit the current file always. Discard any changes to are discarded. Without [!] the command fails if there
the current buffer. This is useful if you want to are unsaved changes, unless 'autowriteall' is set and
start all over again. the file can be written.
Also see |++opt| and |+cmd|. Also see |++opt| and |+cmd|.
*:edit_f* *:edit_f*
:e[dit] [++opt] [+cmd] {file} :e[dit][!] [++opt] [+cmd] {file}
Edit {file}. Edit {file}.
This fails when changes have been made to the current *:edit!_f*
buffer, unless 'hidden' is set or 'autowriteall' is If [!] is given, unsaved changes in the current buffer
set and the file can be written. are discarded. Without [!] the command fails if there
are unsaved changes, unless 'hidden' is set or
'autowriteall' is set and the file can be written.
Also see |++opt| and |+cmd|. Also see |++opt| and |+cmd|.
*:edit!_f*
:e[dit]! [++opt] [+cmd] {file}
Edit {file} always. Discard any changes to the
current buffer.
Also see |++opt| and |+cmd|.
*:edit_#* *:e#* *:edit_#* *:e#*
:e[dit] [++opt] [+cmd] #[count] :e[dit] [++opt] [+cmd] #[count]
Edit the [count]th buffer (as shown by |:files|). Edit the [count]th buffer (as shown by |:files|).
@ -1224,10 +1217,10 @@ MULTIPLE WINDOWS AND BUFFERS *window-exit*
*:confirm* *:conf* *:confirm* *:conf*
:conf[irm] {command} Execute {command}, and use a dialog when an :conf[irm] {command} Execute {command}, and use a dialog when an
operation has to be confirmed. Can be used on the operation has to be confirmed. Can be used on the
|:q|, |:qa| and |:w| commands (the latter to override |:edit|, |:q|, |:qa| and |:w| commands (the latter to
a read-only setting), and any other command that can override a read-only setting), and any commands that
fail in such a way, such as |:only|, |:buffer|, can fail because of unsaved changes, such as |:only|,
|:bdelete|, etc. |:buffer|, |:bdelete|, etc.
Examples: > Examples: >
:confirm w foo :confirm w foo

View File

@ -2276,8 +2276,9 @@ v:stderr |channel-id| corresponding to stderr. The value is always 2;
:call chansend(v:stderr, "error: toaster empty\n") :call chansend(v:stderr, "error: toaster empty\n")
< <
*v:swapname* *swapname-variable* *v:swapname* *swapname-variable*
v:swapname Only valid when executing |SwapExists| autocommands: Name of v:swapname Name of the swapfile found.
the swap file found. Read-only. Only valid during |SwapExists| event.
Read-only.
*v:swapchoice* *swapchoice-variable* *v:swapchoice* *swapchoice-variable*
v:swapchoice |SwapExists| autocommands can set this to the selected choice v:swapchoice |SwapExists| autocommands can set this to the selected choice

View File

@ -114,6 +114,12 @@ The following new APIs and features were added.
• Builtin TUI can now recognize "super" (|<D-|) and "meta" (|<T-|) modifiers in a • Builtin TUI can now recognize "super" (|<D-|) and "meta" (|<T-|) modifiers in a
terminal emulator that supports |tui-csiu|. terminal emulator that supports |tui-csiu|.
• Editor
• By default, the swapfile "ATTENTION" |E325| dialog is skipped if the
swapfile is owned by a running Nvim process, instead of prompting. If you
always want the swapfile dialog, delete the default SwapExists handler:
`autocmd! nvim_swapfile`. |default-autocmds|
• LSP • LSP
• LSP method names are available in |vim.lsp.protocol.Methods|. • LSP method names are available in |vim.lsp.protocol.Methods|.
• Implemented LSP inlay hints: |vim.lsp.inlay_hint()| • Implemented LSP inlay hints: |vim.lsp.inlay_hint()|

View File

@ -83,6 +83,15 @@ Detecting an existing swap file ~
You can find this in the user manual, section |11.3|. You can find this in the user manual, section |11.3|.
*W325*
The default |SwapExists| handler (|default-autocmds|) skips the |E325| prompt
(selects "(E)dit") if the swapfile owner process (1) is still running and (2)
was started by the current user. This presumes that you normally don't want
to be bothered with the |ATTENTION| message just because you happen to edit
the same file from multiple Nvim instances. In the worst case (a system
crash) there will be more than one swapfile for the file; use |:recover| to
inspect all of its swapfiles.
Updating the swapfile ~ Updating the swapfile ~

View File

@ -139,6 +139,11 @@ nvim_terminal:
nvim_cmdwin: nvim_cmdwin:
- CmdwinEnter: Limits syntax sync to maxlines=1 in the |cmdwin|. - CmdwinEnter: Limits syntax sync to maxlines=1 in the |cmdwin|.
nvim_swapfile:
- SwapExists: Skips the swapfile prompt (sets |v:swapchoice| to "e") when the
swapfile is owned by a running Nvim process. Shows |W325| "Ignoring
swapfile…" message.
============================================================================== ==============================================================================
New Features *nvim-features* New Features *nvim-features*

View File

@ -1147,11 +1147,28 @@ function vim._init_default_autocmds()
end end
end, end,
}) })
vim.api.nvim_create_autocmd({ 'CmdwinEnter' }, { vim.api.nvim_create_autocmd({ 'CmdwinEnter' }, {
pattern = '[:>]', pattern = '[:>]',
group = vim.api.nvim_create_augroup('nvim_cmdwin', {}), group = vim.api.nvim_create_augroup('nvim_cmdwin', {}),
command = 'syntax sync minlines=1 maxlines=1', command = 'syntax sync minlines=1 maxlines=1',
}) })
vim.api.nvim_create_autocmd({ 'SwapExists' }, {
pattern = '*',
group = vim.api.nvim_create_augroup('nvim_swapfile', {}),
callback = function()
local info = vim.fn.swapinfo(vim.v.swapname)
local user = vim.uv.os_get_passwd().username
local iswin = 1 == vim.fn.has('win32')
if info.error or info.pid <= 0 or (not iswin and info.user ~= user) then
vim.v.swapchoice = '' -- Show the prompt.
return
end
vim.v.swapchoice = 'e' -- Choose "(E)dit".
vim.notify(('W325: Ignoring swapfile from Nvim process %d'):format(info.pid))
end,
})
end end
function vim._init_defaults() function vim._init_defaults()

View File

@ -9312,8 +9312,8 @@ function vim.fn.swapfilelist() end
--- user user name --- user user name
--- host host name --- host host name
--- fname original file name --- fname original file name
--- pid PID of the Vim process that created the swap --- pid PID of the Nvim process that created the swap
--- file --- file, or zero if not running.
--- mtime last modification time in seconds --- mtime last modification time in seconds
--- inode Optional: INODE number of the file --- inode Optional: INODE number of the file
--- dirty 1 if file was modified, 0 if not --- dirty 1 if file was modified, 0 if not

View File

@ -11123,8 +11123,8 @@ M.funcs = {
user user name user user name
host host name host host name
fname original file name fname original file name
pid PID of the Vim process that created the swap pid PID of the Nvim process that created the swap
file file, or zero if not running.
mtime last modification time in seconds mtime last modification time in seconds
inode Optional: INODE number of the file inode Optional: INODE number of the file
dirty 1 if file was modified, 0 if not dirty 1 if file was modified, 0 if not

View File

@ -8236,7 +8236,7 @@ static void f_swapfilelist(typval_T *argvars, typval_T *rettv, EvalFuncData fptr
static void f_swapinfo(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) static void f_swapinfo(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{ {
tv_dict_alloc_ret(rettv); tv_dict_alloc_ret(rettv);
get_b0_dict(tv_get_string(argvars), rettv->vval.v_dict); swapfile_dict(tv_get_string(argvars), rettv->vval.v_dict);
} }
/// "swapname(expr)" function /// "swapname(expr)" function

View File

@ -2491,7 +2491,7 @@ void ex_function(exarg_T *eap)
} else if (line_arg != NULL && *skipwhite(line_arg) != NUL) { } else if (line_arg != NULL && *skipwhite(line_arg) != NUL) {
nextcmd = line_arg; nextcmd = line_arg;
} else if (*p != NUL && *p != '"' && p_verbose > 0) { } else if (*p != NUL && *p != '"' && p_verbose > 0) {
give_warning2(_("W22: Text found after :endfunction: %s"), p, true); swmsg(true, _("W22: Text found after :endfunction: %s"), p);
} }
if (nextcmd != NULL) { if (nextcmd != NULL) {
// Another command follows. If the line came from "eap" we // Another command follows. If the line came from "eap" we

View File

@ -168,11 +168,9 @@ enum {
B0_MAGIC_CHAR = 0x55, B0_MAGIC_CHAR = 0x55,
}; };
// Block zero holds all info about the swap file. This is the first block in // Block zero holds all info about the swapfile. This is the first block in the file.
// the file.
// //
// NOTE: DEFINITION OF BLOCK 0 SHOULD NOT CHANGE! It would make all existing // NOTE: DEFINITION OF BLOCK 0 SHOULD NOT CHANGE! It would make all existing swapfiles unusable!
// swap files unusable!
// //
// If size of block0 changes anyway, adjust MIN_SWAP_PAGE_SIZE in vim.h!! // If size of block0 changes anyway, adjust MIN_SWAP_PAGE_SIZE in vim.h!!
// //
@ -210,8 +208,7 @@ typedef struct {
// EOL_MAC + 1. // EOL_MAC + 1.
#define B0_FF_MASK 3 #define B0_FF_MASK 3
// Swap file is in directory of edited file. Used to find the file from // Swapfile is in directory of edited file. Used to find the file from different mount points.
// different mount points.
#define B0_SAME_DIR 4 #define B0_SAME_DIR 4
// The 'fileencoding' is at the end of b0_fname[], with a NUL in front of it. // The 'fileencoding' is at the end of b0_fname[], with a NUL in front of it.
@ -724,10 +721,13 @@ static void add_b0_fenc(ZeroBlock *b0p, buf_T *buf)
} }
} }
/// Return true if the process with number "b0p->b0_pid" is still running. /// Returns the PID of the process that owns the swapfile, if it is running.
/// "swap_fname" is the name of the swap file, if it's from before a reboot then ///
/// the result is false; /// @param b0p swapfile data
static bool swapfile_process_running(const ZeroBlock *b0p, const char *swap_fname) /// @param swap_fname Name of the swapfile. If it's from before a reboot, the result is 0.
///
/// @return PID, or 0 if process is not running or the swapfile is from before a reboot.
static int swapfile_process_running(const ZeroBlock *b0p, const char *swap_fname)
{ {
FileInfo st; FileInfo st;
double uptime; double uptime;
@ -736,15 +736,15 @@ static bool swapfile_process_running(const ZeroBlock *b0p, const char *swap_fnam
if (os_fileinfo(swap_fname, &st) if (os_fileinfo(swap_fname, &st)
&& uv_uptime(&uptime) == 0 && uv_uptime(&uptime) == 0
&& (Timestamp)st.stat.st_mtim.tv_sec < os_time() - (Timestamp)uptime) { && (Timestamp)st.stat.st_mtim.tv_sec < os_time() - (Timestamp)uptime) {
return false; return 0;
} }
return os_proc_running((int)char_to_long(b0p->b0_pid)); int pid = (int)char_to_long(b0p->b0_pid);
return os_proc_running(pid) ? pid : 0;
} }
/// Try to recover curbuf from the .swp file. /// Try to recover curbuf from the .swp file.
/// ///
/// @param checkext if true, check the extension and detect whether it is a /// @param checkext if true, check the extension and detect whether it is a swapfile.
/// swap file.
void ml_recover(bool checkext) void ml_recover(bool checkext)
{ {
buf_T *buf = NULL; buf_T *buf = NULL;
@ -1456,12 +1456,13 @@ char *make_percent_swname(const char *dir, const char *name)
return d; return d;
} }
static bool process_still_running; // PID of swapfile owner, or zero if not running.
static int process_running;
/// This is used by the swapinfo() function. /// For Vimscript "swapinfo()".
/// ///
/// @return information found in swapfile "fname" in dictionary "d". /// @return information found in swapfile "fname" in dictionary "d".
void get_b0_dict(const char *fname, dict_T *d) void swapfile_dict(const char *fname, dict_T *d)
{ {
int fd; int fd;
ZeroBlock b0; ZeroBlock b0;
@ -1482,7 +1483,7 @@ void get_b0_dict(const char *fname, dict_T *d)
tv_dict_add_str_len(d, S_LEN("fname"), b0.b0_fname, tv_dict_add_str_len(d, S_LEN("fname"), b0.b0_fname,
B0_FNAME_SIZE_ORG); B0_FNAME_SIZE_ORG);
tv_dict_add_nr(d, S_LEN("pid"), char_to_long(b0.b0_pid)); tv_dict_add_nr(d, S_LEN("pid"), swapfile_process_running(&b0, fname));
tv_dict_add_nr(d, S_LEN("mtime"), char_to_long(b0.b0_mtime)); tv_dict_add_nr(d, S_LEN("mtime"), char_to_long(b0.b0_mtime));
tv_dict_add_nr(d, S_LEN("dirty"), b0.b0_dirty ? 1 : 0); tv_dict_add_nr(d, S_LEN("dirty"), b0.b0_dirty ? 1 : 0);
tv_dict_add_nr(d, S_LEN("inode"), char_to_long(b0.b0_ino)); tv_dict_add_nr(d, S_LEN("inode"), char_to_long(b0.b0_ino));
@ -1496,7 +1497,7 @@ void get_b0_dict(const char *fname, dict_T *d)
} }
} }
/// Give information about an existing swap file. /// Loads info from swapfile `fname`, and displays it to the user.
/// ///
/// @return timestamp (0 when unknown). /// @return timestamp (0 when unknown).
static time_t swapfile_info(char *fname) static time_t swapfile_info(char *fname)
@ -1567,9 +1568,8 @@ static time_t swapfile_info(char *fname)
if (char_to_long(b0.b0_pid) != 0L) { if (char_to_long(b0.b0_pid) != 0L) {
msg_puts(_("\n process ID: ")); msg_puts(_("\n process ID: "));
msg_outnum((int)char_to_long(b0.b0_pid)); msg_outnum((int)char_to_long(b0.b0_pid));
if (swapfile_process_running(&b0, fname)) { if ((process_running = swapfile_process_running(&b0, fname))) {
msg_puts(_(" (STILL RUNNING)")); msg_puts(_(" (STILL RUNNING)"));
process_still_running = true;
} }
} }
@ -1589,8 +1589,7 @@ static time_t swapfile_info(char *fname)
return x; return x;
} }
/// @return true if the swap file looks OK and there are no changes, thus it /// @return true if the swapfile looks OK and there are no changes, thus it can be safely deleted.
/// can be safely deleted.
static bool swapfile_unchanged(char *fname) static bool swapfile_unchanged(char *fname)
{ {
ZeroBlock b0; ZeroBlock b0;
@ -3175,13 +3174,10 @@ char *makeswapname(char *fname, char *ffname, buf_T *buf, char *dir_name)
} }
/// Get file name to use for swapfile or backup file. /// Get file name to use for swapfile or backup file.
/// Use the name of the edited file "fname" and an entry in the 'dir' or 'bdir' /// Use the name of the edited file "fname" and an entry in the 'dir' or 'bdir' option "dname".
/// option "dname".
/// - If "dname" is ".", return "fname" (swapfile in dir of file). /// - If "dname" is ".", return "fname" (swapfile in dir of file).
/// - If "dname" starts with "./", insert "dname" in "fname" (swap file /// - If "dname" starts with "./", insert "dname" in "fname" (swapfile relative to dir of file).
/// relative to dir of file). /// - Otherwise, prepend "dname" to the tail of "fname" (swapfile in specific dir).
/// - Otherwise, prepend "dname" to the tail of "fname" (swap file in specific
/// dir).
/// ///
/// The return value is an allocated string and can be NULL. /// The return value is an allocated string and can be NULL.
/// ///
@ -3333,7 +3329,7 @@ static char *findswapname(buf_T *buf, char **dirp, char *old_fname, bool *found_
char *dir_name = xmalloc(dir_len); char *dir_name = xmalloc(dir_len);
(void)copy_option_part(dirp, dir_name, dir_len, ","); (void)copy_option_part(dirp, dir_name, dir_len, ",");
// we try different names until we find one that does not exist yet // We try different swapfile names until we find one that does not exist yet.
char *fname = makeswapname(buf_fname, buf->b_ffname, buf, dir_name); char *fname = makeswapname(buf_fname, buf->b_ffname, buf, dir_name);
while (true) { while (true) {
@ -3365,17 +3361,17 @@ static char *findswapname(buf_T *buf, char **dirp, char *old_fname, bool *found_
// Give an error message, unless recovering, no file name, we are // Give an error message, unless recovering, no file name, we are
// viewing a help file or when the path of the file is different // viewing a help file or when the path of the file is different
// (happens when all .swp files are in one directory). // (happens when all .swp files are in one directory).
if (!recoverymode && buf_fname != NULL if (!recoverymode && buf_fname != NULL && !buf->b_help && !(buf->b_flags & BF_DUMMY)) {
&& !buf->b_help && !(buf->b_flags & BF_DUMMY)) {
int fd; int fd;
ZeroBlock b0; ZeroBlock b0;
int differ = false; int differ = false;
// Try to read block 0 from the swap file to get the original // Try to read block 0 from the swapfile to get the original file name (and inode number).
// file name (and inode number).
fd = os_open(fname, O_RDONLY, 0); fd = os_open(fname, O_RDONLY, 0);
if (fd >= 0) { if (fd >= 0) {
if (read_eintr(fd, &b0, sizeof(b0)) == sizeof(b0)) { if (read_eintr(fd, &b0, sizeof(b0)) == sizeof(b0)) {
process_running = swapfile_process_running(&b0, fname);
// If the swapfile has the same directory as the // If the swapfile has the same directory as the
// buffer don't compare the directory names, they can // buffer don't compare the directory names, they can
// have a different mountpoint. // have a different mountpoint.
@ -3393,8 +3389,7 @@ static char *findswapname(buf_T *buf, char **dirp, char *old_fname, bool *found_
} }
} }
} else { } else {
// The name in the swap file may be // The name in the swapfile may be "~user/path/file". Expand it first.
// "~user/path/file". Expand it first.
expand_env(b0.b0_fname, NameBuff, MAXPATHL); expand_env(b0.b0_fname, NameBuff, MAXPATHL);
if (fnamecmp_ino(buf->b_ffname, NameBuff, if (fnamecmp_ino(buf->b_ffname, NameBuff,
char_to_long(b0.b0_ino))) { char_to_long(b0.b0_ino))) {
@ -3405,13 +3400,13 @@ static char *findswapname(buf_T *buf, char **dirp, char *old_fname, bool *found_
close(fd); close(fd);
} }
// give the ATTENTION message when there is an old swap file // Show the ATTENTION message when:
// for the current file, and the buffer was not recovered. // - there is an old swapfile for the current file
// - the buffer was not recovered
if (differ == false && !(curbuf->b_flags & BF_RECOVERED) if (differ == false && !(curbuf->b_flags & BF_RECOVERED)
&& vim_strchr(p_shm, SHM_ATTENTION) == NULL) { && vim_strchr(p_shm, SHM_ATTENTION) == NULL) {
int choice = 0; int choice = 0;
process_still_running = false;
// It's safe to delete the swapfile if all these are true: // It's safe to delete the swapfile if all these are true:
// - the edited file exists // - the edited file exists
// - the swapfile has no changes and looks OK // - the swapfile has no changes and looks OK
@ -3430,6 +3425,7 @@ static char *findswapname(buf_T *buf, char **dirp, char *old_fname, bool *found_
choice = do_swapexists(buf, fname); choice = do_swapexists(buf, fname);
} }
process_running = 0; // Set by attention_message..swapfile_info.
if (choice == 0) { if (choice == 0) {
// Show info about the existing swapfile. // Show info about the existing swapfile.
attention_message(buf, fname); attention_message(buf, fname);
@ -3459,14 +3455,14 @@ static char *findswapname(buf_T *buf, char **dirp, char *old_fname, bool *found_
xstrlcat(name, sw_msg_2, name_len); xstrlcat(name, sw_msg_2, name_len);
choice = do_dialog(VIM_WARNING, _("VIM - ATTENTION"), choice = do_dialog(VIM_WARNING, _("VIM - ATTENTION"),
name, name,
process_still_running process_running
? _("&Open Read-Only\n&Edit anyway\n&Recover" ? _("&Open Read-Only\n&Edit anyway\n&Recover"
"\n&Quit\n&Abort") : "\n&Quit\n&Abort") :
_("&Open Read-Only\n&Edit anyway\n&Recover" _("&Open Read-Only\n&Edit anyway\n&Recover"
"\n&Delete it\n&Quit\n&Abort"), "\n&Delete it\n&Quit\n&Abort"),
1, NULL, false); 1, NULL, false);
if (process_still_running && choice >= 4) { if (process_running && choice >= 4) {
choice++; // Skip missing "Delete it" button. choice++; // Skip missing "Delete it" button.
} }
xfree(name); xfree(name);
@ -3477,27 +3473,27 @@ static char *findswapname(buf_T *buf, char **dirp, char *old_fname, bool *found_
if (choice > 0) { if (choice > 0) {
switch (choice) { switch (choice) {
case 1: case 1: // "Open Read-Only"
buf->b_p_ro = true; buf->b_p_ro = true;
break; break;
case 2: case 2: // "Edit anyway"
break; break;
case 3: case 3: // "Recover"
swap_exists_action = SEA_RECOVER; swap_exists_action = SEA_RECOVER;
break; break;
case 4: case 4: // "Delete it"
os_remove(fname); os_remove(fname);
break; break;
case 5: case 5: // "Quit"
swap_exists_action = SEA_QUIT; swap_exists_action = SEA_QUIT;
break; break;
case 6: case 6: // "Abort"
swap_exists_action = SEA_QUIT; swap_exists_action = SEA_QUIT;
got_int = true; got_int = true;
break; break;
} }
// If the file was deleted this fname can be used. // If the swapfile was deleted this `fname` can be used.
if (!os_path_exists(fname)) { if (!os_path_exists(fname)) {
break; break;
} }
@ -3512,10 +3508,10 @@ static char *findswapname(buf_T *buf, char **dirp, char *old_fname, bool *found_
} }
} }
// Change the ".swp" extension to find another file that can be used. // Permute the ".swp" extension to find a unique swapfile name.
// First decrement the last char: ".swo", ".swn", etc. // First decrement the last char: ".swo", ".swn", etc.
// If that still isn't enough decrement the last but one char: ".svz" // If that still isn't enough decrement the last but one char: ".svz"
// Can happen when editing many "No Name" buffers. // Can happen when many Nvim instances are editing the same file (including "No Name" buffers).
if (fname[n - 1] == 'a') { // ".s?a" if (fname[n - 1] == 'a') { // ".s?a"
if (fname[n - 2] == 'a') { // ".saa": tried enough, give up if (fname[n - 2] == 'a') { // ".saa": tried enough, give up
emsg(_("E326: Too many swap files found")); emsg(_("E326: Too many swap files found"));

View File

@ -475,7 +475,14 @@ void trunc_string(const char *s, char *buf, int room_in, int buflen)
} }
} }
// Note: Caller of smsg() must check the resulting string is shorter than IOSIZE!!! /// Shows a printf-style message with attributes.
///
/// Note: Caller must check the resulting string is shorter than IOSIZE!!!
///
/// @see semsg
/// @see swmsg
///
/// @param s printf-style format message
int smsg(int attr, const char *s, ...) int smsg(int attr, const char *s, ...)
FUNC_ATTR_PRINTF(2, 3) FUNC_ATTR_PRINTF(2, 3)
{ {
@ -757,6 +764,8 @@ void emsg_invreg(int name)
} }
/// Print an error message with unknown number of arguments /// Print an error message with unknown number of arguments
///
/// @return whether the message was displayed
bool semsg(const char *const fmt, ...) bool semsg(const char *const fmt, ...)
FUNC_ATTR_PRINTF(1, 2) FUNC_ATTR_PRINTF(1, 2)
{ {
@ -3337,9 +3346,22 @@ void give_warning(const char *message, bool hl)
no_wait_return--; no_wait_return--;
} }
void give_warning2(const char *const message, const char *const a1, bool hl) /// Shows a warning, with optional highlighting.
///
/// @param hl enable highlighting
/// @param fmt printf-style format message
///
/// @see smsg
/// @see semsg
void swmsg(bool hl, const char *const fmt, ...)
FUNC_ATTR_PRINTF(2, 3)
{ {
vim_snprintf(IObuff, IOSIZE, message, a1); va_list args;
va_start(args, fmt);
vim_vsnprintf(IObuff, IOSIZE, fmt, args);
va_end(args);
give_warning(IObuff, hl); give_warning(IObuff, hl);
} }

View File

@ -171,6 +171,7 @@ describe('swapfile detection', function()
local screen2 = Screen.new(256, 40) local screen2 = Screen.new(256, 40)
screen2:attach() screen2:attach()
exec(init) exec(init)
command('autocmd! nvim_swapfile') -- Delete the default handler (which skips the dialog).
-- With shortmess+=F -- With shortmess+=F
command('set shortmess+=F') command('set shortmess+=F')
@ -219,11 +220,29 @@ describe('swapfile detection', function()
nvim2:close() nvim2:close()
end) end)
it('default SwapExists handler selects "(E)dit" and skips prompt', function()
exec(init)
command('edit Xfile1')
command("put ='some text...'")
command('preserve') -- Make sure the swap file exists.
local nvimpid = funcs.getpid()
local nvim1 = spawn(new_argv(), true, nil, true)
set_session(nvim1)
local screen = Screen.new(75, 18)
screen:attach()
exec(init)
feed(':edit Xfile1\n')
screen:expect({ any = ('W325: Ignoring swapfile from Nvim process %d'):format(nvimpid) })
nvim1:close()
end)
-- oldtest: Test_swap_prompt_splitwin() -- oldtest: Test_swap_prompt_splitwin()
it('selecting "q" in the attention prompt', function() it('selecting "q" in the attention prompt', function()
exec(init) exec(init)
command('edit Xfile1') command('edit Xfile1')
command('preserve') -- should help to make sure the swap file exists command('preserve') -- Make sure the swap file exists.
local screen = Screen.new(75, 18) local screen = Screen.new(75, 18)
screen:set_default_attr_ids({ screen:set_default_attr_ids({
@ -235,7 +254,9 @@ describe('swapfile detection', function()
set_session(nvim1) set_session(nvim1)
screen:attach() screen:attach()
exec(init) exec(init)
command('autocmd! nvim_swapfile') -- Delete the default handler (which skips the dialog).
feed(':split Xfile1\n') feed(':split Xfile1\n')
-- The default SwapExists handler does _not_ skip this prompt.
screen:expect({ screen:expect({
any = pesc('{1:[O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort: }^') any = pesc('{1:[O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort: }^')
}) })
@ -267,6 +288,7 @@ describe('swapfile detection', function()
set_session(nvim2) set_session(nvim2)
screen:attach() screen:attach()
exec(init) exec(init)
command('autocmd! nvim_swapfile') -- Delete the default handler (which skips the dialog).
command('set more') command('set more')
command('au bufadd * let foo_w = wincol()') command('au bufadd * let foo_w = wincol()')
feed(':e Xfile1<CR>') feed(':e Xfile1<CR>')
@ -300,8 +322,9 @@ describe('swapfile detection', function()
nvim2:close() nvim2:close()
end) end)
-- oldtest: Test_nocatch_process_still_running() --- @param swapexists boolean Enable the default SwapExists handler.
it('allows deleting swapfile created before boot vim-patch:8.2.2586', function() --- @param on_swapfile_running fun(screen: any) Called after swapfile ("STILL RUNNING") prompt.
local function test_swapfile_after_reboot(swapexists, on_swapfile_running)
local screen = Screen.new(75, 30) local screen = Screen.new(75, 30)
screen:set_default_attr_ids({ screen:set_default_attr_ids({
[0] = {bold = true, foreground = Screen.colors.Blue}, -- NonText [0] = {bold = true, foreground = Screen.colors.Blue}, -- NonText
@ -311,6 +334,9 @@ describe('swapfile detection', function()
screen:attach() screen:attach()
exec(init) exec(init)
if not swapexists then
command('autocmd! nvim_swapfile') -- Delete the default handler (which skips the dialog).
end
command('set nohidden') command('set nohidden')
exec([=[ exec([=[
@ -347,12 +373,7 @@ describe('swapfile detection', function()
os.rename('Xswap', swname) os.rename('Xswap', swname)
feed(':edit Xswaptest<CR>') feed(':edit Xswaptest<CR>')
screen:expect({any = table.concat({ on_swapfile_running(screen)
pesc('{2:E325: ATTENTION}'),
'file name: .*Xswaptest',
'process ID: %d* %(STILL RUNNING%)',
pesc('{1:[O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort: }^'),
}, '.*')})
feed('e') feed('e')
@ -374,9 +395,28 @@ describe('swapfile detection', function()
}, '.*')}) }, '.*')})
feed('e') feed('e')
end
-- oldtest: Test_nocatch_process_still_running()
it('swapfile created before boot vim-patch:8.2.2586', function()
test_swapfile_after_reboot(false, function(screen)
screen:expect({any = table.concat({
pesc('{2:E325: ATTENTION}'),
'file name: .*Xswaptest',
'process ID: %d* %(STILL RUNNING%)',
pesc('{1:[O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort: }^'),
}, '.*')})
end) end)
end) end)
it('swapfile created before boot + default SwapExists handler', function()
test_swapfile_after_reboot(true, function(screen)
screen:expect({ any = 'W325: Ignoring swapfile from Nvim process' })
end)
end)
end)
describe('quitting swapfile dialog on startup stops TUI properly', function() describe('quitting swapfile dialog on startup stops TUI properly', function()
local swapdir = luv.cwd()..'/Xtest_swapquit_dir' local swapdir = luv.cwd()..'/Xtest_swapquit_dir'
local testfile = 'Xtest_swapquit_file1' local testfile = 'Xtest_swapquit_file1'