mirror of
https://github.com/neovim/neovim
synced 2025-07-16 09:11:51 +00:00
fix(tui): avoid flushing buffer halfway an OSC 2 sequence (#30793)
Problem: Setting title while TUI buffer is almost full may cause the end of a flush to be treated as a part of an OSC 2 or OSC 0 sequence, leading to problems like invisible cursor. Solution: Make the whole sequence to set title a unibi_ext string.
This commit is contained in:
@ -134,11 +134,12 @@ struct TUIData {
|
|||||||
int resize_screen;
|
int resize_screen;
|
||||||
int reset_scroll_region;
|
int reset_scroll_region;
|
||||||
int set_cursor_style, reset_cursor_style;
|
int set_cursor_style, reset_cursor_style;
|
||||||
int save_title, restore_title;
|
int save_title, restore_title, set_title;
|
||||||
int set_underline_style;
|
int set_underline_style;
|
||||||
int set_underline_color;
|
int set_underline_color;
|
||||||
int sync;
|
int sync;
|
||||||
} unibi_ext;
|
} unibi_ext;
|
||||||
|
char *set_title;
|
||||||
char *space_buf;
|
char *space_buf;
|
||||||
size_t space_buf_len;
|
size_t space_buf_len;
|
||||||
bool stopped;
|
bool stopped;
|
||||||
@ -536,6 +537,7 @@ static void terminfo_stop(TUIData *tui)
|
|||||||
abort();
|
abort();
|
||||||
}
|
}
|
||||||
unibi_destroy(tui->ut);
|
unibi_destroy(tui->ut);
|
||||||
|
XFREE_CLEAR(tui->set_title);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void tui_terminal_start(TUIData *tui)
|
static void tui_terminal_start(TUIData *tui)
|
||||||
@ -1567,8 +1569,7 @@ void tui_suspend(TUIData *tui)
|
|||||||
|
|
||||||
void tui_set_title(TUIData *tui, String title)
|
void tui_set_title(TUIData *tui, String title)
|
||||||
{
|
{
|
||||||
if (!(unibi_get_str(tui->ut, unibi_to_status_line)
|
if (!unibi_get_ext_str(tui->ut, (unsigned)tui->unibi_ext.set_title)) {
|
||||||
&& unibi_get_str(tui->ut, unibi_from_status_line))) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (title.size > 0) {
|
if (title.size > 0) {
|
||||||
@ -1577,9 +1578,9 @@ void tui_set_title(TUIData *tui, String title)
|
|||||||
unibi_out_ext(tui, tui->unibi_ext.save_title);
|
unibi_out_ext(tui, tui->unibi_ext.save_title);
|
||||||
tui->title_enabled = true;
|
tui->title_enabled = true;
|
||||||
}
|
}
|
||||||
unibi_out(tui, unibi_to_status_line);
|
UNIBI_SET_NUM_VAR(tui->params[0], 0);
|
||||||
out(tui, title.data, title.size);
|
UNIBI_SET_STR_VAR(tui->params[1], title.data);
|
||||||
unibi_out(tui, unibi_from_status_line);
|
unibi_out_ext(tui, tui->unibi_ext.set_title);
|
||||||
} else if (tui->title_enabled) {
|
} else if (tui->title_enabled) {
|
||||||
// Restore title/icon from the "stack". #4063
|
// Restore title/icon from the "stack". #4063
|
||||||
unibi_out_ext(tui, tui->unibi_ext.restore_title);
|
unibi_out_ext(tui, tui->unibi_ext.restore_title);
|
||||||
@ -1803,13 +1804,18 @@ static void unibi_goto(TUIData *tui, int row, int col)
|
|||||||
memset(&vars, 0, sizeof(vars)); \
|
memset(&vars, 0, sizeof(vars)); \
|
||||||
tui->cork = true; \
|
tui->cork = true; \
|
||||||
retry: \
|
retry: \
|
||||||
|
/* Copy parameters on every retry, as unibi_format() may modify them. */ \
|
||||||
memcpy(params, tui->params, sizeof(params)); \
|
memcpy(params, tui->params, sizeof(params)); \
|
||||||
unibi_format(vars, vars + 26, str, params, out, tui, pad, tui); \
|
unibi_format(vars, vars + 26, str, params, out, tui, pad, tui); \
|
||||||
if (tui->overflow) { \
|
if (tui->overflow) { \
|
||||||
tui->bufpos = orig_pos; \
|
tui->bufpos = orig_pos; \
|
||||||
|
/* If orig_pos is 0, there's nothing to flush and retrying won't work. */ \
|
||||||
|
/* TODO(zeertzjq): should this situation still be handled? */ \
|
||||||
|
if (orig_pos > 0) { \
|
||||||
flush_buf(tui); \
|
flush_buf(tui); \
|
||||||
goto retry; \
|
goto retry; \
|
||||||
} \
|
} \
|
||||||
|
} \
|
||||||
tui->cork = false; \
|
tui->cork = false; \
|
||||||
} \
|
} \
|
||||||
} while (0)
|
} while (0)
|
||||||
@ -1840,6 +1846,7 @@ static void out(void *ctx, const char *str, size_t len)
|
|||||||
}
|
}
|
||||||
flush_buf(tui);
|
flush_buf(tui);
|
||||||
}
|
}
|
||||||
|
// TODO(zeertzjq): handle string longer than buffer size? #30794
|
||||||
|
|
||||||
memcpy(tui->buf + tui->bufpos, str, len);
|
memcpy(tui->buf + tui->bufpos, str, len);
|
||||||
tui->bufpos += len;
|
tui->bufpos += len;
|
||||||
@ -2378,6 +2385,19 @@ static void augment_terminfo(TUIData *tui, const char *term, int vte_version, in
|
|||||||
tui->unibi_ext.save_title = (int)unibi_add_ext_str(ut, "ext.save_title", "\x1b[22;0t");
|
tui->unibi_ext.save_title = (int)unibi_add_ext_str(ut, "ext.save_title", "\x1b[22;0t");
|
||||||
tui->unibi_ext.restore_title = (int)unibi_add_ext_str(ut, "ext.restore_title", "\x1b[23;0t");
|
tui->unibi_ext.restore_title = (int)unibi_add_ext_str(ut, "ext.restore_title", "\x1b[23;0t");
|
||||||
|
|
||||||
|
const char *tsl = unibi_get_str(ut, unibi_to_status_line);
|
||||||
|
const char *fsl = unibi_get_str(ut, unibi_from_status_line);
|
||||||
|
if (tsl != NULL && fsl != NULL) {
|
||||||
|
// Add a single extended capability for the whole sequence to set title,
|
||||||
|
// as it is usually an OSC sequence that cannot be cut in half.
|
||||||
|
// Use %p2 for the title string, as to_status_line may take an argument.
|
||||||
|
size_t set_title_len = strlen(tsl) + strlen("%p2%s") + strlen(fsl);
|
||||||
|
char *set_title = xmallocz(set_title_len);
|
||||||
|
snprintf(set_title, set_title_len + 1, "%s%s%s", tsl, "%p2%s", fsl);
|
||||||
|
tui->unibi_ext.set_title = (int)unibi_add_ext_str(ut, "ext.set_title", set_title);
|
||||||
|
tui->set_title = set_title;
|
||||||
|
}
|
||||||
|
|
||||||
/// Terminals usually ignore unrecognized private modes, and there is no
|
/// Terminals usually ignore unrecognized private modes, and there is no
|
||||||
/// known ambiguity with these. So we just set them unconditionally.
|
/// known ambiguity with these. So we just set them unconditionally.
|
||||||
tui->unibi_ext.enable_lr_margin =
|
tui->unibi_ext.enable_lr_margin =
|
||||||
|
@ -1856,6 +1856,7 @@ describe('TUI', function()
|
|||||||
retry(nil, nil, function()
|
retry(nil, nil, function()
|
||||||
eq({ true, 330 }, { child_session:request('nvim_win_get_height', 0) })
|
eq({ true, 330 }, { child_session:request('nvim_win_get_height', 0) })
|
||||||
end)
|
end)
|
||||||
|
child_session:request('nvim_set_option_value', 'cursorline', true, {})
|
||||||
-- Use full screen message so that redrawing afterwards is more deterministic.
|
-- Use full screen message so that redrawing afterwards is more deterministic.
|
||||||
child_session:notify('nvim_command', 'intro')
|
child_session:notify('nvim_command', 'intro')
|
||||||
screen:expect({ any = 'Nvim is open source and freely distributable' })
|
screen:expect({ any = 'Nvim is open source and freely distributable' })
|
||||||
@ -1865,14 +1866,7 @@ describe('TUI', function()
|
|||||||
-- The whole line needs 3 + 9 + 3 * 21838 + 3 = 65529 bytes.
|
-- The whole line needs 3 + 9 + 3 * 21838 + 3 = 65529 bytes.
|
||||||
-- The cursor_address that comes after will overflow the 65535-byte buffer.
|
-- The cursor_address that comes after will overflow the 65535-byte buffer.
|
||||||
local line = ('Ꝩ'):rep(21838) .. '℃'
|
local line = ('Ꝩ'):rep(21838) .. '℃'
|
||||||
child_session:notify(
|
child_session:notify('nvim_buf_set_lines', 0, 0, -1, true, { line, 'b' })
|
||||||
'nvim_exec_lua',
|
|
||||||
[[
|
|
||||||
vim.api.nvim_buf_set_lines(0, 0, -1, true, {...})
|
|
||||||
vim.o.cursorline = true
|
|
||||||
]],
|
|
||||||
{ line, 'b' }
|
|
||||||
)
|
|
||||||
-- Close the :intro message and redraw the lines.
|
-- Close the :intro message and redraw the lines.
|
||||||
feed_data('\n')
|
feed_data('\n')
|
||||||
screen:expect([[
|
screen:expect([[
|
||||||
@ -1887,6 +1881,46 @@ describe('TUI', function()
|
|||||||
]])
|
]])
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('draws correctly when setting title overflows #30793', function()
|
||||||
|
screen:try_resize(67, 327)
|
||||||
|
retry(nil, nil, function()
|
||||||
|
eq({ true, 324 }, { child_session:request('nvim_win_get_height', 0) })
|
||||||
|
end)
|
||||||
|
child_exec_lua([[
|
||||||
|
vim.o.cmdheight = 0
|
||||||
|
vim.o.laststatus = 0
|
||||||
|
vim.o.ruler = false
|
||||||
|
vim.o.showcmd = false
|
||||||
|
vim.o.termsync = false
|
||||||
|
vim.o.title = true
|
||||||
|
]])
|
||||||
|
retry(nil, nil, function()
|
||||||
|
eq('[No Name] - Nvim', api.nvim_buf_get_var(0, 'term_title'))
|
||||||
|
eq({ true, 326 }, { child_session:request('nvim_win_get_height', 0) })
|
||||||
|
end)
|
||||||
|
-- Use full screen message so that redrawing afterwards is more deterministic.
|
||||||
|
child_session:notify('nvim_command', 'intro')
|
||||||
|
screen:expect({ any = 'Nvim is open source and freely distributable' })
|
||||||
|
-- Going to top-left corner needs 3 bytes.
|
||||||
|
-- A Ꝩ character takes 3 bytes.
|
||||||
|
-- The whole line needs 3 + 3 * 21842 = 65529 bytes.
|
||||||
|
-- The title will be updated because the buffer is now modified.
|
||||||
|
-- The start of the OSC 0 sequence to set title can fit in the 65535-byte buffer,
|
||||||
|
-- but the title string cannot.
|
||||||
|
local line = ('Ꝩ'):rep(21842)
|
||||||
|
child_session:notify('nvim_buf_set_lines', 0, 0, -1, true, { line })
|
||||||
|
-- Close the :intro message and redraw the lines.
|
||||||
|
feed_data('\n')
|
||||||
|
screen:expect([[
|
||||||
|
{1:Ꝩ}ꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨ|
|
||||||
|
ꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨ|*325
|
||||||
|
{3:-- TERMINAL --} |
|
||||||
|
]])
|
||||||
|
retry(nil, nil, function()
|
||||||
|
eq('[No Name] + - Nvim', api.nvim_buf_get_var(0, 'term_title'))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
it('visual bell (padding) does not crash #21610', function()
|
it('visual bell (padding) does not crash #21610', function()
|
||||||
feed_data ':set visualbell\n'
|
feed_data ':set visualbell\n'
|
||||||
screen:expect {
|
screen:expect {
|
||||||
|
Reference in New Issue
Block a user