mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +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,12 +1804,17 @@ 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; \ | ||||||
|         flush_buf(tui); \ |         /* If orig_pos is 0, there's nothing to flush and retrying won't work. */ \ | ||||||
|         goto retry; \ |         /* TODO(zeertzjq): should this situation still be handled? */ \ | ||||||
|  |         if (orig_pos > 0) { \ | ||||||
|  |           flush_buf(tui); \ | ||||||
|  |           goto retry; \ | ||||||
|  |         } \ | ||||||
|       } \ |       } \ | ||||||
|       tui->cork = false; \ |       tui->cork = false; \ | ||||||
|     } \ |     } \ | ||||||
| @@ -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
	 zeertzjq
					zeertzjq