mirror of
https://github.com/neovim/neovim.git
synced 2025-10-05 09:26:30 +00:00
feat(tui): add nvim_ui_send (#35406)
This function allows the Nvim core to write arbitrary data to a TTY connected to a UI's stdout.
This commit is contained in:
@@ -276,6 +276,10 @@ the editor.
|
||||
to an internal buffer, this is the time to display the redrawn parts
|
||||
to the user.
|
||||
|
||||
["ui_send", content] ~
|
||||
Write {content} to the connected TTY. Only UIs that have the
|
||||
"stdout_tty" |ui-option| set will receive this event.
|
||||
|
||||
==============================================================================
|
||||
Grid Events (line-based) *ui-linegrid*
|
||||
|
||||
|
@@ -3598,6 +3598,18 @@ nvim_ui_pum_set_height({height}) *nvim_ui_pum_set_height()*
|
||||
Parameters: ~
|
||||
• {height} (`integer`) Popupmenu height, must be greater than zero.
|
||||
|
||||
nvim_ui_send({content}) *nvim_ui_send()*
|
||||
WARNING: This feature is experimental/unstable.
|
||||
|
||||
Sends arbitrary data to a UI.
|
||||
|
||||
This sends a "ui_send" event to any UI that has the "stdout_tty"
|
||||
|ui-option| set. UIs are expected to write the received data to a
|
||||
connected TTY if one exists.
|
||||
|
||||
Parameters: ~
|
||||
• {content} (`string`) Content to write to the TTY
|
||||
|
||||
nvim_ui_set_focus({gained}) *nvim_ui_set_focus()*
|
||||
Tells the nvim server if focus was gained or lost by the GUI
|
||||
|
||||
|
@@ -1067,7 +1067,7 @@ TermResponse When Nvim receives a DA1, OSC, DCS, or APC response from
|
||||
local r, g, b = resp:match("\027%]4;1;rgb:(%w+)/(%w+)/(%w+)")
|
||||
end,
|
||||
})
|
||||
io.stdout:write("\027]4;1;?\027\\")
|
||||
vim.api.nvim_ui_send("\027]4;1;?\027\\")
|
||||
<
|
||||
*TextChanged*
|
||||
TextChanged After a change was made to the text in the
|
||||
|
@@ -678,11 +678,11 @@ LspProgress *LspProgress*
|
||||
callback = function(ev)
|
||||
local value = ev.data.params.value
|
||||
if value.kind == 'begin' then
|
||||
io.stdout:write('\027]9;4;1;0\027\\')
|
||||
vim.api.nvim_ui_send('\027]9;4;1;0\027\\')
|
||||
elseif value.kind == 'end' then
|
||||
io.stdout:write('\027]9;4;0\027\\')
|
||||
vim.api.nvim_ui_send('\027]9;4;0\027\\')
|
||||
elseif value.kind == 'report' then
|
||||
io.stdout:write(string.format('\027]9;4;1;%d\027\\', value.percentage or 0))
|
||||
vim.api.nvim_ui_send(string.format('\027]9;4;1;%d\027\\', value.percentage or 0))
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
@@ -140,6 +140,8 @@ API
|
||||
• Added |vim.lsp.is_enabled()| to check if a given LSP config has been enabled
|
||||
by |vim.lsp.enable()|.
|
||||
• |nvim_echo()| can set the |ui-messages| kind with which to emit the message.
|
||||
• |nvim_ui_send()| writes arbitrary data to a UI's stdout. Use this to write
|
||||
escape sequences to the terminal when Nvim is running in the |TUI|.
|
||||
|
||||
BUILD
|
||||
|
||||
|
@@ -211,9 +211,9 @@ local function try_query_terminal_color(color)
|
||||
end,
|
||||
})
|
||||
if type(color) == 'number' then
|
||||
io.stdout:write(('\027]%s;%s;?\027\\'):format(parameter, color))
|
||||
vim.api.nvim_ui_send(('\027]%s;%s;?\027\\'):format(parameter, color))
|
||||
else
|
||||
io.stdout:write(('\027]%s;?\027\\'):format(parameter))
|
||||
vim.api.nvim_ui_send(('\027]%s;?\027\\'):format(parameter))
|
||||
end
|
||||
vim.wait(100, function()
|
||||
return hex and true or false
|
||||
|
@@ -839,7 +839,7 @@ do
|
||||
end,
|
||||
})
|
||||
|
||||
io.stdout:write('\027]11;?\007')
|
||||
vim.api.nvim_ui_send('\027]11;?\007')
|
||||
end
|
||||
|
||||
--- If the TUI (term_has_truecolor) was able to determine that the host
|
||||
@@ -927,7 +927,7 @@ do
|
||||
local decrqss = '\027P$qm\027\\'
|
||||
|
||||
-- Reset attributes first, as other code may have set attributes.
|
||||
io.stdout:write(string.format('\027[0m\027[48;2;%d;%d;%dm%s', r, g, b, decrqss))
|
||||
vim.api.nvim_ui_send(string.format('\027[0m\027[48;2;%d;%d;%dm%s', r, g, b, decrqss))
|
||||
|
||||
timer:start(1000, 0, function()
|
||||
-- Delete the autocommand if no response was received
|
||||
|
8
runtime/lua/vim/_meta/api.lua
generated
8
runtime/lua/vim/_meta/api.lua
generated
@@ -2340,6 +2340,14 @@ function vim.api.nvim_tabpage_set_var(tabpage, name, value) end
|
||||
--- @param win integer `window-ID`, must already belong to {tabpage}
|
||||
function vim.api.nvim_tabpage_set_win(tabpage, win) end
|
||||
|
||||
--- Sends arbitrary data to a UI.
|
||||
---
|
||||
--- This sends a "ui_send" event to any UI that has the "stdout_tty" `ui-option` set. UIs are
|
||||
--- expected to write the received data to a connected TTY if one exists.
|
||||
---
|
||||
--- @param content string Content to write to the TTY
|
||||
function vim.api.nvim_ui_send(content) end
|
||||
|
||||
--- Calls a function with window as temporary current window.
|
||||
---
|
||||
---
|
||||
|
@@ -71,7 +71,7 @@ function M.query(caps, cb)
|
||||
|
||||
local query = string.format('\027P+q%s\027\\', table.concat(encoded, ';'))
|
||||
|
||||
io.stdout:write(query)
|
||||
vim.api.nvim_ui_send(query)
|
||||
|
||||
timer:start(1000, 0, function()
|
||||
-- Delete the autocommand if no response was received
|
||||
|
@@ -14,8 +14,7 @@ function M.copy(reg)
|
||||
return function(lines)
|
||||
local s = table.concat(lines, '\n')
|
||||
-- The data to be written here can be quite long.
|
||||
-- Use nvim_chan_send() as io.stdout:write() doesn't handle EAGAIN. #26688
|
||||
vim.api.nvim_chan_send(2, osc52(clipboard, vim.base64.encode(s)))
|
||||
vim.api.nvim_ui_send(osc52(clipboard, vim.base64.encode(s)))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,7 +33,7 @@ function M.paste(reg)
|
||||
end,
|
||||
})
|
||||
|
||||
io.stdout:write(osc52(clipboard, '?'))
|
||||
vim.api.nvim_ui_send(osc52(clipboard, '?'))
|
||||
|
||||
local ok, res
|
||||
|
||||
|
@@ -89,7 +89,7 @@ vim.api.nvim_create_autocmd('UIEnter', {
|
||||
})
|
||||
|
||||
-- Write DA1 request
|
||||
io.stdout:write('\027[c')
|
||||
vim.api.nvim_ui_send('\027[c')
|
||||
end,
|
||||
})
|
||||
|
||||
|
@@ -1001,6 +1001,17 @@ void remote_ui_flush(RemoteUI *ui)
|
||||
}
|
||||
}
|
||||
|
||||
void remote_ui_ui_send(RemoteUI *ui, String content)
|
||||
{
|
||||
if (!ui->stdout_tty) {
|
||||
return;
|
||||
}
|
||||
|
||||
MAXSIZE_TEMP_ARRAY(args, 1);
|
||||
ADD_C(args, STRING_OBJ(content));
|
||||
push_call(ui, "ui_send", args);
|
||||
}
|
||||
|
||||
void remote_ui_flush_pending_data(RemoteUI *ui)
|
||||
{
|
||||
ui_flush_buf(ui, false);
|
||||
@@ -1103,3 +1114,17 @@ void remote_ui_event(RemoteUI *ui, char *name, Array args)
|
||||
free_ret:
|
||||
arena_mem_free(arena_finish(&arena));
|
||||
}
|
||||
|
||||
/// Sends arbitrary data to a UI.
|
||||
///
|
||||
/// This sends a "ui_send" event to any UI that has the "stdout_tty" |ui-option| set. UIs are
|
||||
/// expected to write the received data to a connected TTY if one exists.
|
||||
///
|
||||
/// @param channel_id
|
||||
/// @param content Content to write to the TTY
|
||||
/// @param[out] err Error details, if any
|
||||
void nvim_ui_send(uint64_t channel_id, String content, Error *err)
|
||||
FUNC_API_SINCE(14)
|
||||
{
|
||||
ui_call_ui_send(content);
|
||||
}
|
||||
|
@@ -46,6 +46,8 @@ void chdir(String path)
|
||||
// Stop event is not exported as such, represented by EOF in the msgpack stream.
|
||||
void stop(void)
|
||||
FUNC_API_NOEXPORT;
|
||||
void ui_send(String content)
|
||||
FUNC_API_SINCE(14) FUNC_API_REMOTE_IMPL;
|
||||
|
||||
// First revision of the grid protocol, used by default
|
||||
void update_fg(Integer fg)
|
||||
|
@@ -1533,6 +1533,19 @@ void tui_default_colors_set(TUIData *tui, Integer rgb_fg, Integer rgb_bg, Intege
|
||||
invalidate(tui, 0, tui->grid.height, 0, tui->grid.width);
|
||||
}
|
||||
|
||||
/// Writes directly to the TTY, bypassing the buffer.
|
||||
void tui_ui_send(TUIData *tui, String content)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
uv_write_t req;
|
||||
uv_buf_t buf = { .base = content.data, .len = UV_BUF_LEN(content.size) };
|
||||
int ret = uv_write(&req, (uv_stream_t *)&tui->output_handle, &buf, 1, NULL);
|
||||
if (ret) {
|
||||
ELOG("uv_write failed: %s", uv_strerror(ret));
|
||||
}
|
||||
uv_run(&tui->write_loop, UV_RUN_DEFAULT);
|
||||
}
|
||||
|
||||
/// Flushes TUI grid state to a buffer (which is later flushed to the TTY by `flush_buf`).
|
||||
///
|
||||
/// @see flush_buf
|
||||
|
@@ -10,7 +10,9 @@ local exec = n.exec
|
||||
local feed = n.feed
|
||||
local api = n.api
|
||||
local request = n.request
|
||||
local poke_eventloop = n.poke_eventloop
|
||||
local pcall_err = t.pcall_err
|
||||
local uv = vim.uv
|
||||
|
||||
describe('nvim_ui_attach()', function()
|
||||
before_each(function()
|
||||
@@ -71,6 +73,72 @@ describe('nvim_ui_attach()', function()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('nvim_ui_send', function()
|
||||
before_each(function()
|
||||
clear()
|
||||
end)
|
||||
|
||||
it('works with stdout_tty', function()
|
||||
local fds = assert(uv.pipe())
|
||||
|
||||
local read_pipe = assert(uv.new_pipe())
|
||||
read_pipe:open(fds.read)
|
||||
|
||||
local read_data = {}
|
||||
read_pipe:read_start(function(err, data)
|
||||
assert(not err, err)
|
||||
if data then
|
||||
table.insert(read_data, data)
|
||||
end
|
||||
end)
|
||||
|
||||
local screen = Screen.new(50, 10, { stdout_tty = true })
|
||||
screen:set_stdout(fds.write)
|
||||
|
||||
api.nvim_ui_send('Hello world')
|
||||
|
||||
poke_eventloop()
|
||||
|
||||
screen:expect([[
|
||||
^ |
|
||||
{1:~ }|*8
|
||||
|
|
||||
]])
|
||||
|
||||
eq('Hello world', table.concat(read_data))
|
||||
end)
|
||||
|
||||
it('ignores ui_send event for UIs without stdout_tty', function()
|
||||
local fds = assert(uv.pipe())
|
||||
|
||||
local read_pipe = assert(uv.new_pipe())
|
||||
read_pipe:open(fds.read)
|
||||
|
||||
local read_data = {}
|
||||
read_pipe:read_start(function(err, data)
|
||||
assert(not err, err)
|
||||
if data then
|
||||
table.insert(read_data, data)
|
||||
end
|
||||
end)
|
||||
|
||||
local screen = Screen.new(50, 10)
|
||||
screen:set_stdout(fds.write)
|
||||
|
||||
api.nvim_ui_send('Hello world')
|
||||
|
||||
poke_eventloop()
|
||||
|
||||
screen:expect([[
|
||||
^ |
|
||||
{1:~ }|*8
|
||||
|
|
||||
]])
|
||||
|
||||
eq('', table.concat(read_data))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('autocmds UIEnter/UILeave', function()
|
||||
clear { args_rm = { '--headless' } }
|
||||
exec([[
|
||||
|
@@ -48,6 +48,7 @@
|
||||
local t = require('test.testutil')
|
||||
local n = require('test.functional.testnvim')()
|
||||
local busted = require('busted')
|
||||
local uv = vim.uv
|
||||
|
||||
local deepcopy = vim.deepcopy
|
||||
local shallowcopy = t.shallowcopy
|
||||
@@ -89,6 +90,7 @@ end
|
||||
--- @field private _grid_win_extmarks table<integer,table>
|
||||
--- @field private _attr_table table<integer,table>
|
||||
--- @field private _hl_info table<integer,table>
|
||||
--- @field private _stdout uv.uv_pipe_t?
|
||||
local Screen = {}
|
||||
Screen.__index = Screen
|
||||
|
||||
@@ -235,6 +237,7 @@ function Screen.new(width, height, options, session)
|
||||
col = 1,
|
||||
},
|
||||
_busy = false,
|
||||
_stdout = nil,
|
||||
}, Screen)
|
||||
|
||||
local function ui(method, ...)
|
||||
@@ -278,6 +281,12 @@ function Screen:set_rgb_cterm(val)
|
||||
self._rgb_cterm = val
|
||||
end
|
||||
|
||||
--- @param fd number
|
||||
function Screen:set_stdout(fd)
|
||||
self._stdout = assert(uv.new_pipe())
|
||||
self._stdout:open(fd)
|
||||
end
|
||||
|
||||
--- @param session? test.Session
|
||||
function Screen:attach(session)
|
||||
session = session or get_session()
|
||||
@@ -1416,6 +1425,12 @@ function Screen:_handle_msg_history_show(entries, prev_cmd)
|
||||
self.msg_history = { entries, prev_cmd }
|
||||
end
|
||||
|
||||
function Screen:_handle_ui_send(content)
|
||||
if self._stdout then
|
||||
self._stdout:write(content)
|
||||
end
|
||||
end
|
||||
|
||||
function Screen:_clear_block(grid, top, bot, left, right)
|
||||
for i = top, bot do
|
||||
self:_clear_row_section(grid, i, left, right)
|
||||
|
Reference in New Issue
Block a user