diff options
author | zeertzjq <zeertzjq@outlook.com> | 2024-10-15 07:34:13 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-10-15 07:34:13 +0800 |
commit | 4846bf05dc639592c73135fb6f1a69f410c1c40f (patch) | |
tree | 224ef8987948656e65b2015a87b8658d2c02198d | |
parent | b7e896671500a6e9a0c773bc6bac2d073e588eba (diff) | |
download | rneovim-4846bf05dc639592c73135fb6f1a69f410c1c40f.tar.gz rneovim-4846bf05dc639592c73135fb6f1a69f410c1c40f.tar.bz2 rneovim-4846bf05dc639592c73135fb6f1a69f410c1c40f.zip |
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.
-rw-r--r-- | src/nvim/tui/tui.c | 36 | ||||
-rw-r--r-- | test/functional/terminal/tui_spec.lua | 50 |
2 files changed, 70 insertions, 16 deletions
diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index fa50a8252d..2839a665da 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -134,11 +134,12 @@ struct TUIData { int resize_screen; int reset_scroll_region; 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_color; int sync; } unibi_ext; + char *set_title; char *space_buf; size_t space_buf_len; bool stopped; @@ -536,6 +537,7 @@ static void terminfo_stop(TUIData *tui) abort(); } unibi_destroy(tui->ut); + XFREE_CLEAR(tui->set_title); } static void tui_terminal_start(TUIData *tui) @@ -1567,8 +1569,7 @@ void tui_suspend(TUIData *tui) void tui_set_title(TUIData *tui, String title) { - if (!(unibi_get_str(tui->ut, unibi_to_status_line) - && unibi_get_str(tui->ut, unibi_from_status_line))) { + if (!unibi_get_ext_str(tui->ut, (unsigned)tui->unibi_ext.set_title)) { return; } 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); tui->title_enabled = true; } - unibi_out(tui, unibi_to_status_line); - out(tui, title.data, title.size); - unibi_out(tui, unibi_from_status_line); + UNIBI_SET_NUM_VAR(tui->params[0], 0); + UNIBI_SET_STR_VAR(tui->params[1], title.data); + unibi_out_ext(tui, tui->unibi_ext.set_title); } else if (tui->title_enabled) { // Restore title/icon from the "stack". #4063 unibi_out_ext(tui, tui->unibi_ext.restore_title); @@ -1803,12 +1804,17 @@ static void unibi_goto(TUIData *tui, int row, int col) memset(&vars, 0, sizeof(vars)); \ tui->cork = true; \ retry: \ + /* Copy parameters on every retry, as unibi_format() may modify them. */ \ memcpy(params, tui->params, sizeof(params)); \ unibi_format(vars, vars + 26, str, params, out, tui, pad, tui); \ if (tui->overflow) { \ tui->bufpos = orig_pos; \ - flush_buf(tui); \ - goto retry; \ + /* 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); \ + goto retry; \ + } \ } \ tui->cork = false; \ } \ @@ -1840,6 +1846,7 @@ static void out(void *ctx, const char *str, size_t len) } flush_buf(tui); } + // TODO(zeertzjq): handle string longer than buffer size? #30794 memcpy(tui->buf + tui->bufpos, str, 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.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 /// known ambiguity with these. So we just set them unconditionally. tui->unibi_ext.enable_lr_margin = diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 79a9c8674e..d150231243 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -1856,6 +1856,7 @@ describe('TUI', function() retry(nil, nil, function() eq({ true, 330 }, { child_session:request('nvim_win_get_height', 0) }) end) + child_session:request('nvim_set_option_value', 'cursorline', true, {}) -- 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' }) @@ -1865,14 +1866,7 @@ describe('TUI', function() -- The whole line needs 3 + 9 + 3 * 21838 + 3 = 65529 bytes. -- The cursor_address that comes after will overflow the 65535-byte buffer. local line = ('Ꝩ'):rep(21838) .. '℃' - child_session:notify( - 'nvim_exec_lua', - [[ - vim.api.nvim_buf_set_lines(0, 0, -1, true, {...}) - vim.o.cursorline = true - ]], - { line, 'b' } - ) + child_session:notify('nvim_buf_set_lines', 0, 0, -1, true, { line, 'b' }) -- Close the :intro message and redraw the lines. feed_data('\n') screen:expect([[ @@ -1887,6 +1881,46 @@ describe('TUI', function() ]]) 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() feed_data ':set visualbell\n' screen:expect { |