From eeefe7ca65dfe6d32d17d2dd438bd8139c3dfa47 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sun, 23 Aug 2026 19:32:07 -0400 Subject: [PATCH] fix(cwd): E812 when a message opens a window during a file read #41458 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 b296666e419c, 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. --- src/nvim/fileio.c | 38 ++++++++++-------- .../swapfile_preserve_recover_spec.lua | 40 +++++++++++++++++++ 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index 087e890bc5..887ac6b0d0 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -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; } } diff --git a/test/functional/ex_cmds/swapfile_preserve_recover_spec.lua b/test/functional/ex_cmds/swapfile_preserve_recover_spec.lua index d15ea06edd..c2aaa58808 100644 --- a/test/functional/ex_cmds/swapfile_preserve_recover_spec.lua +++ b/test/functional/ex_cmds/swapfile_preserve_recover_spec.lua @@ -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')