diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt index b673592a26..c5fa6b588b 100644 --- a/runtime/doc/autocmd.txt +++ b/runtime/doc/autocmd.txt @@ -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 diff --git a/runtime/lua/vim/_core/defaults.lua b/runtime/lua/vim/_core/defaults.lua index 73d85a7283..b508827c3b 100644 --- a/runtime/lua/vim/_core/defaults.lua +++ b/runtime/lua/vim/_core/defaults.lua @@ -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 diff --git a/runtime/lua/vim/_meta/events.lua b/runtime/lua/vim/_meta/events.lua index 364d706bf6..a88464ac5c 100644 --- a/runtime/lua/vim/_meta/events.lua +++ b/runtime/lua/vim/_meta/events.lua @@ -60,4 +60,5 @@ error('Cannot require a meta file') --- @field cursor integer[] --- @class vim.event.termresponse.data +--- @field chan integer --- @field sequence string diff --git a/runtime/lua/vim/tty.lua b/runtime/lua/vim/tty.lua index 6771efe849..06b8eddcec 100644 --- a/runtime/lua/vim/tty.lua +++ b/runtime/lua/vim/tty.lua @@ -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 diff --git a/src/nvim/api/events.c b/src/nvim/api/events.c index a1cd21ed67..81a02072b7 100644 --- a/src/nvim/api/events.c +++ b/src/nvim/api/events.c @@ -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); } } diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c index f9f2ce84b1..7b013dfd37 100644 --- a/src/nvim/autocmd.c +++ b/src/nvim/autocmd.c @@ -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); } } diff --git a/test/functional/api/ui_spec.lua b/test/functional/api/ui_spec.lua index dff37d2a02..6ecddaffa1 100644 --- a/test/functional/api/ui_spec.lua +++ b/test/functional/api/ui_spec.lua @@ -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() diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 41952f18a8..9e7c803c19 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -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.