refactor(tui): use synchronized updates around actual buf flush (#26478)

Rather than writing the synchronized update begin and end sequences into
the TUI's internal buffer (where it is later flushed to the TTY), write
these sequences directly to the TTY before and after the TUI's internal
buffer is itself flushed to the TTY.

This guarantees that a synchronized update is always used when we are
actually sending data to the TTY. This means we do not need to keep
track of the TUI's "dirty" state (any sequences which affect the TUI
state will be written in the TUI's internal buffer, which is now
guaranteed to only ever be written when a synchronized update is
active).
This commit is contained in:
Gregory Anders 2023-12-08 18:44:16 -08:00 committed by GitHub
parent f45bf44176
commit c651fb3042
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 76 additions and 64 deletions

View File

@ -42,9 +42,6 @@
# include "nvim/os/tty.h"
#endif
// Space reserved in two output buffers to make the cursor normal or invisible
// when flushing. No existing terminal will require 32 bytes to do that.
#define CNORM_COMMAND_MAX_SIZE 32
#define OUTBUF_SIZE 0xffff
#define TOO_MANY_EVENTS 1000000
@ -76,7 +73,6 @@ struct TUIData {
size_t bufpos;
TermInput input;
uv_loop_t write_loop;
bool dirty; ///< whether there has been drawing since the last tui_flush()
unibi_term *ut;
char *term; ///< value of $TERM
union {
@ -901,8 +897,6 @@ static void print_cell_at_pos(TUIData *tui, int row, int col, UCell *cell, bool
return;
}
tui_set_dirty(tui);
cursor_goto(tui, row, col);
char buf[MAX_SCHAR_SIZE];
@ -928,8 +922,6 @@ static void clear_region(TUIData *tui, int top, int bot, int left, int right, in
{
UGrid *grid = &tui->grid;
tui_set_dirty(tui);
update_attrs(tui, attr_id);
// Background is set to the default color and the right edge matches the
@ -1247,8 +1239,6 @@ void tui_grid_scroll(TUIData *tui, Integer g, Integer startrow, Integer endrow,
|| tui->can_set_left_right_margin)));
if (can_scroll) {
tui_set_dirty(tui);
// Change terminal scroll region and move cursor to the top
if (!tui->scroll_region_is_full_screen) {
set_scroll_region(tui, top, bot, left, right);
@ -1318,48 +1308,6 @@ void tui_default_colors_set(TUIData *tui, Integer rgb_fg, Integer rgb_bg, Intege
invalidate(tui, 0, tui->grid.height, 0, tui->grid.width);
}
static void tui_set_dirty(TUIData *tui)
FUNC_ATTR_NONNULL_ALL
{
if (!tui->dirty) {
tui->dirty = true;
tui_flush_start(tui);
}
}
/// Begin flushing the TUI. If 'termsync' is set and the terminal supports synchronized updates,
/// begin a synchronized update. Otherwise, hide the cursor to avoid cursor jumping.
static void tui_flush_start(TUIData *tui)
FUNC_ATTR_NONNULL_ALL
{
if (tui->sync_output && tui->unibi_ext.sync != -1) {
UNIBI_SET_NUM_VAR(tui->params[0], 1);
unibi_out_ext(tui, tui->unibi_ext.sync);
} else if (!tui->is_invisible) {
unibi_out(tui, unibi_cursor_invisible);
tui->is_invisible = true;
}
}
/// Finish flushing the TUI. If 'termsync' is set and the terminal supports synchronized updates,
/// end a synchronized update. Otherwise, make the cursor visible again.
static void tui_flush_end(TUIData *tui)
FUNC_ATTR_NONNULL_ALL
{
if (tui->sync_output && tui->unibi_ext.sync != -1) {
UNIBI_SET_NUM_VAR(tui->params[0], 0);
unibi_out_ext(tui, tui->unibi_ext.sync);
}
bool should_invisible = tui->busy || tui->want_invisible;
if (tui->is_invisible && !should_invisible) {
unibi_out(tui, unibi_cursor_normal);
tui->is_invisible = false;
} else if (!tui->is_invisible && should_invisible) {
unibi_out(tui, unibi_cursor_invisible);
tui->is_invisible = true;
}
}
void tui_flush(TUIData *tui)
{
UGrid *grid = &tui->grid;
@ -1376,8 +1324,6 @@ void tui_flush(TUIData *tui)
tui_busy_stop(tui); // avoid hidden cursor
}
tui_set_dirty(tui);
while (kv_size(tui->invalid_regions)) {
Rect r = kv_pop(tui->invalid_regions);
assert(r.bot <= grid->height && r.right <= grid->width);
@ -1405,10 +1351,7 @@ void tui_flush(TUIData *tui)
cursor_goto(tui, tui->row, tui->col);
assert(tui->dirty);
tui_flush_end(tui);
flush_buf(tui);
tui->dirty = false;
}
/// Dumps termcap info to the messages area, if 'verbose' >= 3.
@ -2312,23 +2255,93 @@ static void augment_terminfo(TUIData *tui, const char *term, int vte_version, in
}
}
/// Write the sequence to begin flushing output to `buf`.
/// If 'termsync' is set and the terminal supports synchronized output, begin synchronized update.
/// Otherwise, hide the cursor to avoid cursor jumping.
///
/// @param buf the buffer to write the sequence to
/// @param len the length of `buf`
static size_t flush_buf_start(TUIData *tui, char *buf, size_t len)
FUNC_ATTR_NONNULL_ALL
{
const char *str = NULL;
if (tui->sync_output && tui->unibi_ext.sync != -1) {
UNIBI_SET_NUM_VAR(tui->params[0], 1);
str = unibi_get_ext_str(tui->ut, (size_t)tui->unibi_ext.sync);
} else if (!tui->is_invisible) {
str = unibi_get_str(tui->ut, unibi_cursor_invisible);
tui->is_invisible = true;
}
if (str == NULL) {
return 0;
}
return unibi_run(str, tui->params, buf, len);
}
/// Write the sequence to end flushing output to `buf`.
/// If 'termsync' is set and the terminal supports synchronized output, end synchronized update.
/// Otherwise, make the cursor visible again.
///
/// @param buf the buffer to write the sequence to
/// @param len the length of `buf`
static size_t flush_buf_end(TUIData *tui, char *buf, size_t len)
FUNC_ATTR_NONNULL_ALL
{
size_t offset = 0;
if (tui->sync_output && tui->unibi_ext.sync != -1) {
UNIBI_SET_NUM_VAR(tui->params[0], 0);
const char *str = unibi_get_ext_str(tui->ut, (size_t)tui->unibi_ext.sync);
offset = unibi_run(str, tui->params, buf, len);
}
const char *str = NULL;
bool should_invisible = tui->busy || tui->want_invisible;
if (tui->is_invisible && !should_invisible) {
str = unibi_get_str(tui->ut, unibi_cursor_normal);
tui->is_invisible = false;
} else if (!tui->is_invisible && should_invisible) {
str = unibi_get_str(tui->ut, unibi_cursor_invisible);
tui->is_invisible = true;
}
if (str != NULL) {
assert(len >= offset);
offset += unibi_run(str, tui->params, buf + offset, len - offset);
}
return offset;
}
static void flush_buf(TUIData *tui)
{
uv_write_t req;
uv_buf_t buf;
uv_buf_t bufs[3];
char pre[32];
char post[32];
if (tui->bufpos <= 0) {
return;
}
buf.base = tui->buf;
buf.len = UV_BUF_LEN(tui->bufpos);
bufs[0].base = pre;
bufs[0].len = UV_BUF_LEN(flush_buf_start(tui, pre, sizeof(pre)));
bufs[1].base = tui->buf;
bufs[1].len = UV_BUF_LEN(tui->bufpos);
bufs[2].base = post;
bufs[2].len = UV_BUF_LEN(flush_buf_end(tui, post, sizeof(post)));
if (tui->screenshot) {
fwrite(buf.base, buf.len, 1, tui->screenshot);
for (size_t i = 0; i < ARRAY_SIZE(bufs); i++) {
fwrite(bufs[i].base, bufs[i].len, 1, tui->screenshot);
}
} else {
int ret
= uv_write(&req, (uv_stream_t *)&tui->output_handle, &buf, 1, NULL);
= uv_write(&req, (uv_stream_t *)&tui->output_handle, bufs, ARRAY_SIZE(bufs), NULL);
if (ret) {
ELOG("uv_write failed: %s", uv_strerror(ret));
}

View File

@ -1671,10 +1671,9 @@ describe('TUI', function()
-- Use full screen message so that redrawing afterwards is more deterministic.
child_session:notify('nvim_command', 'intro')
screen:expect({any = 'Nvim'})
-- Hiding the cursor needs 6 bytes.
-- Going to top-left corner needs 3 bytes.
-- Setting underline attribute needs 9 bytes.
-- The whole line needs 6 + 3 + 9 + 65513 + 3 = 65534 bytes.
-- The whole line needs 3 + 9 + 65513 + 3 = 65528 bytes.
-- The cursor_address that comes after will overflow the 65535-byte buffer.
local line = ('a'):rep(65513) .. ''
child_session:notify('nvim_exec_lua', [[