fix(cwd): E812 when a message opens a window during a file read #41458

Problem:
When ui2 is enabled, opening an already-open file in another Neovim
instance results in the error `E812: Autocommands changed buffer…`.

Analysis:
On E812 the file is not loaded.  The default SwapExists handler notifies
`W325: Ignoring swapfile…`, and ui2 shows that message by opening
a window, which is a temp context switch.  `ctx_dirs_restore()`
re-shortens every buffer name on the way out, and `shorten_buf_fname()`
always frees and reallocates `b_sfname`.  `readfile()` aliases `b_fname`
across `check_need_swap()` and compares the pointer to detect a rename.

Regression by b296666e41, which replaced the `cs_save_sfname` restore
(that kept curbuf's pointer) with `shorten_fnames(true)`.

Solution:
Keep the allocation in `shorten_buf_fname()` when the short name is
unchanged.  Pointer stability is what the E200/E201/E812 guards actually
assert.
This commit is contained in:
Justin M. Keyes
2026-08-23 19:32:07 -04:00
committed by GitHub
parent 716835c232
commit eeefe7ca65
2 changed files with 62 additions and 16 deletions

View File

@@ -2366,22 +2366,28 @@ static char *check_for_bom(const char *p_in, int size, int *lenp, int flags)
/// name.
void shorten_buf_fname(buf_T *buf, char *dirname, int force)
{
if (buf->b_fname != NULL
&& !bt_nofilename(buf)
&& !path_with_url(buf->b_fname)
&& (force
|| buf->b_sfname == NULL
|| path_is_absolute(buf->b_sfname))) {
if (buf->b_sfname != buf->b_ffname) {
XFREE_CLEAR(buf->b_sfname);
}
char *p = path_shorten_fname(buf->b_ffname, dirname);
if (p != NULL && *p != NUL) {
buf->b_sfname = xstrdup(p);
buf->b_fname = buf->b_sfname;
} else {
buf->b_fname = buf->b_ffname;
}
if (buf->b_fname == NULL
|| bt_nofilename(buf)
|| path_with_url(buf->b_fname)
|| !(force || buf->b_sfname == NULL || path_is_absolute(buf->b_sfname))) {
return;
}
char *p = path_shorten_fname(buf->b_ffname, dirname);
if (p != NULL && *p != NUL && buf->b_sfname != NULL && strcmp(p, buf->b_sfname) == 0) {
// Same name: keep the allocation. Callers (readfile()) alias `b_fname` across autocommands and
// check the pointer to detect a rename. Very cool... #41454
return;
}
if (buf->b_sfname != buf->b_ffname) {
XFREE_CLEAR(buf->b_sfname);
}
if (p != NULL && *p != NUL) {
buf->b_sfname = xstrdup(p);
buf->b_fname = buf->b_sfname;
} else {
buf->b_fname = buf->b_ffname;
}
}

View File

@@ -436,6 +436,46 @@ describe('swapfile detection', function()
nvim1:close()
end)
-- ui2 opens windows to show a message, which chdirs in and out of the target window's context.
-- readfile() aliases the buffer name across that.
it('ui2 message from the SwapExists handler does not abort the read #41454', function()
exec(init)
write_file('Xfile1', 'some text...\n')
command('edit Xfile1')
command('preserve') -- Make sure the swap file exists.
local initfile = 'Xtest_swapdialog_init.lua'
write_file(
initfile,
('vim.opt.directory:prepend(%q)\nrequire("vim._core.ui2").enable()\n'):format(swapdir .. '//')
)
finally(function()
os.remove(initfile)
os.remove('Xfile1')
end)
local nvim1 = n.new_session(true)
set_session(nvim1)
local screen = Screen.new(75, 10)
-- ui2 needs a UI attached while the file argument is read, i.e. a real TTY.
fn.jobstart({ nvim_prog, '-i', 'NONE', '--noplugin', '-u', initfile, 'Xfile1' }, {
term = true,
env = { VIMRUNTIME = os.getenv('VIMRUNTIME') },
})
screen:expect({
grid = [[
^some text... |
~ |*6
Xfile1 1,1 All|
{MATCH:W325: Ignoring swapfile from Nvim process %d+ *}|
|
]],
attr_ids = {},
})
nvim1:close()
end)
it('attention message kind', function()
exec(init)
command('edit Xfile1')