mirror of
https://github.com/neovim/neovim.git
synced 2026-08-26 09:01:57 +00:00
fix(cwd): stale buffer names after temp context-switch #41433
Problem: 1. `ctx_dirs_restore()` is the only chdir site that doesn't re-shorten buffer names; `post_chdir()`, `update_cwd()` and `do_autochdir()` all call `shorten_fnames(true)`. So after a temp window-context switch that moved the CWD, every buffer's `b_fname` is still relative to the other directory. ui2 renders messages in a float, and entering it is such a switch, so with a `:bcd` in a `BufReadPost` handler 'statusline' "%f" shows ".config/nvim/init.lua" while the CWD is already `~/.config/nvim`, and :write resolves the name against it: ``` E212: Can't open file for writing: no such file or directory ``` 2. `msg_multihl()` leaves `msg_ext_id` pointing at the caller's storage when nothing was emitted (e.g. 'msg_silent'): the reset only ran on a flush that produced chunks. The next message then ships a dead stack frame as its msg_show id. After ":silent write" the id is buf_write()'s `msg_id[MAXPATHL + 32]`: ``` id = "\0\0\0\0\0\0\0\0\29\0(<C6>k\24R\17p<C7><C7><C7>\1\0\0\0..." ``` Solution: 1. `shorten_fnames(true)` after restoring the CWD. Drop `cs_save_sfname`, which was a partial workaround for the same bug. 2. Release the id in `msg_multihl()`, where it is set and the caller's frame is still alive. Stop reading `.data.integer` out of a String union member.
This commit is contained in:
@@ -33,6 +33,7 @@
|
||||
#include "nvim/eval/userfunc.h"
|
||||
#include "nvim/eval/vars.h"
|
||||
#include "nvim/ex_docmd.h"
|
||||
#include "nvim/fileio.h"
|
||||
#include "nvim/globals.h"
|
||||
#include "nvim/hashtab.h"
|
||||
#include "nvim/keycodes.h"
|
||||
@@ -348,9 +349,6 @@ static void ctx_dirs_save(CtxSwitch *cs, win_T *wp, tabpage_T *tp, buf_T *buf)
|
||||
// If 'acd' is set, check we are using that directory. If yes, then
|
||||
// apply 'acd' afterwards, otherwise restore the current directory.
|
||||
if (cs->cs_cwd != NULL && p_acd) {
|
||||
if (curbuf->b_sfname != NULL && curbuf->b_fname == curbuf->b_sfname) {
|
||||
cs->cs_save_sfname = xstrdup(curbuf->b_sfname);
|
||||
}
|
||||
do_autochdir();
|
||||
char autocwd[MAXPATHL];
|
||||
if (os_dirname(autocwd, MAXPATHL) == OK) {
|
||||
@@ -387,17 +385,11 @@ static void ctx_dirs_restore(CtxSwitch *cs)
|
||||
|
||||
// Restore the CWD itself. After an explicit chdir, ctx_restore() re-derives it instead.
|
||||
if (cs->cs_apply_acd) {
|
||||
xfree(cs->cs_save_sfname);
|
||||
do_autochdir();
|
||||
} else if (cs->cs_cwd != NULL && ((cs->cs_flags & kCtxKeepDirs) || !_ctx_did_chdir)) {
|
||||
os_chdir(cs->cs_cwd);
|
||||
if (cs->cs_save_sfname != NULL) {
|
||||
xfree(curbuf->b_sfname);
|
||||
curbuf->b_sfname = cs->cs_save_sfname;
|
||||
curbuf->b_fname = curbuf->b_sfname;
|
||||
}
|
||||
} else {
|
||||
xfree(cs->cs_save_sfname);
|
||||
// Buffer names are relative to the CWD, so they must follow it back. #41424
|
||||
shorten_fnames(true);
|
||||
}
|
||||
XFREE_CLEAR(cs->cs_cwd);
|
||||
}
|
||||
|
||||
@@ -116,5 +116,4 @@ typedef struct {
|
||||
char *cs_globaldir; ///< Saved globaldir
|
||||
char *cs_cwd; ///< Saved CWD (kCtxKeepCwd/kCtxKeepDirs).
|
||||
bool cs_apply_acd; ///< Re-apply 'autochdir' on ctx_restore().
|
||||
char *cs_save_sfname; ///< Saved b_sfname (kCtxKeepCwd/kCtxKeepDirs).
|
||||
} CtxSwitch;
|
||||
|
||||
@@ -415,6 +415,9 @@ MsgID msg_multihl(MsgID id, HlMessage hl_msg, const char *kind, bool history, bo
|
||||
if (hl_msg_updated && !(history && kv_size(hl_msg))) {
|
||||
hl_msg_free(hl_msg);
|
||||
}
|
||||
// Release the id: it belongs to this message, and a String id only borrows the caller's storage
|
||||
// (often a stack buffer). Redundant unless nothing was emitted (e.g. 'msg_silent').
|
||||
msg_ext_id = INTEGER_OBJ(msg_id_next);
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -3462,7 +3465,10 @@ void msg_ext_ui_flush(void)
|
||||
msg_ext_append = false;
|
||||
msg_ext_fast = true;
|
||||
msg_ext_kind = NULL;
|
||||
msg_id_next += (msg_ext_id.data.integer == msg_id_next);
|
||||
// Consume the pre-allocated id, if the message did not get one from msg_multihl().
|
||||
if (msg_ext_id.type == kObjectTypeInteger && msg_ext_id.data.integer == msg_id_next) {
|
||||
msg_id_next++;
|
||||
}
|
||||
msg_ext_id = INTEGER_OBJ(msg_id_next);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,6 +572,23 @@ describe('cd during temp context-switch', function()
|
||||
eq({ 1, tabdir, tabdir, startdir }, { tlwd(), tcwd(), cwd(), cwd(-1, -1) })
|
||||
end)
|
||||
|
||||
it('does not linger in buffer names after switch back #41424', function()
|
||||
local bufdir = join(startdir, directories.buffer)
|
||||
command('edit ' .. join(bufdir, tmpfile))
|
||||
command('bcd ' .. bufdir)
|
||||
command('split Xtest-cd-other') -- Buffer with no local dir.
|
||||
local otherwin = call('win_getid')
|
||||
command('wincmd p')
|
||||
eq({ bufdir, tmpfile }, { cwd(), call('bufname', '%') })
|
||||
|
||||
-- Entering a window leaves `bufdir`, since the buffer there has no local dir.
|
||||
call('win_execute', otherwin, 'split | close')
|
||||
|
||||
-- Names relative to the old CWD would cause ":write" to target nonsense.
|
||||
eq({ bufdir, tmpfile }, { cwd(), call('bufname', '%') })
|
||||
command('write')
|
||||
end)
|
||||
|
||||
it("nvim_open_win / nvim_win_set_buf keep the caller's cwd", function()
|
||||
local bufdir = join(startdir, directories.buffer)
|
||||
command('bcd ' .. bufdir)
|
||||
|
||||
@@ -6,6 +6,7 @@ local clear = n.clear
|
||||
local eq = t.eq
|
||||
local fn = n.fn
|
||||
local command = n.command
|
||||
local api = n.api
|
||||
local mkdir = t.mkdir
|
||||
|
||||
describe("'autochdir'", function()
|
||||
@@ -44,4 +45,26 @@ describe("'autochdir'", function()
|
||||
n.rmdir(dir_a)
|
||||
n.rmdir(dir_b)
|
||||
end)
|
||||
|
||||
it('win_execute() keeps buffer names #41417', function()
|
||||
local root = vim.fs.normalize(t.tmpname(false))
|
||||
mkdir(root)
|
||||
mkdir(root .. '/a')
|
||||
mkdir(root .. '/b')
|
||||
clear()
|
||||
command('set shellslash')
|
||||
command('edit ' .. root .. '/a/file_a')
|
||||
command('vsplit ' .. root .. '/b/file_b')
|
||||
local win_b, buf_b = api.nvim_get_current_win(), api.nvim_get_current_buf()
|
||||
command('wincmd p')
|
||||
command('set autochdir')
|
||||
-- Sit in an ancestor of both files: 'autochdir' moves away from it during win_execute(), and
|
||||
-- both buffer names stay shortened, i.e. relative to the CWD.
|
||||
command('cd ' .. root)
|
||||
eq({ 'a/file_a', 'b/file_b' }, { fn.bufname('%'), fn.bufname(buf_b) })
|
||||
|
||||
fn.win_execute(win_b, 'echo')
|
||||
|
||||
eq({ 'a/file_a', 'b/file_b' }, { fn.bufname('%'), fn.bufname(buf_b) })
|
||||
end)
|
||||
end)
|
||||
|
||||
@@ -3859,6 +3859,26 @@ describe('progress-message', function()
|
||||
eq(8, id8)
|
||||
end)
|
||||
|
||||
it('msg-id is not inherited by the next message #41417', function()
|
||||
local fname = 'Xtest_progress_msgid'
|
||||
finally(function()
|
||||
os.remove(fname)
|
||||
end)
|
||||
-- The write emits nothing ('msg_silent'), but must still release its msg-id.
|
||||
command('silent write ' .. fname)
|
||||
feed(':echoerr "boom"<CR>')
|
||||
screen:expect({
|
||||
messages = {
|
||||
{
|
||||
content = { { 'boom', 9, 'ErrorMsg' } },
|
||||
history = true,
|
||||
id = 1,
|
||||
kind = 'echoerr',
|
||||
},
|
||||
},
|
||||
})
|
||||
end)
|
||||
|
||||
it('accepts caller-defined id (string)', function()
|
||||
-- string id works
|
||||
local id = api.nvim_echo({ { 'supports str-id' } }, true, {
|
||||
|
||||
@@ -1597,17 +1597,23 @@ function Screen:_extstate_repr(attr_state, exp)
|
||||
|
||||
local messages = {}
|
||||
for i, entry in ipairs(self.messages) do
|
||||
local exp_msg = exp and exp.messages and exp.messages[i]
|
||||
-- Late addition, only include when expected state includes it.
|
||||
local trigger = nil
|
||||
if exp and exp.messages and exp.messages[i] and exp.messages[i].trigger ~= nil then
|
||||
-- Late addition, only include when expected state includes it.
|
||||
if exp_msg and exp_msg.trigger ~= nil then
|
||||
trigger = entry.trigger
|
||||
end
|
||||
-- Progress messages are identified by their id, so always show it for them.
|
||||
local id = nil
|
||||
if entry.kind == 'progress' or (exp_msg and exp_msg.id ~= nil) then
|
||||
id = entry.id
|
||||
end
|
||||
messages[i] = {
|
||||
kind = entry.kind,
|
||||
content = self:_chunks_repr(entry.content, attr_state),
|
||||
history = entry.history or nil,
|
||||
append = entry.append or nil,
|
||||
id = entry.kind == 'progress' and entry.id or nil,
|
||||
id = id,
|
||||
trigger = trigger,
|
||||
}
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user