mirror of
https://github.com/neovim/neovim.git
synced 2026-07-10 03:19:44 +00:00
fix(tui): attribute TermResponse to source channel #40330
Problem: Attach-time terminal probes cannot distinguish responses from different attached UIs. Solution: Identify the UI by RPC channel id in `TermResponse` and make `vim.tty.request()` filter responses by channel.
This commit is contained in:
@@ -1227,6 +1227,7 @@ TermResponse When Nvim receives a DA1, OSC, DCS, or APC response from
|
||||
the host terminal. Sets |v:termresponse|.
|
||||
|
||||
The |event-data| has these keys (type: `vim.event.termresponse.data`):
|
||||
- chan: the source channel id
|
||||
- sequence: the received sequence
|
||||
|
||||
This is triggered even when inside an
|
||||
|
||||
@@ -922,7 +922,7 @@ do
|
||||
--- @param sync boolean When true (a TTY is present at startup), also send a
|
||||
--- DSR probe and synchronously wait so 'background' is set before user config,
|
||||
--- warning (E1568) if the terminal never answers the DSR.
|
||||
local function detect_background(sync)
|
||||
local function detect_background(sync, chan)
|
||||
-- Re-create (clear) the handler's augroup on each call so only the
|
||||
-- most-recently-attached TUI's handler remains.
|
||||
local bg_group = vim.api.nvim_create_augroup('nvim.tty.background', {})
|
||||
@@ -938,36 +938,40 @@ do
|
||||
-- also reacts to runtime theme changes; the per-response bg_user_set() guard
|
||||
-- stops it once the user pins 'background'.
|
||||
local did_dsr_response = false
|
||||
vim.tty.request(osc11 .. (sync and dsr or ''), { group = bg_group, timeout = 0 }, function(resp)
|
||||
-- DSR response that should come after the OSC 11 response if the terminal
|
||||
-- supports it.
|
||||
if sync and string.match(resp, '^\027%[0n$') then
|
||||
did_dsr_response = true
|
||||
-- Don't stop listening: the bg response may come after the DSR response
|
||||
-- if the terminal handles requests out of sequence. In that case, the bg
|
||||
-- will simply be set later in the startup sequence.
|
||||
return
|
||||
end
|
||||
vim.tty.request(
|
||||
osc11 .. (sync and dsr or ''),
|
||||
{ group = bg_group, timeout = 0, chan = chan },
|
||||
function(resp)
|
||||
-- DSR response that should come after the OSC 11 response if the terminal
|
||||
-- supports it.
|
||||
if sync and string.match(resp, '^\027%[0n$') then
|
||||
did_dsr_response = true
|
||||
-- Don't stop listening: the bg response may come after the DSR response
|
||||
-- if the terminal handles requests out of sequence. In that case, the bg
|
||||
-- will simply be set later in the startup sequence.
|
||||
return
|
||||
end
|
||||
|
||||
-- Never override an explicit user value: stop once the user pins it.
|
||||
if bg_user_set() then
|
||||
return true
|
||||
end
|
||||
-- Never override an explicit user value: stop once the user pins it.
|
||||
if bg_user_set() then
|
||||
return true
|
||||
end
|
||||
|
||||
local r, g, b = parseosc11(resp)
|
||||
if r and g and b then
|
||||
local rr = parsecolor(r)
|
||||
local gg = parsecolor(g)
|
||||
local bb = parsecolor(b)
|
||||
local r, g, b = parseosc11(resp)
|
||||
if r and g and b then
|
||||
local rr = parsecolor(r)
|
||||
local gg = parsecolor(g)
|
||||
local bb = parsecolor(b)
|
||||
|
||||
if rr and gg and bb then
|
||||
local luminance = (0.299 * rr) + (0.587 * gg) + (0.114 * bb)
|
||||
local bg = luminance < 0.5 and 'dark' or 'light'
|
||||
-- Use :noautocmd to suppress OptionSet event; OSC11 response may arrive after VimEnter.
|
||||
vim.cmd('noautocmd set background=' .. bg)
|
||||
if rr and gg and bb then
|
||||
local luminance = (0.299 * rr) + (0.587 * gg) + (0.114 * bb)
|
||||
local bg = luminance < 0.5 and 'dark' or 'light'
|
||||
-- Use :noautocmd to suppress OptionSet event; OSC11 response may arrive after VimEnter.
|
||||
vim.cmd('noautocmd set background=' .. bg)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
)
|
||||
|
||||
if not sync then
|
||||
return
|
||||
@@ -1133,7 +1137,7 @@ do
|
||||
-- runtime theme changes (mode 2031 -> TUI re-queries -> |TermResponse|);
|
||||
-- the per-response bg_user_set() guard preserves an explicit user value.
|
||||
if vim.o.ttyfast then
|
||||
detect_background(false)
|
||||
detect_background(false, ui.chan)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -60,4 +60,5 @@ error('Cannot require a meta file')
|
||||
--- @field cursor integer[]
|
||||
|
||||
--- @class vim.event.termresponse.data
|
||||
--- @field chan integer
|
||||
--- @field sequence string
|
||||
|
||||
@@ -10,10 +10,11 @@ local M = {}
|
||||
---
|
||||
---@param payload string Sequence to send via nvim_ui_send(). Use empty string ('') to just register
|
||||
--- a listener (no sending).
|
||||
---@param opts? { timeout?: integer, on_timeout?: fun(), group?: integer|string }
|
||||
---@param opts? { timeout?: integer, on_timeout?: fun(), group?: integer|string, chan?: integer }
|
||||
--- - `timeout` (default: 1000) ms to wait before giving up, or 0 for never (caller must remove the autocmd).
|
||||
--- - `on_timeout` optional fn called when the timeout fires.
|
||||
--- - `group`: augroup for the TermResponse autocmd.
|
||||
--- - `chan`: only handle responses from this channel.
|
||||
---@param on_response fun(resp:string):boolean? Called for each TermResponse. Return `true` to stop listening.
|
||||
---@return integer # autocmd id of the TermResponse handler.
|
||||
function M.request(payload, opts, on_response)
|
||||
@@ -32,7 +33,11 @@ function M.request(payload, opts, on_response)
|
||||
'TermResponse',
|
||||
opts.group,
|
||||
{ nested = true },
|
||||
---@param ev {data: vim.event.termresponse.data}
|
||||
function(ev)
|
||||
if opts.chan and ev.data.chan ~= opts.chan then
|
||||
return
|
||||
end
|
||||
local stop = on_response(ev.data.sequence)
|
||||
-- If on_response is done, cancel the timeout so on_timeout doesn't fire spuriously.
|
||||
if stop and timer and not timer:is_closing() then
|
||||
|
||||
@@ -66,6 +66,6 @@ void nvim_ui_term_event(uint64_t channel_id, String event, Object value, Error *
|
||||
|
||||
const String termresponse = value.data.string;
|
||||
set_vim_var_string(VV_TERMRESPONSE, termresponse.data, (ptrdiff_t)termresponse.size);
|
||||
do_termresponse_autocmd(termresponse);
|
||||
do_termresponse_autocmd(termresponse, channel_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ static bool autocmd_nested = false;
|
||||
static bool autocmd_include_groups = false;
|
||||
|
||||
static bool termresponse_changed = false;
|
||||
static uint64_t termresponse_chan_id = 0;
|
||||
|
||||
// Map of autocmd group names and ids.
|
||||
// name -> ID
|
||||
@@ -2085,13 +2086,15 @@ BYPASS_AU:
|
||||
return retval;
|
||||
}
|
||||
|
||||
void do_termresponse_autocmd(const String sequence)
|
||||
void do_termresponse_autocmd(const String sequence, uint64_t channel_id)
|
||||
{
|
||||
MAXSIZE_TEMP_DICT(data, 1);
|
||||
MAXSIZE_TEMP_DICT(data, 2);
|
||||
PUT_C(data, "sequence", STRING_OBJ(sequence));
|
||||
PUT_C(data, "chan", INTEGER_OBJ((Integer)channel_id));
|
||||
apply_autocmds_group(EVENT_TERMRESPONSE, NULL, NULL, true, AUGROUP_ALL, NULL, NULL,
|
||||
&DICT_OBJ(data), false);
|
||||
termresponse_changed = true;
|
||||
termresponse_chan_id = channel_id;
|
||||
}
|
||||
|
||||
// Block triggering autocommands until unblock_autocmd() is called.
|
||||
@@ -2101,6 +2104,7 @@ void block_autocmds(void)
|
||||
// Detect if v:termresponse is set while blocked.
|
||||
if (!is_autocmd_blocked()) {
|
||||
termresponse_changed = false;
|
||||
termresponse_chan_id = 0;
|
||||
}
|
||||
autocmd_blocked++;
|
||||
}
|
||||
@@ -2115,7 +2119,7 @@ void unblock_autocmds(void)
|
||||
if (!is_autocmd_blocked() && termresponse_changed && has_event(EVENT_TERMRESPONSE)) {
|
||||
// Copied to a new allocation, as termresponse may be freed during the event.
|
||||
const String sequence = cstr_to_string(get_vim_var_str(VV_TERMRESPONSE));
|
||||
do_termresponse_autocmd(sequence);
|
||||
do_termresponse_autocmd(sequence, termresponse_chan_id);
|
||||
api_free_string(sequence);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ local command = n.command
|
||||
local eq = t.eq
|
||||
local eval = n.eval
|
||||
local exec = n.exec
|
||||
local exec_lua = n.exec_lua
|
||||
local feed = n.feed
|
||||
local api = n.api
|
||||
local request = n.request
|
||||
@@ -172,9 +173,10 @@ describe('nvim_ui_send', function()
|
||||
end)
|
||||
end)
|
||||
|
||||
it('autocmds UIEnter/UILeave', function()
|
||||
clear { args_rm = { '--headless' } }
|
||||
exec([[
|
||||
describe('UI event channels', function()
|
||||
it('sets chan for UIEnter/UILeave', function()
|
||||
clear { args_rm = { '--headless' } }
|
||||
exec([[
|
||||
let g:evs = []
|
||||
autocmd UIEnter * call add(g:evs, "UIEnter") | let g:uienter_ev = deepcopy(v:event)
|
||||
autocmd UILeave * call add(g:evs, "UILeave") | let g:uileave_ev = deepcopy(v:event)
|
||||
@@ -182,62 +184,105 @@ it('autocmds UIEnter/UILeave', function()
|
||||
autocmd VimLeave * call add(g:evs, "VimLeave")
|
||||
]])
|
||||
|
||||
local screen = Screen.new()
|
||||
eq({ chan = 1 }, eval('g:uienter_ev'))
|
||||
eq({ 'VimEnter', 'UIEnter' }, eval('g:evs'))
|
||||
local screen = Screen.new()
|
||||
eq({ chan = 1 }, eval('g:uienter_ev'))
|
||||
eq({ 'VimEnter', 'UIEnter' }, eval('g:evs'))
|
||||
|
||||
screen:detach()
|
||||
eq({ chan = 1 }, eval('g:uileave_ev'))
|
||||
eq({ 'VimEnter', 'UIEnter', 'UILeave' }, eval('g:evs'))
|
||||
screen:detach()
|
||||
eq({ chan = 1 }, eval('g:uileave_ev'))
|
||||
eq({ 'VimEnter', 'UIEnter', 'UILeave' }, eval('g:evs'))
|
||||
|
||||
local servername = api.nvim_get_vvar('servername')
|
||||
local servername = api.nvim_get_vvar('servername')
|
||||
|
||||
local session2 = n.connect(servername)
|
||||
local status2, chan2 = session2:request('nvim_get_chan_info', 0)
|
||||
t.ok(status2)
|
||||
local session2 = n.connect(servername)
|
||||
local status2, chan2 = session2:request('nvim_get_chan_info', 0)
|
||||
t.ok(status2)
|
||||
|
||||
local session3 = n.connect(servername)
|
||||
local status3, chan3 = session3:request('nvim_get_chan_info', 0)
|
||||
t.ok(status3)
|
||||
local session3 = n.connect(servername)
|
||||
local status3, chan3 = session3:request('nvim_get_chan_info', 0)
|
||||
t.ok(status3)
|
||||
|
||||
local screen2 = Screen.new(nil, nil, nil, session2)
|
||||
eq({ chan = chan2.id }, eval('g:uienter_ev'))
|
||||
eq({ 'VimEnter', 'UIEnter', 'UILeave', 'UIEnter' }, eval('g:evs'))
|
||||
local screen2 = Screen.new(nil, nil, nil, session2)
|
||||
eq({ chan = chan2.id }, eval('g:uienter_ev'))
|
||||
eq({ 'VimEnter', 'UIEnter', 'UILeave', 'UIEnter' }, eval('g:evs'))
|
||||
|
||||
screen2:detach()
|
||||
eq({ chan = chan2.id }, eval('g:uileave_ev'))
|
||||
eq({ 'VimEnter', 'UIEnter', 'UILeave', 'UIEnter', 'UILeave' }, eval('g:evs'))
|
||||
screen2:detach()
|
||||
eq({ chan = chan2.id }, eval('g:uileave_ev'))
|
||||
eq({ 'VimEnter', 'UIEnter', 'UILeave', 'UIEnter', 'UILeave' }, eval('g:evs'))
|
||||
|
||||
command('let g:evs = ["…"]')
|
||||
command('let g:evs = ["…"]')
|
||||
|
||||
screen2:attach(session2)
|
||||
eq({ chan = chan2.id }, eval('g:uienter_ev'))
|
||||
eq({ '…', 'UIEnter' }, eval('g:evs'))
|
||||
screen2:attach(session2)
|
||||
eq({ chan = chan2.id }, eval('g:uienter_ev'))
|
||||
eq({ '…', 'UIEnter' }, eval('g:evs'))
|
||||
|
||||
Screen.new(nil, nil, nil, session3)
|
||||
eq({ chan = chan3.id }, eval('g:uienter_ev'))
|
||||
eq({ '…', 'UIEnter', 'UIEnter' }, eval('g:evs'))
|
||||
Screen.new(nil, nil, nil, session3)
|
||||
eq({ chan = chan3.id }, eval('g:uienter_ev'))
|
||||
eq({ '…', 'UIEnter', 'UIEnter' }, eval('g:evs'))
|
||||
|
||||
screen:attach(n.get_session())
|
||||
eq({ chan = 1 }, eval('g:uienter_ev'))
|
||||
eq({ '…', 'UIEnter', 'UIEnter', 'UIEnter' }, eval('g:evs'))
|
||||
screen:attach(n.get_session())
|
||||
eq({ chan = 1 }, eval('g:uienter_ev'))
|
||||
eq({ '…', 'UIEnter', 'UIEnter', 'UIEnter' }, eval('g:evs'))
|
||||
|
||||
session3:close()
|
||||
t.retry(nil, 1000, function()
|
||||
eq({}, api.nvim_get_chan_info(chan3.id))
|
||||
session3:close()
|
||||
t.retry(nil, 1000, function()
|
||||
eq({}, api.nvim_get_chan_info(chan3.id))
|
||||
end)
|
||||
eq({ chan = chan3.id }, eval('g:uileave_ev'))
|
||||
eq({ '…', 'UIEnter', 'UIEnter', 'UIEnter', 'UILeave' }, eval('g:evs'))
|
||||
|
||||
command('let g:evs = ["…"]')
|
||||
command('autocmd UILeave * call writefile(g:evs, "Xevents.log")')
|
||||
finally(function()
|
||||
os.remove('Xevents.log')
|
||||
end)
|
||||
n.expect_exit(command, 'qall!')
|
||||
n.check_close() -- Wait for process exit.
|
||||
-- UILeave should have been triggered for both remaining UIs.
|
||||
eq('…\nVimLeave\nUILeave\nUILeave\n', t.read_file('Xevents.log'))
|
||||
end)
|
||||
eq({ chan = chan3.id }, eval('g:uileave_ev'))
|
||||
eq({ '…', 'UIEnter', 'UIEnter', 'UIEnter', 'UILeave' }, eval('g:evs'))
|
||||
|
||||
command('let g:evs = ["…"]')
|
||||
command('autocmd UILeave * call writefile(g:evs, "Xevents.log")')
|
||||
finally(function()
|
||||
os.remove('Xevents.log')
|
||||
it('sets chan for TermResponse and filters tty requests', function()
|
||||
clear()
|
||||
local main_chan = api.nvim_get_chan_info(0).id
|
||||
local session2 = n.connect(api.nvim_get_vvar('servername'))
|
||||
local status2, chan2 = session2:request('nvim_get_chan_info', 0)
|
||||
t.ok(status2)
|
||||
|
||||
exec_lua([[
|
||||
_G.responses = {}
|
||||
vim.api.nvim_create_autocmd('TermResponse', {
|
||||
callback = function(ev)
|
||||
table.insert(_G.responses, ev.data)
|
||||
end,
|
||||
})
|
||||
]])
|
||||
|
||||
request('nvim_ui_term_event', 'termresponse', 'main')
|
||||
session2:request('nvim_ui_term_event', 'termresponse', 'other')
|
||||
|
||||
eq({
|
||||
{ sequence = 'main', chan = main_chan },
|
||||
{ sequence = 'other', chan = chan2.id },
|
||||
}, exec_lua('return _G.responses'))
|
||||
|
||||
exec_lua(
|
||||
[[
|
||||
_G.filtered = {}
|
||||
vim.tty.request('', { timeout = 0, chan = ... }, function(resp)
|
||||
table.insert(_G.filtered, resp)
|
||||
return true
|
||||
end)
|
||||
]],
|
||||
chan2.id
|
||||
)
|
||||
|
||||
request('nvim_ui_term_event', 'termresponse', 'ignored')
|
||||
session2:request('nvim_ui_term_event', 'termresponse', 'accepted')
|
||||
eq({ 'accepted' }, exec_lua('return _G.filtered'))
|
||||
|
||||
session2:close()
|
||||
end)
|
||||
n.expect_exit(command, 'qall!')
|
||||
n.check_close() -- Wait for process exit.
|
||||
-- UILeave should have been triggered for both remaining UIs.
|
||||
eq('…\nVimLeave\nUILeave\nUILeave\n', t.read_file('Xevents.log'))
|
||||
end)
|
||||
|
||||
it('autocmds VimSuspend/VimResume #22041', function()
|
||||
|
||||
@@ -2974,13 +2974,14 @@ describe('TUI', function()
|
||||
end,
|
||||
})
|
||||
]])
|
||||
local chan = child_exec_lua('return vim.api.nvim_list_uis()[1].chan')
|
||||
feed_data('\027P0$r\027\\')
|
||||
retry(nil, 4000, function()
|
||||
eq('\027P0$r', child_exec_lua('return vim.v.termresponse'))
|
||||
end)
|
||||
eq(vim.NIL, child_exec_lua('return _G.data'))
|
||||
child_exec_lua('require("ffi").C.unblock_autocmds()')
|
||||
eq({ sequence = '\027P0$r' }, child_exec_lua('return _G.data'))
|
||||
eq({ sequence = '\027P0$r', chan = chan }, child_exec_lua('return _G.data'))
|
||||
|
||||
-- If TermResponse during TermResponse changes v:termresponse, data.sequence contains the actual
|
||||
-- response that triggered the autocommand.
|
||||
|
||||
Reference in New Issue
Block a user